From 2926645b10627236c06f76d6620f4dd84c0f3d1b Mon Sep 17 00:00:00 2001 From: teernisse Date: Thu, 26 Feb 2026 15:25:02 -0500 Subject: [PATCH] docs: add implementation plans for upcoming features Planning documents for future AMC features: PLAN-slash-autocomplete.md: - Slash-command autocomplete for SimpleInput - Skills API endpoint, SlashMenu dropdown, keyboard navigation - 8 implementation steps with file locations and dependencies plans/agent-spawning.md: - Agent spawning acceptance criteria documentation - Spawn command integration, status tracking, error handling - Written as testable acceptance criteria (AC-1 through AC-10) Co-Authored-By: Claude Opus 4.5 --- PLAN-slash-autocomplete.md | 509 +++++++++++++++++++++ plans/agent-spawning.md | 910 +++++++++++++++++++++++++++++++++++++ 2 files changed, 1419 insertions(+) create mode 100644 PLAN-slash-autocomplete.md create mode 100644 plans/agent-spawning.md diff --git a/PLAN-slash-autocomplete.md b/PLAN-slash-autocomplete.md new file mode 100644 index 0000000..a96c41e --- /dev/null +++ b/PLAN-slash-autocomplete.md @@ -0,0 +1,509 @@ +# Plan: Skill Autocomplete for Agent Sessions + +## Summary + +Add autocomplete functionality to the SimpleInput component that displays available skills when the user types the agent-specific trigger character (`/` for Claude, `$` for Codex). Autocomplete triggers at the start of input or after any whitespace, enabling quick skill discovery and selection mid-message. + +## User Workflow + +1. User opens a session modal or card with the input field +2. User types the trigger character (`/` for Claude, `$` for Codex): + - At position 0, OR + - After a space/newline (mid-message) +3. Autocomplete dropdown appears showing available skills (alphabetically sorted) +4. User can: + - Continue typing to filter the list + - Use arrow keys to navigate + - Press Enter/Tab to select and insert the skill name + - Press Escape or click outside to dismiss +5. Selected skill replaces the trigger with `{trigger}skill-name ` (e.g., `/commit ` or `$yeet `) + +## Acceptance Criteria + +### Core Functionality +- **AC-1**: Autocomplete triggers when trigger character is typed at position 0 or after whitespace +- **AC-2**: Claude sessions use `/` trigger; Codex sessions use `$` trigger +- **AC-3**: Wrong trigger character for agent type is ignored (no autocomplete) +- **AC-4**: Dropdown displays skill names with trigger prefix and descriptions +- **AC-5**: Skills are sorted alphabetically by name +- **AC-6**: Typing additional characters filters the skill list (case-insensitive match on name) +- **AC-7**: Arrow up/down navigates the highlighted option +- **AC-8**: Enter or Tab inserts the selected skill name (with trigger) followed by a space +- **AC-9**: Escape, clicking outside, or backspacing over the trigger character dismisses the dropdown without insertion +- **AC-10**: Cursor movement (arrow left/right) is ignored while autocomplete is open; dropdown position is locked to trigger location +- **AC-11**: If no skills match the filter, dropdown shows "No matching skills" + +### Data Flow +- **AC-12**: On session open, an agent-specific config is loaded containing: (a) trigger character (`/` for Claude, `$` for Codex), (b) enumerated skills list +- **AC-13**: Claude skills are enumerated from `~/.claude/skills/` +- **AC-14**: Codex skills are loaded from `~/.codex/vendor_imports/skills-curated-cache.json` plus `~/.codex/skills/` +- **AC-15**: If session has no skills, dropdown shows "No skills available" when trigger is typed + +### UX Polish +- **AC-16**: Dropdown positions above the input (bottom-anchored), aligned left +- **AC-17**: Dropdown has max height with vertical scroll for long lists +- **AC-18**: Currently highlighted item is visually distinct +- **AC-19**: Dropdown respects the existing color scheme +- **AC-20**: After skill insertion, cursor is positioned after the trailing space, ready to continue typing + +### Known Limitations (Out of Scope) +- **Duplicate skill names**: If curated and user skills share a name, both appear (no deduplication) +- **Long skill names**: No truncation; names may overflow if extremely long +- **Accessibility**: ARIA roles, active-descendant, screen reader support deferred to future iteration +- **IME/composition**: Japanese/Korean input edge cases not handled in v1 +- **Server-side caching**: Skills re-enumerated on each request; mtime-based cache could improve performance at scale + +## Architecture + +### Autocomplete Config Per Agent + +Each session gets an autocomplete config loaded at modal open: + +```typescript +type AutocompleteConfig = { + trigger: '/' | '$'; + skills: Array<{ name: string; description: string }>; +} +``` + +| Agent | Trigger | Skill Sources | +|-------|---------|---------------| +| Claude | `/` | Enumerate `~/.claude/skills/*/` | +| Codex | `$` | `~/.codex/vendor_imports/skills-curated-cache.json` + `~/.codex/skills/*/` | + +### Server-Side: New Endpoint for Skills + +**Endpoint**: `GET /api/skills?agent={claude|codex}` + +**Response**: +```json +{ + "trigger": "/", + "skills": [ + { "name": "commit", "description": "Create a git commit with a message" }, + { "name": "review-pr", "description": "Review a pull request" } + ] +} +``` + +**Rationale**: Skills are agent-global, not session-specific. The client already knows `session.agent` from state, so no session_id is needed. Server enumerates skill directories directly. + +### Component Structure + +``` +SimpleInput.js +├── Props: sessionId, status, onRespond, agent, autocompleteConfig +├── State: text, focused, sending, error +├── State: showAutocomplete, selectedIndex +├── Derived: triggerMatch (detects trigger at valid position) +├── Derived: filterText, filteredSkills (alphabetically sorted) +├── onInput: detect trigger character at pos 0 or after whitespace +├── onKeyDown: arrow/enter/escape handling for autocomplete +└── Render: textarea + autocomplete dropdown +``` + +### Data Flow + +``` +┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────┐ +│ Modal opens │────▶│ GET /api/skills?agent│────▶│ SimpleInput │ +│ (session) │ │ (server) │ │ (dropdown) │ +└─────────────────┘ └──────────────────────┘ └─────────────────┘ +``` + +Skills are agent-global, so the same response can be cached client-side per agent type. + +## Implementation Specifications + +### IMP-1: Server-side skill enumeration (fulfills AC-12, AC-13, AC-14, AC-15) + +**Location**: `amc_server/mixins/skills.py` (new file) + +```python +class SkillsMixin: + def _serve_skills(self, agent): + """Return autocomplete config for a session.""" + if agent == "codex": + trigger = "$" + skills = self._enumerate_codex_skills() + else: # claude + trigger = "/" + skills = self._enumerate_claude_skills() + + # Sort alphabetically + skills.sort(key=lambda s: s["name"].lower()) + + self._send_json(200, {"trigger": trigger, "skills": skills}) + + def _enumerate_codex_skills(self): + """Load Codex skills from cache + user directory.""" + skills = [] + + # Curated skills from cache + cache_file = Path.home() / ".codex/vendor_imports/skills-curated-cache.json" + if cache_file.exists(): + try: + data = json.loads(cache_file.read_text()) + for skill in data.get("skills", []): + skills.append({ + "name": skill.get("id", skill.get("name", "")), + "description": skill.get("shortDescription", skill.get("description", ""))[:100] + }) + except (json.JSONDecodeError, OSError): + pass + + # User-installed skills + user_skills_dir = Path.home() / ".codex/skills" + if user_skills_dir.exists(): + for skill_dir in user_skills_dir.iterdir(): + if skill_dir.is_dir() and not skill_dir.name.startswith("."): + skill_md = skill_dir / "SKILL.md" + description = "" + if skill_md.exists(): + # Parse first non-empty line as description + try: + for line in skill_md.read_text().splitlines(): + line = line.strip() + if line and not line.startswith("#"): + description = line[:100] + break + except OSError: + pass + skills.append({ + "name": skill_dir.name, + "description": description or f"User skill: {skill_dir.name}" + }) + + return skills + + def _enumerate_claude_skills(self): + """Load Claude skills from user directory. + + Note: Checks SKILL.md first (canonical casing used by Claude Code), + then falls back to lowercase variants for compatibility. + """ + skills = [] + skills_dir = Path.home() / ".claude/skills" + + if skills_dir.exists(): + for skill_dir in skills_dir.iterdir(): + if skill_dir.is_dir() and not skill_dir.name.startswith("."): + # Look for SKILL.md (canonical), then fallbacks + description = "" + for md_name in ["SKILL.md", "skill.md", "prompt.md", "README.md"]: + md_file = skill_dir / md_name + if md_file.exists(): + try: + content = md_file.read_text() + # Find first meaningful line + for line in content.splitlines(): + line = line.strip() + if line and not line.startswith("#") and not line.startswith("