nexus/.planning/phases/28-ollama-integration/28-02-PLAN.md
Nexus Dev 51eb2edf0b chore: complete v1.5 Smart Onboarding + Personal AI Assistant milestone
6 phases, 13 plans, 21 requirements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 23:03:46 +00:00

198 lines
11 KiB
Markdown

---
phase: 28-ollama-integration
plan: 02
type: execute
wave: 2
depends_on: [28-01]
files_modified:
- ui/src/api/ollama.ts
- ui/src/adapters/hermes-local/config-fields.tsx
- ui/src/pages/AgentDetail.tsx
autonomous: true
requirements: [OLLA-02, OLLA-03, OLLA-05, HERM-05]
must_haves:
truths:
- "When Ollama is installed, the Hermes agent config shows a model dropdown listing all locally pulled models"
- "When an Ollama model is selected, adapterConfig saves model + provider:custom + base_url:http://localhost:11434/v1"
- "When Ollama is absent, the config shows an install callout with a link to https://ollama.com/download"
- "Recommended models are visually highlighted in the dropdown"
- "Hermes native skills show an 'Hermes skill' badge in the Skills tab"
- "Manual model entry still works as fallback when Ollama is absent"
artifacts:
- path: "ui/src/api/ollama.ts"
provides: "API client for Ollama status and model listing"
exports: ["ollamaApi"]
- path: "ui/src/adapters/hermes-local/config-fields.tsx"
provides: "Ollama model dropdown, install callout, manual fallback"
contains: "ollamaApi"
- path: "ui/src/pages/AgentDetail.tsx"
provides: "Hermes skill badge in AgentSkillsTab"
contains: "Hermes skill"
key_links:
- from: "ui/src/adapters/hermes-local/config-fields.tsx"
to: "ui/src/api/ollama.ts"
via: "useQuery + ollamaApi.status / ollamaApi.models"
pattern: "ollamaApi"
- from: "ui/src/adapters/hermes-local/config-fields.tsx"
to: "server/src/routes/ollama.ts"
via: "fetch /companies/:companyId/ollama/*"
pattern: "ollama"
---
<objective>
Create the UI surface for Ollama model selection in Hermes agent config and improve Hermes skill visibility.
Purpose: Users can discover, browse, and select local Ollama models when configuring a Hermes agent, with hardware-aware recommendations highlighted. When Ollama is absent, users see install instructions. Hermes native skills are clearly labeled in the Skills tab.
Output: Working model selector dropdown, install callout, and Hermes skill badges.
</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/28-ollama-integration/28-RESEARCH.md
@.planning/phases/28-ollama-integration/28-01-SUMMARY.md
@ui/src/adapters/hermes-local/config-fields.tsx
@ui/src/adapters/types.ts
@ui/src/api/client.ts
@ui/src/pages/AgentDetail.tsx (AgentSkillsTab around line 2362, unmanagedSkillRows around 2566)
<interfaces>
<!-- From Plan 01 server routes — endpoints the UI will call -->
GET /api/companies/:companyId/ollama/status
Response: { installed: boolean; version: string | null; installUrl: string }
GET /api/companies/:companyId/ollama/models
Response: { models: OllamaModel[]; ramGb: number }
Where OllamaModel = {
name: string; // e.g. "qwen2.5-coder:32b"
parameterSize: string; // e.g. "32.8B"
quantization: string; // e.g. "Q4_K_M"
sizeBytes: number;
family: string; // e.g. "qwen2"
recommended: boolean;
recommendationReason: string | null;
}
<!-- From ui/src/adapters/types.ts -->
AdapterConfigFieldsProps: { mode, isCreate, adapterType, values, set, config, eff, mark, models, hideInstructionsFile }
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create ollamaApi client and enhance HermesLocalConfigFields with model dropdown</name>
<files>ui/src/api/ollama.ts, ui/src/adapters/hermes-local/config-fields.tsx</files>
<read_first>
- ui/src/api/client.ts (full file — for api.get pattern)
- ui/src/api/health.ts (for simple API client pattern)
- ui/src/adapters/hermes-local/config-fields.tsx (full file — current state)
- ui/src/adapters/types.ts (AdapterConfigFieldsProps interface)
- .planning/phases/28-ollama-integration/28-RESEARCH.md (Pattern 3, Pattern 4, Pitfall 1, Pitfall 4)
</read_first>
<action>
1. Create `ui/src/api/ollama.ts`:
- Import `api` from "./client" (the request helper)
- Define types: `OllamaStatus { installed: boolean; version: string | null; installUrl: string }`, `OllamaModel { name: string; parameterSize: string; quantization: string; sizeBytes: number; family: string; recommended: boolean; recommendationReason: string | null }`, `OllamaModelsResponse { models: OllamaModel[]; ramGb: number }`
- Export `ollamaApi` object with:
- `status(companyId: string): Promise<OllamaStatus>` — GET `/companies/${companyId}/ollama/status`
- `models(companyId: string): Promise<OllamaModelsResponse>` — GET `/companies/${companyId}/ollama/models`
2. Rewrite `ui/src/adapters/hermes-local/config-fields.tsx`:
- Add imports: `useQuery` from `@tanstack/react-query`, `ollamaApi` from `../../api/ollama`
- Need companyId: extract from URL params using `useParams` from react-router-dom, or accept via a context. Check existing config-fields patterns for how companyId is obtained — look at the component's parent to find how it gets companyId. If not available via props, use `useParams<{ companyId?: string }>()` matching the route pattern `/companies/:companyId/agents/...`.
- Add two queries (only enabled when companyId is truthy):
```
const { data: ollamaStatus } = useQuery({ queryKey: ["ollama", "status", companyId], queryFn: () => ollamaApi.status(companyId!), enabled: Boolean(companyId), staleTime: 60_000 })
const { data: ollamaModels } = useQuery({ queryKey: ["ollama", "models", companyId], queryFn: () => ollamaApi.models(companyId!), enabled: Boolean(companyId && ollamaStatus?.installed), staleTime: 60_000 })
```
- Replace the current free-text Model `<DraftInput>` with a hybrid control:
- **When ollamaStatus?.installed AND ollamaModels?.models.length > 0**: Render a `<select>` dropdown with an empty option ("Select a model..."), then each ollamaModels.models item as an `<option>`. Recommended models get a star prefix in label (e.g., "* qwen2.5-coder:7b (7B, Q4_K_M) - Recommended for your system"). Non-recommended models show plain label (e.g., "qwen2.5-coder:32b (32.8B, Q4_K_M)"). Add a final option "Other (manual entry)..." that switches to a text input.
- **When ollamaStatus?.installed === false**: Render an install callout div: "Ollama is not detected." with a link to ollamaStatus.installUrl ("Install Ollama"). Below the callout, keep the existing DraftInput for manual model entry (user may have a remote provider).
- **When ollamaStatus is loading or undefined**: Show the existing DraftInput as fallback.
- CRITICAL (Pitfall 1 + Pitfall 4): When an Ollama model is selected from the dropdown, set ALL THREE fields atomically:
- Create mode: `set!({ model: selectedModel, provider: "custom", base_url: "http://localhost:11434/v1" })`
- Edit mode: `mark("adapterConfig", "model", selectedModel)`, `mark("adapterConfig", "provider", "custom")`, `mark("adapterConfig", "base_url", "http://localhost:11434/v1")`
- When "Other (manual entry)" is selected or when using the fallback text input, do NOT set provider/base_url — user manages those fields themselves.
- Style the select with the existing `inputClass` CSS classes for consistency.
- Style the install callout: use `rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm` with an external link icon or similar.
</action>
<verify>
<automated>cd /opt/nexus/ui && npx tsc --noEmit 2>&1 | head -20</automated>
</verify>
<acceptance_criteria>
- grep -q "ollamaApi" ui/src/api/ollama.ts
- grep -q "status" ui/src/api/ollama.ts
- grep -q "models" ui/src/api/ollama.ts
- grep -q "ollamaApi" ui/src/adapters/hermes-local/config-fields.tsx
- grep -q "useQuery" ui/src/adapters/hermes-local/config-fields.tsx
- grep -q 'provider.*custom' ui/src/adapters/hermes-local/config-fields.tsx
- grep -q 'base_url.*localhost:11434' ui/src/adapters/hermes-local/config-fields.tsx
- grep -q "ollama.com/download" ui/src/adapters/hermes-local/config-fields.tsx
- grep -q "recommended" ui/src/adapters/hermes-local/config-fields.tsx
</acceptance_criteria>
<done>Hermes config shows Ollama model dropdown when Ollama is detected, install callout when absent, and manual fallback. Selecting an Ollama model sets provider:custom + base_url atomically. TypeScript compiles without errors.</done>
</task>
<task type="auto">
<name>Task 2: Add Hermes skill badge rendering in AgentSkillsTab</name>
<files>ui/src/pages/AgentDetail.tsx</files>
<read_first>
- ui/src/pages/AgentDetail.tsx (lines 2550-2600 for unmanagedSkillRows, lines 2950-2970 for originLabel rendering, lines 3050-3070 for unmanagedSkillRows section header)
</read_first>
<action>
Update the `AgentSkillsTab` component in `AgentDetail.tsx` to better distinguish Hermes native skills:
1. In the unmanagedSkillRows section header (around line 3063), change the label text to be conditional:
- If `agent.adapterType === "hermes_local"`: display `(${unmanagedSkillRows.length}) Hermes native skills & user-installed skills`
- Otherwise: keep existing text `(${unmanagedSkillRows.length}) User-installed skills, not managed by ${VOCAB.appName}`
2. In the `renderSkillRow` function (around line 2960-2961), where `skill.readOnly && skill.originLabel` renders a `<p>` with originLabel text:
- When `skill.originLabel === "Hermes skill"`, render a small inline badge instead of plain text:
`<span className="inline-flex items-center rounded-full bg-purple-500/10 px-2 py-0.5 text-xs font-medium text-purple-400">Hermes skill</span>`
- Keep the existing `<p>` rendering for other originLabel values unchanged.
3. The `unmanagedSkillRows` data already flows from the skill registry API which includes Hermes native skills with `originLabel: "Hermes skill"` and `readOnly: true`. No data changes needed.
</action>
<verify>
<automated>cd /opt/nexus/ui && npx tsc --noEmit 2>&1 | head -20</automated>
</verify>
<acceptance_criteria>
- grep -q "Hermes skill" ui/src/pages/AgentDetail.tsx
- grep -q "hermes_local" ui/src/pages/AgentDetail.tsx
- grep -q "Hermes native" ui/src/pages/AgentDetail.tsx
- grep -q "purple" ui/src/pages/AgentDetail.tsx
</acceptance_criteria>
<done>Hermes native skills show a purple "Hermes skill" badge in the Skills tab. Section header indicates "Hermes native skills" when viewing a Hermes agent. TypeScript compiles without errors.</done>
</task>
</tasks>
<verification>
- `cd /opt/nexus/ui && npx tsc --noEmit` — no type errors
- Config-fields dropdown renders when Ollama is available (verified by code structure)
- Install callout renders when Ollama is absent (verified by code structure)
- Hermes skill badge uses distinct purple styling
</verification>
<success_criteria>
- ollamaApi client exists with status() and models() methods
- HermesLocalConfigFields shows model dropdown when Ollama detected, install callout when absent
- Selecting an Ollama model atomically sets model + provider:custom + base_url
- Manual model entry works as fallback
- Hermes native skills show "Hermes skill" badge in Skills tab
- All TypeScript compiles without errors
</success_criteria>
<output>
After completion, create `.planning/phases/28-ollama-integration/28-02-SUMMARY.md`
</output>