{"id":"bd-1ba","title":"Add fetchSkills API helper","description":"## Overview\nAdd fetchSkills() function to dashboard/utils/api.js for calling the skills endpoint.\n\n## Background\nThis is the client-side helper that fetches the autocomplete config. It's a simple wrapper around fetch() following the existing API pattern in the file.\n\n## Implementation (from plan IMP-3)\n```javascript\nexport const API_SKILLS = '/api/skills';\n\nexport async function fetchSkills(agent) {\n const url = \\`\\${API_SKILLS}?agent=\\${encodeURIComponent(agent)}\\`;\n const response = await fetch(url);\n if (\\!response.ok) return null;\n return response.json();\n}\n```\n\n## Return Type\n```typescript\ntype AutocompleteConfig = {\n trigger: '/' | '$';\n skills: Array<{ name: string; description: string }>;\n}\n```\n\n## Error Handling\n- HTTP error: return null (graceful degradation, no autocomplete)\n- Network failure: returns null via failed fetch\n\n## Why Return null on Error\n- Autocomplete is a convenience feature, not critical\n- Modal should still work without autocomplete\n- Avoids error toasts for non-critical failure\n\n## Success Criteria\n- Function exported from api.js\n- Agent param properly encoded in URL\n- Returns parsed JSON on success\n- Returns null on any failure","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:08:42.713976Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:08:47.543516Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-1ba","depends_on_id":"bd-30p","type":"blocks","created_at":"2026-02-26T20:08:47.543493Z","created_by":"tayloreernisse"}]} {"id":"bd-1y3","title":"Add autocomplete state management","description":"## Overview\nAdd state variables and refs for managing autocomplete visibility and selection in SimpleInput.js.\n\n## Background\nThe autocomplete dropdown needs state to track:\n- Whether it's currently visible\n- Which item is highlighted (for keyboard navigation)\n- The current trigger info (for insertion)\n- Refs for DOM elements (for click-outside detection)\n\n## Implementation\nAdd to SimpleInput component:\n```javascript\n// State\nconst [showAutocomplete, setShowAutocomplete] = useState(false);\nconst [selectedIndex, setSelectedIndex] = useState(0);\n\n// Refs for click-outside detection\nconst autocompleteRef = useRef(null);\n\n// Compute triggerInfo on input change\nconst [triggerInfo, setTriggerInfo] = useState(null);\n\n// Update trigger info and visibility on text/cursor change\nuseEffect(() => {\n const textarea = textareaRef.current;\n if (!textarea) return;\n \n const info = getTriggerInfo(text, textarea.selectionStart);\n setTriggerInfo(info);\n setShowAutocomplete(!!info);\n \n // Reset selection when dropdown opens\n if (info && !triggerInfo) {\n setSelectedIndex(0);\n }\n}, [text, getTriggerInfo]);\n```\n\n## State Flow\n1. User types -> text state updates\n2. useEffect computes triggerInfo\n3. If trigger detected -> showAutocomplete = true\n4. selectedIndex tracks keyboard navigation\n5. On selection/dismiss -> showAutocomplete = false\n\n## Reset Behavior\n- selectedIndex resets to 0 when dropdown opens\n- When filter changes, clamp selectedIndex to valid range\n\n## Success Criteria\n- showAutocomplete correctly reflects trigger state\n- selectedIndex tracks current selection\n- State resets appropriately on open/close\n- Refs available for DOM interaction","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:09:43.067983Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:09:45.583079Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-1y3","depends_on_id":"bd-29o","type":"blocks","created_at":"2026-02-26T20:09:45.583055Z","created_by":"tayloreernisse"}]} {"id":"bd-253","title":"Implement autocomplete dropdown UI","description":"## Overview\nAdd the autocomplete dropdown JSX to SimpleInput.js render method, positioned above the input.\n\n## Background\nThe dropdown shows filtered skills with visual highlighting. Per AC-11, AC-15, AC-16-19:\n- Positioned above input (bottom-anchored)\n- Left-aligned\n- Max height with scroll\n- Highlighted item visually distinct\n- Respects color scheme\n- Handles both \"no skills available\" AND \"no matching skills\"\n\n## Implementation (from plan IMP-9, with AC-15 fix)\n```javascript\n${showAutocomplete && html`\n \n ${autocompleteConfig.skills.length === 0 ? html`\n \n
No skills available
\n ` : filteredSkills.length === 0 ? html`\n \n
No matching skills
\n ` : filteredSkills.map((skill, i) => html`\n insertSkill(skill)}\n onMouseEnter=${() => setSelectedIndex(i)}\n >\n
\n ${autocompleteConfig.trigger}${skill.name}\n
\n
${skill.description}
\n \n `)}\n \n`}\n```\n\n## Two Empty States (IMPORTANT)\n1. **\"No skills available\"** (AC-15): `autocompleteConfig.skills.length === 0`\n - Agent has zero skills installed\n - Dropdown still appears to inform user\n \n2. **\"No matching skills\"** (AC-11): `filteredSkills.length === 0`\n - Skills exist, but filter text matches none\n - User typed something that does not match any skill\n\n## Styling Decisions\n- **bottom-full mb-1**: Positions above input with 4px gap\n- **max-h-48**: ~192px max height (AC-17)\n- **overflow-y-auto**: Vertical scroll for long lists\n- **z-50**: Ensures dropdown above other content\n- **bg-selection/50**: Highlighted item background\n- **text-dim/text-bright**: Color hierarchy\n\n## Key Details\n- Uses index as key (plan note: handles duplicate skill names from curated + user)\n- onMouseEnter updates selectedIndex (mouse hover selection)\n- onClick calls insertSkill directly\n- Nested ternary: skills.length → filteredSkills.length → map\n\n## Container Requirements\nSimpleInput outer container needs position: relative for absolute positioning.\n\n## Success Criteria\n- Dropdown appears above input when showAutocomplete true\n- \"No skills available\" when agent has zero skills\n- \"No matching skills\" when filter matches nothing\n- Selected item visually highlighted\n- Scrollable when many skills\n- Mouse hover updates selection\n- Click inserts skill","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:10:35.317055Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:18:15.324880Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-253","depends_on_id":"bd-2uj","type":"blocks","created_at":"2026-02-26T20:10:38.204758Z","created_by":"tayloreernisse"},{"issue_id":"bd-253","depends_on_id":"bd-3us","type":"blocks","created_at":"2026-02-26T20:12:27.289036Z","created_by":"tayloreernisse"}]} {"id":"bd-29o","title":"Implement filtered skills computation","description":"## Overview\nAdd useMemo for filteredSkills in SimpleInput.js that filters the skills list based on user input.\n\n## Background\nAs the user types after the trigger character, the list should filter to show only matching skills. This provides instant feedback and makes finding skills faster.\n\n## Implementation (from plan IMP-6)\n```javascript\nconst filteredSkills = useMemo(() => {\n if (!autocompleteConfig || !triggerInfo) return [];\n \n const { skills } = autocompleteConfig;\n const { filterText } = triggerInfo;\n \n let filtered = skills;\n if (filterText) {\n filtered = skills.filter(s =>\n s.name.toLowerCase().includes(filterText)\n );\n }\n \n // Already sorted by server, but ensure alphabetical\n return filtered.sort((a, b) => a.name.localeCompare(b.name));\n}, [autocompleteConfig, triggerInfo]);\n```\n\n## Filtering Behavior (from AC-6)\n- Case-insensitive matching\n- Matches anywhere in skill name (not just prefix)\n- Empty filterText shows all skills\n- No matches returns empty array (handled by UI)\n\n## Why useMemo\n- Skills list could be large (50+)\n- Filter runs on every keystroke\n- Sorting on each render would be wasteful\n- Memoization prevents unnecessary recalculation\n\n## Key Details\n- triggerInfo.filterText is already lowercase\n- Server pre-sorts, but we re-sort after filtering (stability)\n- localeCompare for proper alphabetical ordering\n\n## Success Criteria\n- Returns empty array when no autocompleteConfig\n- Filters based on filterText (case-insensitive)\n- Results sorted alphabetically\n- Empty input shows all skills\n- Memoized to prevent unnecessary recalculation","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:09:29.436812Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:09:31.898798Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-29o","depends_on_id":"bd-3s3","type":"blocks","created_at":"2026-02-26T20:09:31.898778Z","created_by":"tayloreernisse"}]} {"id":"bd-2a1","title":"Implement backspace dismissal of autocomplete","description":"## Overview\nEnsure that backspacing over the trigger character dismisses the autocomplete dropdown (AC-9).\n\n## Background\nWhen the user types '/com' and then backspaces to '/', they might continue backspacing to remove the '/'. At that point, the autocomplete should dismiss.\n\n## Implementation\nThis is already handled by the existing trigger detection:\n- When text changes, triggerInfo is recomputed\n- If cursor is before the trigger char, getTriggerInfo returns null\n- showAutocomplete becomes false\n\n## Verification Needed\n- Type '/com'\n- Backspace to '/'\n- Backspace to ''\n- Dropdown should dismiss when '/' is deleted\n\n## Success Criteria\n- Backspacing past trigger dismisses dropdown\n- No special code needed if detection is correct\n- Add test case if not already covered","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-26T20:12:00.191481Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:12:03.544224Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-2a1","depends_on_id":"bd-3s3","type":"blocks","created_at":"2026-02-26T20:12:03.544203Z","created_by":"tayloreernisse"}]} {"id":"bd-2n7","title":"Load autocomplete config in Modal","description":"## Overview\nAdd useEffect in Modal.js to load skills when a session is opened, and pass autocompleteConfig to SimpleInput.\n\n## Background\nSkills are agent-global, not session-specific. We fetch once when session.agent changes and cache client-side. This follows the plan's data flow: Modal opens -> GET /api/skills -> SimpleInput gets config.\n\n## Implementation (from plan IMP-4)\n```javascript\nconst [autocompleteConfig, setAutocompleteConfig] = useState(null);\n\n// Load skills when agent type changes\nuseEffect(() => {\n if (!session) {\n setAutocompleteConfig(null);\n return;\n }\n \n const agent = session.agent || 'claude';\n fetchSkills(agent)\n .then(config => setAutocompleteConfig(config))\n .catch(() => setAutocompleteConfig(null));\n}, [session?.agent]);\n\n// In render, pass to SimpleInput:\n<\\${SimpleInput}\n ...\n autocompleteConfig=\\${autocompleteConfig}\n/>\n```\n\n## Dependency Array\n- Uses session?.agent to re-fetch when agent type changes\n- NOT session.id (would refetch on every session switch unnecessarily)\n- Skills are agent-global, so same agent = same skills\n\n## Error Handling\n- fetch failure: autocompleteConfig stays null\n- null config: SimpleInput disables autocomplete silently\n\n## Props to Add\nSimpleInput gets new optional prop:\n- autocompleteConfig: { trigger, skills } | null\n\n## Success Criteria\n- useEffect fetches on session.agent change\n- autocompleteConfig state managed correctly\n- Passed to SimpleInput as prop\n- Null on error (graceful degradation)","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:08:58.959680Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:09:01.617221Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-2n7","depends_on_id":"bd-1ba","type":"blocks","created_at":"2026-02-26T20:09:01.617198Z","created_by":"tayloreernisse"}]} {"id":"bd-2uj","title":"Implement skill insertion","description":"## Overview\nAdd insertSkill() function in SimpleInput.js that replaces the trigger+filter text with the full skill name.\n\n## Background\nWhen user selects a skill (Enter, Tab, or click), we need to:\n1. Replace the trigger+partial text with trigger+full skill name\n2. Add a trailing space\n3. Position cursor after the inserted text\n\n## Implementation (from plan IMP-8)\n```javascript\nconst insertSkill = useCallback((skill) => {\n if (\\!triggerInfo || \\!autocompleteConfig) return;\n \n const { trigger } = autocompleteConfig;\n const { replaceStart, replaceEnd } = triggerInfo;\n \n const before = text.slice(0, replaceStart);\n const after = text.slice(replaceEnd);\n const inserted = \\`\\${trigger}\\${skill.name} \\`;\n \n setText(before + inserted + after);\n setShowAutocomplete(false);\n \n // Move cursor after inserted text\n const newCursorPos = replaceStart + inserted.length;\n setTimeout(() => {\n if (textareaRef.current) {\n textareaRef.current.selectionStart = newCursorPos;\n textareaRef.current.selectionEnd = newCursorPos;\n textareaRef.current.focus();\n }\n }, 0);\n}, [text, triggerInfo, autocompleteConfig]);\n```\n\n## Example\nUser types: 'please run /com|' (cursor at |)\nUser selects 'commit'\nResult: 'please run /commit |'\n\n## Why setTimeout for Cursor\n- React may not have updated the textarea value yet\n- setTimeout(0) defers until after React render\n- Ensures cursor positioning happens on updated DOM\n\n## AC-20 Compliance\n'After skill insertion, cursor is positioned after the trailing space, ready to continue typing.'\n\n## Success Criteria\n- Replaces trigger+filter with trigger+skill+space\n- Preserves text before and after\n- Cursor positioned at end of insertion\n- Dropdown closes after insertion\n- Focus remains on textarea","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:10:14.663971Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:10:18.401505Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-2uj","depends_on_id":"bd-3vd","type":"blocks","created_at":"2026-02-26T20:10:18.401490Z","created_by":"tayloreernisse"}]} {"id":"bd-30p","title":"Add /api/skills route to HttpMixin","description":"## Overview\nAdd the GET /api/skills endpoint to HttpMixin.do_GET that routes to SkillsMixin._serve_skills().\n\n## Background\nThe skills endpoint lives in HttpMixin because that's where all GET routing is handled. The handler just composes mixins. This follows the existing architecture pattern.\n\n## Implementation (from plan IMP-2)\nIn amc_server/mixins/http.py, add to do_GET method:\n```python\nelif path == '/api/skills':\n agent = query_params.get('agent', ['claude'])[0]\n self._serve_skills(agent)\n```\n\n## API Design\n- **Endpoint**: GET /api/skills?agent={claude|codex}\n- **Default agent**: 'claude' (if param missing)\n- **Response**: JSON from _serve_skills()\n\n## Why This Location\n- HttpMixin.do_GET handles ALL GET routing\n- Keeps routing logic centralized\n- SkillsMixin provides the implementation\n- Handler composes both mixins\n\n## Required Import\nAdd SkillsMixin to handler.py mixin list:\n```python\nfrom amc_server.mixins.skills import SkillsMixin\n# Add to AMCHandler class inheritance\n```\n\n## Success Criteria\n- Route /api/skills accessible via GET\n- Query param 'agent' passed to _serve_skills\n- Missing agent param defaults to 'claude'\n- Follows existing routing patterns in do_GET","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:08:29.491653Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:08:32.597481Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-30p","depends_on_id":"bd-g9t","type":"blocks","created_at":"2026-02-26T20:08:32.597460Z","created_by":"tayloreernisse"}]} {"id":"bd-35a","title":"Integration: Wire up complete autocomplete feature","description":"## Overview\nFinal integration step: ensure all pieces work together in Modal.js and SimpleInput.js.\n\n## Background\nThis is the capstone bead that verifies the complete feature is wired up correctly. All previous beads implement individual pieces; this ensures they connect.\n\n## Checklist\n1. **SkillsMixin added to handler**\n - Import SkillsMixin in handler.py\n - Add to AMCHandler class inheritance\n\n2. **Modal loads config**\n - fetchSkills called on session.agent change\n - autocompleteConfig passed to SimpleInput\n\n3. **SimpleInput receives config**\n - New prop: autocompleteConfig\n - State management wired up\n - Keyboard handlers connected\n - Dropdown renders correctly\n\n## Verification Steps\n1. Start dev server\n2. Open a session modal\n3. Type '/' -> dropdown should appear\n4. Arrow down, Enter -> skill inserted\n5. Check Codex session with '$'\n\n## Common Issues to Check\n- Import paths correct\n- Mixin in correct position in inheritance\n- Props threaded through correctly\n- No console errors\n\n## Success Criteria\n- Feature works end-to-end\n- No console errors\n- Both Claude and Codex agents work\n- Manual testing checklist passes","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:12:39.799374Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:12:43.180050Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-35a","depends_on_id":"bd-2a1","type":"blocks","created_at":"2026-02-26T20:12:43.160167Z","created_by":"tayloreernisse"},{"issue_id":"bd-35a","depends_on_id":"bd-362","type":"blocks","created_at":"2026-02-26T20:12:43.135755Z","created_by":"tayloreernisse"},{"issue_id":"bd-35a","depends_on_id":"bd-3ny","type":"blocks","created_at":"2026-02-26T20:12:43.111964Z","created_by":"tayloreernisse"},{"issue_id":"bd-35a","depends_on_id":"bd-4lc","type":"blocks","created_at":"2026-02-26T20:12:43.180034Z","created_by":"tayloreernisse"}]} {"id":"bd-362","title":"Unit tests for SkillsMixin","description":"## Overview\nAdd comprehensive unit tests for amc_server/mixins/skills.py in tests/test_skills.py.\n\n## Background\nThe skill enumeration is the foundation of autocomplete. We need to test:\n- Both enumeration methods\n- Edge cases (missing files, bad JSON, empty directories)\n- Sorting behavior\n- Response format\n\n## Test Cases\n\n### Claude Enumeration\n```python\ndef test_enumerate_claude_skills_empty_directory(tmp_home):\n '''Returns empty list when ~/.claude/skills doesn't exist'''\n \ndef test_enumerate_claude_skills_reads_skill_md(tmp_home, claude_skill):\n '''Reads description from SKILL.md'''\n \ndef test_enumerate_claude_skills_fallback_to_readme(tmp_home):\n '''Falls back to README.md when SKILL.md missing'''\n \ndef test_enumerate_claude_skills_skips_hidden_dirs(tmp_home):\n '''Ignores directories starting with .'''\n \ndef test_enumerate_claude_skills_truncates_description(tmp_home):\n '''Description truncated to 100 chars'''\n \ndef test_enumerate_claude_skills_skips_headers(tmp_home):\n '''First non-header line used as description'''\n```\n\n### Codex Enumeration\n```python\ndef test_enumerate_codex_skills_reads_cache(tmp_home):\n '''Reads from skills-curated-cache.json'''\n \ndef test_enumerate_codex_skills_invalid_json(tmp_home):\n '''Continues without cache if JSON invalid'''\n \ndef test_enumerate_codex_skills_combines_cache_and_user(tmp_home):\n '''Returns both curated and user skills'''\n \ndef test_enumerate_codex_skills_no_deduplication(tmp_home):\n '''Duplicate names from cache and user both appear'''\n```\n\n### Serve Skills\n```python\ndef test_serve_skills_claude_trigger(mock_handler):\n '''Returns / trigger for claude agent'''\n \ndef test_serve_skills_codex_trigger(mock_handler):\n '''Returns $ trigger for codex agent'''\n \ndef test_serve_skills_default_to_claude(mock_handler):\n '''Unknown agent defaults to claude'''\n \ndef test_serve_skills_alphabetical_sort(mock_handler, skills_fixture):\n '''Skills sorted alphabetically (case-insensitive)'''\n```\n\n## Fixtures\n- tmp_home: pytest fixture that sets HOME to temp directory\n- claude_skill: creates a skill directory with SKILL.md\n- mock_handler: SkillsMixin instance with mocked _send_json\n\n## Success Criteria\n- All enumeration paths tested\n- Error handling verified (bad JSON, missing files)\n- Sorting correctness verified\n- Response format validated","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-26T20:11:15.502753Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:11:18.589476Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-362","depends_on_id":"bd-30p","type":"blocks","created_at":"2026-02-26T20:11:18.589458Z","created_by":"tayloreernisse"}]} {"id":"bd-3cc","title":"E2E tests for autocomplete workflow","description":"## Overview\nCreate end-to-end test script that validates the complete autocomplete workflow from typing to insertion.\n\n## Background\nThe plan includes a manual testing checklist (lines 462-480). We should automate key workflows to ensure the feature works end-to-end.\n\n## Test Scenarios (from plan's testing checklist)\n\n### Core Flow\n```\n1. Claude session: Type \"/\" -> dropdown appears with Claude skills\n2. Codex session: Type \"$\" -> dropdown appears with Codex skills\n3. Claude session: Type \"$\" -> nothing happens (wrong trigger)\n4. Type \"/com\" -> list filters to skills containing \"com\"\n5. Mid-message: Type \"please run /commit\" -> autocomplete triggers on \"/\"\n6. Arrow keys navigate, Enter selects\n7. Escape dismisses without selection\n8. Click outside dismisses\n9. Selected skill shows as \"{trigger}skill-name \" in input\n10. Verify alphabetical ordering of skills\n11. Verify vertical scroll with many skills\n```\n\n### Edge Cases (from plan section)\n```\n- Session without skills (dropdown shows \"No skills available\")\n- Single skill (still shows dropdown)\n- Very long skill descriptions (CSS truncates with ellipsis - visual check)\n- Multiple triggers in one message (each \"/\" can trigger independently)\n- Backspace over trigger (dismisses autocomplete)\n```\n\n### Multiple Triggers Test (important edge case)\nUser types: \"first /commit then /review-pr finally\"\n- First \"/\" at position 6 can trigger\n- After inserting \"/commit \", cursor at position 14\n- Second \"/\" at position after text can trigger again\n- Verify each trigger works independently\n\n## Implementation Approach\nUse the Playwright MCP tools to:\n1. Navigate to dashboard\n2. Open a session modal\n3. Type trigger character\n4. Verify dropdown appears\n5. Navigate with arrows\n6. Select with Enter\n7. Verify insertion\n\n## Logging Requirements\n- Log each step being performed\n- Log expected vs actual behavior\n- Log timing for performance visibility\n- Log any errors with context\n\n## Test Script Location\ntests/e2e/test_autocomplete.py or similar\n\n## Success Criteria\n- All core scenarios pass\n- Edge cases handled\n- Detailed logging for debugging\n- Can run in CI environment","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-26T20:11:47.556903Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:18:56.881498Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3cc","depends_on_id":"bd-3ny","type":"blocks","created_at":"2026-02-26T20:11:50.670698Z","created_by":"tayloreernisse"}]} {"id":"bd-3eu","title":"Implement Codex skill enumeration","description":"## Overview\nImplement _enumerate_codex_skills() method in SkillsMixin to discover skills from both the curated cache and user directory.\n\n## Background\nCodex stores skills in two locations:\n1. **Curated cache**: ~/.codex/vendor_imports/skills-curated-cache.json (pre-installed skills)\n2. **User skills**: ~/.codex/skills/*/ (user-created or installed)\n\nBoth need to be combined for the full skills list.\n\n## Implementation (from plan IMP-1)\n```python\ndef _enumerate_codex_skills(self):\n skills = []\n \n # 1. Curated skills from cache\n cache_file = Path.home() / '.codex/vendor_imports/skills-curated-cache.json'\n if cache_file.exists():\n try:\n data = json.loads(cache_file.read_text())\n for skill in data.get('skills', []):\n skills.append({\n 'name': skill.get('id', skill.get('name', '')),\n 'description': skill.get('shortDescription', skill.get('description', ''))[:100]\n })\n except (json.JSONDecodeError, OSError):\n pass # Continue without curated skills\n \n # 2. User-installed skills\n user_skills_dir = Path.home() / '.codex/skills'\n if user_skills_dir.exists():\n for skill_dir in user_skills_dir.iterdir():\n if skill_dir.is_dir() and not skill_dir.name.startswith('.'):\n skill_md = skill_dir / 'SKILL.md'\n description = ''\n if skill_md.exists():\n try:\n for line in skill_md.read_text().splitlines():\n line = line.strip()\n if line and not line.startswith('#'):\n description = line[:100]\n break\n except OSError:\n pass\n skills.append({\n 'name': skill_dir.name,\n 'description': description or f'User skill: {skill_dir.name}'\n })\n \n return skills\n```\n\n## Key Decisions\n- **Cache file structure**: Expected format {skills: [{id, shortDescription}, ...]}\n- **Fallback for missing fields**: Use 'name' if 'id' missing, 'description' if 'shortDescription' missing\n- **No deduplication**: If curated and user skills share a name, both appear (per Known Limitations)\n- **Error resilience**: JSON parse errors don't prevent user skills from loading\n\n## Out of Scope (per plan)\n- Duplicate skill names are NOT deduplicated\n- Server-side caching of enumeration results\n\n## Success Criteria\n- Returns combined list from cache + user directory\n- Handles missing files/directories gracefully\n- Truncates descriptions to 100 chars\n- JSON parse errors don't crash enumeration","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:07:58.579276Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:08:01.832064Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3eu","depends_on_id":"bd-3q1","type":"blocks","created_at":"2026-02-26T20:08:01.832043Z","created_by":"tayloreernisse"}]} {"id":"bd-3ny","title":"Implement click-outside dismissal","description":"## Overview\nAdd useEffect in SimpleInput.js that dismisses the autocomplete dropdown when clicking outside.\n\n## Background\nAC-9 requires clicking outside to dismiss the dropdown. This is a common UX pattern that requires:\n- Event listener on document\n- Check if click target is inside dropdown or textarea\n- Cleanup on unmount\n\n## Implementation (from plan IMP-10)\n```javascript\nuseEffect(() => {\n if (!showAutocomplete) return;\n \n const handleClickOutside = (e) => {\n if (autocompleteRef.current && !autocompleteRef.current.contains(e.target) &&\n textareaRef.current && !textareaRef.current.contains(e.target)) {\n setShowAutocomplete(false);\n }\n };\n \n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n}, [showAutocomplete]);\n```\n\n## Why mousedown (not click)\n- mousedown fires immediately on press\n- click fires after release (feels sluggish)\n- Standard pattern for dropdown dismissal\n\n## What Counts as 'Inside'\n- Inside autocompleteRef (the dropdown)\n- Inside textareaRef (the input)\n- Both should keep dropdown open\n\n## Cleanup\n- Effect only adds listener when dropdown is visible\n- Cleanup removes listener when:\n - Dropdown closes\n - Component unmounts\n - Dependencies change\n\n## Success Criteria\n- Clicking outside dropdown+textarea dismisses\n- Clicking inside dropdown doesn't dismiss (onClick handles selection)\n- Clicking in textarea doesn't dismiss (keeps typing)\n- Listener cleaned up properly","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:10:49.738217Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:10:52.252361Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3ny","depends_on_id":"bd-253","type":"blocks","created_at":"2026-02-26T20:10:52.252341Z","created_by":"tayloreernisse"}]} {"id":"bd-3q1","title":"Create SkillsMixin class for skill enumeration","description":"## Overview\nCreate a new SkillsMixin class in amc_server/mixins/skills.py that handles enumerating available skills for both Claude and Codex agents.\n\n## Background & Rationale\nThe autocomplete feature needs to know what skills are available for each agent type. Skills are agent-global (not session-specific), so we enumerate them from well-known filesystem locations. This follows the existing mixin pattern used throughout the server (HttpMixin, StateMixin, etc.).\n\n## Implementation Details\n- Create new file: amc_server/mixins/skills.py\n- Class: SkillsMixin with methods:\n - _serve_skills(agent: str) -> serves JSON response\n - _enumerate_claude_skills() -> list of {name, description}\n - _enumerate_codex_skills() -> list of {name, description}\n- Sort skills alphabetically by name (case-insensitive)\n- Return JSON: {trigger: '/' or '$', skills: [...]}\n\n## File Locations\n- Claude skills: ~/.claude/skills/*/\n- Codex curated: ~/.codex/vendor_imports/skills-curated-cache.json\n- Codex user: ~/.codex/skills/*/\n\n## Acceptance Criteria (from plan)\n- AC-12: On session open, agent-specific config loaded with trigger + skills\n- AC-13: Claude skills from ~/.claude/skills/\n- AC-14: Codex skills from cache + user directory\n- AC-15: Empty skills list handled gracefully\n\n## Error Handling\n- Directory doesn't exist: return empty list\n- JSON parse error (Codex cache): skip cache, continue with user skills\n- File read error: skip that skill, continue enumeration\n\n## Success Criteria\n- SkillsMixin class exists with all three methods\n- Claude enumeration reads SKILL.md (canonical), falls back to skill.md, prompt.md, README.md\n- Codex enumeration reads curated cache + user skills directory\n- All skills sorted alphabetically\n- Empty directories return empty list (no error)","status":"open","priority":1,"issue_type":"feature","created_at":"2026-02-26T20:07:30.389323Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:07:30.389323Z","compaction_level":0,"original_size":0} {"id":"bd-3s3","title":"Implement trigger detection logic","description":"## Overview\nAdd getTriggerInfo() function in SimpleInput.js that detects when autocomplete should activate.\n\n## Background\nAutocomplete triggers when the agent-specific trigger character (/ for Claude, $ for Codex) appears at:\n1. Position 0 (start of input)\n2. After whitespace (space, newline, tab)\n\nThis enables mid-message skill invocation like \"please run /commit\".\n\n## Acceptance Criteria Covered\n- **AC-1**: Triggers at position 0 or after whitespace\n- **AC-2**: Claude uses /, Codex uses $ (from autocompleteConfig.trigger)\n- **AC-3**: Wrong trigger for agent type is ignored (returns null)\n\n## Implementation (from plan IMP-5)\n```javascript\nconst getTriggerInfo = useCallback((value, cursorPos) => {\n // AC-3: If no config, ignore all triggers\n if (\\!autocompleteConfig) return null;\n \n const { trigger } = autocompleteConfig;\n \n // Find the start of the current \"word\" (after last whitespace before cursor)\n let wordStart = cursorPos;\n while (wordStart > 0 && \\!/\\s/.test(value[wordStart - 1])) {\n wordStart--;\n }\n \n // AC-3: Only match THIS agent's trigger, ignore others\n // Claude session typing \"$\" returns null (no autocomplete)\n // Codex session typing \"/\" returns null (no autocomplete)\n if (value[wordStart] === trigger) {\n return {\n trigger,\n filterText: value.slice(wordStart + 1, cursorPos).toLowerCase(),\n replaceStart: wordStart,\n replaceEnd: cursorPos\n };\n }\n \n return null;\n}, [autocompleteConfig]);\n```\n\n## Return Value\n```typescript\ntype TriggerInfo = {\n trigger: string; // The trigger char (/ or $)\n filterText: string; // Text after trigger for filtering (lowercase)\n replaceStart: number; // Start position for replacement\n replaceEnd: number; // End position for replacement (cursor pos)\n} | null;\n```\n\n## AC-3 Behavior (Wrong Trigger Ignored)\nThe function only checks for `autocompleteConfig.trigger`. If user types a different character:\n- Claude session: \"$\" is just a dollar sign, no autocomplete\n- Codex session: \"/\" is just a slash, no autocomplete\n\nThis is implicit in the implementation - we compare against ONE trigger character only.\n\n## Edge Cases\n- Empty input: wordStart = 0, checks value[0]\n- Mid-word: wordStart finds last whitespace, checks that position\n- Multiple triggers in message: each \"/\" or \"$\" can independently trigger\n- Backspace over trigger: cursorPos moves, wordStart recalculates\n\n## When This Returns null\n1. No autocompleteConfig (not loaded yet)\n2. Cursor not preceded by trigger character\n3. Wrong trigger for this agent type (AC-3)\n\n## Success Criteria\n- Returns TriggerInfo when trigger at valid position\n- Returns null for wrong trigger (AC-3)\n- Returns null when no config\n- filterText is lowercase for case-insensitive matching\n- replaceStart/End correct for skill insertion","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:09:15.230355Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:18:36.037711Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3s3","depends_on_id":"bd-2n7","type":"blocks","created_at":"2026-02-26T20:09:18.843732Z","created_by":"tayloreernisse"}]} {"id":"bd-3us","title":"Ensure SimpleInput container has relative positioning","description":"## Overview\nVerify/add position: relative to SimpleInput's container for proper dropdown positioning.\n\n## Background\nThe autocomplete dropdown uses absolute positioning with 'bottom-full'. This requires the parent container to have position: relative.\n\n## Implementation\nCheck SimpleInput.js render:\n```javascript\n// The outer wrapper needs position: relative\n
\n