6 phases, 13 plans, 21 requirements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
198 lines
11 KiB
Markdown
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>
|