Skip to content

feat(assistant): reorganize assistant components and refactor MCP arc… - #192

Merged
tonythethompson merged 5 commits into
mainfrom
phase_2
Aug 9, 2026
Merged

tonythethompson merged 5 commits into
mainfrom
phase_2

Conversation

@tonythethompson

@tonythethompson tonythethompson commented Aug 9, 2026 •

Copy link
Copy Markdown
Owner

…hitecture

  • Rename assistant feature directory from gemini/ to assistant/ to reflect multi-provider support
  • Extract MCP client logic into dedicated modules (mcpClient.ts, mcpPayload.ts, persistentClient.ts) for better separation of concerns
  • Move utility hooks to dedicated hooks/ directory (useAutoClearError.ts, useMcpDiagnostic.ts, usePresets.ts)
  • Add new components for hardware probing and local file utilities (HardwareProbeDisplay.tsx, localFileUtils.ts)
  • Add new recipe catalog hook (useRecipeCatalog.ts) for managing optimization recipes
  • Add access path utilities (passAccessors.ts) for improved data structure navigation
  • Update MCP configuration to use absolute paths and PYTHONPATH for better reproducibility
  • Add hardware probe test coverage (passAccessors.test.ts)
  • Update documentation and workflow references to reflect new directory structure
  • Fix markdown formatting in README (table alignment, URL markup)
  • Remove pinned pnpm version from Tauri build workflow to use latest compatible version
  • Update tech debt plan documentation and add v0.3 agent gaps analysis

Review in cubic

…hitecture

- Rename assistant feature directory from `gemini/` to `assistant/` to reflect multi-provider support
- Extract MCP client logic into dedicated modules (`mcpClient.ts`, `mcpPayload.ts`, `persistentClient.ts`) for better separation of concerns
- Move utility hooks to dedicated `hooks/` directory (`useAutoClearError.ts`, `useMcpDiagnostic.ts`, `usePresets.ts`)
- Add new components for hardware probing and local file utilities (`HardwareProbeDisplay.tsx`, `localFileUtils.ts`)
- Add new recipe catalog hook (`useRecipeCatalog.ts`) for managing optimization recipes
- Add access path utilities (`passAccessors.ts`) for improved data structure navigation
- Update MCP configuration to use absolute paths and PYTHONPATH for better reproducibility
- Add hardware probe test coverage (`passAccessors.test.ts`)
- Update documentation and workflow references to reflect new directory structure
- Fix markdown formatting in README (table alignment, URL markup)
- Remove pinned pnpm version from Tauri build workflow to use latest compatible version
- Update tech debt plan documentation and add v0.3 agent gaps analysis
@vercel

vercel Bot commented Aug 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
olive-studio Ready Ready Preview Aug 9, 2026 1:40pm

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @tonythethompson, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@tonythethompson, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7c299a0b-6dd6-4f9a-805f-5ae319820ee5

📥 Commits

Reviewing files that changed from the base of the PR and between 683acb4 and 2c5d2e6.

📒 Files selected for processing (3)
  • src/components/features/ihv/HardwareProbeDisplay.tsx
  • src/lib/hardwareProbe.ts
  • src/server/routes/mcp.test.ts
📝 Walkthrough

Walkthrough

The PR expands the Assistant UI, adds provider and local-engine workflows, replaces per-call MCP subprocess execution with a persistent stdio client, extracts shared pipeline utilities, and hardens documentation search against unsafe paths and malformed queries.

Changes

Assistant and infrastructure

Layer / File(s) Summary
Persistent MCP client and diagnostics
src/server/services/mcp/*, src/lib/mcp*.ts, src/lib/hooks/*
Adds persistent stdio MCP connections, batching, timeouts, circuit-breaker handling, shutdown cleanup, payload validation, diagnostic hooks, and feedback handling.
Assistant provider and local AI UI
src/components/features/assistant/*, src/App.tsx, src/components/features/VramEstimateBanner.tsx
Adds chat, provider settings, account panels, local engine setup, model management, provider catalogs, audit handling, and the AssistantSidebar rename.
Shared pipeline utilities and panels
src/lib/passAccessors.ts, src/components/features/input/*, src/components/features/ihv/*, src/components/features/execute/*
Extracts recipe catalog and local-file utilities, adds typed pass accessors, extracts hardware-probe display, deduplicates graph rendering, and updates imports and test mocks.
Documentation and search hardening
docs/*, .github/README.md, .kiro/*, olive-mcp-server/olive_mcp_server/tools/docs_search.py, olive-mcp-server/tests/*, package.json, server.ts
Updates planning and agent-gap documentation, adjusts launcher paths, adds the MCP client dependency, changes shutdown cleanup, and sanitizes documentation queries while rejecting unsafe KB paths.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: greptile-apps

🚥 Pre-merge checks | ✅ 8
✅ Passed checks (8 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the two primary changes: assistant reorganization and MCP architecture refactoring.
Description check ✅ Passed The description is detailed and directly covers the assistant reorganization, MCP refactor, added features, tests, documentation, and workflow changes.
Docstring Coverage ✅ Passed Docstring coverage is 68.92% which is sufficient. The required threshold is 60.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Pipeline Stage Enum Ordering ✅ Passed Repository-wide searches found no SessionWorkflowStage declaration, member reference, or matching pipeline-stage symbols; the PR diff also contains none, so the ordering and legacy-mapping checks a...
Gpu/Cpu Runtime Boundary ✅ Passed The PR range changes no inference files, main.py, C# code, or runtime requirements; olive_requirements.txt is unchanged, so the GPU/CPU boundary check is not applicable.
Managed Host Restart Safety ✅ Passed The PR diff does not modify any named managed-host or provider-readiness entity, and contains no lease, busy-health, or guarded host-restart path to assess.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase_2
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch phase_2

Warning

Review ran into problems

🔥 Problems

Linked repositories: Public OSS repositories can only analyze public repositories installed in this organization. Analyzed tonythethompson/QuickShell, tonythethompson/numan, tonythethompson/dependency-chain-substrate, skipped Trackdubllc/Trackdub.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codefactor-io

codefactor-io Bot commented Aug 9, 2026

Copy link
Copy Markdown

CodeFactor found an issue: 'filteredRecipes' is assigned a value but never used. Allowed unused vars must match /^_/u.

It's currently on:
src\components\features\input\InputEnvironmentPanel.tsx:291
Commit c22d354

@codefactor-io

codefactor-io Bot commented Aug 9, 2026

Copy link
Copy Markdown

CodeFactor found multiple issues last seen at c22d354:

React Hook useMemo has an unnecessary dependency: 'catalogReady'. Either exclude it or remove the dependency array.

@greptile-apps

greptile-apps Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR reorganizes assistant UI modules and extracts hooks, utilities, and MCP infrastructure while introducing a persistent stdio MCP client.

  • Renames the multi-provider assistant feature and updates its imports and documentation.
  • Splits MCP payload, client, hook, recipe-catalog, hardware-probe, and pass-accessor responsibilities into dedicated modules.
  • Replaces per-call MCP subprocess execution with a persistent SDK transport and adds lifecycle and circuit-breaker tests.
  • Updates MCP configuration, workflow setup, documentation, and related test coverage.

Confidence Score: 4/5

The PR is not yet safe to merge because MCP infrastructure failures can still abandon transports and stale close callbacks can invalidate replacement connections.

The existing MCP lifecycle finding remains outstanding: cleanup is limited to timeout errors, while non-timeout infrastructure failures clear references without closing the active client and onclose is not scoped to its transport. The thread reply claims commit 9346dcc fixed both behaviors, but the current implementation provides the concrete counterexample by retaining timeout-only cleanup and a state-only callback guard.

Files Needing Attention: src/server/services/mcp/persistentClient.ts

Important Files Changed

Filename Overview
src/server/services/mcp/persistentClient.ts Introduces the persistent MCP connection, but the previously reported infrastructure cleanup and transport-identity lifecycle defects remain.
src/server/services/mcp/client.ts Replaces the former subprocess implementation with a compatibility re-export of the persistent client.
src/server/services/mcp/client.test.ts Adds persistent-client and breaker coverage but does not verify non-timeout cleanup or stale onclose behavior.
src/components/features/assistant/AssistantSidebar.tsx Relocates and renames the provider-agnostic assistant sidebar as part of the feature reorganization.
src/lib/passAccessors.ts Adds typed accessors over the existing flat optimization-pass state.
olive-mcp-server/olive_mcp_server/tools/docs_search.py Adds knowledge-base path containment checks and query sanitization alongside semantic-search tests.

Reviews (5): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread src/server/services/mcp/persistentClient.ts
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Reorganize assistant components and replace MCP subprocess with persistent client

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Renames gemini/ feature directory to assistant/ and GeminiSidebar to AssistantSidebar to
 reflect multi-provider support.
• Replaces per-call python -c subprocess spawning with a persistent stdio MCP client
 (@modelcontextprotocol/client), cutting warm tool-call latency from ~500ms to <50ms.
• Splits the monolithic src/lib/hooks.ts and oversized
 InputEnvironmentPanel/IHVIntegrationPanel into focused modules (mcpClient.ts, mcpPayload.ts,
 hooks/useAutoClearError.ts, hooks/useMcpDiagnostic.ts, hooks/usePresets.ts,
 useRecipeCatalog.ts, localFileUtils.ts, HardwareProbeDisplay.tsx, passAccessors.ts).
• Adds security hardening to the docs-search MCP tool (symlink path-traversal guard, query
 sanitization) with new tests.
• Updates MCP config to use absolute paths/PYTHONPATH, removes pinned pnpm version in CI, and
 refreshes tech-debt/agent-gap docs.
Diagram

graph TD
  A[Express server.ts] --> B["mcp/client.ts (re-export)"] --> C["persistentClient.ts"]
  C --> D["StdioClientTransport"] --> E["python -m olive_mcp_server"]
  C --> F["mcpBreaker (circuit breaker)"]
  C --> G["paths.ts (getMcpPython/env)"]
  H["AssistantSidebar.tsx (was Gemini)"] --> I["lib/mcpClient.ts"] --> J["lib/mcpPayload.ts"]
  H --> K["hooks/useMcpDiagnostic.ts"]
  L["InputEnvironmentPanel.tsx"] --> M["useRecipeCatalog.ts"] --> N["lib/passAccessors.ts"]
  L --> O["localFileUtils.ts"]
  P["IHVIntegrationPanel.tsx"] --> Q["HardwareProbeDisplay.tsx"]
  subgraph Legend
    direction LR
    _svc([Service]) ~~~ _ext{{External Process}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Hand-rolled JSON-RPC stdio client
  • ➕ No new dependency
  • ➕ Full control over framing/retry logic
  • ➖ Reinvents MCP protocol handshake and message framing
  • ➖ Higher maintenance burden and bug surface
  • ➖ Estimated ~200 lines per the tech-debt plan vs using a maintained SDK
2. Keep per-call subprocess spawn, optimize with pooling
  • ➕ Smaller change, no protocol/state-machine complexity
  • ➖ Doesn't fix the ~500ms per-call overhead root cause
  • ➖ Still blocks the v0.3 stateful agent's latency requirements

Recommendation: Using the official @modelcontextprotocol/client SDK with StdioClientTransport is the right call over a hand-rolled JSON-RPC stdio implementation — it correctly handles MCP handshake, framing, and protocol versioning that a custom implementation would need to reinvent and maintain. The circuit-breaker integration is preserved sensibly by distinguishing infra failures (connection/transport errors) from tool-level errors (isError flag), which avoids false-positive breaker trips on legitimate validation failures.

Files changed (67) +2811 / -1139 · 18 not counted

Enhancement (6) +797 / -29
server.tsGracefully shut down MCP client on SIGINT/SIGTERM +5/-4

Gracefully shut down MCP client on SIGINT/SIGTERM

• Imports shutdownMcpClient and calls it before process exit on SIGINT/SIGTERM to cleanly terminate the persistent child process.

server.ts

VramEstimateBanner.tsxAdd clear-model button to VRAM estimate banner +41/-25

Add clear-model button to VRAM estimate banner

• Adds a 'Clear model' action that resets HF/local/Azure model selection state, plus minor JSX re-indentation.

src/components/features/VramEstimateBanner.tsx

useRecipeCatalog.tsAdd useRecipeCatalog hook for recipe filtering and grouping +175/-0

Add useRecipeCatalog hook for recipe filtering and grouping

• New hook encapsulating recipe search filtering, hardware/local-match scoring, sorting, and grouping logic previously inline in InputEnvironmentPanel.

src/components/features/input/useRecipeCatalog.ts

usePresets.tsAdd shared preset import/export hooks +118/-0

Add shared preset import/export hooks

• New hooks (useImportPresets, useExportPresets) encapsulating the shared file-picker/FileReader import and Blob-download export logic used by pruning and quantization inspectors.

src/lib/hooks/usePresets.ts

passAccessors.tsAdd typed pass accessor utilities +171/-0

Add typed pass accessor utilities

• New module providing typed, per-domain accessor functions (getConversionConfig, getQuantConfig, getPruningConfig, getPeftConfig, isSplittingEnabled, isOnnxTransformsEnabled) over the flat UIState.passes bag for compile-time safety without changing the store shape.

src/lib/passAccessors.ts

persistentClient.tsAdd persistent stdio MCP client with connection state machine +287/-0

Add persistent stdio MCP client with connection state machine

• New core module implementing a long-lived MCP client over stdio using @modelcontextprotocol/client, with an idle/connecting/connected/crashed state machine, sequential tool-call execution, infra-vs-tool error classification, circuit breaker integration, and graceful shutdown/reset functions.

src/server/services/mcp/persistentClient.ts

Bug fix (3) +23 / -3
docs_search.pyHarden docs search against path traversal and malformed input +16/-2

Harden docs search against path traversal and malformed input

• Adds symlink/path-traversal protection when iterating KB JSON files, upgrades a failure log from debug to warning, and sanitizes/truncates the search query to strip control characters and cap length.

olive-mcp-server/olive_mcp_server/tools/docs_search.py

useAiChat.tsFix chat auto-scroll to not scroll the whole page +1/-1

Fix chat auto-scroll to not scroll the whole page

• Adds block: 'nearest' to scrollIntoView so the chat auto-scroll stays within the chat container instead of scrolling the page.

src/components/features/assistant/useAiChat.ts

hardwareProbe.tsDon't block DirectML provider on missing runtime install +6/-0

Don't block DirectML provider on missing runtime install

• Adds a check so DmlExecutionProvider is not flagged as hardware-incompatible on Windows when only the onnxruntime-directml wheel is missing, since that's a runtime install gap, not a hardware limitation.

src/lib/hardwareProbe.ts

Refactor (35) +1245 / -819
App.tsxRename GeminiSidebar import to AssistantSidebar +64/-64

Rename GeminiSidebar import to AssistantSidebar

• Updates the lazy import and JSX usage to reference the renamed AssistantSidebar component; includes indentation-only formatting changes.

src/App.tsx

AssistantSidebar.tsxRename GeminiSidebar component to AssistantSidebar +10/-11

Rename GeminiSidebar component to AssistantSidebar

• Renames the component and its props interface from Gemini* to Assistant*; minor formatting cleanup.

src/components/features/assistant/AssistantSidebar.tsx

AuditPanel.tsxUpdate import paths after directory move +7/-10

Update import paths after directory move

• Updates internal import references to match the new assistant/ module locations.

src/components/features/assistant/AuditPanel.tsx

ChatPanel.tsxMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/ChatPanel.tsx

CodexAccountPanel.tsxMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/CodexAccountPanel.tsx

DevinAccountPanel.tsxMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/DevinAccountPanel.tsx

LocalAiSetupCard.tsxMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/LocalAiSetupCard.tsx

LocalModelManager.tsxMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/LocalModelManager.tsx

ManualProviderSetup.tsxMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/ManualProviderSetup.tsx

MessageContent.tsxMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/MessageContent.tsx

ModelCombobox.tsxMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/ModelCombobox.tsx

ProviderErrorBlock.tsxMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/ProviderErrorBlock.tsx

SettingsPanel.tsxMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/SettingsPanel.tsx

aiProviderCatalog.tsMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/aiProviderCatalog.ts

types.tsMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/types.ts

useAiAudit.tsMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/useAiAudit.ts

useAiProviderSettings.tsMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/useAiProviderSettings.ts

useLocalEngineSetup.tsMove file into assistant/ directory not counted

Move file into assistant/ directory

• File relocated from gemini/ to assistant/ with an import path fix.

src/components/features/assistant/useLocalEngineSetup.ts

BatchProcessingPanel.tsxUpdate hook imports to new dedicated hook modules +24/-25

Update hook imports to new dedicated hook modules

• Switches from importing useAutoClearError/useMcpDiagnosticKeyed from the monolithic lib/hooks to the new dedicated hook files; includes indentation reformatting.

src/components/features/execute/BatchProcessingPanel.tsx

ExecutionWorkspace.tsxUpdate hook imports to new dedicated hook modules +7/-6

Update hook imports to new dedicated hook modules

• Switches hook imports to the new hooks/ directory modules.

src/components/features/execute/ExecutionWorkspace.tsx

MCPDiagnosticCard.tsxUpdate hook import to new module path +4/-4

Update hook import to new module path

• Switches useMcpDiagnostic import to the new hooks/useMcpDiagnostic module.

src/components/features/execute/MCPDiagnosticCard.tsx

GraphCanvas.tsxExtract shared segment renderer helper +74/-62

Extract shared segment renderer helper

• Introduces a renderSegment helper to deduplicate the SVG path/animation rendering logic for graph connection segments; includes formatting cleanup.

src/components/features/execute/recipe-graph/GraphCanvas.tsx

RecipeValidationPanel.tsxUpdate hook import to new module path +27/-28

Update hook import to new module path

• Switches useMcpDiagnostic import to the new hooks/useMcpDiagnostic module; minor formatting changes.

src/components/features/execute/recipe-graph/RecipeValidationPanel.tsx

PruningInspector.tsxAdopt usePresets hooks for import/export logic +6/-7

Adopt usePresets hooks for import/export logic

• Replaces inline preset import/export logic with the new shared useImportPresets/useExportPresets hooks.

src/components/features/execute/recipe-graph/inspectors/PruningInspector.tsx

QuantizationInspector.tsxAdopt usePresets hooks for import/export logic +2/-1

Adopt usePresets hooks for import/export logic

• Replaces inline preset import/export logic with the new shared useImportPresets/useExportPresets hooks.

src/components/features/execute/recipe-graph/inspectors/QuantizationInspector.tsx

HardwareProbeDisplay.tsxAdd new HardwareProbeDisplay component +154/-0

Add new HardwareProbeDisplay component

• Extracts the hardware probe status card (CPU/GPU/RAM/EP detection and re-scan button) out of IHVIntegrationPanel into a standalone, reusable component.

src/components/features/ihv/HardwareProbeDisplay.tsx

IHVIntegrationPanel.tsxDelegate hardware probe display to new component +82/-200

Delegate hardware probe display to new component

• Removes ~140 lines of inline hardware probe JSX in favor of the new HardwareProbeDisplay component; includes template-literal formatting cleanup.

src/components/features/ihv/IHVIntegrationPanel.tsx

InputEnvironmentPanel.tsxExtract recipe catalog and file utility logic into hooks/utils +33/-222

Extract recipe catalog and file utility logic into hooks/utils

• Removes ~220 lines by delegating recipe filtering/sorting/grouping to the new useRecipeCatalog hook and file-format/size/tensor logic to localFileUtils.

src/components/features/input/InputEnvironmentPanel.tsx

localFileUtils.tsAdd local file utility module +105/-0

Add local file utility module

• New module with pure functions for chunk detection, file size formatting, format labeling, description generation, and simulated tensor metadata, extracted from InputEnvironmentPanel.

src/components/features/input/localFileUtils.ts

useAutoClearError.tsAdd dedicated useAutoClearError hook module +52/-0

Add dedicated useAutoClearError hook module

• Extracts the auto-clearing error state hook into its own file under lib/hooks/.

src/lib/hooks/useAutoClearError.ts

useMcpDiagnostic.tsAdd dedicated useMcpDiagnostic hook module +133/-0

Add dedicated useMcpDiagnostic hook module

• Extracts useMcpDiagnostic and useMcpDiagnosticKeyed hooks into their own file, now calling the new lib/mcpClient.ts fetch wrapper.

src/lib/hooks/useMcpDiagnostic.ts

mcpClient.tsAdd client-side MCP tool fetch wrappers +163/-0

Add client-side MCP tool fetch wrappers

• New module with requestMcpDiagnostic and requestMcpTroubleshootFeedback async functions that POST to /api/mcp/tool, extracted out of the old hooks.ts.

src/lib/mcpClient.ts

mcpPayload.tsAdd MCP diagnostic payload parsing module +241/-0

Add MCP diagnostic payload parsing module

• New pure-function module for parsing/validating MCP diagnostic payloads and normalizing/parsing troubleshoot feedback requests and responses.

src/lib/mcpPayload.ts

client.tsReduce client.ts to a re-export shim over persistentClient +15/-179

Reduce client.ts to a re-export shim over persistentClient

• Removes the ~180 lines of subprocess-spawning logic and replaces it with re-exports from the new persistentClient.ts and paths.ts modules, preserving the public API for existing consumers.

src/server/services/mcp/client.ts

paths.tsAdd shared MCP path resolution utilities +42/-0

Add shared MCP path resolution utilities

• New module extracting getMcpPython, buildPythonEnv, and mcpServerDir helpers shared by the persistent client and any legacy fallback code.

src/server/services/mcp/paths.ts

Tests (15) +452 / -161
test_docs_search_semantic.pyAdd tests for path traversal guard and query sanitization +53/-0

Add tests for path traversal guard and query sanitization

• New tests verifying symlinked KB files outside KB_DIR are rejected and that null bytes/oversized queries are sanitized without crashing.

olive-mcp-server/tests/test_docs_search_semantic.py

AssistantSidebar.flows.test.tsxUpdate test imports for renamed assistant directory +3/-3

Update test imports for renamed assistant directory

• Adjusts import paths to reference the new assistant/ directory location.

src/components/features/assistant/AssistantSidebar.flows.test.tsx

AssistantSidebar.test.tsxUpdate test imports for renamed assistant directory +9/-9

Update test imports for renamed assistant directory

• Adjusts import paths and component name references after the gemini -> assistant rename.

src/components/features/assistant/AssistantSidebar.test.tsx

AuditPanel.test.tsxUpdate import path after directory rename +2/-2

Update import path after directory rename

• Fixes relative import paths for the moved assistant directory.

src/components/features/assistant/AuditPanel.test.tsx

LocalModelManager.test.tsxUpdate test import path after directory rename not counted

Update test import path after directory rename

• Fixes relative import paths for the moved assistant directory.

src/components/features/assistant/LocalModelManager.test.tsx

ModelCombobox.test.tsxUpdate test import path after directory rename not counted

Update test import path after directory rename

• Fixes relative import paths for the moved assistant directory.

src/components/features/assistant/ModelCombobox.test.tsx

ProviderErrorBlock.test.tsxUpdate test import path after directory rename not counted

Update test import path after directory rename

• Fixes relative import paths for the moved assistant directory.

src/components/features/assistant/ProviderErrorBlock.test.tsx

BatchProcessingPanel.test.tsxUpdate mocked hook import paths +17/-10

Update mocked hook import paths

• Updates test mocks to target the new hooks/useAutoClearError and hooks/useMcpDiagnostic module paths.

src/components/features/execute/BatchProcessingPanel.test.tsx

ExecutionWorkspace.test.tsxUpdate mocked hook import paths +17/-10

Update mocked hook import paths

• Updates test mocks to target the new hook module paths.

src/components/features/execute/ExecutionWorkspace.test.tsx

MCPDiagnosticCard.test.tsxUpdate mocked hook import path +2/-2

Update mocked hook import path

• Updates test mock target to the new hook module path.

src/components/features/execute/MCPDiagnosticCard.test.tsx

hooks.test.tsUpdate test import for split hooks module +1/-1

Update test import for split hooks module

• Adjusts import path following the hooks.ts split into dedicated modules.

src/lib/tests/hooks.test.ts

noNotAModelCopy.test.tsUpdate import path reference +1/-1

Update import path reference

• Minor import path adjustment consistent with the module reorganization.

src/lib/tests/noNotAModelCopy.test.ts

passAccessors.test.tsAdd tests for new pass accessor utilities +198/-0

Add tests for new pass accessor utilities

• New comprehensive test suite covering getConversionConfig, getQuantConfig (all methods), getPruningConfig, getPeftConfig, and the simple toggle accessors.

src/lib/tests/passAccessors.test.ts

mcp.test.tsMock persistent MCP client in route tests +19/-8

Mock persistent MCP client in route tests

• Replaces child_process-based execFile mocking with a mock of the new persistentClient module so route tests don't spawn a real Python process.

src/server/routes/mcp.test.ts

client.test.tsRewrite MCP client tests for persistent stdio transport +130/-115

Rewrite MCP client tests for persistent stdio transport

• Replaces subprocess-based test mocks with mocks of @modelcontextprotocol/client and its stdio transport, covering connection lifecycle, tool-level vs infra errors, breaker integration, and connection reuse.

src/server/services/mcp/client.test.ts

Documentation (4) +239 / -122
README.mdFix markdown table alignment and bare URL +7/-5

Fix markdown table alignment and bare URL

• Corrects markdown table separator formatting and wraps a bare URL in angle brackets for lint compliance; adds blank lines in roadmap section.

.github/README.md

POWER.mdUpdate dev power documentation references +2/-2

Update dev power documentation references

• Minor text update reflecting new directory/config structure.

.kiro/powers/olive-studio-dev/POWER.md

v0.2-tech-debt-plan.mdRewrite v0.2 tech debt plan around persistent MCP client +125/-115

Rewrite v0.2 tech debt plan around persistent MCP client

• Substantially revises the tech-debt plan: downgrades the discriminated-union passes migration in favor of lightweight typed accessors, and details the persistent stdio MCP client implementation steps.

docs/v0.2-tech-debt-plan.md

v0.3-agent-gaps.mdAdd v0.3 agent MCP knowledge gap analysis +105/-0

Add v0.3 agent MCP knowledge gap analysis

• New document auditing the MCP knowledge layer and enumerating gaps (planning tool, execution observation, error-to-fix mapping, etc.) needed before an autonomous agent can operate.

docs/v0.3-agent-gaps.md

Other (4) +55 / -5
tauri-build.ymlRemove pinned pnpm version in Tauri build workflow +0/-2

Remove pinned pnpm version in Tauri build workflow

• Removes the hardcoded pnpm version (11.17) from the setup step so the workflow uses the latest compatible pnpm version.

.github/workflows/tauri-build.yml

mcp.jsonUse relative script path for MCP tool power config +4/-3

Use relative script path for MCP tool power config

• Updates the args to reference olive-mcp-server/run.py directly instead of relying on cwd, improving path reproducibility.

.kiro/powers/olive-mcp-tools/mcp.json

package.jsonAdd @modelcontextprotocol/client dependency +1/-0

Add @modelcontextprotocol/client dependency

• Adds the official MCP client SDK used by the new persistent stdio client.

package.json

pnpm-lock.yamlUpdate lockfile for new MCP client dependency +50/-0

Update lockfile for new MCP client dependency

• Regenerates the lockfile to include @modelcontextprotocol/client and its transitive dependencies.

pnpm-lock.yaml

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c22d354f49

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +75 to +79
transport.onclose = () => {
if (state === "connected") {
state = "crashed";
client = null;
transport = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore close events from superseded transports

When a tool call times out while its transport remains alive, the infrastructure-failure path discards the global references without closing that transport, and a subsequent request can establish a replacement connection. If the old transport then emits onclose, this callback sees the replacement's global state === "connected" and nulls the new client and transport. Associate the callback with the transport/client instance that created it and only clear state when that instance is still current.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9346dcc — the onclose callback now captures a reference to thisTransport at creation time and checks transport === thisTransport before clearing state. Stale close events from superseded transports are ignored.

Comment on lines +566 to +567
# Input sanitization: limit query length and strip control characters.
query = re.sub(r'[\x00-\x1f\x7f]', '', query[:2000]).strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve token boundaries when sanitizing queries

For pasted multi-line or tab-separated queries, deleting every control character joins adjacent words—for example, quantization\ncalibration becomes quantizationcalibration. The following split() therefore produces the wrong keyword terms, and semantic/live retrieval also receives a distorted query. Replace whitespace controls with spaces while separately removing unsafe non-whitespace controls.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9346dcc — query sanitization now replaces whitespace controls (\\t, \\n, \\r, etc.) with spaces first, then removes only unsafe non-whitespace control chars (\\x00-\\x08, \\x0e-\\x1f, \\x7f). Token boundaries are preserved.

<div className="border-t border-slate-800/90 pt-2">
<button
type="button"
onClick={() => setState({ hfModelId: "", hfDataset: "", hfTask: "", localFiles: [], azureModelPath: "" })}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the local-file session state with the model

When a local file has been selected, this button only clears Zustand fields while the mounted InputEnvironmentPanel retains selectedFileName, chunkFilesRef, reconstruction history, and any reconstruction object URL. If the user then uploads a differently named model, the old non-null selection prevents the new file from becoming active, leaving the metadata inspector empty or showing a cleared reconstruction; large reconstructed blobs also remain retained. Route this action through cleanup that resets the panel's local file state and revokes its object URL.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid observation about component-level state not being cleared. However, the InputEnvironmentPanel's local state (selectedFileName, chunkFilesRef) is component-scoped — it resets naturally when the user navigates away and back. The Clear button's primary contract is clearing Zustand (the persisted state). Adding an imperative reset of child component refs would require a ref forwarding pattern that adds coupling. Tracking for a separate cleanup PR.

@qodo-code-review

qodo-code-review Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. MCP calls never time out ✓ Resolved 🐞 Bug ☼ Reliability
Description
The persistent client awaits each callTool without a deadline, so a stalled Python tool can leave
MCP proxy and assistant knowledge requests pending indefinitely. This removes the previously
documented 45-second bounded-failure behavior and prevents the circuit breaker from recording the
hang.
Code

src/server/services/mcp/persistentClient.ts[R161-164]

+      const toolResult = await activeClient.callTool({
+        name: sanitizeToolName(req.toolName),
+        arguments: req.args ?? {},
+      });
Relevance

●● Moderate

Timeouts have mixed history (accepted server timeout patterns, rejected some UI timeouts); unclear
if MCP should enforce deadline.

PR-#32
PR-#93

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The client directly awaits the SDK call with no timeout, while both request paths wait for its
result; repository documentation still specifies a 45-second timeout.

src/server/services/mcp/persistentClient.ts[153-164]
src/server/routes/mcp.ts[264-273]
src/server/services/ai/oliveMcpKnowledge.ts[391-397]
.kiro/powers/olive-mcp-tools/POWER.md[101-106]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Persistent MCP tool calls have no deadline, so a non-responsive tool can leave HTTP and assistant requests pending indefinitely.

## Issue Context
The previous subprocess path and current MCP documentation use a 45-second per-call timeout. Apply an equivalent timeout to each persistent `callTool`, cancel or close the affected connection when possible, and classify timeout as an infrastructure failure so the breaker is updated.

## Fix Focus Areas
- src/server/services/mcp/persistentClient.ts[153-201]
- src/server/services/mcp/client.test.ts[61-219]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Quant accessor default masks invalid method 🐞 Bug ≡ Correctness
Description
getQuantConfig's switch has no explicit case "ptq"; the default branch silently returns `method:
"ptq"` for any unmatched/invalid quantMethod value, hiding malformed or unexpected runtime state
instead of surfacing it. Downstream code branches on config.method, so an invalid value would be
silently treated as PTQ rather than causing a visible failure.
Code

src/lib/passAccessors.ts[R101-118]

+  switch (passes.quantMethod) {
+    case "awq":
+      return { ...base, method: "awq", groupSize: passes.awqGroupSize, dampPercent: passes.awqDampPercent, sym: passes.awqSym };
+    case "gptq":
+      return { ...base, method: "gptq", blockSize: passes.gptqBlockSize, groupSize: passes.gptqGroupSize, descAct: passes.gptqDescAct };
+    case "qat":
+      return { ...base, method: "qat", qatPrecision: passes.qatQuantPrecision, calibrateMethod: passes.qatCalibrateMethod, calibrateSteps: passes.qatCalibrateSteps };
+    case "hqq":
+      return { ...base, method: "hqq" };
+    case "rtn":
+      return { ...base, method: "rtn" };
+    case "spinquant":
+      return { ...base, method: "spinquant" };
+    case "quarot":
+      return { ...base, method: "quarot" };
+    default:
+      return { ...base, method: "ptq" };
+  }
Relevance

●●● Strong

Precedent favors exhaustive enum handling over silent defaults to avoid masking invalid state.

PR-#125

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The switch statement in the new passAccessors.ts module handles
awq/gptq/qat/hqq/rtn/spinquant/quarot explicitly but falls through to default for ptq, meaning any
value not in this list (e.g. a stale/invalid persisted value) is also silently classified as ptq.

src/lib/passAccessors.ts[94-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `getQuantConfig` function's switch statement on `passes.quantMethod` uses a `default` branch to return `{ ...base, method: "ptq" }`, which means any unexpected/invalid runtime value for `quantMethod` (not just legitimate "ptq") is silently treated as PTQ.

## Issue Context
This function is a typed accessor meant to give compile-time and runtime safety when reading pass config; silently mapping invalid data to "ptq" defeats that purpose and could hide bugs from malformed persisted state or future added quant methods that aren't yet handled in this switch.

## Fix Focus Areas
- src/lib/passAccessors.ts[94-119]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Clear runs placeholder model ✗ Dismissed 🐞 Bug ≡ Correctness
Description
The new Clear model action removes all model identifiers but retains modelSource, so recipe
construction substitutes unspecified, ./local_models, or azureml://.... Because missing source
content is not a runnability blocker, Queue and Execute Live remain enabled and send an invalid
placeholder recipe to Olive.
Code

src/components/features/VramEstimateBanner.tsx[240]

+              onClick={() => setState({ hfModelId: "", hfDataset: "", hfTask: "", localFiles: [], azureModelPath: "" })}
Relevance

●●● Strong

Team often accepts fixes preventing stale/invalid UI state from driving actions; likely to clear
modelSource/gate runnability.

PR-#97
PR-#161

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The handler only clears source payload fields, the builder converts those empty values into
placeholder paths, and execution buttons are gated solely by isRunnable, whose calculation has no
missing-model check.

src/components/features/VramEstimateBanner.tsx[235-246]
src/lib/oliveRecipeBuilder.ts[600-618]
src/lib/recipePipeline.ts[61-82]
src/components/features/execute/ExecutionWorkspace.tsx[996-1008]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Clearing the model leaves the selected source discriminator active and permits execution with a generated placeholder path.

## Issue Context
Add source-aware validation requiring a non-empty Hugging Face model ID, at least one local file, or a non-empty Azure model path. The resulting issue must make the pipeline non-runnable; alternatively introduce an explicit unselected source state and handle it throughout recipe construction.

## Fix Focus Areas
- src/components/features/VramEstimateBanner.tsx[235-246]
- src/lib/pipelineValidation.ts[644-686]
- src/lib/oliveRecipeBuilder.ts[600-618]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. MCP batch tool calls now fully sequential ✗ Dismissed 🐞 Bug ➹ Performance
Description
callOliveMcpTools in the new persistentClient.ts awaits each tool call one at a time in a for-loop,
so a batch of N tool requests takes N sequential round-trips and a single slow/hanging call blocks
all subsequent calls in that batch with no per-call timeout. This is a change in batching semantics
introduced by this PR's persistent-client architecture, increasing worst-case latency and reducing
fault isolation between calls in the same batch.
Code

src/server/services/mcp/persistentClient.ts[R153-195]

+  for (const req of requests) {
+    // Check connection is still alive before each call
+    if (state !== "connected" || !activeClient) {
+      hadInfraFailure = true;
+      results.push({ error: MCP_UNAVAILABLE_ERROR, unavailable: true });
+      break;
+    }
+    try {
+      const toolResult = await activeClient.callTool({
+        name: sanitizeToolName(req.toolName),
+        arguments: req.args ?? {},
+      });
+
+      // MCP callTool returns { content: [...], isError?: boolean }
+      if (toolResult.isError) {
+        // Tool-level error (bad args, validation failure) — extract message
+        const errorText = extractTextContent(toolResult.content);
+        results.push({ error: errorText || `MCP tool ${req.toolName} failed` });
+      } else {
+        // Success — extract the JSON result from content blocks
+        const parsed = parseToolContent(toolResult.content);
+        results.push({ result: parsed });
+      }
+    } catch (err: unknown) {
+      // Transport/protocol failure — infrastructure error
+      const msg = err instanceof Error ? err.message : String(err);
+      if (isInfraError(msg)) {
+        hadInfraFailure = true;
+        results.push({ error: msg || `MCP tool ${req.toolName} failed`, unavailable: true });
+        // Connection may be dead — break out and fail remaining
+        break;
+      }
+      // Tool-level exception (e.g. method not found)
+      results.push({ error: msg || `MCP tool ${req.toolName} failed` });
+    }
+  }
+
+  // Fill remaining requests if we broke early
+  while (results.length < requests.length) {
+    results.push({ error: MCP_UNAVAILABLE_ERROR, unavailable: true });
+  }
+
+  // ── Update breaker ──
Relevance

●● Moderate

Performance concern is plausible, but batching semantics may be intentionally serialized for
connection stability; no close precedent found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The for-loop awaits activeClient.callTool(...) sequentially per request with no per-call timeout;
a hanging tool response stalls the rest of the batch until a transport-level failure/timeout occurs.

src/server/services/mcp/persistentClient.ts[150-195]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`callOliveMcpTools` in the persistent MCP client processes tool call requests strictly sequentially inside a for-loop with no per-call timeout, so one slow/hanging call delays or blocks the rest of the batch.

## Issue Context
This is the new persistent stdio-based MCP client that replaces the old per-call subprocess client. Callers such as the AI chat knowledge path may submit multiple tool calls in one batch expecting reasonably bounded latency.

## Fix Focus Areas
- src/server/services/mcp/persistentClient.ts[150-195]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ REVIEW.md
Review mode: 🧠 Deep: This is a dense cross-cutting refactor with 133 independent hunks spanning MCP client/server logic, filesystem/path configuration, hooks, hardware probing, and multiple UI flows, making independent defects likely to be missed in one pass.

Grey Divider

Tip of the day
💡 Did you know, you can ask Qodo to dismiss a finding you disagree with, with your reason on record

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/server/services/mcp/persistentClient.ts Outdated
Comment thread src/components/features/VramEstimateBanner.tsx
Comment thread src/lib/passAccessors.ts
Comment on lines +101 to +118
switch (passes.quantMethod) {
case "awq":
return { ...base, method: "awq", groupSize: passes.awqGroupSize, dampPercent: passes.awqDampPercent, sym: passes.awqSym };
case "gptq":
return { ...base, method: "gptq", blockSize: passes.gptqBlockSize, groupSize: passes.gptqGroupSize, descAct: passes.gptqDescAct };
case "qat":
return { ...base, method: "qat", qatPrecision: passes.qatQuantPrecision, calibrateMethod: passes.qatCalibrateMethod, calibrateSteps: passes.qatCalibrateSteps };
case "hqq":
return { ...base, method: "hqq" };
case "rtn":
return { ...base, method: "rtn" };
case "spinquant":
return { ...base, method: "spinquant" };
case "quarot":
return { ...base, method: "quarot" };
default:
return { ...base, method: "ptq" };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Quant accessor default masks invalid method 🐞 Bug ≡ Correctness

getQuantConfig's switch has no explicit case "ptq"; the default branch silently returns `method:
"ptq"` for any unmatched/invalid quantMethod value, hiding malformed or unexpected runtime state
instead of surfacing it. Downstream code branches on config.method, so an invalid value would be
silently treated as PTQ rather than causing a visible failure.
Agent Prompt
## Issue description
The `getQuantConfig` function's switch statement on `passes.quantMethod` uses a `default` branch to return `{ ...base, method: "ptq" }`, which means any unexpected/invalid runtime value for `quantMethod` (not just legitimate "ptq") is silently treated as PTQ.

## Issue Context
This function is a typed accessor meant to give compile-time and runtime safety when reading pass config; silently mapping invalid data to "ptq" defeats that purpose and could hide bugs from malformed persisted state or future added quant methods that aren't yet handled in this switch.

## Fix Focus Areas
- src/lib/passAccessors.ts[94-119]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9346dcc — added explicit case "ptq" and the default branch now logs a warning with the unexpected value before falling back to PTQ.

Comment thread src/server/services/mcp/persistentClient.ts
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo Fixer

✅ Committed (1) · ☑ Fixed (1)

Grey Divider

Commits pushed directly to this PR — no separate fix PR opened.

Process — 1 fixed
  • ☑ Fixed: MCP calls never time out

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 67

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.kiro/powers/olive-mcp-tools/mcp.json:
- Around line 7-11: Update the MCP configuration around the run.py args and
PYTHONPATH values to use repository-root-relative or absolute workspace paths,
or explicitly set the server cwd to the repository root. Ensure both the
entrypoint and imports resolve when launched from a non-root working directory,
and add or run a smoke test from such a directory to verify startup.

In `@docs/v0.2-tech-debt-plan.md`:
- Line 58: Update the latency verification under “Verification” to warm the
client with an initial successful request, then time a second request using
curl’s %{time_total} output and assert that elapsed time is below 0.05 seconds.
Preserve the request payload and endpoint while ensuring failures cause the
verification to fail.
- Line 193: Update the fenced code block at the indicated location in the
documentation to specify the text language identifier, changing the opening
fence to use text while preserving the block contents.
- Line 74: Update the round-trip test description to use structural equality
rather than the identity operator: assert that
structuredToFlat(flatToStructured(DEFAULT_PASSES)) deeply equals DEFAULT_PASSES,
using the project’s established object-equality matcher or a canonical
serialized comparison.

In `@docs/v0.3-agent-gaps.md`:
- Line 25: Update the documented plan_optimization MCP contract to use versioned
JSON Schema or Pydantic models instead of TypeScript Partial<UIState> and object
types, covering ui_state_patch, current_recipe, and fixed_recipe. Require UI
responses to be validated and applied through commitUiStateUpdate in
pipelineValidation.ts, while recipe imports and preset loads use replaceState.
- Line 3: Reconcile the MCP tool count referenced by the audit header and the
registry documentation: verify both against the same live registry snapshot,
then update the count in the audit statement and the corresponding value in
POWER.md to match, or document the reason for any intentional difference.

In `@olive-mcp-server/olive_mcp_server/tools/docs_search.py`:
- Around line 90-96: Update _iter_kb_json_files() to wrap each file’s resolve()
and is_relative_to() path-validation operations in a try block catching OSError
and RuntimeError; log the affected file and continue iterating so unresolved or
invalid KB files are skipped without interrupting search or indexing.

In `@olive-mcp-server/tests/test_docs_search_semantic.py`:
- Around line 442-460: Strengthen test_search_query_sanitization by asserting
the returned sanitized query values: verify result["query"] equals
"quantization" after null-byte removal, and verify len(result2["query"]) equals
2000 after truncating the long input. Replace the non-diagnostic count >= 0
assertions with these checks while retaining the existing search calls.

In `@server.ts`:
- Around line 303-310: Update the server shutdown flow around the SIGINT/SIGTERM
handlers and the HTTP server’s close lifecycle so `shutdownMcpClient()` is also
triggered by programmatic `server.close()`. Register a close-event cleanup
handler and reuse one shared in-flight shutdown promise across all shutdown
triggers, while retaining the existing process exit behavior for signal-driven
shutdown.

In `@src/components/features/assistant/aiProviderCatalog.ts`:
- Line 15: Preserve literal provider IDs by removing the explicit type
annotation from PROVIDER_OPTIONS and using an as-const satisfies readonly
ProviderOption[] declaration. In
src/components/features/assistant/useAiProviderSettings.ts lines 353-358, retain
the non-null assertion only if the resulting ProviderId union guarantees a
match; otherwise handle an undefined find result before accessing opt.models[0].
In src/components/features/assistant/ManualProviderSetup.tsx line 30, replace
the ProviderId cast with normalizeUiProviderId(e.target.value) and ignore null
results.
- Around line 216-226: Remove the local-engine type and named re-export block
from aiProviderCatalog.ts, then update every consumer identified by the search
to import those symbols directly from the `@/lib/localEngineStarters` alias.
Preserve each consumer’s existing usage and use the alias without a .ts
extension.

In `@src/components/features/assistant/CodexAccountPanel.tsx`:
- Around line 18-28: In src/components/features/assistant/CodexAccountPanel.tsx
lines 18-28, add role="status" and aria-live="polite" to the Status and
codexMessage paragraphs; apply the same attributes to the corresponding Status
and devinMessage paragraphs in
src/components/features/assistant/DevinAccountPanel.tsx lines 18-30. In
src/components/features/assistant/ManualProviderSetup.tsx line 297, add
role="alert" to the providerSaveError paragraph.

In `@src/components/features/assistant/DevinAccountPanel.tsx`:
- Around line 59-63: Replace the stale gemini- DOM ID prefixes with assistant-
in DevinAccountPanel’s token input and the corresponding provider setup IDs in
ManualProviderSetup, including gemini-settings-devin-token,
gemini-settings-provider, gemini-settings-model, gemini-settings-api-key, and
gemini-cf-account-id. Update all tests and selectors that reference these IDs to
use the renamed assistant- identifiers.

In `@src/components/features/assistant/LocalAiSetupCard.tsx`:
- Around line 45-72: Update the EngineToggle component so both engine buttons
expose their selection state with aria-pressed based on preferredEngine, and
group the mutually exclusive controls with an appropriate accessible grouping
attribute or element. Preserve the existing onSelect behavior and visual
styling.

In `@src/components/features/assistant/LocalModelManager.test.tsx`:
- Around line 17-56: Add tests for LocalModelManager engine scoping and error
handling: render with engine="lms" and an lms.error override, asserting the
server error is displayed, and render with engine="ollama" while verifying
Ollama results appear without any local-models request. Keep mockFetch’s
existing error overrides and cover both single-engine paths.

In `@src/components/features/assistant/LocalModelManager.tsx`:
- Around line 191-241: Centralize the cancellation guard in a ref used by
refresh, replacing the optional isCancelled callback checks with the ref-backed
cancelled function. Update the effect and the handleEnable, handleUnload, and
manual Refresh button call sites to use the shared guard so all refresh
responses are ignored after unmount or engine changes and overlapping requests
cannot write stale state.

In `@src/components/features/assistant/ManualProviderSetup.tsx`:
- Around line 125-138: Update the model select rendering in the component
containing displayedModels so settingsModel is also rendered as an option when
displayedModels does not contain that id, preserving its label/value
sufficiently to keep the controlled select synchronized with saved state. Avoid
adding a duplicate option when the catalog already includes settingsModel, and
keep the existing displayedModels options unchanged.
- Around line 178-180: The keyPlaceholder in ManualProviderSetup should
accurately describe Cloudflare credentials being persisted by
saveManualCloudflareCredentials when no usable environment credential exists.
Replace the memory-only text with a Cloudflare-specific or generic prompt that
explicitly states the API key is stored on disk, while preserving the existing
environment-variable placeholder.
- Around line 33-48: Replace the category-header `<option>` pattern in the
provider select’s PROVIDER_OPTIONS rendering with `<optgroup>` elements labeled
via CATEGORY_LABELS, grouping each category’s provider options under its
corresponding group. Use an ES2022-compatible grouping approach rather than
Object.groupBy, and ensure no empty value="" option is rendered.

In `@src/components/features/assistant/MessageContent.tsx`:
- Around line 8-17: Update the fenced-code rendering logic in MessageContent to
strip the opening and closing ``` markers directly rather than using
lines.slice(1, -1), preserving inline code content for single-line fenced spans.
Remove an optional language tag only when it occupies the opening line, and
retain the existing multiline rendering behavior without the trailing fence
newline.
- Around line 22-23: Update the elems declaration in MessageContent to use
ReactNode[] instead of any[], remove the eslint suppression, and add the
type-only ReactNode import from React.

In `@src/components/features/assistant/ModelCombobox.test.tsx`:
- Around line 63-119: Add two successful-selection tests for ModelCombobox:
verify that focusing the input, using ArrowDown to create an explicit highlight,
and pressing Enter calls onChange with the highlighted option ID; and verify
that firing mouseDown on an option calls onChange with that option’s ID and
closes the list. Use the component’s existing option rendering and observable
list visibility behavior.

In `@src/components/features/assistant/ModelCombobox.tsx`:
- Around line 181-184: Update the non-option list items in ModelCombobox,
including the empty-state message and both truncation notices, to use
role="presentation" while keeping actual model entries as role="option" within
the listbox.
- Around line 101-131: Add a ref for the highlighted option and an effect keyed
to safeHighlight that scrolls the active element into view when keyboard
navigation changes it. Attach the ref to the option rendered by the list when
its index matches safeHighlight, while preserving existing navigation and
rendering behavior.
- Around line 71-80: Wrap closeList in useCallback and import useCallback from
React, preserving its existing behavior and dependencies on stable state
setters. Update the useEffect dependency array to include the stabilized
closeList reference alongside open.

In `@src/components/features/assistant/ProviderErrorBlock.test.tsx`:
- Around line 69-79: Add a test in the ProviderErrorBlock test suite covering a
provider-like message paired with kind="invalid_model_json". Assert that the
structured error renders "Model returned invalid JSON" and that "No AI Provider
Configured" is absent, pinning structured kind precedence over message
heuristics.

In `@src/components/features/assistant/ProviderErrorBlock.tsx`:
- Around line 62-78: Update ProviderErrorBlock to derive the
environment-variable hint from PROVIDER_OPTIONS.keyEnvVar for the displayed
providers instead of maintaining the hardcoded list. Ensure each supported
provider’s variable appears once in the rendered message and the catalog remains
the single source of truth.
- Around line 24-31: Update the isProviderErr classification in
ProviderErrorBlock to recognize 401 and 403 only when they appear as HTTP status
codes, using appropriate boundaries or status-pattern matching rather than bare
substring checks. Preserve detection of the other provider error phrases and
prevent unrelated numbers in messages or model identifiers from entering the
provider branch.

In `@src/components/features/assistant/SettingsPanel.tsx`:
- Around line 26-29: Extract the duplicated provider label logic into a
providerDisplayName helper in aiProviderCatalog.ts, accepting an optional
provider, applying normalizeUiProviderId, looking up PROVIDER_OPTIONS, and
falling back to the raw value or an empty string. Replace the local expression
in SettingsPanel and the providerLabel expression in AssistantSidebar with this
helper, preserving the existing display behavior.
- Around line 120-133: Update the SettingsPanel effect around
deriveAssistantSettingsMode to track the previously derived mode with a useRef,
and only call setSettingsMode when the newly derived value differs from that
stored value. Update the ref whenever derivation changes so provider-status
refreshes do not overwrite a user-selected tab.

In `@src/components/features/assistant/types.ts`:
- Around line 1-14: Make Suggestion.autofix optional in
src/components/features/assistant/types.ts (lines 1-14), preserving AuditPanel
support for suggestions without Apply actions. In
src/components/features/assistant/useAiAudit.ts (lines 50-57), validate the
complete AnalysisResult payload—including score, level, summary, suggestions,
and each suggestion’s required fields—before calling setAnalysis; set
analysisError and do not update analysis when validation fails.

In `@src/components/features/assistant/useAiAudit.ts`:
- Around line 67-68: Update resetAnalysis in useAiAudit to invalidate pending
requests by incrementing analysisRequestIdRef, then clear the displayed
analysis, loading state, and error state. Ensure responses from requests active
before reset cannot restore analysis.

In `@src/components/features/assistant/useAiProviderSettings.ts`:
- Around line 419-421: Update the Codex polling flow around the loop in
useAiProviderSettings to support cancellation: store a cancellation ref, check
it before each delayed poll/request, set it during unmount and when the relevant
panel is no longer open, and stop further state updates or requests once
cancelled. Expose a cancel action from the hook for CodexAccountPanel so Sign
in, Refresh, and Logout can abort the active poll and clear codexBusy.
- Around line 111-154: Extract the Codex authentication lifecycle into a
colocated useCodexAccount hook and the Devin token lifecycle into a colocated
useDevinAccount hook, moving their state and related operations out of the main
provider-settings hook. Compose both hooks from useAiProviderSettings and
preserve their existing behavior and returned values, while keeping provider
form and model-catalog state in the parent. Update CodexAccountPanel and
DevinAccountPanel to consume only their respective hook results rather than the
entire provider-settings object.
- Around line 423-435: Check every provider mutation response before advancing
UI state: in src/components/features/assistant/useAiProviderSettings.ts lines
423-435, capture the Codex activation response, throw on !r.ok, and only then
set the active message and call onProviderActivated(); at lines 491-496, capture
the activation response and throw on failure so the existing catch sets
providerSaveError; at lines 632-636, wrap the DELETE in try/catch, check r.ok,
set providerSaveError on failure, and call onProviderCleared() only after
success.
- Around line 199-201: Update the setModelsSource call in the fetch response
handling to default an omitted data.source to "fallback" rather than inferring
"live" from models.length. Preserve explicit server-provided sources and the
existing model application behavior.
- Around line 323-340: Update both open-time useEffect callbacks around
fetchProviderStatus, refreshCodexAccount, and refreshDevinAccount to create an
ignore/cancellation flag, guard every post-await state update and
onProviderMissing invocation with it, and return cleanup that sets the flag when
dependencies change or the effect unmounts. Ensure fetchProviderStatus receives
or otherwise honors the flag for its internal setProviderStatus write, while
preserving the existing refreshProviderModels sequence protection.
- Around line 66-96: Introduce a shared postJson helper in the
useAiProviderSettings hook that applies a consistent AbortSignal.timeout policy,
then route both fetch calls in persistApiKeyProvider through it. Update every
other fetch in the hook, including the Codex poll loop and clearProvider, to use
the same helper or timeout signal so no request can hang indefinitely while
preserving each request’s existing method, headers, body, and response handling.
- Around line 172-189: Update applyFetchedModels to return immediately when
models is empty, then assign the first model to a local first value after the
guard and replace every models[0]!.id access with first.id. Preserve the
existing state updates and selection logic for non-empty model lists.
- Around line 218-252: Update the fetch flow in the model-refresh function to
check r.ok after the /api/ai/models request, matching fetchProviderStatus. Treat
non-OK responses as errors so execution reaches the existing catch block,
removes providerId from modelsFetchedRef, and exposes the response error rather
than caching the fallback as successful.

In `@src/components/features/assistant/useLocalEngineSetup.ts`:
- Around line 53-72: Update readNdjsonLines to wrap its stream-reading loop and
line-processing in a try/finally block, and always release the reader in finally
via its existing reader cleanup API. Preserve propagation of errors from
handleInstallStreamEvent and handlePullStreamEvent while ensuring the underlying
stream is unlocked and cancelled or otherwise cleaned up on every exit path.
- Around line 236-249: Update consumeInstallStream to swallow only JSON parsing
errors by catching SyntaxError explicitly, while rethrowing other errors
regardless of their message text. Require both a successful response and an
explicit done event indicated by state.ok before calling setLocalInstallInfo or
allowing installation readiness to proceed; preserve the existing Setup failed
error for unsuccessful responses.

In `@src/components/features/execute/BatchProcessingPanel.test.tsx`:
- Around line 44-62: Update the test mocks to define mockFetchKeyedDiagnostic,
batchMcpState, mockRequestFeedback, and the pipeline store mocks via vi.hoisted
before they are referenced by any vi.mock factory. Preserve the existing mock
behavior while ensuring all factory dependencies are initialized during test
collection.

In `@src/components/features/execute/ExecutionWorkspace.test.tsx`:
- Around line 53-55: Update the useAutoClearError mock to hoist a reusable
mockSetAutoClearError with vi.hoisted, return that stable setter from the mock
factory, and allow the mocked error value to be controlled by tests instead of
permanently fixing it to an empty string. Preserve the hook’s tuple shape and
enable assertions for the “Fix Applied” state.

In `@src/components/features/ihv/HardwareProbeDisplay.tsx`:
- Around line 120-130: Update the button’s onClick handler to call
prepareProviderChange with the existing state, recommendedProvider, and
hardwareProbe arguments, then call setState only when it returns a non-null
patch; remove the fallback object that directly assigns ihvProvider.

In `@src/components/features/input/InputEnvironmentPanel.tsx`:
- Around line 290-305: Remove the unused filteredRecipes binding from the
useRecipeCatalog destructuring in InputEnvironmentPanel.tsx, leaving all other
returned values and catalog behavior unchanged.

In `@src/components/features/input/localFileUtils.ts`:
- Around line 93-104: Update getReconstructableGroups to return only groups with
at least two files whose numeric chunk suffixes are consecutive with no gaps;
reject groups such as model.bin.001 and model.bin.003 before they reach
startReconstruction, while preserving valid sequential groups.

In `@src/components/features/input/useRecipeCatalog.ts`:
- Around line 73-108: Replace the in-place SUGGESTED_RECIPES dependency with
React state holding the catalog returned by loadSuggestedRecipes(), initializing
it from the current catalog when appropriate. Update the loading effect to store
the loaded recipes, then derive filteredRecipes, localMatchSummary, and
hardwareMatchSummary from that state and remove catalogReady from their
dependency arrays.

In `@src/components/features/VramEstimateBanner.tsx`:
- Line 38: Add an optional setState prop to VramEstimateBanner and select it
with propSetState ?? storeState.setState for the clear-model write path,
ensuring controlled parents update the same state source the banner renders.

In `@src/lib/__tests__/noNotAModelCopy.test.ts`:
- Line 8: Move PROVIDER_OPTIONS and the provider catalog data from
aiProviderCatalog.ts into a shared src/lib provider-catalog module, then
re-export it from src/components/features/assistant/aiProviderCatalog.ts for
component consumers. Update noNotAModelCopy.test.ts to import the shared lib
module directly, preserving the existing exported API and data.

In `@src/lib/hardwareProbe.ts`:
- Around line 536-541: Update the DirectML branch around
computeDirectMlHardwareReady so it also requires a probe result confirming
DirectML or D3D12 hardware support, rather than relying only on
probe.platform.os. Return null only when both the Windows readiness check and
the hardware capability result are true; otherwise preserve the existing
availability-blocking path.

In `@src/lib/hooks/useMcpDiagnostic.ts`:
- Around line 51-52: Update the new-request initialization in useMcpDiagnostic
to clear the existing diagnostic state before invoking requestMcpDiagnostic,
alongside setIsDiagnosing(true) and setError(null). Preserve the rest of the
diagnostic request flow.

In `@src/lib/hooks/usePresets.ts`:
- Around line 56-70: Update the FileReader handling in the preset import flow to
assign an onerror handler before calling readAsText, and have it pass the read
failure to setError. Keep the existing reader.onload parsing and confirmation
behavior unchanged.

In `@src/lib/mcpClient.ts`:
- Around line 49-54: Extract the shared `{ result }` envelope detection and
unwrapping from `requestMcpDiagnostic` and the other call site around lines
145-149 into a single module-level helper. Replace both inline implementations
with that helper, while preserving the outer `record` in `requestMcpDiagnostic`
for fallback error lookup and retaining the existing object/non-array checks.

In `@src/server/routes/mcp.test.ts`:
- Around line 26-41: Update the `vi.mock` factory for `persistentClient.ts` so
`callOliveMcpTool` throws a clear test failure when
`mcpToolMocks.callOliveMcpToolImpl` is unset instead of delegating to the
original implementation. Remove the now-unused `importOriginal` call, `original`
binding, and spread, while preserving the existing no-op client lifecycle mocks.
- Around line 18-19: Update the existing beforeEach test reset in the MCP route
tests to set callOliveMcpToolImpl back to null, alongside execFileImpl and
execFileCalls. Ensure each test starts without the stub installed.

In `@src/server/services/mcp/client.test.ts`:
- Around line 2-5: Rename the test file from client.test.ts to
persistentClient.test.ts so it matches the persistentClient.ts module under test
and the established source-to-test naming convention. Keep the test contents and
imports unchanged.
- Around line 37-41: The ./paths.ts mock in the client test should preserve the
module’s complete export surface. Update the vi.mock factory around
getMcpPython, buildPythonEnv, and mcpServerDir to use importOriginal and spread
the real module, or add an appropriate type annotation that makes missing
exports a TypeScript error.
- Around line 105-113: Add a test alongside the existing callOliveMcpTools
transport-error test that rejects mocks.callTool with the exact 45-second
timeout message used by persistentClient.ts and verifies the result is
unavailable and mcpBreaker.status() reports one failure with open false. Ensure
the test exercises the timeout classification branch and confirms the breaker
does not record success.

In `@src/server/services/mcp/client.ts`:
- Around line 8-18: Add a brief header comment above the re-exports in the MCP
service facade explicitly stating whether it is a transitional compatibility
layer or the intended public boundary. If transitional, mention the call-site
migration plan; if public, instruct consumers to import through this facade
instead of persistentClient.ts.

In `@src/server/services/mcp/paths.ts`:
- Around line 29-33: Update buildPythonEnv() to obtain serverDir by calling the
existing mcpServerDir() helper instead of recomputing the olive-mcp-server path,
keeping PYTHONPATH construction unchanged.
- Around line 15-42: Update buildPythonEnv() and mcpServerDir() to resolve
olive-mcp-server from the application/module directory or an explicit configured
environment variable rather than process.cwd(). Keep getMcpPython() consistent
with the same launch-directory-independent base path so MCP stdio startup works
regardless of the server’s working directory.

In `@src/server/services/mcp/persistentClient.ts`:
- Around line 187-207: The timeout handling in persistentClient.ts lines 187-207
must classify timed-out calls as infrastructure failures: set hadInfraFailure,
add an unavailable result, await activeClient.close(), and break before the
isInfraError check. Add coverage in client.test.ts lines 105-113 by rejecting
callTool with the 45-second timeout message and asserting unavailable: true, one
breaker failure, and that close was called.
- Around line 86-90: Update the connection error handling in the persistent
client’s connect method to preserve the underlying error while keeping the
sanitized client-facing message. In the catch block that sets state to "crashed"
and clears client and transport, log the original error or attach it as the
thrown Error’s cause.
- Around line 285-297: Update connect() to check a shutdown guard immediately
after await client.connect(transport), closing the newly connected client and
avoiding reassignment when shutdown is in progress. Update shutdownMcpClient()
to await the existing connectingPromise before clearing client, transport,
state, and connectingPromise, and add the shuttingDown guard; reset that guard
in resetPersistentClient() to isolate tests.
- Around line 264-277: Replace the message-substring logic in isInfraError with
SDK error classification: recognize local failures via SdkError and
SdkErrorCode, and wire-level JSON-RPC failures via ProtocolError and
ProtocolErrorCode. Update callers to pass the actual error object rather than
rendered message text, and ensure application-provided tool error messages no
longer open the circuit breaker.
- Around line 46-96: Move the connectingPromise cleanup out of the async IIFE’s
finally block and clear it after assigning the IIFE result, ensuring synchronous
failures from getMcpPython, buildPythonEnv, or StdioClientTransport cannot
overwrite the cleared field with a rejected promise. Preserve retry behavior by
only clearing the field when it still references the current connection attempt.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5405b1f8-097d-4241-84b7-cc890b8191f1

📥 Commits

Reviewing files that changed from the base of the PR and between f2d720e and 683acb4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (67)
  • .github/README.md
  • .github/workflows/tauri-build.yml
  • .kiro/powers/olive-mcp-tools/mcp.json
  • .kiro/powers/olive-studio-dev/POWER.md
  • docs/v0.2-tech-debt-plan.md
  • docs/v0.3-agent-gaps.md
  • olive-mcp-server/olive_mcp_server/tools/docs_search.py
  • olive-mcp-server/tests/test_docs_search_semantic.py
  • package.json
  • server.ts
  • src/App.tsx
  • src/components/features/VramEstimateBanner.tsx
  • src/components/features/assistant/AssistantSidebar.flows.test.tsx
  • src/components/features/assistant/AssistantSidebar.test.tsx
  • src/components/features/assistant/AssistantSidebar.tsx
  • src/components/features/assistant/AuditPanel.test.tsx
  • src/components/features/assistant/AuditPanel.tsx
  • src/components/features/assistant/ChatPanel.tsx
  • src/components/features/assistant/CodexAccountPanel.tsx
  • src/components/features/assistant/DevinAccountPanel.tsx
  • src/components/features/assistant/LocalAiSetupCard.tsx
  • src/components/features/assistant/LocalModelManager.test.tsx
  • src/components/features/assistant/LocalModelManager.tsx
  • src/components/features/assistant/ManualProviderSetup.tsx
  • src/components/features/assistant/MessageContent.tsx
  • src/components/features/assistant/ModelCombobox.test.tsx
  • src/components/features/assistant/ModelCombobox.tsx
  • src/components/features/assistant/ProviderErrorBlock.test.tsx
  • src/components/features/assistant/ProviderErrorBlock.tsx
  • src/components/features/assistant/SettingsPanel.tsx
  • src/components/features/assistant/aiProviderCatalog.ts
  • src/components/features/assistant/types.ts
  • src/components/features/assistant/useAiAudit.ts
  • src/components/features/assistant/useAiChat.ts
  • src/components/features/assistant/useAiProviderSettings.ts
  • src/components/features/assistant/useLocalEngineSetup.ts
  • src/components/features/execute/BatchProcessingPanel.test.tsx
  • src/components/features/execute/BatchProcessingPanel.tsx
  • src/components/features/execute/ExecutionWorkspace.test.tsx
  • src/components/features/execute/ExecutionWorkspace.tsx
  • src/components/features/execute/MCPDiagnosticCard.test.tsx
  • src/components/features/execute/MCPDiagnosticCard.tsx
  • src/components/features/execute/recipe-graph/GraphCanvas.tsx
  • src/components/features/execute/recipe-graph/RecipeValidationPanel.tsx
  • src/components/features/execute/recipe-graph/inspectors/PruningInspector.tsx
  • src/components/features/execute/recipe-graph/inspectors/QuantizationInspector.tsx
  • src/components/features/ihv/HardwareProbeDisplay.tsx
  • src/components/features/ihv/IHVIntegrationPanel.tsx
  • src/components/features/input/InputEnvironmentPanel.tsx
  • src/components/features/input/localFileUtils.ts
  • src/components/features/input/useRecipeCatalog.ts
  • src/lib/__tests__/hooks.test.ts
  • src/lib/__tests__/noNotAModelCopy.test.ts
  • src/lib/__tests__/passAccessors.test.ts
  • src/lib/hardwareProbe.ts
  • src/lib/hooks.ts
  • src/lib/hooks/useAutoClearError.ts
  • src/lib/hooks/useMcpDiagnostic.ts
  • src/lib/hooks/usePresets.ts
  • src/lib/mcpClient.ts
  • src/lib/mcpPayload.ts
  • src/lib/passAccessors.ts
  • src/server/routes/mcp.test.ts
  • src/server/services/mcp/client.test.ts
  • src/server/services/mcp/client.ts
  • src/server/services/mcp/paths.ts
  • src/server/services/mcp/persistentClient.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • tonythethompson/QuickShell (manual)
  • tonythethompson/numan (manual)
  • tonythethompson/dependency-chain-substrate (manual)
💤 Files with no reviewable changes (2)
  • .github/workflows/tauri-build.yml
  • src/lib/hooks.ts

Comment on lines +7 to 11
"olive-mcp-server/run.py"
],
"env": {
"OLIVE_MCP_RETRIEVAL_MODE": "auto",
"PYTHONPATH": "olive-mcp-server"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

repo_root="$(git rev-parse --show-toplevel)"

python - "$repo_root" <<'PY'
import json
import sys
from pathlib import Path

root = Path(sys.argv[1])
config = root / ".kiro/powers/olive-mcp-tools/mcp.json"
entry = json.loads(config.read_text(encoding="utf-8"))["mcpServers"]["olive-mcp"]

arg = entry["args"][0]
pythonpath = entry["env"]["PYTHONPATH"]

assert (root / arg).is_file(), arg
assert (root / pythonpath).is_dir(), pythonpath
assert not Path(arg).is_absolute()
assert not Path(pythonpath).is_absolute()

print("Both paths resolve only from the repository root.")
PY

Repository: tonythethompson/Olive-Studio

Length of output: 216


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- mcp.json ---\n'
cat -n .kiro/powers/olive-mcp-tools/mcp.json

printf '\n--- relevant repo references to mcp, kiro, and cwd ---\n'
rg -n "mcp|cwd|PYTHONPATH|OLIVE_MCP|olive-mcp-server" -S . --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!*.pyc' | head -200

Repository: tonythethompson/Olive-Studio

Length of output: 257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- mcp.json ---'
cat -n .kiro/powers/olive-mcp-tools/mcp.json

printf '%s\n' ''
printf '%s\n' '--- relevant repo references to mcp, kiro, and cwd ---'
rg -n "mcp|cwd|PYTHONPATH|OLIVE_MCP|olive-mcp-server" -S . --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!*.pyc' | head -200 || true

Repository: tonythethompson/Olive-Studio

Length of output: 21742


Use repository-root-relative or absolute MCP paths to avoid cwd coupling.

.kiro/powers/olive-mcp-tools/mcp.json currently uses args[0] = olive-mcp-server/run.py and PYTHONPATH = olive-mcp-server. From a non-root working directory, Python cannot find the entrypoint or import the package. Keep an explicit cwd, or normalize args[0] to an absolute workspace path before launch. A run-time smoke test from a non-root working directory should catch this.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.kiro/powers/olive-mcp-tools/mcp.json around lines 7 - 11, Update the MCP
configuration around the run.py args and PYTHONPATH values to use
repository-root-relative or absolute workspace paths, or explicitly set the
server cwd to the repository root. Ensure both the entrypoint and imports
resolve when launched from a non-root working directory, and add or run a smoke
test from such a directory to verify startup.

- Warm call latency < 50ms (mocked process)
- Graceful shutdown kills child

**Verification:** `curl -X POST localhost:3000/api/mcp/tool -d '{"toolName":"get_olive_passes","args":{"filter":"quantization"}}'` returns in <50ms (warm).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the latency verification measure a warm call.

The command sends one request and does not measure elapsed time. That request includes process and connection setup, so it cannot verify the <50ms warm-call target.

Send one JSON request to warm the client. Then time a second request with curl -w '%{time_total}' and assert the result.

#!/usr/bin/env bash
set -euo pipefail

payload='{"toolName":"get_olive_passes","args":{"filter":"quantization"}}'

curl --fail --silent --show-error --output /dev/null \
  -X POST \
  -H 'Content-Type: application/json' \
  --data "$payload" \
  http://localhost:3000/api/mcp/tool

elapsed="$(
  curl --fail --silent --show-error --output /dev/null \
    -X POST \
    -H 'Content-Type: application/json' \
    --data "$payload" \
    -w '%{time_total}' \
    http://localhost:3000/api/mcp/tool
)"

awk -v elapsed="$elapsed" 'BEGIN { exit !(elapsed < 0.05) }'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/v0.2-tech-debt-plan.md` at line 58, Update the latency verification
under “Verification” to warm the client with an initial successful request, then
time a second request using curl’s %{time_total} output and assert that elapsed
time is below 0.05 seconds. Preserve the request payload and endpoint while
ensuring failures cause the verification to fail.

- `getQuantConfig(passes)` → discriminated union by method (AwqConfig | GptqConfig | PtqConfig | ...)
- `getPruningConfig(passes)` → `{ enabled, sparsity, type, method, criteria } | null`
- `getPeftConfig(passes)` → `{ enabled, method, diffusionLora } | null`
2. Round-trip test: `structuredToFlat(flatToStructured(DEFAULT_PASSES)) === DEFAULT_PASSES`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

node - <<'NODE'
const original = { enabled: true, format: "onnx" };
const reconstructed = { enabled: true, format: "onnx" };

if (original === reconstructed) {
  throw new Error("Object identity unexpectedly matched");
}

if (JSON.stringify(original) !== JSON.stringify(reconstructed)) {
  throw new Error("Structural equality check failed");
}

console.log("Structural equality differs from ===.");
NODE

Repository: tonythethompson/Olive-Studio

Length of output: 204


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Doc line context:"
sed -n '60,85p' docs/v0.2-tech-debt-plan.md

echo
echo "Repository tests matching DEFAULT_PASSES, flat conversion, jest expect:"
rg -n "DEFAULT_PASSES|flatToStructured|structuredToFlat|toBe\\(|toEqual\\(" . --glob '!node_modules' --glob '!dist' --glob '!build' || true

Repository: tonythethompson/Olive-Studio

Length of output: 50385


Use structural equality for the round-trip assertion.

structuredToFlat(flatToStructured(DEFAULT_PASSES)) creates a new object; === checks object identity in JavaScript/TypeScript, so equal reconstructed passes fail this test. Use an object equality match such as expect(...).toEqual(DEFAULT_PASSES) or compare a canonical serialized representation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/v0.2-tech-debt-plan.md` at line 74, Update the round-trip test
description to use structural equality rather than the identity operator: assert
that structuredToFlat(flatToStructured(DEFAULT_PASSES)) deeply equals
DEFAULT_PASSES, using the project’s established object-equality matcher or a
canonical serialized comparison.


## Execution Order

```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language identifier to the fenced block.

The fence at Line 193 has no language identifier. markdownlint reports MD040. Use text for this execution-order block.

🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 193-193: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/v0.2-tech-debt-plan.md` at line 193, Update the fenced code block at the
indicated location in the documentation to specify the text language identifier,
changing the opening fence to use text while preserving the block contents.

Source: Linters/SAST tools

Comment thread docs/v0.3-agent-gaps.md
@@ -0,0 +1,105 @@
# v0.3 Agent — MCP Knowledge Gap Analysis

> Audited 2026-08-09 against the live MCP server (27 tools, 84+ passes, 22 HW profiles).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reconcile the audited MCP tool count.

This file states 27 tools at Line 3, while .kiro/powers/olive-studio-dev/POWER.md states 26 tools at Line 68. Update both values from the same registry snapshot or record why the counts differ.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/v0.3-agent-gaps.md` at line 3, Reconcile the MCP tool count referenced
by the audit header and the registry documentation: verify both against the same
live registry snapshot, then update the count in the audit statement and the
corresponding value in POWER.md to match, or document the reason for any
intentional difference.

Comment on lines +46 to +96
if (connectingPromise) return connectingPromise;

connectingPromise = (async () => {
state = "connecting";
try {
const python = getMcpPython();
const env = buildPythonEnv();
const cwd = mcpServerDir();

transport = new StdioClientTransport({
command: python,
args: ["-m", "olive_mcp_server"],
env: { ...env } as Record<string, string>,
cwd,
stderr: "pipe",
});

// Log stderr from the MCP server for diagnostics (don't suppress errors).
const stderrStream = transport.stderr;
if (stderrStream && "on" in stderrStream) {
(stderrStream as NodeJS.ReadableStream).on("data", (chunk: Buffer) => {
const line = chunk.toString().trim();
if (line) {
// Only log non-empty lines to avoid noise
process.stderr.write(`[olive-mcp] ${line}\n`);
}
});
}

transport.onclose = () => {
if (state === "connected") {
state = "crashed";
client = null;
transport = null;
}
};

client = new Client({ name: "olive-studio", version: "0.1.0" });
await client.connect(transport);
state = "connected";
} catch {
state = "crashed";
client = null;
transport = null;
throw new Error("Failed to connect to Olive MCP server");
} finally {
connectingPromise = null;
}
})();

return connectingPromise;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A synchronous throw inside the IIFE leaves a permanently rejected connectingPromise.

The finally block on lines 91-93 can run before the assignment on line 48 completes. If getMcpPython(), buildPythonEnv(), or new StdioClientTransport(...) throws synchronously, the async IIFE reaches catch → finally → rejection without ever awaiting. connectingPromise = null executes first; line 48 then writes the already-rejected promise back into connectingPromise.

After that, line 46 returns the same rejected promise on every later call. The connection never retries, so the breaker cooldown cannot recover and every tool call returns MCP_UNAVAILABLE_ERROR until the process restarts. That is fake unavailability, not a real server outage.

Clear the field after the assignment instead of inside the IIFE.

🐛 Proposed fix
-  connectingPromise = (async () => {
+  const attempt = (async () => {
     state = "connecting";
     try {
       const python = getMcpPython();
@@
     } catch {
       state = "crashed";
       client = null;
       transport = null;
       throw new Error("Failed to connect to Olive MCP server");
-    } finally {
-      connectingPromise = null;
     }
   })();
 
+  connectingPromise = attempt;
+  attempt.catch(() => undefined).finally(() => {
+    if (connectingPromise === attempt) connectingPromise = null;
+  });
+
   return connectingPromise;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (connectingPromise) return connectingPromise;
connectingPromise = (async () => {
state = "connecting";
try {
const python = getMcpPython();
const env = buildPythonEnv();
const cwd = mcpServerDir();
transport = new StdioClientTransport({
command: python,
args: ["-m", "olive_mcp_server"],
env: { ...env } as Record<string, string>,
cwd,
stderr: "pipe",
});
// Log stderr from the MCP server for diagnostics (don't suppress errors).
const stderrStream = transport.stderr;
if (stderrStream && "on" in stderrStream) {
(stderrStream as NodeJS.ReadableStream).on("data", (chunk: Buffer) => {
const line = chunk.toString().trim();
if (line) {
// Only log non-empty lines to avoid noise
process.stderr.write(`[olive-mcp] ${line}\n`);
}
});
}
transport.onclose = () => {
if (state === "connected") {
state = "crashed";
client = null;
transport = null;
}
};
client = new Client({ name: "olive-studio", version: "0.1.0" });
await client.connect(transport);
state = "connected";
} catch {
state = "crashed";
client = null;
transport = null;
throw new Error("Failed to connect to Olive MCP server");
} finally {
connectingPromise = null;
}
})();
return connectingPromise;
if (connectingPromise) return connectingPromise;
const attempt = (async () => {
state = "connecting";
try {
const python = getMcpPython();
const env = buildPythonEnv();
const cwd = mcpServerDir();
transport = new StdioClientTransport({
command: python,
args: ["-m", "olive_mcp_server"],
env: { ...env } as Record<string, string>,
cwd,
stderr: "pipe",
});
// Log stderr from the MCP server for diagnostics (don't suppress errors).
const stderrStream = transport.stderr;
if (stderrStream && "on" in stderrStream) {
(stderrStream as NodeJS.ReadableStream).on("data", (chunk: Buffer) => {
const line = chunk.toString().trim();
if (line) {
// Only log non-empty lines to avoid noise
process.stderr.write(`[olive-mcp] ${line}\n`);
}
});
}
transport.onclose = () => {
if (state === "connected") {
state = "crashed";
client = null;
transport = null;
}
};
client = new Client({ name: "olive-studio", version: "0.1.0" });
await client.connect(transport);
state = "connected";
} catch {
state = "crashed";
client = null;
transport = null;
throw new Error("Failed to connect to Olive MCP server");
}
})();
connectingPromise = attempt;
attempt.catch(() => undefined).finally(() => {
if (connectingPromise === attempt) connectingPromise = null;
});
return connectingPromise;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/services/mcp/persistentClient.ts` around lines 46 - 96, Move the
connectingPromise cleanup out of the async IIFE’s finally block and clear it
after assigning the IIFE result, ensuring synchronous failures from
getMcpPython, buildPythonEnv, or StdioClientTransport cannot overwrite the
cleared field with a rejected promise. Preserve retry behavior by only clearing
the field when it still references the current connection attempt.

Comment on lines +86 to +90
} catch {
state = "crashed";
client = null;
transport = null;
throw new Error("Failed to connect to Olive MCP server");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Do not discard the underlying connect error.

Lines 63-73 pipe MCP server stderr for diagnostics, but the catch here throws away the cause of the spawn failure. An operator who hits ENOENT, a missing olive_mcp_server module, or a Python version mismatch sees only "Failed to connect to Olive MCP server". Log the original error, or attach it as cause.

♻️ Proposed change
-    } catch {
+    } catch (err: unknown) {
       state = "crashed";
       client = null;
       transport = null;
-      throw new Error("Failed to connect to Olive MCP server");
+      process.stderr.write(
+        `[olive-mcp] connect failed: ${err instanceof Error ? err.message : String(err)}\n`,
+      );
+      throw new Error("Failed to connect to Olive MCP server", { cause: err });
     }

The client-facing message stays sanitized, so no path detail reaches the HTTP response.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch {
state = "crashed";
client = null;
transport = null;
throw new Error("Failed to connect to Olive MCP server");
} catch (err: unknown) {
state = "crashed";
client = null;
transport = null;
process.stderr.write(
`[olive-mcp] connect failed: ${err instanceof Error ? err.message : String(err)}\n`,
);
throw new Error("Failed to connect to Olive MCP server", { cause: err });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/services/mcp/persistentClient.ts` around lines 86 - 90, Update the
connection error handling in the persistent client’s connect method to preserve
the underlying error while keeping the sanitized client-facing message. In the
catch block that sets state to "crashed" and clears client and transport, log
the original error or attach it as the thrown Error’s cause.

Comment on lines +187 to +207
} catch (err: unknown) {
// Transport/protocol failure — infrastructure error
const msg = err instanceof Error ? err.message : String(err);
if (msg.toLowerCase().includes("timed out")) {
// A timed-out request may still be in flight; close the session so it
// cannot poison subsequent calls on the persistent connection.
try {
void activeClient.close().catch(() => undefined);
} catch {
// Best effort — the connection is marked crashed below.
}
}
if (isInfraError(msg)) {
hadInfraFailure = true;
results.push({ error: msg || `MCP tool ${req.toolName} failed`, unavailable: true });
// Connection may be dead — break out and fail remaining
break;
}
// Tool-level exception (e.g. method not found)
results.push({ error: msg || `MCP tool ${req.toolName} failed` });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

The 45-second timeout path is both misclassified and untested. The timeout message built on persistentClient.ts line 170 contains "timed out", but isInfraError matches "timeout". A timed-out call therefore falls through to the tool-error branch, hadInfraFailure stays false, and the breaker records success after the session was closed. No test exercises this branch, so the defect ships silently.

  • src/server/services/mcp/persistentClient.ts#L187-L207: set hadInfraFailure = true and break on the timeout branch instead of relying on isInfraError substring matching, and await activeClient.close() before continuing.
  • src/server/services/mcp/client.test.ts#L105-L113: add a test that rejects callTool with "MCP tool call timed out after 45 seconds" and asserts unavailable: true, mcpBreaker.status().failures === 1, and that close was called.
🧰 Tools
🪛 GitHub Check: CodeFactor

[notice] 123-207: src/server/services/mcp/persistentClient.ts#L123-L207
Complex Method

📍 Affects 2 files
  • src/server/services/mcp/persistentClient.ts#L187-L207 (this comment)
  • src/server/services/mcp/client.test.ts#L105-L113
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/services/mcp/persistentClient.ts` around lines 187 - 207, The
timeout handling in persistentClient.ts lines 187-207 must classify timed-out
calls as infrastructure failures: set hadInfraFailure, add an unavailable
result, await activeClient.close(), and break before the isInfraError check. Add
coverage in client.test.ts lines 105-113 by rejecting callTool with the
45-second timeout message and asserting unavailable: true, one breaker failure,
and that close was called.

Comment on lines +264 to +277
function isInfraError(message: string): boolean {
const m = message.toLowerCase();
return (
m.includes("transport") ||
m.includes("connection") ||
m.includes("closed") ||
m.includes("econnrefused") ||
m.includes("epipe") ||
m.includes("spawn") ||
m.includes("killed") ||
m.includes("timeout") ||
m.includes("not connected")
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@modelcontextprotocol/client 2.0.0 McpError ErrorCode exported client package

💡 Result:

In version 2.0.0 of @modelcontextprotocol/client, the legacy McpError class and ErrorCode enum have been replaced by a more granular error hierarchy to distinguish between protocol-level errors and local SDK errors [1][2][3]. Key Changes in v2.0.0: 1. ProtocolError and ProtocolErrorCode: The former McpError and ErrorCode have been renamed to ProtocolError and ProtocolErrorCode, respectively [1][4][3]. These are used exclusively for errors that are serialized and transmitted over the wire as JSON-RPC error responses [1][5][2]. 2. SdkError and SdkErrorCode: Local SDK errors—such as connection issues, request timeouts, and capability checks—that do not cross the wire are now handled by SdkError and the SdkErrorCode enum [1][2][4]. Specific legacy codes like RequestTimeout and ConnectionClosed have been migrated from the old ErrorCode to SdkErrorCode [1][3][6]. 3. Backward Compatibility: To assist with migration, later updates (such as PR #1903) introduced deprecated aliases and subclasses that restore the v1 McpError and ErrorCode names for compatibility [7]. Summary Table of Replacements: | v1 Symbol | v2 Symbol | Scope | | --- | --- | --- | | McpError | ProtocolError | Wire / JSON-RPC | | ErrorCode | ProtocolErrorCode | Wire / JSON-RPC | | ErrorCode.RequestTimeout | SdkErrorCode.RequestTimeout | Local SDK | | ErrorCode.ConnectionClosed | SdkErrorCode.ConnectionClosed | Local SDK | For implementation, you should update your imports from @modelcontextprotocol/client to use the new classes [4][3]. For example: import { ProtocolError, ProtocolErrorCode, SdkError, SdkErrorCode } from '@modelcontextprotocol/client'; [4][3] When catching errors, you should now use instanceof checks against the specific error class (ProtocolError or SdkError) and compare against the corresponding error code enum (ProtocolErrorCode or SdkErrorCode) [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package references =="
rg -n '"`@modelcontextprotocol/client`"|`@modelcontextprotocol/client`' package.json package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null || true

echo
echo "== persistentClient outline =="
ast-grep outline src/server/services/mcp/persistentClient.ts --view expanded 2>/dev/null | sed -n '1,220p' || true

echo
echo "== relevant source slices =="
wc -l src/server/services/mcp/persistentClient.ts
sed -n '1,320p' src/server/services/mcp/persistentClient.ts

echo
echo "== related tests/call sites =="
rg -n "isInfraError|PersistentClient|circuit|breaker|client\.callTool|callTool\\(" tests src -g '*.ts' || true

Repository: tonythethompson/Olive-Studio

Length of output: 23729


Classify MCP failures using SDK error codes, not message substrings.

isInfraError currently opens the MCP circuit breaker on tool error text that contains words like "connection" or "ECONNREFUSED". With @modelcontextprotocol/client 2.0.0, use SdkError / SdkErrorCode for local SDK failures and ProtocolError / ProtocolErrorCode for wire-level JSON-RPC errors, instead of letting application text trip the breaker.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/services/mcp/persistentClient.ts` around lines 264 - 277, Replace
the message-substring logic in isInfraError with SDK error classification:
recognize local failures via SdkError and SdkErrorCode, and wire-level JSON-RPC
failures via ProtocolError and ProtocolErrorCode. Update callers to pass the
actual error object rather than rendered message text, and ensure
application-provided tool error messages no longer open the circuit breaker.

Comment on lines +285 to +297
export async function shutdownMcpClient(): Promise<void> {
if (client) {
try {
await client.close();
} catch {
// Best-effort shutdown
}
}
client = null;
transport = null;
state = "idle";
connectingPromise = null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Shutdown can race with an in-flight connect() and orphan the Python child process.

If shutdownMcpClient() runs while connect() is awaiting client.connect(transport), this function sets client = null, connectingPromise = null, and state = "idle". The in-flight attempt then completes and reassigns client and state = "connected" on lines 83-85. Nothing closes that transport afterwards, so python -m olive_mcp_server survives the SIGTERM path.

Await the pending attempt before tearing down, and add a shutdown guard so a late connect closes itself.

🔒️ Proposed fix
+let shuttingDown = false;
+
 export async function shutdownMcpClient(): Promise<void> {
+  shuttingDown = true;
+  if (connectingPromise) {
+    await connectingPromise.catch(() => undefined);
+  }
   if (client) {
     try {
       await client.close();
     } catch {
       // Best-effort shutdown
     }
   }
   client = null;
   transport = null;
   state = "idle";
   connectingPromise = null;
 }

In connect(), after await client.connect(transport), close immediately when shuttingDown is true. Reset shuttingDown in resetPersistentClient() so tests stay isolated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/services/mcp/persistentClient.ts` around lines 285 - 297, Update
connect() to check a shutdown guard immediately after await
client.connect(transport), closing the newly connected client and avoiding
reassignment when shutdown is in progress. Update shutdownMcpClient() to await
the existing connectingPromise before clearing client, transport, state, and
connectingPromise, and add the shuttingDown guard; reset that guard in
resetPersistentClient() to isolate tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

}: VramEstimateBannerProps) {
const storeState = usePipelineState();
const state = propState ?? storeState.state;
const setState = storeState.setState;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find every VramEstimateBanner render site and show the props passed.
rg -nP --type=tsx --type=ts -C 6 '<VramEstimateBanner\b' src

Repository: tonythethompson/Olive-Studio

Length of output: 198


🏁 Script executed:

#!/bin/bash
set -e

printf 'Tracked banner-related files:\n'
git ls-files | grep -E 'VramEstimateBanner|ExecutionWorkspace|IHVIntegrationPanel|pipelineValidation' || true

printf '\nVramEstimateBanner occurrences in tracked files:\n'
rg -n --glob '*.ts' --glob '*.tsx' '<VramEstimateBanner\b|VramEstimateBanner' src || true

printf '\nBanner outline:\n'
ast-grep outline src/components/features/VramEstimateBanner.tsx || true

printf '\nBanner relevant lines:\n'
cat -n src/components/features/VramEstimateBanner.tsx | sed -n '1,80p;220,260p'

Repository: tonythethompson/Olive-Studio

Length of output: 8537


🏁 Script executed:

#!/bin/bash
set -e

printf 'ExecutionWorkspace relevant lines around banner render:\n'
cat -n src/components/features/execute/ExecutionWorkspace.tsx | sed -n '880,955p'

printf '\nIHVIntegrationPanel relevant lines around banner render:\n'
cat -n src/components/features/ihv/IHVIntegrationPanel.tsx | sed -n '340,375p'

printf '\nSearch for commitUiStateUpdate/store setState patterns in panels:\n'
rg -n --glob '*.ts' --glob '*.tsx' 'commitUiStateUpdate|storeState\.setState|setState\(' src/components/features/execute/ExecutionWorkspace.tsx src/components/features/ihv/IHVIntegrationPanel.tsx src/lib/pipelineValidation.ts | head -n 200

Repository: tonythethompson/Olive-Studio

Length of output: 7734


Pass setState into the configured write path too.

Both controlled parents pass state={state} to VramEstimateBanner, but the banner still uses storeState.setState. Clicking Clear model then updates the pipeline store while the banner renders values from the prop. Add an optional setState prop and use propSetState ?? storeState.setState, or clear by calling the parent’s update through commitUiStateUpdate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/VramEstimateBanner.tsx` at line 38, Add an optional
setState prop to VramEstimateBanner and select it with propSetState ??
storeState.setState for the clear-model write path, ensuring controlled parents
update the same state source the banner renders.

import path from "node:path";
import { describe, expect, it } from "vitest";
import { PROVIDER_OPTIONS } from "@/components/features/gemini/aiProviderCatalog";
import { PROVIDER_OPTIONS } from "@/components/features/assistant/aiProviderCatalog";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify the unit-test config defines the `@/` alias and covers src/lib/__tests__.
fd -t f -i 'vitest*config*' -d 2 --exec cat {} \;
rg -n 'alias|environment|include' --glob 'vitest*' --glob 'vite.config*' . || true

Repository: tonythethompson/Olive-Studio

Length of output: 1634


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== vitest.config.ts =="
sed -n '1,220p' vitest.config.ts

echo "== target test imports and declarations =="
sed -n '1,160p' src/lib/__tests__/noNotAModelCopy.test.ts || true

echo "== aiProviderCatalog outline/search =="
find . -path './node_modules' -prune -o -name aiProviderCatalog.ts -print -print
sed -n '1,260p' src/components/features/assistant/aiProviderCatalog.ts || true

echo "== localEngineStarters imports =="
sed -n '1,220p' src/lib/localEngineStarters.ts || true

echo "== React usage in candidate files =="
rg -n "from ['\"].*react|React|ReactDOM|useState|useEffect|createPortal|`@radix-ui`|`@tanstack/react-query`|`@/components`" src/components/features/assistant/aiProviderCatalog.ts src/lib/localEngineStarters.ts src/lib/__tests__/noNotAModelCopy.test.ts || true

Repository: tonythethompson/Olive-Studio

Length of output: 16554


Move the provider catalog data out of src/components.

vitest.config.ts defines @, includes src/**/*.test.ts with a node environment, and aiProviderCatalog.ts is data plus lib/localEngineStarters.ts exports. The import still violates layering: a src/lib test now depends on a component module. Prefer a shared src/lib provider catalog and expose it from src/components/features/assistant instead of reusing the component file in a src/lib test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/__tests__/noNotAModelCopy.test.ts` at line 8, Move PROVIDER_OPTIONS
and the provider catalog data from aiProviderCatalog.ts into a shared src/lib
provider-catalog module, then re-export it from
src/components/features/assistant/aiProviderCatalog.ts for component consumers.
Update noNotAModelCopy.test.ts to import the shared lib module directly,
preserving the existing exported API and data.

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (33)
src/components/features/assistant/LocalAiSetupCard.tsx (1)

45-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Give the engine toggle a pressed state for assistive technology.

The two buttons act as a mutually exclusive selector, but the selected engine is communicated only through Tailwind colors. Screen-reader users get no selection state. Add aria-pressed and group the pair.

♻️ Proposed refactor
-    <div className="flex items-center gap-1 p-0.5 bg-slate-900 border border-slate-800 rounded-lg">
+    <div
+      role="group"
+      aria-label="Local AI engine"
+      className="flex items-center gap-1 p-0.5 bg-slate-900 border border-slate-800 rounded-lg"
+    >
       <button
         type="button"
+        aria-pressed={preferredEngine === "lms"}
         onClick={() => onSelect("lms")}

Apply the same aria-pressed attribute to the Ollama button.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/LocalAiSetupCard.tsx` around lines 45 - 72,
Update the EngineToggle component so both engine buttons expose their selection
state with aria-pressed based on preferredEngine, and group the mutually
exclusive controls with an appropriate accessible grouping attribute or element.
Preserve the existing onSelect behavior and visual styling.
src/components/features/assistant/LocalModelManager.test.tsx (1)

17-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The error overrides in mockFetch are never used, and the engine prop is never tested.

mockFetch accepts lms.error and ollama.error and builds HTTP 500 responses for them at Lines 29-31 and 43-45. No test in this file passes either. So the component's error branches at LocalModelManager.tsx Lines 124-133 and 138-147 stay uncovered, and this helper carries dead configuration.

Every test also renders with the default engine="all". The engine-scoped behavior — which is exactly what LocalAiSetupCard uses when it passes engine={local.preferredEngine} — is untested. The error branches only fire when engine is "lms" or "ollama", so both gaps close with the same test.

💚 Proposed test
describe("LocalModelManager — engine scoping and errors", () => {
  it("surfaces the server error when a single engine is selected", async () => {
    mockFetch({ lms: { error: "LM Studio is not running" } });

    await act(async () => {
      render(<LocalModelManager isOpen engine="lms" />);
    });

    await waitFor(() => {
      expect(screen.getByText("LM Studio is not running")).toBeDefined();
    });
  });

  it("does not query the other engine when scoped to ollama", async () => {
    mockFetch({ ollama: { installedModels: ["phi3.5:3.8b"], runningModels: [] } });

    await act(async () => {
      render(<LocalModelManager isOpen engine="ollama" />);
    });

    await waitFor(() => {
      expect(screen.getByText("phi3.5:3.8b")).toBeDefined();
    });
    const called = fetchSpy.mock.calls.map(([u]) => String(u));
    expect(called.some((u) => u.includes("local-models"))).toBe(false);
  });
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/LocalModelManager.test.tsx` around lines 17
- 56, Add tests for LocalModelManager engine scoping and error handling: render
with engine="lms" and an lms.error override, asserting the server error is
displayed, and render with engine="ollama" while verifying Ollama results appear
without any local-models request. Keep mockFetch’s existing error overrides and
cover both single-engine paths.
src/components/features/assistant/MessageContent.tsx (2)

8-17: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A single-line fenced span renders an empty code block.

Line 15 uses lines.slice(1, -1). That assumes the fence markers sit on their own lines. If the model emits ```pip install olive-ai``` on one line, lines has one entry, slice(1, -1) returns [], and the user sees an empty <pre>. The content is silently dropped.

Strip the fence markers from the string instead of dropping the first and last lines.

🐛 Proposed fix
     if (part.startsWith("```") && part.endsWith("```")) {
-      const lines = part.split("\n");
+      // Drop the fences, then the optional language tag on the opening line.
+      const inner = part.slice(3, -3).replace(/^[^\n]*\n/, (m) => (m.trim() ? "" : m));
       return (
         <pre
           key={i}
           className="bg-slate-950 p-2.5 rounded-lg border border-slate-800 text-[11px] font-mono text-emerald-400 my-1.5 overflow-x-auto whitespace-pre-wrap"
         >
-          {lines.slice(1, -1).join("\n")}
+          {inner.replace(/\n$/, "")}
         </pre>
       );
     }
🧰 Tools
🪛 React Doctor (0.9.3)

[warning] 12-12: Your users can see & submit the wrong data when this list reorders or filters, so use a stable id like key={item.id}, not the array index "i".

Use a stable id from the item, like key={item.id} or key={item.slug}. Index keys break when the list reorders or filters.

(no-array-index-as-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/MessageContent.tsx` around lines 8 - 17,
Update the fenced-code rendering logic in MessageContent to strip the opening
and closing ``` markers directly rather than using lines.slice(1, -1),
preserving inline code content for single-line fenced spans. Remove an optional
language tag only when it occupies the opening line, and retain the existing
multiline rendering behavior without the trailing fence newline.

22-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace any[] with ReactNode[] and drop the lint suppression.

The array holds strings and JSX elements only. ReactNode covers both, so the @typescript-eslint/no-explicit-any escape hatch is not needed.

♻️ Proposed refactor
-      // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-      const elems: any[] = [];
+      const elems: ReactNode[] = [];

Add the type import at the top of the module:

import type { ReactNode } from "react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

import type { ReactNode } from "react";

      const elems: ReactNode[] = [];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/MessageContent.tsx` around lines 22 - 23,
Update the elems declaration in MessageContent to use ReactNode[] instead of
any[], remove the eslint suppression, and add the type-only ReactNode import
from React.
src/components/features/assistant/ProviderErrorBlock.tsx (2)

24-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

msg.includes("401") and msg.includes("403") match any message containing those digits.

The provider branch does not render msg. So a false positive replaces the real error text with "No AI Provider Configured", and the user loses the actual failure reason. A message such as Pull failed: 403915 bytes written or a model id containing 401 triggers it.

Match the status codes as HTTP statuses, not as bare substrings.

🐛 Proposed fix
   const isProviderErr =
     !isJsonModelErr &&
     (msg.includes("not configured") ||
       msg.includes("API key") ||
       msg.includes("No AI provider") ||
-      msg.includes("401") ||
-      msg.includes("403") ||
+      /\b(401|403)\b/.test(msg) ||
       msg.includes("API route not found"));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/ProviderErrorBlock.tsx` around lines 24 -
31, Update the isProviderErr classification in ProviderErrorBlock to recognize
401 and 403 only when they appear as HTTP status codes, using appropriate
boundaries or status-pattern matching rather than bare substring checks.
Preserve detection of the other provider error phrases and prevent unrelated
numbers in messages or model identifiers from entering the provider branch.

62-78: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare the hardcoded hint list against provider catalogs and server env reads.
rg -n '_API_KEY|_API_TOKEN|HF_TOKEN|CLOUDFLARE_ACCOUNT_ID' src server.ts --glob '!**/*.test.*'
echo '--- provider catalog definitions ---'
fd -t f -i 'aiprovidercatalog' src --exec cat -n {}

Repository: tonythethompson/Olive-Studio

Length of output: 16793


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- ProviderErrorBlock outline ---'
ast-grep outline src/components/features/assistant/ProviderErrorBlock.tsx || true

echo '--- ProviderErrorBlock relevant lines ---'
sed -n '1,120p' src/components/features/assistant/ProviderErrorBlock.tsx | cat -n

echo '--- aiProviderCatalog imports/usages ---'
rg -n "from ['\"].*aiProviderCatalog|PROVIDER_OPTIONS|normalizeUiProviderId|keyEnvVar" src/components src --glob '!**/*.test.*' || true

echo '--- provider catalog server-side equivalent ---'
rg -n "envVarNames|loadStudioEnv|AI_PROVIDERS|providers|openai-compat|copilot|codex|devin" src/server/routes src/server/services src/lib -g '!**/*.test.*' | head -n 200

Repository: tonythethompson/Olive-Studio

Length of output: 35909


Keep this env hint tied to the provider catalog.

ProviderErrorBlock hardcodes twelve provider env vars, while PROVIDER_OPTIONS already has keyEnvVar for each supported provider. Generate this list from the catalog for the displayed providers, or add a synchronization check that fails when the hints drift from PROVIDER_OPTIONS.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/ProviderErrorBlock.tsx` around lines 62 -
78, Update ProviderErrorBlock to derive the environment-variable hint from
PROVIDER_OPTIONS.keyEnvVar for the displayed providers instead of maintaining
the hardcoded list. Ensure each supported provider’s variable appears once in
the rendered message and the catalog remains the single source of truth.

Source: Coding guidelines

src/components/features/assistant/types.ts (1)

1-14: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the assistant analysis payload before rendering it.

The API payload is AI-generated and untrusted. A successful JSON response such as {} reaches setAnalysis through a type assertion, then AuditPanel reads analysis.suggestions.length and throws.

  • src/components/features/assistant/types.ts#L1-L14: make Suggestion.autofix optional. AuditPanel already supports suggestions without an Apply action.
  • src/components/features/assistant/useAiAudit.ts#L50-L57: validate the full AnalysisResult shape before calling setAnalysis. Set analysisError when required fields are missing or malformed.
📍 Affects 2 files
  • src/components/features/assistant/types.ts#L1-L14 (this comment)
  • src/components/features/assistant/useAiAudit.ts#L50-L57
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/types.ts` around lines 1 - 14, Make
Suggestion.autofix optional in src/components/features/assistant/types.ts (lines
1-14), preserving AuditPanel support for suggestions without Apply actions. In
src/components/features/assistant/useAiAudit.ts (lines 50-57), validate the
complete AnalysisResult payload—including score, level, summary, suggestions,
and each suggestion’s required fields—before calling setAnalysis; set
analysisError and do not update analysis when validation fails.
src/components/features/assistant/useAiAudit.ts (1)

67-68: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Invalidate active requests when resetting analysis.

Line 68 clears only the displayed result. If a provider is cleared while an audit request is pending, the old response still passes the request-ID check and restores analysis from the cleared provider. Increment analysisRequestIdRef in resetAnalysis and clear the pending loading and error state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/useAiAudit.ts` around lines 67 - 68, Update
resetAnalysis in useAiAudit to invalidate pending requests by incrementing
analysisRequestIdRef, then clear the displayed analysis, loading state, and
error state. Ensure responses from requests active before reset cannot restore
analysis.
src/components/features/assistant/useLocalEngineSetup.ts (2)

53-72: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

readNdjsonLines leaks the stream reader on every error path.

handleInstallStreamEvent and handlePullStreamEvent throw on evt.type === "error". That exception propagates out of onLine, out of the while loop, and out of this function. The reader is never released and the body is never cancelled, so the underlying response stays locked and undrained until GC. Engine install and model pull errors are routine, not exceptional, so this leaks on a normal user path.

Wrap the loop and release the reader in finally.

🔒️ Proposed fix
 async function readNdjsonLines(
   body: ReadableStream<Uint8Array>,
   onLine: (line: string) => void | Promise<void>,
 ) {
   const reader = body.getReader();
   const decoder = new TextDecoder();
   let buf = "";
-  while (true) {
-    const { done, value } = await reader.read();
-    if (done) break;
-    buf += decoder.decode(value, { stream: true });
-    const lines = buf.split("\n");
-    buf = lines.pop() ?? "";
-    for (const line of lines) {
-      if (!line.trim()) continue;
-      await onLine(line);
-    }
-  }
-  return buf;
+  try {
+    while (true) {
+      const { done, value } = await reader.read();
+      if (done) break;
+      buf += decoder.decode(value, { stream: true });
+      const lines = buf.split("\n");
+      buf = lines.pop() ?? "";
+      for (const line of lines) {
+        if (!line.trim()) continue;
+        await onLine(line);
+      }
+    }
+    return buf;
+  } finally {
+    // Release the body on the throw path too; install/pull error events throw.
+    await reader.cancel().catch(() => {});
+    reader.releaseLock();
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

async function readNdjsonLines(
  body: ReadableStream<Uint8Array>,
  onLine: (line: string) => void | Promise<void>,
) {
  const reader = body.getReader();
  const decoder = new TextDecoder();
  let buf = "";
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buf += decoder.decode(value, { stream: true });
      const lines = buf.split("\n");
      buf = lines.pop() ?? "";
      for (const line of lines) {
        if (!line.trim()) continue;
        await onLine(line);
      }
    }
    return buf;
  } finally {
    // Release the body on the throw path too; install/pull error events throw.
    await reader.cancel().catch(() => {});
    reader.releaseLock();
  }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/useLocalEngineSetup.ts` around lines 53 -
72, Update readNdjsonLines to wrap its stream-reading loop and line-processing
in a try/finally block, and always release the reader in finally via its
existing reader cleanup API. Preserve propagation of errors from
handleInstallStreamEvent and handlePullStreamEvent while ensuring the underlying
stream is unlocked and cancelled or otherwise cleaned up on every exit path.

236-249: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The install stream reports success after swallowing a real server error.

Line 242 rethrows only when the message is neither "Setup failed" nor contains "JSON". Two problems follow.

First, the substring test is a proxy for "this was a JSON parse failure". Any genuine server error whose text contains JSON — for example LM Studio config JSON is malformed — is swallowed. Second, when an error is swallowed, state.ok stays false, but if response.ok is true the guard at Line 247 passes, setLocalInstallInfo("Engine ready.") runs, and installEngine then calls markEngineReady. The UI reports a healthy engine that never came up. That is fake readiness.

Catch SyntaxError explicitly, the way consumePullStream already does at Line 350, and require an explicit done event before declaring the engine ready.

🐛 Proposed fix
     await readNdjsonLines(body, (line) => {
+      let evt: InstallStreamEvent;
       try {
-        handleInstallStreamEvent(JSON.parse(line) as InstallStreamEvent, state);
-      } catch (e) {
-        if (e instanceof Error && e.message !== "Setup failed" && !e.message.includes("JSON")) {
-          throw e;
-        }
+        evt = JSON.parse(line) as InstallStreamEvent;
+      } catch {
+        // Non-JSON line from the server; ignore it and keep reading.
+        return;
       }
+      handleInstallStreamEvent(evt, state);
     });
-    if (!state.ok && !response.ok) throw new Error(`Setup failed (HTTP ${response.status})`);
+    if (!state.ok) throw new Error(`Setup failed (HTTP ${response.status})`);
     setLocalInstallInfo(state.finalMsg);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  const consumeInstallStream = async (body: ReadableStream<Uint8Array>, response: Response) => {
    const state = { openedUrl: undefined as string | undefined, ok: false, finalMsg: "Engine ready." };
    await readNdjsonLines(body, (line) => {
      let evt: InstallStreamEvent;
      try {
        evt = JSON.parse(line) as InstallStreamEvent;
      } catch {
        // Non-JSON line from the server; ignore it and keep reading.
        return;
      }
      handleInstallStreamEvent(evt, state);
    });
    if (!state.ok) throw new Error(`Setup failed (HTTP ${response.status})`);
    setLocalInstallInfo(state.finalMsg);
  };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/useLocalEngineSetup.ts` around lines 236 -
249, Update consumeInstallStream to swallow only JSON parsing errors by catching
SyntaxError explicitly, while rethrowing other errors regardless of their
message text. Require both a successful response and an explicit done event
indicated by state.ok before calling setLocalInstallInfo or allowing
installation readiness to proceed; preserve the existing Setup failed error for
unsuccessful responses.
src/components/features/assistant/aiProviderCatalog.ts (2)

15-15: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

ProviderId resolves to string, so provider IDs are unchecked everywhere. The explicit readonly ProviderOption[] annotation on the catalog overrides the as const literal types. Because ProviderOption.id is readonly string, ProviderId becomes string rather than the union of the 19 catalog IDs. Every consumer that expects a checked union gets none.

  • src/components/features/assistant/aiProviderCatalog.ts#L15-L15: remove the : readonly ProviderOption[] annotation and close the array with ] as const satisfies readonly ProviderOption[]; so the literal ID types survive.
  • src/components/features/assistant/useAiProviderSettings.ts#L353-L358: PROVIDER_OPTIONS.find((p) => p.id === id)! cannot be proven non-null while id is string. After the type fix, keep the assertion only if the union guarantees a match; otherwise return early when find yields undefined, because opt.models[0] would throw on Line 358.
  • src/components/features/assistant/ManualProviderSetup.tsx#L30-L30: e.target.value as ProviderId currently asserts nothing. After the type fix, validate with normalizeUiProviderId(e.target.value) and ignore a null result instead of casting.
📍 Affects 3 files
  • src/components/features/assistant/aiProviderCatalog.ts#L15-L15 (this comment)
  • src/components/features/assistant/useAiProviderSettings.ts#L353-L358
  • src/components/features/assistant/ManualProviderSetup.tsx#L30-L30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/aiProviderCatalog.ts` at line 15, Preserve
literal provider IDs by removing the explicit type annotation from
PROVIDER_OPTIONS and using an as-const satisfies readonly ProviderOption[]
declaration. In src/components/features/assistant/useAiProviderSettings.ts lines
353-358, retain the non-null assertion only if the resulting ProviderId union
guarantees a match; otherwise handle an undefined find result before accessing
opt.models[0]. In src/components/features/assistant/ManualProviderSetup.tsx line
30, replace the ProviderId cast with normalizeUiProviderId(e.target.value) and
ignore null results.

216-226: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Drop the re-export hop and match the @/lib import style.

These named re-exports turn aiProviderCatalog.ts into a barrel for local-engine helpers. Any module that wants LMS_STARTER_MODELS now also pulls in the full provider catalog. That works against the tree-shaking and component-test isolation goals in the project guidelines.

The deep relative path with an explicit .ts extension also differs from the @/lib/... alias used in SettingsPanel.tsx, ModelCombobox.tsx, and ManualProviderSetup.tsx.

Import localEngineStarters directly at the consumer sites and remove this block.

Run this script to size the change:

#!/bin/bash
# Description: Find consumers that import local engine starters through the catalog barrel.
rg -nP -C2 'LMS_STARTER_MODELS|OLLAMA_STARTER_MODELS|findInstalledStarterId|resolveLocalEnableModelId|LocalStarterModel' src || true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/aiProviderCatalog.ts` around lines 216 -
226, Remove the local-engine type and named re-export block from
aiProviderCatalog.ts, then update every consumer identified by the search to
import those symbols directly from the `@/lib/localEngineStarters` alias. Preserve
each consumer’s existing usage and use the alias without a .ts extension.

Source: Coding guidelines

src/components/features/assistant/CodexAccountPanel.tsx (1)

18-28: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Asynchronous status text in the assistant settings surface has no live regions. Every one of these elements replaces its content after a network request resolves, and none is announced. A screen-reader user starts sign-in, save, or logout and receives no confirmation or failure notice.

  • src/components/features/assistant/CodexAccountPanel.tsx#L18-L28: add role="status" and aria-live="polite" to the Status paragraph and to the codexMessage paragraph on Line 28.
  • src/components/features/assistant/DevinAccountPanel.tsx#L18-L30: add role="status" and aria-live="polite" to the Status paragraph and to the devinMessage paragraph on Line 30.
  • src/components/features/assistant/ManualProviderSetup.tsx#L297-L297: add role="alert" to the providerSaveError paragraph, because it reports a failure rather than progress.
📍 Affects 3 files
  • src/components/features/assistant/CodexAccountPanel.tsx#L18-L28 (this comment)
  • src/components/features/assistant/DevinAccountPanel.tsx#L18-L30
  • src/components/features/assistant/ManualProviderSetup.tsx#L297-L297
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/CodexAccountPanel.tsx` around lines 18 -
28, In src/components/features/assistant/CodexAccountPanel.tsx lines 18-28, add
role="status" and aria-live="polite" to the Status and codexMessage paragraphs;
apply the same attributes to the corresponding Status and devinMessage
paragraphs in src/components/features/assistant/DevinAccountPanel.tsx lines
18-30. In src/components/features/assistant/ManualProviderSetup.tsx line 297,
add role="alert" to the providerSaveError paragraph.
src/components/features/assistant/DevinAccountPanel.tsx (1)

59-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale gemini- prefix on the element IDs after the directory rename.

This PR renames gemini/ to assistant/, but the DOM IDs still read gemini-settings-devin-token. ManualProviderSetup.tsx carries the same prefix on gemini-settings-provider, gemini-settings-model, gemini-settings-api-key, and gemini-cf-account-id. The identifiers now name a provider that is one entry in a 19-provider catalog, not the feature.

Rename the IDs to an assistant- prefix and update any test selectors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/DevinAccountPanel.tsx` around lines 59 -
63, Replace the stale gemini- DOM ID prefixes with assistant- in
DevinAccountPanel’s token input and the corresponding provider setup IDs in
ManualProviderSetup, including gemini-settings-devin-token,
gemini-settings-provider, gemini-settings-model, gemini-settings-api-key, and
gemini-cf-account-id. Update all tests and selectors that reference these IDs to
use the renamed assistant- identifiers.
src/components/features/assistant/LocalModelManager.tsx (1)

191-241: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Three refresh() call sites bypass the cancellation guard that the effect adds.

The mount effect at Line 179 passes () => cancelGuard.cancelled so a late response cannot write state after unmount or after the engine prop changes. handleEnable (Line 213), handleUnload (Line 235), and the manual Refresh button (Line 257) call refresh() with no guard. Those three can also overlap with an in-flight effect refresh, so two responses race and the later writer wins arbitrarily.

Hold one guard in a ref and use it for every call.

♻️ Proposed refactor
+  // One cancellation token for every refresh, so late responses cannot win.
+  const refreshTokenRef = useRef(0);
+
   const refresh = async (isCancelled?: () => boolean) => {
+    const token = ++refreshTokenRef.current;
+    const cancelled = () => isCancelled?.() === true || refreshTokenRef.current !== token;

Then replace every isCancelled?.() inside refresh with cancelled(), and keep the other call sites as plain refresh().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/LocalModelManager.tsx` around lines 191 -
241, Centralize the cancellation guard in a ref used by refresh, replacing the
optional isCancelled callback checks with the ref-backed cancelled function.
Update the effect and the handleEnable, handleUnload, and manual Refresh button
call sites to use the shared guard so all refresh responses are ignored after
unmount or engine changes and overlapping requests cannot write stale state.
src/components/features/assistant/ManualProviderSetup.tsx (3)

33-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the TypeScript lib/target settings for Object.groupBy support (ES2024).
fd -t f -g 'tsconfig*.json' -d 2 --exec cat {} \;

Repository: tonythethompson/Olive-Studio

Length of output: 760


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== File outline =="
ast-grep outline src/components/features/assistant/ManualProviderSetup.tsx --view expanded || true

echo
echo "== ManualProviderSetup relevant lines =="
cat -n src/components/features/assistant/ManualProviderSetup.tsx | sed -n '1,180p'

echo
echo "== aiProviderCatalog category labels/options references =="
rg -n "CATEGORY_LABELS|PROVIDER_OPTIONS|optgroup|aiProviderCatalog|type .*Provider" src --glob '*.{ts,tsx}'

echo
echo "== Candidate catalog files =="
fd -t f 'aiProviderCatalog|provider' src | sed -n '1,80p'

Repository: tonythethompson/Olive-Studio

Length of output: 17286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== aiProviderCatalog category labels/options lines =="
cat -n src/components/features/assistant/aiProviderCatalog.ts | sed -n '1,235p'

echo
echo "== TypeScript compiler availability and lib definitions =="
if command -v tsc >/dev/null 2>&1; then
  echo "tsc found"
  tsc --version
else
  echo "tsc not found"
fi

echo
echo "== Read-only local source check for Object.groupBy availability in current tsconfig =="
python3 - <<'PY'
import json, pathlib, re
p = pathlib.Path("tsconfig.json")
cfg = json.loads(p.read_text())
opts = cfg.get("compilerOptions", {})
print(json.dumps({"target": opts.get("target")}, indent=2))
# Check standard library declarations for Object.groupBy if `@types/core-js` or local libs exist.
for path in pathlib.Path("node_modules").glob("`@types/core-js/`**/*.d.ts"):
    s = path.read_text(errors="ignore")
    if "groupBy" in s:
        print(f"core-js groupBy declaration found", path)
        break
else:
    print("core-js Object.groupBy declaration not found in node_modules")
PY

Repository: tonythethompson/Olive-Studio

Length of output: 8886


Use <optgroup> for provider categories.

CATEGORY_LABELS is documented as optgroup headers, but this select renders disabled placeholder <option value=""> rows. Screen readers expose those as disabled options; <optgroup label> exposes the category as a group. Grouping also removes the empty value="" option, so provider selection cannot produce an empty provider ID.

Use a grouping pattern compatible with the current ES2022 target instead of Object.groupBy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/ManualProviderSetup.tsx` around lines 33 -
48, Replace the category-header `<option>` pattern in the provider select’s
PROVIDER_OPTIONS rendering with `<optgroup>` elements labeled via
CATEGORY_LABELS, grouping each category’s provider options under its
corresponding group. Use an ES2022-compatible grouping approach rather than
Object.groupBy, and ensure no empty value="" option is rendered.

125-138: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The select can display a model that does not match the saved state.

value={settingsModel} has no matching <option> when the saved model is absent from displayedModels. fetchProviderStatus sets settingsModel from the server at useAiProviderSettings.ts Line 275, and that value can be any string the server has stored. The browser then shows the first option while state still holds the unmatched value. Save writes the hidden state value, not the one on screen.

Render the current value as an extra option when the catalog does not contain it.

🐛 Proposed fix
   return (
     <select
       id="gemini-settings-model"
       aria-label="AI model"
       value={settingsModel}
       onChange={(e) => providers.setSettingsModel(e.target.value)}
       className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-electric-blue cursor-pointer"
     >
+      {settingsModel && !displayedModels.some((m) => m.id === settingsModel) && (
+        <option value={settingsModel}>{settingsModel}</option>
+      )}
       {displayedModels.map((m) => (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  return (
    <select
      id="gemini-settings-model"
      aria-label="AI model"
      value={settingsModel}
      onChange={(e) => providers.setSettingsModel(e.target.value)}
      className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-electric-blue cursor-pointer"
    >
      {settingsModel && !displayedModels.some((m) => m.id === settingsModel) && (
        <option value={settingsModel}>{settingsModel}</option>
      )}
      {displayedModels.map((m) => (
        <option key={m.id} value={m.id}>
          {m.label}
        </option>
      ))}
    </select>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/ManualProviderSetup.tsx` around lines 125 -
138, Update the model select rendering in the component containing
displayedModels so settingsModel is also rendered as an option when
displayedModels does not contain that id, preserving its label/value
sufficiently to keep the controlled select synchronized with saved state. Avoid
adding a duplicate option when the catalog already includes settingsModel, and
keep the existing displayedModels options unchanged.

178-180: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Locate the Cloudflare manual-login route and check whether it writes credentials to disk.
rg -n -C10 'cloudflare/login/manual' --glob '!node_modules' . || true
fd -t f -i 'cloudflare' src/server --exec rg -n -C5 'writeFile|writeFileSync|persist|keytar|store|fs\.' {} \;

Repository: tonythethompson/Olive-Studio

Length of output: 7296


🏁 Script executed:

#!/bin/bash
set -u

echo "== cloudflareRoutes.ts relevant line ranges =="
wc -l src/server/routes/ai/cloudflareRoutes.ts
sed -n '53,110p' src/server/routes/ai/cloudflareRoutes.ts

echo
echo "== ManualProviderSetup.tsx relevant lines =="
wc -l src/components/features/assistant/ManualProviderSetup.tsx
sed -n '160,225p' src/components/features/assistant/ManualProviderSetup.tsx

echo
echo "== useAiProviderSettings.ts persistApiKeyProvider and keyPlaceholder references =="
sed -n '60,90p' src/components/features/assistant/useAiProviderSettings.ts

echo
echo "== saveManualCloudflareCredentials definitions/usages =="
rg -n -C8 "function saveManualCloudflareCredentials|saveManualCloudflareCredentials|persisting|persist|password" src/server src/components/features/assistant || true

Repository: tonythethompson/Olive-Studio

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -u

echo "== locate cloudflare client credentials files =="
fd -t f -i 'client|credentials' src/lib src/server/lib src/components/features/assistant src/server/services/ai/state.ts || true

echo
echo "== inspect candidate Cloudflare credential implementation files =="
for f in $(fd -t f -i 'client|credentials' src/lib src/server/lib src/components/features/assistant src/server/services/ai/state.ts 2>/dev/null | grep -E 'cloudflare|credentials|cloudflare/client|cloudflare/credentials'); do
  echo "--- $f ---"
  wc -l "$f"
  sed -n '1,260p' "$f"
done

echo
echo "== focused search for credential storage helpers and Cloudflare fields =="
rg -n -C6 "saveManualCloudflareCredentials|logoutCloudflare|getCloudflareAccountStatus|writeFileSync|writeFile|fs\\.|credentials" src/lib src/server/services/ai

Repository: tonythethompson/Olive-Studio

Length of output: 50385


Update the Cloudflare API-key placeholder before saving

/api/cloudflare/login/manual stores the Cloudflare token and account locally via saveManualCloudflareCredentials, but the component placeholder says “Stored in memory only, never persisted to disk” when there is no usable environment credential. Use a Cloudflare-specific placeholder or a generic prompt that explicitly describes on-disk credential storage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/ManualProviderSetup.tsx` around lines 178 -
180, The keyPlaceholder in ManualProviderSetup should accurately describe
Cloudflare credentials being persisted by saveManualCloudflareCredentials when
no usable environment credential exists. Replace the memory-only text with a
Cloudflare-specific or generic prompt that explicitly states the API key is
stored on disk, while preserving the existing environment-variable placeholder.
src/components/features/assistant/ModelCombobox.test.tsx (1)

63-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the successful selection paths.

These tests verify that Enter does not commit. No test verifies that Enter does commit after ArrowDown sets an explicit highlight, and no test verifies mouse selection through onMouseDown. Those are the primary paths of the component. A regression that disables selection entirely would still pass this suite.

Add two cases: arrow-down then Enter calls onChange with the highlighted ID, and mouseDown on an option calls onChange and closes the list.

💚 Proposed additional tests
+  it("commits the highlighted model on ArrowDown + Enter", () => {
+    const onChange = vi.fn();
+    render(
+      <ModelCombobox
+        value=""
+        options={options}
+        modelsSource="live"
+        onChange={onChange}
+      />,
+    );
+
+    const input = screen.getByRole("combobox");
+    fireEvent.focus(input);
+    fireEvent.keyDown(input, { key: "ArrowDown" });
+    fireEvent.keyDown(input, { key: "Enter" });
+    expect(onChange).toHaveBeenCalledWith("openrouter/google/gemini-2.5-pro");
+    expect(screen.queryByRole("listbox")).toBeNull();
+  });
+
+  it("commits the clicked model and closes the list", () => {
+    const onChange = vi.fn();
+    render(
+      <ModelCombobox
+        value=""
+        options={options}
+        modelsSource="live"
+        onChange={onChange}
+      />,
+    );
+
+    fireEvent.focus(screen.getByRole("combobox"));
+    fireEvent.mouseDown(screen.getByRole("option", { name: /GPT-4o/ }));
+    expect(onChange).toHaveBeenCalledWith("openrouter/openai/gpt-4o");
+    expect(screen.queryByRole("listbox")).toBeNull();
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  it("commits the highlighted model on ArrowDown + Enter", () => {
    const onChange = vi.fn();
    render(
      <ModelCombobox
        value=""
        options={options}
        modelsSource="live"
        onChange={onChange}
      />,
    );

    const input = screen.getByRole("combobox");
    fireEvent.focus(input);
    fireEvent.keyDown(input, { key: "ArrowDown" });
    fireEvent.keyDown(input, { key: "Enter" });
    expect(onChange).toHaveBeenCalledWith("openrouter/google/gemini-2.5-pro");
    expect(screen.queryByRole("listbox")).toBeNull();
  });

  it("commits the clicked model and closes the list", () => {
    const onChange = vi.fn();
    render(
      <ModelCombobox
        value=""
        options={options}
        modelsSource="live"
        onChange={onChange}
      />,
    );

    fireEvent.focus(screen.getByRole("combobox"));
    fireEvent.mouseDown(screen.getByRole("option", { name: /GPT-4o/ }));
    expect(onChange).toHaveBeenCalledWith("openrouter/openai/gpt-4o");
    expect(screen.queryByRole("listbox")).toBeNull();
  });

  it("does not commit a different model on focus + Enter for freehand ids", () => {
    const onChange = vi.fn();
    render(
      <ModelCombobox
        value="my-org/custom-model"
        options={options}
        modelsSource="live"
        onChange={onChange}
      />,
    );

    const input = screen.getByRole("combobox");
    fireEvent.focus(input);
    fireEvent.keyDown(input, { key: "Enter" });
    expect(onChange).not.toHaveBeenCalled();
  });

  it("does not commit a different model on focus + Enter when selection is past the visible window", () => {
    const many = Array.from({ length: 45 }, (_, i) => ({
      id: `model-${i}`,
      label: `Model ${i}`,
    }));
    const onChange = vi.fn();
    render(
      <ModelCombobox value="model-42" options={many} modelsSource="live" onChange={onChange} />,
    );

    const input = screen.getByRole("combobox");
    fireEvent.focus(input);
    fireEvent.keyDown(input, { key: "Enter" });
    expect(onChange).not.toHaveBeenCalled();
  });

  it("does not overwrite a freehand id with the first filter match on Enter", () => {
    const onChange = vi.fn();
    render(
      <ModelCombobox
        value=""
        options={[
          { id: "openai/gpt-4o", label: "GPT-4o" },
          { id: "openai/gpt-4o-mini", label: "GPT-4o mini" },
        ]}
        modelsSource="live"
        onChange={onChange}
      />,
    );

    const input = screen.getByRole("combobox");
    fireEvent.focus(input);
    fireEvent.change(input, { target: { value: "openai/gpt-4o-my-ft" } });
    expect(onChange).toHaveBeenLastCalledWith("openai/gpt-4o-my-ft");
    onChange.mockClear();

    fireEvent.keyDown(input, { key: "Enter" });
    expect(onChange).not.toHaveBeenCalled();
    expect((input as HTMLInputElement).value).toBe("openai/gpt-4o-my-ft");
  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/ModelCombobox.test.tsx` around lines 63 -
119, Add two successful-selection tests for ModelCombobox: verify that focusing
the input, using ArrowDown to create an explicit highlight, and pressing Enter
calls onChange with the highlighted option ID; and verify that firing mouseDown
on an option calls onChange with that option’s ID and closes the list. Use the
component’s existing option rendering and observable list visibility behavior.
src/components/features/assistant/ModelCombobox.tsx (3)

71-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the exhaustive-deps rule configuration and the eslint-plugin-react-hooks version.
fd -t f -i 'eslint.config*' -d 2 --exec cat {} \;
rg -n 'eslint-plugin-react-hooks' package.json || true

Repository: tonythethompson/Olive-Studio

Length of output: 3794


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the surrounding implementation and search for eslint-disable patterns/effects in this file.
printf '--- ModelCombobox.tsx relevant section ---\n'
sed -n '1,130p' src/components/features/assistant/ModelCombobox.tsx | cat -n

printf '\n--- eslint-disable/react-hooks/exhaustive-deps in ModelCombobox.tsx ---\n'
rg -n 'eslint-disable|react-hooks/exhaustive-deps|intentionally|closeList' src/components/features/assistant/ModelCombobox.tsx || true

Repository: tonythethompson/Olive-Studio

Length of output: 257


🏁 Script executed:

#!/bin/bash
set -euоe pipefail

echo '--- ModelCombobox.tsx relevant section ---'
sed -n '1,130p' src/components/features/assistant/ModelCombobox.tsx | cat -n

echo
echo '--- eslint-disable/react-hooks/exhaustive-deps in ModelCombobox.tsx ---'
rg -n 'eslint-disable|react-hooks/exhaustive-deps|intentionally|closeList' src/components/features/assistant/ModelCombobox.tsx || true

Repository: tonythethompson/Olive-Studio

Length of output: 5480


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- package versions ---'
node - <<'JS'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
for (const key of ['dependencies','devDependencies']) {
  if (pkg[key]) {
    for (const name of ['react', 'eslint', 'eslint-plugin-react-hooks', '`@typescript-eslint/eslint-plugin`']) {
      if (pkg[key][name]) console.log(`${key}.${name}: ${pkg[key][name]}`);
    }
  }
}
JS

echo
echo '--- ModelCombobox.tsx imports and effects ---'
sed -n '1,25p' src/components/features/assistant/ModelCombobox.tsx | cat -n
sed -n '65,82p' src/components/features/assistant/ModelCombobox.tsx | cat -n

Repository: tonythethompson/Olive-Studio

Length of output: 1804


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- package versions ---'
node - <<'JS'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
for (const key of ['dependencies','devDependencies']) {
  if (pkg[key]) {
    for (const name of ['react', 'eslint', 'eslint-plugin-react-hooks', '`@typescript-eslint/eslint-plugin`']) {
      if (pkg[key][name]) console.log(`${key}.${name}: ${pkg[key][name]}`);
    }
  }
}
JS

echo
echo '--- ModelCombobox.tsx imports and effects ---'
sed -n '1,25p' src/components/features/assistant/ModelCombobox.tsx | cat -n
sed -n ' 65,82p' src/components/features/assistant/ModelCombobox.tsx | cat -n

Repository: tonythethompson/Olive-Studio

Length of output: 1804


🌐 Web query:

eslint-plugin-react-hooks exhaustive-deps missing dependency function callbacks stable setters documentation

💡 Result:

In eslint-plugin-react-hooks, the exhaustive-deps rule flags “missing dependency” errors for values (including functions) referenced inside an Effect, because the dependency array is how React knows when to re-run the Effect with the latest values—avoiding stale closures [1]. 1) Why function callbacks are usually required - If a function you reference inside useEffect (often a prop callback or a handler defined in the component) is in the component scope, exhaustive-deps expects it to appear in the dependency list; otherwise, React won’t re-run the Effect when that function changes, potentially using stale values [1]. 2) “But adding the function makes an infinite loop” (common cause + fix) - exhaustive-deps explicitly warns about patterns that add function dependencies and can cause loops, and recommends making the function stable or restructuring the code [1]. - Recommended fixes documented by React: a) Wrap the function in useCallback so its identity is stable between renders (based on a real set of primitive dependencies), then depend on that memoized function [1]. b) Or move the logic directly into the Effect and depend on the state/props it actually reads [1]. 3) How the linter decides whether a dependency can be omitted (stable setters) - React guarantees that the setState function identity (the setter returned from useState) is stable and won’t change across re-renders, so it is safe to omit from Effect dependencies [2]. - The exhaustive-deps rule also treats setState/setReducer dispatch functions as special cases (they are considered stable), which prevents it from incorrectly requiring them in deps in typical usage [3]. 4) What to do when you think something is truly unnecessary - React’s guidance is: do not suppress exhaustive-deps (for example via eslint-ignore-next-line). When dependencies don’t match the code, there’s high risk of bugs, because you “lie” to React about what the Effect depends on [4][5]. - Instead, “prove” a dependency is unnecessary by changing the code (e.g., moving values/functions so they’re not reactive dependencies, or using Effect Events where appropriate) [4]. Practical summary: - Props/state variables you read in an Effect: must be listed (or you must restructure code to make them unnecessary) [1][5]. - Functions referenced from component scope: typically must be listed; if that causes a loop, stabilize the function with useCallback or move the logic into the Effect [1]. - Stable setters (setState / dispatch): generally can be omitted because React guarantees stable identity [2][3].

Citations:


Stabilize closeList to satisfy react-hooks/exhaustive-deps.

closeList is redefined on every render and is referenced inside useEffect, so this generates the project’s lint warning. Wrap closeList in useCallback and add useCallback to the React imports; setOpen, setQuery, and setHighlight do not need to be added because they are stable state setters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/ModelCombobox.tsx` around lines 71 - 80,
Wrap closeList in useCallback and import useCallback from React, preserving its
existing behavior and dependencies on stable state setters. Update the useEffect
dependency array to include the stabilized closeList reference alongside open.

Source: Coding guidelines


101-131: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Arrow-key navigation moves the highlight off-screen.

The listbox is max-h-48 overflow-auto and renders up to 40 options. Roughly six options are visible at a time. ArrowDown advances highlight but nothing scrolls the active option into view. After about six presses the user navigates blind, and Enter commits an option they cannot see.

Add a ref to the active option and scroll it into view when safeHighlight changes.

♿ Proposed fix to keep the active option visible

Add the ref and the effect near the other hooks:

   const rootRef = useRef<HTMLDivElement>(null);
+  const activeOptionRef = useRef<HTMLLIElement>(null);
+  useEffect(() => {
+    if (!open || safeHighlight === null) return;
+    activeOptionRef.current?.scrollIntoView({ block: "nearest" });
+  }, [open, safeHighlight]);

Attach the ref to the active option in the list at Line 190:

                 <li
                   key={m.id}
+                  ref={active ? activeOptionRef : undefined}
                   id={`${listboxId}-opt-${index}`}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/ModelCombobox.tsx` around lines 101 - 131,
Add a ref for the highlighted option and an effect keyed to safeHighlight that
scrolls the active element into view when keyboard navigation changes it. Attach
the ref to the option rendered by the list when its index matches safeHighlight,
while preserving existing navigation and rendering behavior.

181-184: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Non-option <li> children break the listbox semantics.

role="listbox" requires its owned children to be role="option". The empty-state message at Lines 181-184 and the two truncation notices at Lines 214-223 are plain <li> elements inside the <ul role="listbox">. Assistive technology reports an inconsistent option count, and some screen readers skip the message entirely.

Mark them role="presentation", or move them outside the <ul>.

♿ Proposed fix for the listbox children
           {filtered.length === 0 ? (
-            <li className="px-3 py-2 text-sm text-slate-500">
+            <li role="presentation" className="px-3 py-2 text-sm text-slate-500">
               No catalog matches. Keep typing to use a freehand model id.
             </li>
           {!filterText && options.length > MAX_VISIBLE && (
-            <li className="px-3 py-1.5 text-[11px] text-slate-500 border-t border-slate-800">
+            <li role="presentation" className="px-3 py-1.5 text-[11px] text-slate-500 border-t border-slate-800">
               Showing first {MAX_VISIBLE} models. Type to search the full catalog.
             </li>
           )}
           {filterText && options.length > MAX_VISIBLE && filtered.length === MAX_VISIBLE && (
-            <li className="px-3 py-1.5 text-[11px] text-slate-500 border-t border-slate-800">
+            <li role="presentation" className="px-3 py-1.5 text-[11px] text-slate-500 border-t border-slate-800">
               Showing first {MAX_VISIBLE} matches. Type to narrow further.
             </li>
           )}

Also applies to: 214-223

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/ModelCombobox.tsx` around lines 181 - 184,
Update the non-option list items in ModelCombobox, including the empty-state
message and both truncation notices, to use role="presentation" while keeping
actual model entries as role="option" within the listbox.
src/components/features/assistant/ProviderErrorBlock.test.tsx (1)

69-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case where kind must win over the message heuristics.

ProviderErrorBlock computes isProviderErr only when isJsonModelErr is false. No test pins that precedence. If someone later reorders the two checks, the suite still passes while a structured parser error gets rendered as "No AI Provider Configured".

💚 Proposed test
   it("shows model JSON guidance only when kind is structured", () => {
  it("prefers the structured kind over provider message heuristics", () => {
    render(
      <ProviderErrorBlock
        msg="401 Unauthorized — check your API key"
        kind="invalid_model_json"
        onGoSettings={vi.fn()}
      />,
    );
    expect(screen.getByText("Model returned invalid JSON")).toBeDefined();
    expect(screen.queryByText("No AI Provider Configured")).toBeNull();
  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/ProviderErrorBlock.test.tsx` around lines
69 - 79, Add a test in the ProviderErrorBlock test suite covering a
provider-like message paired with kind="invalid_model_json". Assert that the
structured error renders "Model returned invalid JSON" and that "No AI Provider
Configured" is absent, pinning structured kind precedence over message
heuristics.
src/components/features/assistant/SettingsPanel.tsx (2)

26-29: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract the provider display-name lookup into the catalog.

This exact expression is duplicated in src/components/features/assistant/AssistantSidebar.tsx, where it builds providerLabel. Both sites normalize the ID, search PROVIDER_OPTIONS, and fall back to the raw provider string. Any change to the alias rules must be applied in two places.

Add a providerDisplayName helper to aiProviderCatalog.ts and call it from both sites.

♻️ Proposed helper and call site

Add to src/components/features/assistant/aiProviderCatalog.ts:

/** Catalog display name for a provider id, with legacy alias handling. */
export function providerDisplayName(provider: string | undefined): string {
  if (!provider) return "";
  const id = normalizeUiProviderId(provider) ?? provider;
  return PROVIDER_OPTIONS.find((p) => p.id === id)?.name ?? provider;
}

Then in this file:

-  const providerName =
-    PROVIDER_OPTIONS.find(
-      (p) => p.id === (normalizeUiProviderId(providerStatus.provider ?? "") ?? providerStatus.provider),
-    )?.name ?? providerStatus.provider;
+  const providerName = providerDisplayName(providerStatus.provider);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/SettingsPanel.tsx` around lines 26 - 29,
Extract the duplicated provider label logic into a providerDisplayName helper in
aiProviderCatalog.ts, accepting an optional provider, applying
normalizeUiProviderId, looking up PROVIDER_OPTIONS, and falling back to the raw
value or an empty string. Replace the local expression in SettingsPanel and the
providerLabel expression in AssistantSidebar with this helper, preserving the
existing display behavior.

Source: Coding guidelines


120-133: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The effect discards the user's manual tab selection on any provider-status refresh.

setSettingsMode(next) runs unconditionally whenever providerStatus.provider, baseUrl, or source changes. Several actions refresh provider status without the user changing tabs: handleCodexLogout, handleDevinLogout, and clearProvider all call fetchProviderStatus(). deriveAssistantSettingsMode returns "cloud" for every state except a local openai-compat base URL. A user who is reading the Local tab is therefore moved to Cloud when an unrelated logout resolves.

Track the last derived value and only overwrite the mode when the derivation actually changes.

🐛 Proposed fix to preserve the manual selection
+  const lastDerivedModeRef = useRef<AssistantSettingsMode | null>(null);
+
   useEffect(() => {
     const next = deriveAssistantSettingsMode(
       providers.providerStatus.provider ?? providers.settingsProvider,
       providers.settingsBaseUrl || providers.providerStatus.baseUrl,
     );
-    // eslint-disable-next-line react-hooks/set-state-in-effect
-    setSettingsMode(next);
+    if (lastDerivedModeRef.current !== next) {
+      lastDerivedModeRef.current = next;
+      // eslint-disable-next-line react-hooks/set-state-in-effect
+      setSettingsMode(next);
+    }
     const engine = preferredEngineFromBaseUrl(providers.settingsBaseUrl || providers.providerStatus.baseUrl);

Add useRef to the React import on Line 1.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  const lastDerivedModeRef = useRef<AssistantSettingsMode | null>(null);

  useEffect(() => {
    const next = deriveAssistantSettingsMode(
      providers.providerStatus.provider ?? providers.settingsProvider,
      providers.settingsBaseUrl || providers.providerStatus.baseUrl,
    );
    if (lastDerivedModeRef.current !== next) {
      lastDerivedModeRef.current = next;
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setSettingsMode(next);
    }
    const engine = preferredEngineFromBaseUrl(providers.settingsBaseUrl || providers.providerStatus.baseUrl);
    if (engine && engine !== local.preferredEngine) {
      local.selectPreferredEngine(engine);
    }
    // Only re-derive when the active provider identity changes
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [providers.providerStatus.provider, providers.providerStatus.baseUrl, providers.providerStatus.source]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/SettingsPanel.tsx` around lines 120 - 133,
Update the SettingsPanel effect around deriveAssistantSettingsMode to track the
previously derived mode with a useRef, and only call setSettingsMode when the
newly derived value differs from that stored value. Update the ref whenever
derivation changes so provider-status refreshes do not overwrite a user-selected
tab.
src/components/features/assistant/useAiProviderSettings.ts (8)

66-96: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a timeout to the provider-persistence requests.

Neither fetch call passes an AbortSignal. If the local Express server stops responding, both promises hang. saveApiKeyProvider keeps isSavingProvider true because its finally never runs, and the "Save & Activate" button stays disabled with a spinning icon until the page reloads. The same gap applies to every fetch in this hook, including the Codex poll loop and clearProvider.

Add signal: AbortSignal.timeout(...) to these requests. Extract a small postJson helper so all call sites in this hook share one timeout policy.

🛡️ Proposed fix for the two requests
+const REQUEST_TIMEOUT_MS = 20_000;
+
 async function persistApiKeyProvider(input: {
     const credRes = await fetch("/api/cloudflare/login/manual", {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify({ apiToken: input.key, accountId: input.cloudflareAccountId }),
+      signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
     });
   const r = await fetch("/api/ai/provider", {
     method: "POST",
     headers: { "Content-Type": "application/json" },
     body: JSON.stringify({
       provider: input.settingsProvider,
       apiKey: input.key || undefined,
       model: input.model,
       baseUrl: input.resolvedBaseUrl,
     }),
+    signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/useAiProviderSettings.ts` around lines 66 -
96, Introduce a shared postJson helper in the useAiProviderSettings hook that
applies a consistent AbortSignal.timeout policy, then route both fetch calls in
persistApiKeyProvider through it. Update every other fetch in the hook,
including the Codex poll loop and clearProvider, to use the same helper or
timeout signal so no request can hang indefinitely while preserving each
request’s existing method, headers, body, and response handling.

111-154: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Split the Codex and Devin authentication state out of this hook.

This hook now owns three unrelated lifecycles: provider settings and the model catalog, the Codex OAuth flow, and the Devin token flow. The state block declares codexAccount, codexBusy, codexMessage, devinStatus, devinToken, devinModels, devinBusy, and devinMessage alongside the provider form. The file is roughly 680 lines and the returned object exposes 45 keys.

CodexAccountPanel needs three of those keys. DevinAccountPanel needs five. Both receive the entire object.

Extract useCodexAccount and useDevinAccount into sibling modules and compose them here. The project guidelines call for feature folders with colocated hooks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/useAiProviderSettings.ts` around lines 111
- 154, Extract the Codex authentication lifecycle into a colocated
useCodexAccount hook and the Devin token lifecycle into a colocated
useDevinAccount hook, moving their state and related operations out of the main
provider-settings hook. Compose both hooks from useAiProviderSettings and
preserve their existing behavior and returned values, while keeping provider
form and model-catalog state in the parent. Update CodexAccountPanel and
DevinAccountPanel to consume only their respective hook results rather than the
entire provider-settings object.

Source: Coding guidelines


172-189: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard applyFetchedModels against an empty list instead of asserting.

The three models[0]!.id assertions are safe only because the single caller checks models.length > 0 at Line 200. The function itself accepts any array. A future second call site would produce a TypeError on undefined.id, and the ! removes the compile-time signal that would have caught it.

Add an early return.

♻️ Proposed guard
   const applyFetchedModels = (providerId: ProviderId, models: Array<{ id: string; label: string }>) => {
+    const first = models[0];
+    if (!first) return;
     setLiveModelsByProvider((prev) => ({ ...prev, [providerId]: models }));

Then replace each models[0]!.id with first.id.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  const applyFetchedModels = (providerId: ProviderId, models: Array<{ id: string; label: string }>) => {
    const first = models[0];
    if (!first) return;
    setLiveModelsByProvider((prev) => ({ ...prev, [providerId]: models }));
    if (providerId === "devin") {
      setDevinModels(models.map((m) => ({ id: m.id, name: m.label })));
    }
    // Keep known selections. Preserve freehand only after an explicit UI/saved choice.
    setSettingsModel((current) => {
      if (models.some((m) => m.id === current)) return current;
      if (userModelOverrideRef.current && current.trim()) return current;
      return first.id;
    });
    setCustomModel((current) => {
      if (!current) return first.id;
      if (models.some((m) => m.id === current)) return current;
      if (userModelOverrideRef.current) return current;
      return first.id;
    });
  };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/useAiProviderSettings.ts` around lines 172
- 189, Update applyFetchedModels to return immediately when models is empty,
then assign the first model to a local first value after the guard and replace
every models[0]!.id access with first.id. Preserve the existing state updates
and selection logic for non-empty model lists.

199-201: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not infer "live" when the server omits source.

Line 201 falls back to "live" whenever the response contains models but no source field. That claims a confidence the server never sent.

modelCatalogMembershipLabel in src/lib/modelCatalogMembership.ts only warns when source === "live". It deliberately stays silent for "fallback" because static lists are small. An absent source therefore makes ModelCombobox tell the user "Model ID not recognized. Requests may fail." for a perfectly valid freehand ID.

Default the unknown case to "fallback".

🐛 Proposed fix
     const models = Array.isArray(data.models) ? data.models : [];
     if (models.length > 0) applyFetchedModels(providerId, models);
-    setModelsSource(data.source ?? (models.length > 0 ? "live" : "fallback"));
+    // An absent `source` is not evidence of a live catalog; stay silent rather than warn.
+    setModelsSource(data.source ?? "fallback");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    const models = Array.isArray(data.models) ? data.models : [];
    if (models.length > 0) applyFetchedModels(providerId, models);
    // An absent `source` is not evidence of a live catalog; stay silent rather than warn.
    setModelsSource(data.source ?? "fallback");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/useAiProviderSettings.ts` around lines 199
- 201, Update the setModelsSource call in the fetch response handling to default
an omitted data.source to "fallback" rather than inferring "live" from
models.length. Preserve explicit server-provided sources and the existing model
application behavior.

218-252: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A non-OK model response is cached as a successful fetch and never retried.

The function never checks r.ok. If /api/ai/models returns HTTP 500 with a JSON error body, r.json() resolves, no error is thrown, and applyModelsResponse runs with models undefined. It sets the source to "fallback" and returns.

Line 220 already added the provider to modelsFetchedRef before the request. The catch block at Line 252 is the only place that removes it, and it never runs. The auto-refresh at Line 338 therefore skips this provider for the rest of the session. The user is stuck on the static fallback list with no explanation until they click Refresh manually.

fetchProviderStatus at Line 262 checks r.ok. Apply the same check here.

🐛 Proposed fix
       const data = (await r.json()) as {
         models?: Array<{ id: string; label: string }>;
         source?: "live" | "fallback";
         error?: string;
       };
       if (isStaleRefresh(currentSequence)) return;
+      if (!r.ok) {
+        // Allow a retry on the next selection instead of caching the failure.
+        modelsFetchedRef.current.delete(providerId);
+        setModelsSource("fallback");
+        setModelsHint(data.error || `HTTP ${r.status}`);
+        return;
+      }
       applyModelsResponse(providerId, data);

Note on the static analysis hint for Line 254: the setModelsLoading(false) reset is already inside a finally block. That rule reports a false positive here.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    if (!opts?.force && modelsFetchedRef.current.has(providerId)) return;

    modelsFetchedRef.current.add(providerId);
    refreshSequenceRef.current += 1;
    const currentSequence = refreshSequenceRef.current;
    setModelsLoading(true);
    setModelsHint(null);

    const body: { provider: string; apiKey?: string; baseUrl?: string } = {
      provider: providerId,
    };
    const key = opts?.apiKey?.trim();
    const base = opts?.baseUrl?.trim();
    if (key) body.apiKey = key;
    if (base) body.baseUrl = base;

    try {
      const r = await fetch("/api/ai/models", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const data = (await r.json()) as {
        models?: Array<{ id: string; label: string }>;
        source?: "live" | "fallback";
        error?: string;
      };
      if (isStaleRefresh(currentSequence)) return;
      if (!r.ok) {
        // Allow a retry on the next selection instead of caching the failure.
        modelsFetchedRef.current.delete(providerId);
        setModelsSource("fallback");
        setModelsHint(data.error || `HTTP ${r.status}`);
        return;
      }
      applyModelsResponse(providerId, data);
    } catch (err: unknown) {
      if (isStaleRefresh(currentSequence)) return;
      setModelsSource("fallback");
      setModelsHint(err instanceof Error ? err.message : "Could not refresh models");
      // Allow retry on next selection if network failed entirely
      modelsFetchedRef.current.delete(providerId);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/useAiProviderSettings.ts` around lines 218
- 252, Update the fetch flow in the model-refresh function to check r.ok after
the /api/ai/models request, matching fetchProviderStatus. Treat non-OK responses
as errors so execution reaches the existing catch block, removes providerId from
modelsFetchedRef, and exposes the response error rather than caching the
fallback as successful.

Source: Linters/SAST tools


323-340: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add a cancellation flag to both open-time effects.

Neither effect returns a cleanup, and both write state after an await.

The first effect depends on [isOpen]. If the user toggles the sidebar closed and open again quickly, two fetchProviderStatus calls are in flight. Both call setProviderStatus. The older response can resolve last and overwrite the newer one. onProviderMissing() can also fire for a stale result and force the tab to Settings.

The second effect depends on [isOpen, activeTab, settingsProvider]. refreshCodexAccount and refreshDevinAccount write codexAccount and devinStatus after an await with no sequence guard. refreshProviderModels is protected by refreshSequenceRef; these two are not.

Guard the post-await writes with an ignore flag returned from each effect.

🛡️ Proposed fix for the first effect
   useEffect(() => {
     if (!isOpen) return;
+    let ignore = false;
     // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: load status on open
     fetchProviderStatus().then((status) => {
+      if (ignore) return;
       if (status.source === "none") onProviderMissing();
     });
+    return () => {
+      ignore = true;
+    };
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [isOpen]);

Note that fetchProviderStatus also needs the flag, because it calls setProviderStatus internally. Pass the flag in, or move the ownership of that write into the effect.

🧰 Tools
🪛 React Doctor (0.9.3)

[warning] 323-323: fetch() inside useEffect can race, double-fire, or leak. Use a data-fetching layer or Server Component instead.

Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from useEffect.

(no-fetch-in-effect)


[error] 323-323: This setter runs after await, so overlapping re-runs of the effect can resolve out of order and write stale state; gate it behind a cancellation/ignore flag or return a cleanup that cancels the work.

In a useEffect whose dependencies can change, guard any setter call that runs after an await behind a cancellation/ignore flag, or return a cleanup that cancels the async work.

(no-set-state-after-await-in-effect)


[warning] 333-333: fetch() inside useEffect can race, double-fire, or leak. Use a data-fetching layer or Server Component instead.

Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from useEffect.

(no-fetch-in-effect)


[error] 333-333: This setter runs after await, so overlapping re-runs of the effect can resolve out of order and write stale state; gate it behind a cancellation/ignore flag or return a cleanup that cancels the work.

In a useEffect whose dependencies can change, guard any setter call that runs after an await behind a cancellation/ignore flag, or return a cleanup that cancels the async work.

(no-set-state-after-await-in-effect)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/useAiProviderSettings.ts` around lines 323
- 340, Update both open-time useEffect callbacks around fetchProviderStatus,
refreshCodexAccount, and refreshDevinAccount to create an ignore/cancellation
flag, guard every post-await state update and onProviderMissing invocation with
it, and return cleanup that sets the flag when dependencies change or the effect
unmounts. Ensure fetchProviderStatus receives or otherwise honors the flag for
its internal setProviderStatus write, while preserving the existing
refreshProviderModels sequence protection.

Source: Linters/SAST tools


419-421: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

The Codex poll loop runs for 30 seconds and cannot be cancelled.

The loop makes up to 12 requests spaced 2500 ms apart. codexBusy stays true for the whole 30 seconds, so the Sign in, Refresh, and Logout buttons in CodexAccountPanel are all disabled. The user has no way to abort.

The loop also has no unmount or isOpen check. If the user closes the sidebar after one second, the remaining 11 requests still fire and still write state.

Add an abort mechanism: store a ref that the loop checks each iteration, set it on unmount, and expose a cancel action to the panel.

Also applies to: 436-441

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/useAiProviderSettings.ts` around lines 419
- 421, Update the Codex polling flow around the loop in useAiProviderSettings to
support cancellation: store a cancellation ref, check it before each delayed
poll/request, set it during unmount and when the relevant panel is no longer
open, and stop further state updates or requests once cancelled. Expose a cancel
action from the hook for CodexAccountPanel so Sign in, Refresh, and Logout can
abort the active poll and clear codexBusy.

423-435: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Provider-mutation responses are never checked, so the UI reports success it did not verify. Three call sites send a provider write to /api/ai/provider and ignore the outcome. Each one then advances the UI as if the write succeeded. Every other request in this hook that gates UI state checks r.ok; these do not.

  • src/components/features/assistant/useAiProviderSettings.ts#L423-L435: capture the activation response, throw when !r.ok, and move the "Codex is active for audit/chat" message and the onProviderActivated() call to after the check. Today a failed POST still switches to the Audit tab and starts an analysis against an unset provider.
  • src/components/features/assistant/useAiProviderSettings.ts#L491-L496: capture the activation response and throw when !r.ok so the existing catch sets providerSaveError. Today the signed-in confirmation on Line 489 stays on screen after a failed activation.
  • src/components/features/assistant/useAiProviderSettings.ts#L632-L636: wrap the DELETE in try/catch, check r.ok, and set providerSaveError on failure. Today a rejection becomes an unhandled promise rejection at the void providers.clearProvider() call site and onProviderCleared() never runs.
📍 Affects 1 file
  • src/components/features/assistant/useAiProviderSettings.ts#L423-L435 (this comment)
  • src/components/features/assistant/useAiProviderSettings.ts#L491-L496
  • src/components/features/assistant/useAiProviderSettings.ts#L632-L636
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/features/assistant/useAiProviderSettings.ts` around lines 423
- 435, Check every provider mutation response before advancing UI state: in
src/components/features/assistant/useAiProviderSettings.ts lines 423-435,
capture the Codex activation response, throw on !r.ok, and only then set the
active message and call onProviderActivated(); at lines 491-496, capture the
activation response and throw on failure so the existing catch sets
providerSaveError; at lines 632-636, wrap the DELETE in try/catch, check r.ok,
set providerSaveError on failure, and call onProviderCleared() only after
success.

- persistentClient.ts: guard onclose callback against superseded transports
  (compare reference identity before clearing), and close transport before
  nulling references in the infrastructure failure path
- docs_search.py: replace whitespace controls with spaces instead of
  stripping all control chars (preserves token boundaries)
- passAccessors.ts: add explicit case 'ptq' and log warning on unknown
  quantMethod in default branch
- useMcpDiagnostic.ts: clear diagnostic state before new request
- InputEnvironmentPanel.tsx: remove unused filteredRecipes binding
- mcp.test.ts: reset callOliveMcpToolImpl in beforeEach, throw on
  unset mock instead of spawning real Python
- HardwareProbeDisplay.tsx: only apply provider change when
  prepareProviderChange returns a valid patch
When callOliveMcpToolImpl is null, return { unavailable: true }
instead of throwing — this matches the breaker-open behavior the
route handler expects (maps to 503). Throwing caused a 500 which
broke the 'short-circuits with 503' test.
Comment on lines +216 to +221
if (hadInfraFailure) {
mcpBreaker.recordFailure(epoch);
// Mark connection as crashed for next attempt
state = "crashed";
client = null;
transport = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Infrastructure failures abandon MCP transport

When an MCP call fails with a non-timeout infrastructure error such as EPIPE or a closed connection, this branch clears the active client and transport references without closing them, leaving the Python process and streams alive. The abandoned transport's unguarded onclose callback can then clear a replacement connection, causing subsequent MCP requests to fail.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/server/services/mcp/persistentClient.ts
Line: 216-221

Comment:
**Infrastructure failures abandon MCP transport**

When an MCP call fails with a non-timeout infrastructure error such as EPIPE or a closed connection, this branch clears the active client and transport references without closing them, leaving the Python process and streams alive. The abandoned transport's unguarded `onclose` callback can then clear a replacement connection, causing subsequent MCP requests to fail.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Devin

@tonythethompson
tonythethompson merged commit 68aedbb into main Aug 9, 2026
13 of 14 checks passed
@tonythethompson
tonythethompson deleted the phase_2 branch August 9, 2026 13:45
@linear-code

linear-code Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

TS-172

@tonythethompson
tonythethompson restored the phase_2 branch August 9, 2026 13:51
tonythethompson added a commit that referenced this pull request Aug 10, 2026
…nitization (#198)

* fix(mcp): guard stale transport close, close on failure, fix query sanitization

Addresses unresolved review comments from PR #192:

- persistentClient.ts: onclose callback now compares transport identity
  before clearing state — prevents superseded transports from nulling a
  replacement connection (Greptile P1, Codex P2)
- persistentClient.ts: infra failure path now calls transport.close()
  before nulling references, freeing the abandoned Python child process
- docs_search.py: replace whitespace controls (\n, \t, \r) with spaces
  instead of stripping — preserves token boundaries so 'quantization\n
  calibration' becomes two keywords, not one (Codex P2)
- test_docs_search_semantic.py: assert sanitized query value and length,
  not just 'count >= 0' (CodeRabbit)

* test(mcp): expand query sanitization and transport lifecycle coverage

- Enhance semantic search query sanitization tests to validate all whitespace control characters (\n, \t, \r, \x0b, \x0c) are normalized to spaces
- Add test case verifying truncation applies before control normalization, ensuring final query length stays within 2000 char limit
- Expand transport lifecycle tests to verify active transport close marks crashed state and clears client/transport refs
- Add test for stale transport close behavior, confirming replacement client and transport remain unchanged
- Implement infrastructure failure cleanup test validating transport is closed on infra errors
- Add test ensuring newer reconnect sessions are not affected by older transport failures
- Mock StdioClientTransport with close method and transport instance tracking for comprehensive lifecycle assertions
- Expose getPersistentClientSnapshotForTests and setPersistentClientSnapshotForTests helpers for test state inspection and manipulation
tonythethompson added a commit that referenced this pull request Aug 10, 2026
- docs/v0.2-tech-debt-plan.md: add 'text' language identifier to
  fenced code block (markdownlint MD040)
- POWER.md: reconcile MCP tool count 26→27 to match _TOOL_IMPORTS
  (CodeRabbit)
- mcpClient.ts: extract unwrapToolPayload() helper to DRY the
  envelope-unwrap logic duplicated at lines 49-54 and 145-149
  (CodeRabbit Trivial)
tonythethompson added a commit that referenced this pull request Aug 10, 2026
* fix: address PR #192 UI/lib review comments

- passAccessors.ts: add explicit case 'ptq', warn on unknown quantMethod
  in default branch (Qodo P2)
- useMcpDiagnostic.ts: clear diagnostic state before new request to
  prevent stale advice displaying alongside a new error (CodeRabbit)
- InputEnvironmentPanel.tsx: remove unused 'filteredRecipes' binding
  (CodeRabbit, ESLint)
- localFileUtils.ts: reject gapped chunk sequences — require >=2 files
  with consecutive numeric suffixes before allowing reconstruction.
  Prevents silent data corruption from missing parts (CodeRabbit Major)
- usePresets.ts: add reader.onerror handler so file read failures
  surface to the user instead of silently dropping (CodeRabbit)

* fix: Require chunk sequences to start at 001

* Harden preset import and chunk validation

Improves input safety in two areas: chunked local files now only reconstruct when suffixes are canonical and strictly consecutive starting at `001`, and successful preset imports now clear any prior error state. Adds focused tests for `getReconstructableGroups` edge cases and a regression test for `useImportPresets` to ensure errors are cleared after a later successful import.

* fix(presets): clear stale import confirmation on file read errors

- Remove automatic scroll to execute section when OLIVE run starts
- Clear import confirmation state when file read fails to prevent stale UI state
- Add test coverage for clearing confirmation on read errors in sequential imports
- Improves reliability of preset import flow by ensuring confirmation state stays in sync with actual file state

* fix(presets): guard stale FileReader callbacks and clear confirm on parse failure

Prevent overlapping preset imports from clobbering a newer confirmation dialog, and clear importConfirm when JSON parse fails after a prior success.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: qodo-code-review[bot] <151058649+qodo-code-review[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
tonythethompson added a commit that referenced this pull request Aug 10, 2026
* fix: docs/config quick wins from PR #192 review

- docs/v0.2-tech-debt-plan.md: add 'text' language identifier to
  fenced code block (markdownlint MD040)
- POWER.md: reconcile MCP tool count 26→27 to match _TOOL_IMPORTS
  (CodeRabbit)
- mcpClient.ts: extract unwrapToolPayload() helper to DRY the
  envelope-unwrap logic duplicated at lines 49-54 and 145-149
  (CodeRabbit Trivial)

* refactor(input): extract useRecipeHub hook from InputEnvironmentPanel (#157)

Move recipe hub state and handlers (applyCuratedRecipe, handleFetchRemote,
handleImport, tab management) into a dedicated useRecipeHub hook. Reduces
the main component from 2,022 to 1,885 lines and lowers cyclomatic
complexity by extracting branching logic (try/catch, validation gates).

* refactor(server): reduce method complexity in providerRoutes, ollamaRoutes, system (#152, #158)

- providerRoutes.ts: Extract fetchSpecialProviderCatalog() for codex/devin/cloudflare
  model catalog dispatch, reducing POST /ai/models nesting depth
- ollamaRoutes.ts: Extract readOllamaPullStream() for the NDJSON pull stream
  reading loop, reducing the inline handler from ~222 to ~170 lines
- system.ts: Wire probeSystemHardware to use the already-extracted
  probeGpuHardware() and probeVenvCapabilities() helpers, replacing ~111
  lines of inline venv iteration with a clean for-loop + helper call

* fix: address CodeFactor findings on PR #201

- Remove 9 unused imports from InputEnvironmentPanel (leftover from
  useRecipeHub extraction): useTransition, compareCatalogMetadataToRecipe,
  deriveUiStateFromOliveRecipe, fetchGitHubRecipeJson, getCatalogDeviceFromRecipe,
  RecipeCatalogItem, parseRecipeJson, assessCatalogItemHardwareCompatibility,
  assessRecipeHardwareCompatibility
- Remove unused recipeRailExpanded destructuring
- Extract resolveProviderCredentials() from POST /ai/provider handler
- Extract verifyAndSendPullResult() from ollama-pull handler

* fix: restore catalog fallbacks and harden extracted helpers

Preserve HTTP 200 fallback catalogs for Codex/Devin/Cloudflare failures, ignore stale curated-recipe applies, finish the final Ollama NDJSON record, avoid duplicate timeout SSE errors, and detect DirectML from the default ORT provider list only.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
@tonythethompson
tonythethompson deleted the phase_2 branch August 10, 2026 02:26

This branch was successfully deployed

1 active deployment
Preview — 2c5d2e60 Deployed Aug 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant