---
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>
