chore: another plan update

This commit is contained in:
2026-03-01 09:18:42 +01:00
parent 608205adcd
commit d65272054f

View File

@@ -57,25 +57,27 @@ bDS currently routes all AI chat through the OpenCode Zen gateway (`opencode.ai/
- Extend `ChatReadyStatus` to report per-provider availability, e.g. `providers: { opencode: boolean, mistral: boolean }` - Extend `ChatReadyStatus` to report per-provider availability, e.g. `providers: { opencode: boolean, mistral: boolean }`
- Callers (`Sidebar.tsx`, `sendMessage()`) must gate on the relevant provider, not a single boolean - Callers (`Sidebar.tsx`, `sendMessage()`) must gate on the relevant provider, not a single boolean
**F. Add Mistral request path in `sendMessage()`** **F. Parameterize `sendOpenAIMessage()` for Mistral (no separate method)**
- Route `provider === 'mistral'` to new `sendMistralRequest()` method - Mistral uses the identical OpenAI-compatible chat/completions format — creating a separate `sendMistralRequest()` would be a near-duplicate
- Similar to OpenAI path but: - Instead, parameterize `sendOpenAIMessage()` to accept URL, API key, and provider-specific options:
- URL: `MISTRAL_API_URL` (direct, not through OpenCode gateway) - Add params: `apiUrl: string`, `apiKey: string`, `providerOptions?: { parallelToolCalls?: boolean }`
- Auth: `Authorization: Bearer ${this.mistralApiKey}` - `sendMessage()` determines provider via `detectProvider()` and calls `sendOpenAIMessage()` with the correct URL/key/options
- Context budget: per-model (see Target Models table above) - For OpenCode OpenAI path: URL = `ZEN_OPENAI_URL`, key = `this.apiKey`
- `tool_choice: "auto"` — Mistral's default; do **not** set `"any"` (which forces a tool call every turn even when the model should respond with text). Omit `tool_choice` entirely, since `"auto"` is the default, matching existing OpenCode behavior - For Mistral: URL = `MISTRAL_API_URL`, key = `this.mistralApiKey`, `parallelToolCalls: false`
- `parallel_tool_calls: false` — set explicitly; our tool executor runs tools sequentially, so parallel tool calls would break the execution loop - `tool_choice`: omit entirely for all OpenAI-compatible providers (default `"auto"` is correct)
- `parallel_tool_calls: false` — set explicitly for Mistral only; our tool executor runs tools sequentially, so parallel tool calls would break the execution loop
**F2. Add `MODEL_CONTEXT_BUDGETS` map** **F2. Add `MODEL_CONTEXT_BUDGETS` map**
- New constant map `MODEL_CONTEXT_BUDGETS: Record<string, number>` with per-model token budgets - New constant map `MODEL_CONTEXT_BUDGETS: Record<string, number>` with per-model token budgets
- `truncateToTokenBudget()` (L1654) currently defaults to `maxContextTokens = 150000` - `truncateToTokenBudget()` (L1654) currently defaults to `maxContextTokens = 150000`
- In `sendAnthropicMessage()` and `sendOpenAIMessage()`: pass the model's context budget from the map (defaulting to 150,000 for OpenCode models) - In `sendAnthropicMessage()` and `sendOpenAIMessage()`: pass the model's context budget from the map (defaulting to 150,000 for OpenCode models)
- In `sendMistralRequest()`: look up `MODEL_CONTEXT_BUDGETS[modelId]` and pass to truncation - The parameterized `sendOpenAIMessage()` looks up `MODEL_CONTEXT_BUDGETS[modelId]` for Mistral models and passes to truncation
- Values from Target Models table (35k, 120k, 240k) - Values from Target Models table (35k, 120k, 240k)
**G. Fix tool-call message history in OpenAI-compatible path** **G. Fix tool-call message history in OpenAI-compatible path**
- `sendOpenAIMessage()` currently only keeps `user`/`assistant` messages in history, discarding `tool` role messages - Within a single `sendMessage()` call, the tool loop correctly tracks tool results across rounds
- Tool results must be persisted in conversation history so follow-up rounds have context - However, `tool` role messages are not persisted to DB-backed conversation history — on conversation resume, the model loses context about prior tool results
- Ensure `tool` role messages are included when persisting conversation history so cross-session continuity works
- This affects all OpenAI-compatible providers (OpenCode OpenAI path + Mistral) - This affects all OpenAI-compatible providers (OpenCode OpenAI path + Mistral)
**H. Fix vision in OpenAI-compatible path (affects Mistral too)** **H. Fix vision in OpenAI-compatible path (affects Mistral too)**
@@ -84,10 +86,12 @@ bDS currently routes all AI chat through the OpenCode Zen gateway (`opencode.ai/
`{ type: 'image_url', image_url: { url: 'data:image/webp;base64,...' } }` `{ type: 'image_url', image_url: { url: 'data:image/webp;base64,...' } }`
- This fixes vision for all OpenAI-compatible providers, not just Mistral - This fixes vision for all OpenAI-compatible providers, not just Mistral
**I. Update `getAvailableModels()`** **I. Update `getAvailableModels()` — merge from both providers**
- When Mistral key is set, include Mistral models in returned list - **Model list merge strategy**: fetch models from each configured provider's API endpoint and merge into a single list. When both keys are configured, return models from both; when only one key is set, return only that provider's models; when no key is set, return an empty list (UI disables the dropdown)
- Add `provider` field to model entries so UI can group them - OpenCode models: fetched from existing OpenCode API (as today)
- Invalidate `cachedModels`/`cachedModelsAt` when Mistral key is added or removed - Mistral models: fetched from `GET https://api.mistral.ai/v1/models` when Mistral key is set; cross-reference returned IDs with `MODEL_DISPLAY_NAMES` to use display names + static `vision`/`contextBudget` metadata
- Every model entry carries `provider: 'opencode' | 'mistral'` so the UI and engine can resolve the correct API URL + key
- Invalidate `cachedModels`/`cachedModelsAt` when any provider key is added or removed
**J. Update `generateConversationTitle()` — make configurable in Preferences** **J. Update `generateConversationTitle()` — make configurable in Preferences**
- Currently hardcoded to `claude-haiku-4-5` via `ZEN_ANTHROPIC_URL` with OpenCode key - Currently hardcoded to `claude-haiku-4-5` via `ZEN_ANTHROPIC_URL` with OpenCode key
@@ -107,9 +111,15 @@ bDS currently routes all AI chat through the OpenCode Zen gateway (`opencode.ai/
**L. Update `analyzeTaxonomy()`** **L. Update `analyzeTaxonomy()`**
- Currently uses `this.apiKey` (OpenCode) for both Anthropic and OpenAI paths - Currently uses `this.apiKey` (OpenCode) for both Anthropic and OpenAI paths
- Has an early-return guard `if (!this.apiKey)` that must become provider-aware — check Mistral key when provider is Mistral
- When a Mistral model is selected: use Mistral API key + `MISTRAL_API_URL` - When a Mistral model is selected: use Mistral API key + `MISTRAL_API_URL`
- Must branch on provider to select correct key and URL - Must branch on provider to select correct key and URL
**L2. Update `analyzeMediaImage()` API key guard**
- Same issue: has `if (!this.apiKey)` early-return guard
- Must become provider-aware — check the relevant provider's key based on the selected image analysis model
- When routed to Mistral: check `this.mistralApiKey` instead of `this.apiKey`
**M. Convert chat HTTP calls to SSE streaming** **M. Convert chat HTTP calls to SSE streaming**
Currently `httpRequest()` buffers the entire response body before any text reaches the UI. Users wait 530s per API round with only a bouncing-dots indicator. All three providers (Anthropic, OpenAI, Mistral) support `stream: true` with SSE. Currently `httpRequest()` buffers the entire response body before any text reaches the UI. Users wait 530s per API round with only a bouncing-dots indicator. All three providers (Anthropic, OpenAI, Mistral) support `stream: true` with SSE.
@@ -185,7 +195,7 @@ data: {"type":"message_stop"}
**M4. Request body changes** **M4. Request body changes**
- `sendAnthropicMessage()`: add `"stream": true` to request body - `sendAnthropicMessage()`: add `"stream": true` to request body
- `sendOpenAIMessage()` / `sendMistralRequest()`: add `"stream": true` and `"stream_options": { "include_usage": true }` to request body — this is **required** to receive token usage in streaming mode (without it, usage is omitted from streamed responses) - `sendOpenAIMessage()` (used for both OpenCode OpenAI and Mistral): add `"stream": true` and `"stream_options": { "include_usage": true }` to request body — this is **required** to receive token usage in streaming mode (without it, usage is omitted from streamed responses)
**M5. Tool call accumulation during streaming** **M5. Tool call accumulation during streaming**
- Tool call arguments arrive as partial JSON fragments across many SSE events - Tool call arguments arrive as partial JSON fragments across many SSE events
@@ -217,9 +227,8 @@ data: {"type":"message_stop"}
### 2. `src/main/engine/ChatEngine.ts` - Settings persistence ### 2. `src/main/engine/ChatEngine.ts` - Settings persistence
**A. Add Mistral key helpers** **A. Add Mistral key helpers**
- `getMistralApiKey()` - read from settings table - Use existing generic `getSetting()`/`setSetting()` with key `'mistral_api_key'` — no dedicated methods needed, avoids unnecessary boilerplate
- `setMistralApiKey(key)` - persist to settings table - ChatEngine already exposes generic helpers for reading/writing the settings table
- Settings key: `'mistral_api_key'`
**B. Default model is user-driven** **B. Default model is user-driven**
- `getSelectedModel()` defaults to `'claude-sonnet-4-5'` - `getSelectedModel()` defaults to `'claude-sonnet-4-5'`
@@ -284,8 +293,10 @@ data: {"type":"message_stop"}
- Same pattern: masked display, change button, validation on save - Same pattern: masked display, change button, validation on save
**B. Update model selector** **B. Update model selector**
- Group models by provider in dropdown (optgroup: "OpenCode Zen", "Mistral AI") - SettingsView uses a native `<select>` element — group models by provider using `<optgroup>` labels ("OpenCode Zen", "Mistral AI")
- Show provider badge next to selected model - When no API key is configured for any provider, disable the `<select>` dropdown
- When both keys configured, show merged list from both providers; when only one key set, show only that provider's models
- **Note**: `availableModels` state is currently typed as `{id: string; name: string}[]` — must be updated to `ChatModel[]` (which includes `provider` and `vision` fields) so provider grouping and vision filtering work
**C. Add per-purpose model preferences** **C. Add per-purpose model preferences**
- "Title generation model" dropdown — select cheapest/fastest model for auto-titling conversations - "Title generation model" dropdown — select cheapest/fastest model for auto-titling conversations
@@ -296,8 +307,9 @@ data: {"type":"message_stop"}
### 6. `src/renderer/components/ChatPanel/ChatPanel.tsx` - Chat UI ### 6. `src/renderer/components/ChatPanel/ChatPanel.tsx` - Chat UI
**A. Update model selector in chat** **A. Update model selector in chat**
- Group by provider in dropdown - ChatPanel uses a custom dropdown (CSS `model-dropdown` with `<button>` elements, not a native `<select>`) — add provider group headers (non-clickable divider labels) within the dropdown to visually separate providers
- Only show models for configured providers - Only show models for configured providers; when no keys configured, hide the model selector entirely
- When both providers configured, merge models from both with visual grouping
### 7. `src/renderer/components/AssistantSidebar/` - Assistant UI ### 7. `src/renderer/components/AssistantSidebar/` - Assistant UI
@@ -318,18 +330,24 @@ data: {"type":"message_stop"}
**A. Update model selector** **A. Update model selector**
- Has its own model selector (`ChatModel[]` state + `getAvailableModels()` call) for taxonomy analysis - Has its own model selector (`ChatModel[]` state + `getAvailableModels()` call) for taxonomy analysis
- Currently renders a flat model list with no provider grouping - Currently renders a flat model list with no provider grouping
- Apply same provider grouping as ChatPanel (optgroup by provider) - Apply provider grouping matching the component's existing dropdown pattern
- Default to whatever is set in Preferences as default model (via `getSelectedModel()`) - Default to whatever is set in Preferences as default model (via `getSelectedModel()`)
- Use the shared `ModelSelector` component (see section 9b)
### 9b. `src/renderer/components/shared/ModelSelector.tsx` - Shared model selector component (NEW) ### 9b. Model selector UI approach
**Extract a reusable `ModelSelector` component** used by SettingsView, ChatPanel, and ImportAnalysisView: **Two different dropdown patterns exist** — keep each surface consistent with its current UX:
- Props: `models: ChatModel[]`, `selectedModelId: string`, `onChange: (modelId: string) => void`, `filterVisionOnly?: boolean`, `allowDefault?: boolean`, `disabled?: boolean` - **SettingsView** uses native `<select>` elements → use `<optgroup>` for provider grouping (standard HTML pattern)
- Groups models into `<optgroup>` by `model.provider` (labels: "OpenCode Zen", "Mistral AI") - **ChatPanel** uses a custom CSS dropdown (`model-dropdown` with `<button>` elements) → add non-clickable provider group headers as dividers
- When `filterVisionOnly` is true, only shows models with `vision: true` (for image analysis model selector) - **ImportAnalysisView** — check which pattern it uses and match accordingly
- When `allowDefault` is true, adds a "Default" option at the top (for per-purpose model preferences) - Shared logic (filtering by vision, adding "Default" option, provider grouping) can be extracted into a utility function or hook rather than a full component, since the rendering pattern differs per surface
- All three surfaces currently duplicate this dropdown logic — extracting prevents drift - Props for the shared utility: `models: ChatModel[]`, `filterVisionOnly?: boolean`, `includeDefault?: boolean` → returns grouped/filtered model list
### 9c. `src/renderer/navigation/useChatMessageSender.ts` - Shared chat hook
**A. Verify no provider assumptions**
- Used by both ChatPanel and AssistantSidebar to send messages
- Currently delegates to `sendConversationMessage()` from `chatSession.ts` — verify neither has hardcoded provider/model assumptions
- No code changes expected, but must be verified during implementation
### 10. Preload/IPC registration ### 10. Preload/IPC registration
@@ -355,8 +373,8 @@ Keys needed:
- `settings.ai.imageAnalysisModelLabel` — "Image analysis model" - `settings.ai.imageAnalysisModelLabel` — "Image analysis model"
- `settings.ai.imageAnalysisModelDescription` — description text - `settings.ai.imageAnalysisModelDescription` — description text
- `settings.ai.defaultOption` — "Default" (for per-purpose model selectors) - `settings.ai.defaultOption` — "Default" (for per-purpose model selectors)
- `settings.ai.providerGroupOpenCode` — "OpenCode Zen" (optgroup label) - `settings.ai.providerGroupOpenCode` — "OpenCode Zen" (provider group label)
- `settings.ai.providerGroupMistral` — "Mistral AI" (optgroup label) - `settings.ai.providerGroupMistral` — "Mistral AI" (provider group label)
- `chat.providerKeyMissing` — "The model '{model}' requires a {provider} API key. Go to Settings to configure it." - `chat.providerKeyMissing` — "The model '{model}' requires a {provider} API key. Go to Settings to configure it."
- `chat.apiKeyRequiredTitle` — make generic or multi-provider (currently hardcoded to "OpenCode Zen API Key Required") - `chat.apiKeyRequiredTitle` — make generic or multi-provider (currently hardcoded to "OpenCode Zen API Key Required")
- `chat.apiKeyRequiredDescription` — make generic or multi-provider (currently hardcoded to OpenCode-specific text) - `chat.apiKeyRequiredDescription` — make generic or multi-provider (currently hardcoded to OpenCode-specific text)
@@ -369,11 +387,16 @@ Keys needed:
- No changes needed — AI/chat features are explicitly not exposed via Python API - No changes needed — AI/chat features are explicitly not exposed via Python API
### 14. Main-process i18n locales - `src/main/shared/i18n/locales/`
- No changes expected — chat-related strings are renderer-only
- Verify no main-process strings reference "OpenCode" in a way that needs updating for multi-provider support
## Tests to Update ## Tests to Update
### New tests ### New tests
- OpenCodeManager: Mistral key storage, `detectProvider('mistral-*')` + `detectProvider('devstral-*')` + `detectProvider('codestral-*')` + `detectProvider('pixtral-*')`, `sendMistralRequest()`, vision image conversion in OpenAI path, tool-call message persistence in OpenAI path, `generateConversationTitle()` Mistral routing, model cache invalidation, `MODEL_CONTEXT_BUDGETS` correctness, SSE line parsing (both OpenAI/Mistral and Anthropic formats), `[DONE]` sentinel handling, tool-call argument accumulation during streaming, mid-stream error handling, `stream_options` in request bodies - OpenCodeManager: Mistral key storage, `detectProvider('mistral-*')` + `detectProvider('devstral-*')` + `detectProvider('codestral-*')` + `detectProvider('pixtral-*')`, parameterized `sendOpenAIMessage()` with Mistral URL/key, vision image conversion in OpenAI path, tool-call message persistence in OpenAI path, `generateConversationTitle()` Mistral routing, model cache merge (both providers), `MODEL_CONTEXT_BUDGETS` correctness, SSE line parsing (both OpenAI/Mistral and Anthropic formats), `[DONE]` sentinel handling, tool-call argument accumulation during streaming, mid-stream error handling, `stream_options` in request bodies, provider-aware API key guards in `analyzeTaxonomy()`/`analyzeMediaImage()`
- ChatEngine: `getMistralApiKey()`/`setMistralApiKey()`, `getTitleModel()`/`setTitleModel()`, `getImageAnalysisModel()`/`setImageAnalysisModel()`, default model fallback - ChatEngine: Mistral key via generic `getSetting/setSetting`, `getTitleModel()`/`setTitleModel()`, `getImageAnalysisModel()`/`setImageAnalysisModel()`, default model fallback
- chatHandlers: new Mistral IPC handlers, per-purpose model preference handlers - chatHandlers: new Mistral IPC handlers, per-purpose model preference handlers
### Existing tests to update ### Existing tests to update
@@ -383,7 +406,7 @@ Keys needed:
- `electronApiContract.test.ts``ElectronAPI.chat` shape now includes Mistral methods - `electronApiContract.test.ts``ElectronAPI.chat` shape now includes Mistral methods
- 10 renderer test files that mock `window.electronAPI.chat` (12 mock blocks total) — add Mistral method stubs to mocks: - 10 renderer test files that mock `window.electronAPI.chat` (12 mock blocks total) — add Mistral method stubs to mocks:
- `tests/renderer/components/SidebarChat.test.tsx` - `tests/renderer/components/SidebarChat.test.tsx`
- `tests/renderer/components/SettingsView.test.tsx` (2 mock blocks) - `tests/renderer/components/SettingsView.test.tsx`
- `tests/renderer/components/SettingsView.i18n.test.tsx` - `tests/renderer/components/SettingsView.i18n.test.tsx`
- `tests/renderer/components/TabBar.test.tsx` - `tests/renderer/components/TabBar.test.tsx`
- `tests/renderer/components/EditorDashboardTimeline.test.tsx` - `tests/renderer/components/EditorDashboardTimeline.test.tsx`
@@ -397,13 +420,13 @@ Keys needed:
1. Tests first (per AGENTS.md) 1. Tests first (per AGENTS.md)
2. Types (`electronApi.ts``ChatModel`, `ChatReadyStatus`, `ElectronAPI.chat`) 2. Types (`electronApi.ts``ChatModel`, `ChatReadyStatus`, `ElectronAPI.chat`)
3. Engine (`OpenCodeManager.ts` — constants, `MODEL_CONTEXT_BUDGETS`, detection, key storage, `checkReady()`, request path, vision fix, title generation fallback) 3. Engine (`OpenCodeManager.ts` — constants, `MODEL_CONTEXT_BUDGETS`, detection, key storage, `checkReady()`, parameterized `sendOpenAIMessage()`, vision fix, provider-aware guards, title generation fallback, model cache merge)
4. SSE streaming (`OpenCodeManager.ts``httpRequestStream()`, SSE parsers for Anthropic + OpenAI/Mistral formats, `stream: true` + `stream_options` in request bodies) 4. SSE streaming (`OpenCodeManager.ts``httpRequestStream()`, SSE parsers for Anthropic + OpenAI/Mistral formats, `stream: true` + `stream_options` in request bodies)
5. Persistence (`ChatEngine.ts` — settings helpers, per-purpose model preferences, default model fallback) 5. Persistence (`ChatEngine.ts` — settings helpers, per-purpose model preferences, default model fallback)
6. IPC (`chatHandlers.ts` — new handlers, init flow update) 6. IPC (`chatHandlers.ts` — new handlers, init flow update)
7. Preload (`preload.ts` — bridge new channels) 7. Preload (`preload.ts` — bridge new channels)
8. i18n (all 5 locale files) 8. i18n (all 5 locale files)
9. Shared components (`ModelSelector.tsx` — extract reusable model selector with provider grouping) 9. Shared utilities (model grouping/filtering utility for provider-aware dropdowns)
10. UI (`SettingsView/SettingsView.tsx`, `ChatPanel.tsx`, `ImportAnalysisView.tsx`, `Sidebar.tsx` — use shared `ModelSelector`) 10. UI (`SettingsView/SettingsView.tsx`, `ChatPanel.tsx`, `ImportAnalysisView.tsx`, `Sidebar.tsx` — use shared `ModelSelector`)
11. Update existing test mocks 11. Update existing test mocks
12. Build verification (`npm run build`) 12. Build verification (`npm run build`)
@@ -414,6 +437,7 @@ Keys needed:
|--------|-------------------|----------------------|---------| |--------|-------------------|----------------------|---------|
| Base URL | `opencode.ai/zen/v1/messages` | `opencode.ai/zen/v1/chat/completions` | `api.mistral.ai/v1/chat/completions` | | Base URL | `opencode.ai/zen/v1/messages` | `opencode.ai/zen/v1/chat/completions` | `api.mistral.ai/v1/chat/completions` |
| Auth header | `Bearer ${openCodeKey}` | `Bearer ${openCodeKey}` | `Bearer ${mistralKey}` | | Auth header | `Bearer ${openCodeKey}` | `Bearer ${openCodeKey}` | `Bearer ${mistralKey}` |
| Request method | `sendAnthropicMessage()` | `sendOpenAIMessage(url, key)` | `sendOpenAIMessage(url, key, opts)` (same method, parameterized) |
| Tool choice | not set | not set | not set (default `"auto"`) | | Tool choice | not set | not set | not set (default `"auto"`) |
| Parallel tools | not set | not set | `parallel_tool_calls: false` | | Parallel tools | not set | not set | `parallel_tool_calls: false` |
| Context budget | 150k tokens | 150k tokens | per-model (see Target Models table) | | Context budget | 150k tokens | 150k tokens | per-model (see Target Models table) |
@@ -422,6 +446,7 @@ Keys needed:
| HTTP mode | SSE streaming (`stream: true`) | SSE streaming (`stream: true`) | SSE streaming (`stream: true`) | | HTTP mode | SSE streaming (`stream: true`) | SSE streaming (`stream: true`) | SSE streaming (`stream: true`) |
| Title generation | `claude-haiku-4-5` default | N/A | `mistral-small-2506` default | | Title generation | `claude-haiku-4-5` default | N/A | `mistral-small-2506` default |
| Image analysis | `claude-sonnet-4-5` default | N/A | user-selected vision model | | Image analysis | `claude-sonnet-4-5` default | N/A | user-selected vision model |
| Model source | fetched from OpenCode API | fetched from OpenCode API | fetched from `api.mistral.ai/v1/models` |
## Verification ## Verification
@@ -448,15 +473,21 @@ Keys needed:
5. **Vision in OpenAI path** — Fix `image_url` conversion for all OpenAI-compatible providers (not just Mistral) 5. **Vision in OpenAI path** — Fix `image_url` conversion for all OpenAI-compatible providers (not just Mistral)
6. **MCP server** — N/A; only exposes tools, no bDS-side AI runs 6. **MCP server** — N/A; only exposes tools, no bDS-side AI runs
7. **Python API** — N/A; AI/chat not exposed via Python API 7. **Python API** — N/A; AI/chat not exposed via Python API
8. **Model dropdown grouping**Use `<optgroup>` labels ("OpenCode Zen", "Mistral AI"); extract shared `ModelSelector` component to avoid duplication across 3 surfaces 8. **Model dropdown grouping**SettingsView uses native `<select>` with `<optgroup>`; ChatPanel uses custom CSS dropdown with provider header dividers; shared utility extracts grouping/filtering logic while each surface keeps its own rendering pattern
9. **SSE streaming** — Convert all chat HTTP calls to `stream: true` + SSE parsing; keep one-shot requests (title, image analysis, taxonomy, validation) non-streaming. Renderer needs zero changes — existing `onDelta` pipeline already supports incremental tokens. Token usage requires `stream_options: { include_usage: true }` for OpenAI/Mistral format; Anthropic provides usage in `message_start` + `message_delta` events 9. **SSE streaming** — Convert all chat HTTP calls to `stream: true` + SSE parsing; keep one-shot requests (title, image analysis, taxonomy, validation) non-streaming. Renderer needs zero changes — existing `onDelta` pipeline already supports incremental tokens. Token usage requires `stream_options: { include_usage: true }` for OpenAI/Mistral format; Anthropic provides usage in `message_start` + `message_delta` events
10. **OpenAI tool-call history**Fix `sendOpenAIMessage()` to persist `tool` role messages in conversation history (not just `user`/`assistant`), so follow-up tool rounds have context 10. **OpenAI tool-call history**Within a single `sendMessage()` call, tool results are tracked correctly across rounds. The fix is about persisting `tool` role messages to DB-backed conversation history so cross-session resume works
11. **ImportAnalysisView** — Has its own model selector; apply same provider grouping; default to Preferences model 11. **ImportAnalysisView** — Has its own model selector; apply same provider grouping; default to Preferences model
12. **AssistantSidebar** — No model selector of its own; uses Preferences default model; no code changes needed 12. **AssistantSidebar** — No model selector of its own; uses Preferences default model; no code changes needed
13. **`tool_choice`** — Do NOT set `tool_choice: "any"` for Mistral (this forces tool use every turn). Omit it entirely; Mistral defaults to `"auto"`, same as OpenCode. Set `parallel_tool_calls: false` explicitly since our tool executor is sequential 13. **`tool_choice`** — Do NOT set `tool_choice: "any"` for Mistral (this forces tool use every turn). Omit it entirely; Mistral defaults to `"auto"`, same as OpenCode. Set `parallel_tool_calls: false` explicitly since our tool executor is sequential
14. **`detectProvider()` prefixes** — Cover all Mistral model families: `mistral`, `ministral`, `devstral`, `codestral`, `pixtral` 14. **No separate `sendMistralRequest()`** — Parameterize `sendOpenAIMessage()` with URL/key/options instead of creating a near-duplicate method; Mistral uses the identical OpenAI-compatible format
15. **`detectProvider()` prefixes** — Cover all Mistral model families: `mistral`, `ministral`, `devstral`, `codestral`, `pixtral`
15. **`formatModelName()` / `UPPERCASE_PREFIXES`** — No changes needed; all 5 Mistral models are in `MODEL_DISPLAY_NAMES`; auto-format fallback handles future unknown models correctly 15. **`formatModelName()` / `UPPERCASE_PREFIXES`** — No changes needed; all 5 Mistral models are in `MODEL_DISPLAY_NAMES`; auto-format fallback handles future unknown models correctly
16. **Context budgets** — Stored in `MODEL_CONTEXT_BUDGETS` map; passed explicitly to `truncateToTokenBudget()` per provider path; OpenCode defaults to 150k, Mistral per-model (see Target Models table) 16. **Context budgets** — Stored in `MODEL_CONTEXT_BUDGETS` map; passed explicitly to `truncateToTokenBudget()` per provider path; OpenCode defaults to 150k, Mistral per-model (see Target Models table)
17. **Error UX for removed provider key** — Inline error banner in ChatPanel (not a toast) with link to Settings; `sendMessage()` returns descriptive error string; `checkReady()` stays true if any provider available 17. **Error UX for removed provider key** — Inline error banner in ChatPanel (not a toast) with link to Settings; `sendMessage()` returns descriptive error string; `checkReady()` stays true if any provider available
18. **Zustand store** — No changes needed; provider readiness is ephemeral (fetched on mount), token usage tracking is already in store and is provider-agnostic 18. **Zustand store** — No changes needed; provider readiness is ephemeral (fetched on mount), token usage tracking is already in store and is provider-agnostic
19. **`validateMistralApiKey()`** — Calls `GET https://api.mistral.ai/v1/models` with Bearer token; checks for HTTP 200 + non-empty `data` array; Mistral returns `{ data: [{ id, object, created, owned_by }] }` format 19. **`validateMistralApiKey()`** — Calls `GET https://api.mistral.ai/v1/models` with Bearer token; checks for HTTP 200 + non-empty `data` array; Mistral returns `{ data: [{ id, object, created, owned_by }] }` format
20. **Model cache merge**`getAvailableModels()` fetches from both provider endpoints when both keys are set, merges into a single list with `provider` field on each model; when only one key is set, only that provider's models are returned; when no keys are set, returns empty list and UI disables the model dropdown
21. **Provider-aware API key guards**`analyzeTaxonomy()` and `analyzeMediaImage()` have `if (!this.apiKey)` early-return guards that must become provider-aware (check the relevant provider's key based on the selected model)
22. **`useChatMessageSender` hook** — Shared by ChatPanel and AssistantSidebar; verify no provider assumptions exist (expected: no changes needed)
23. **ChatEngine generic settings** — Use existing `getSetting()`/`setSetting()` for Mistral key storage; no dedicated methods needed
24. **SettingsView model state type** — Currently `{id: string; name: string}[]`; must be updated to `ChatModel[]` to include `provider` and `vision` fields for grouping and filtering