nexus/.planning/milestones/v1.2.1-phases/19-adapter-aware-install-uninstall/19-03-PLAN.md
Mikkel Georgsen 3c85784b6d [nexus] chore: migrate .planning/ from agent repo to nexus repo
Planning artifacts (milestones v1.0-v1.2.1, v1.3 queue, PROJECT.md,
STATE.md, config) now live alongside the code they describe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:08:50 +00:00

242 lines
10 KiB
Markdown

---
phase: 19-adapter-aware-install-uninstall
plan: "03"
type: execute
wave: 2
depends_on: ["19-01"]
files_modified:
- ui/src/api/skillRegistry.ts
- ui/src/pages/SkillBrowser.tsx
- ui/src/components/SkillCard.tsx
autonomous: true
requirements: [HERM-01, HERM-02, HERM-03]
must_haves:
truths:
- "Installed tab for Hermes agents shows two labelled sections: Managed and Native"
- "Native skills display no remove/update/rollback buttons"
- "Managed skills on Hermes agents display full writable actions (install, update, remove)"
- "Install dialog sends agentId in body, not agentSkillsDir"
- "Uninstall sends agentId as query param, not in body"
artifacts:
- path: "ui/src/api/skillRegistry.ts"
provides: "Updated API types and calls using agentId instead of agentSkillsDir"
contains: "agentId"
- path: "ui/src/pages/SkillBrowser.tsx"
provides: "Dual-section Installed tab with Managed/Native labels for Hermes agents"
contains: "managedSkills"
- path: "ui/src/components/SkillCard.tsx"
provides: "isReadOnly prop that hides action buttons for native skills"
contains: "isReadOnly"
key_links:
- from: "ui/src/pages/SkillBrowser.tsx"
to: "ui/src/api/skillRegistry.ts"
via: "listAgentSkills returns AgentSkillEntry[] with source field"
pattern: "listAgentSkills"
- from: "ui/src/pages/SkillBrowser.tsx"
to: "ui/src/components/SkillCard.tsx"
via: "isReadOnly prop passed for native skills"
pattern: "isReadOnly.*native"
---
<objective>
Update the Skill Browser UI to show managed vs native sections for Hermes agents, make native skills read-only, and switch all API calls from agentSkillsDir to agentId.
Purpose: Users managing Hermes agents need to clearly distinguish Nexus-managed skills (full control) from built-in native skills (view/rate only). The UI must also stop sending filesystem paths to the server.
Output: Updated API client, dual-section Installed tab, read-only SkillCard variant.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/19-adapter-aware-install-uninstall/19-RESEARCH.md
@.planning/phases/19-adapter-aware-install-uninstall/19-01-SUMMARY.md
Read these source files before modifying:
- ui/src/api/skillRegistry.ts (or similar — the API client for skill registry)
- ui/src/pages/SkillBrowser.tsx
- ui/src/components/SkillCard.tsx (if exists — may be inline in SkillBrowser)
<interfaces>
<!-- From Plan 01 (updated API response shape) -->
GET /skill-registry/agents/:agentId/skills
Response: Array<{ skillId: string; source: "managed" | "native"; installedAt: number }>
POST /skill-registry/skills/:sourceId/:slug/install
Body: { agentId: string } (was: { agentSkillsDir: string })
DELETE /skill-registry/skills/:sourceId/:slug?agentId=xxx
Query: agentId (was: body.agentSkillsDir)
POST /skill-registry/skills/:sourceId/:slug/rollback
Body: { versionId: string, agentId: string } (was: { versionId, agentSkillsDir })
POST /skill-registry/agents/:agentId/groups
Body: { groupId: string } (was: { groupId, agentSkillsDir? })
DELETE /skill-registry/agents/:agentId/groups/:groupId
(no body needed — agentId in URL)
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Update API client types and calls</name>
<files>
ui/src/api/skillRegistry.ts
</files>
<read_first>
ui/src/api/skillRegistry.ts (or search for skill registry API functions — may be in a different file like ui/src/api/skillGroups.ts or similar)
</read_first>
<action>
1. Add the `AgentSkillEntry` type:
```typescript
export type AgentSkillEntry = {
skillId: string;
source: "managed" | "native";
installedAt: number;
};
```
2. Update `listAgentSkills` return type from `string[]` to `AgentSkillEntry[]`.
3. Update `installSkill` (or equivalent) to send `{ agentId }` in the POST body instead of `{ agentSkillsDir }`.
4. Update `uninstallSkill` (or equivalent) to pass `agentId` as a query parameter on the DELETE request instead of in the body.
5. Update `rollbackSkill` (or equivalent) to send `{ versionId, agentId }` instead of `{ versionId, agentSkillsDir }`.
6. Update group assign/remove calls to NOT send `agentSkillsDir` in the body (agentId is already in the URL).
7. Remove any `agentSkillsDir` parameter from all exported API functions. Replace with `agentId: string` where needed.
</action>
<verify>
<automated>cd nexus && pnpm tsc --noEmit --project ui/tsconfig.json 2>&1 | head -30</automated>
</verify>
<acceptance_criteria>
- AgentSkillEntry type exported with skillId, source, installedAt fields
- listAgentSkills returns AgentSkillEntry[] not string[]
- installSkill sends agentId in body, not agentSkillsDir
- uninstallSkill sends agentId as query param
- rollbackSkill sends agentId in body, not agentSkillsDir
- No references to agentSkillsDir in any API function
- TypeScript compiles without errors
</acceptance_criteria>
<done>API client uses agentId for all skill operations, returns typed AgentSkillEntry objects</done>
</task>
<task type="auto">
<name>Task 2: Dual-section Installed tab and read-only SkillCard</name>
<files>
ui/src/pages/SkillBrowser.tsx,
ui/src/components/SkillCard.tsx
</files>
<read_first>
ui/src/pages/SkillBrowser.tsx,
ui/src/components/SkillCard.tsx (if exists)
</read_first>
<action>
**SkillCard.tsx** (or inline skill card component):
1. Add `isReadOnly?: boolean` and `source?: "managed" | "native"` props to the component's props interface.
2. When `isReadOnly` is true:
- Hide the Remove/Uninstall button
- Hide the Update button
- Hide the Rollback button
- Show a small "Native" badge (e.g., a gray `<Badge>` from the UI library or a Tailwind-styled span)
- Rating/view actions remain visible
3. When `source === "managed"` and skill is installed:
- Show all action buttons as before (this is the default behavior, no change needed)
**SkillBrowser.tsx** — Installed tab changes:
1. **Remove agentSkillsDir state and input.** The install dialog currently has a text input for `agentSkillsDir`. Remove it entirely. The dialog already shows agent selection buttons with `agent.id` — use that directly.
2. **Update install dialog** to pass `agentId` to the API call instead of `agentSkillsDir`.
3. **Update uninstall handler** to pass `agentId` to the API call instead of `agentSkillsDir`.
4. **Per-agent skill query on Installed tab:**
```typescript
const { data: agentInstalledSkills = [] } = useQuery({
queryKey: ["agentInstalledSkills", selectedAgentId],
queryFn: () => skillRegistryApi.listAgentSkills(selectedAgentId),
enabled: tab === "installed" && !!selectedAgentId,
});
```
5. **Split skills into managed/native sections:**
```typescript
const managedSkills = agentInstalledSkills.filter((s) => s.source === "managed");
const nativeSkills = agentInstalledSkills.filter((s) => s.source === "native");
```
6. **Render two labelled sections** when viewing a Hermes agent's installed skills:
- "Managed" section heading — renders `managedSkills` with full SkillCard actions
- "Native" section heading — renders `nativeSkills` with `isReadOnly={true}` and `source="native"`
- Use simple heading elements or dividers:
```tsx
{managedSkills.length > 0 && (
<>
<h3 className="text-sm font-medium text-muted-foreground mt-4 mb-2">Managed</h3>
{managedSkills.map(skill => <SkillCard ... />)}
</>
)}
{nativeSkills.length > 0 && (
<>
<h3 className="text-sm font-medium text-muted-foreground mt-4 mb-2">Native</h3>
{nativeSkills.map(skill => <SkillCard ... isReadOnly={true} source="native" />)}
</>
)}
```
- For non-Hermes agents (where all skills are managed), render a single list without section headings — the experience is unchanged.
7. **Conditional sections:** Only show the Managed/Native split when `nativeSkills.length > 0`. For non-Hermes agents this will always be 0, so no UI change for them.
</action>
<verify>
<automated>cd nexus && pnpm tsc --noEmit --project ui/tsconfig.json 2>&1 | head -30</automated>
</verify>
<acceptance_criteria>
- SkillCard accepts isReadOnly and source props
- isReadOnly=true hides remove/update/rollback buttons
- source="native" shows Native badge
- SkillBrowser Installed tab splits into Managed/Native sections when native skills exist
- Install dialog sends agentId not agentSkillsDir
- No agentSkillsDir text input in the dialog
- Uninstall handler sends agentId as query param
- Non-Hermes agents see unchanged single-list UI
- TypeScript compiles without errors
</acceptance_criteria>
<done>Hermes agents show Managed/Native split in Installed tab, native skills are read-only, all API calls use agentId</done>
</task>
</tasks>
<verification>
- TypeScript compiles: `cd nexus && pnpm tsc --noEmit --project ui/tsconfig.json`
- No agentSkillsDir references: grep for `agentSkillsDir` in ui/src/ should return 0 matches
- isReadOnly prop exists: grep for `isReadOnly` in SkillCard component
- Managed/Native split: grep for `managedSkills` and `nativeSkills` in SkillBrowser
</verification>
<success_criteria>
- Hermes agents show two labelled sections: Managed (full actions) and Native (read-only + badge)
- Non-Hermes agents see unchanged UI (no section headings)
- All API calls use agentId instead of agentSkillsDir
- Install dialog no longer has a text input for skill directory path
- TypeScript compiles clean
</success_criteria>
<output>
After completion, create `.planning/phases/19-adapter-aware-install-uninstall/19-03-SUMMARY.md`
</output>