From 4040bef4b83983eec95da2c795a5940d087e4cbb Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 11 Jan 2026 00:56:25 +0530 Subject: [PATCH] feat: Add OpenCode provider integration with official brand icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit integrates OpenCode as a new AI provider and updates all provider icons with their official brand colors for better visual recognition. **OpenCode Provider Integration:** - Add OpencodeProvider class with CLI-based execution - Support for OpenCode native models (opencode/) and Bedrock models - Proper event normalization for OpenCode streaming format - Correct CLI arguments: --format json (not stream-json) - Event structure: type, part.text, sessionID fields **Provider Icons:** - Add official OpenCode icon (white square frame from opencode.ai) - Add DeepSeek icon (blue whale #4D6BFE) - Add Qwen icon (purple gradient #6336E7 → #6F69F7) - Add Amazon Nova icon (AWS orange #FF9900) - Add Mistral icon (rainbow gradient gold→red) - Add Meta icon (blue #1877F2) - Update existing icons with brand colors: * Claude: #d97757 (terra cotta) * OpenAI/Codex: #74aa9c (teal-green) * Cursor: #5E9EFF (bright blue) **Settings UI Updates:** - Update settings navigation to show OpenCode icon - Update model configuration to use provider-specific icons - Differentiate between OpenCode free models and Bedrock-hosted models - All AI models now display their official brand logos **Model Resolution:** - Add isOpencodeModel() function to detect OpenCode models - Support patterns: opencode/, opencode-*, amazon-bedrock/* - Update getProviderFromModel to recognize opencode provider Note: Some unit tests in opencode-provider.test.ts need updating to match the new event structure and CLI argument format. --- .../server/src/providers/opencode-provider.ts | 291 +++++++++--------- apps/ui/src/components/ui/provider-icon.tsx | 183 ++++++++++- .../cli-status/opencode-cli-status.tsx | 5 +- .../views/settings-view/config/navigation.ts | 5 +- .../opencode-model-configuration.tsx | 27 +- libs/model-resolver/src/resolver.ts | 8 + 6 files changed, 352 insertions(+), 167 deletions(-) diff --git a/apps/server/src/providers/opencode-provider.ts b/apps/server/src/providers/opencode-provider.ts index b54592c3..e20c3593 100644 --- a/apps/server/src/providers/opencode-provider.ts +++ b/apps/server/src/providers/opencode-provider.ts @@ -41,95 +41,103 @@ export interface OpenCodeAuthStatus { /** * Base interface for all OpenCode stream events + * OpenCode uses underscore format: step_start, step_finish, text */ interface OpenCodeBaseEvent { /** Event type identifier */ type: string; - /** Optional session identifier */ - session_id?: string; + /** Timestamp of the event */ + timestamp?: number; + /** Session ID */ + sessionID?: string; + /** Part object containing the actual event data */ + part?: Record; } /** - * Text delta event - Incremental text output from the model + * Text event - Text output from the model + * Format: {"type":"text","part":{"text":"content",...}} */ -export interface OpenCodeTextDeltaEvent extends OpenCodeBaseEvent { - type: 'text-delta'; - /** The incremental text content */ - text: string; -} - -/** - * Text end event - Signals completion of text generation - */ -export interface OpenCodeTextEndEvent extends OpenCodeBaseEvent { - type: 'text-end'; +export interface OpenCodeTextEvent extends OpenCodeBaseEvent { + type: 'text'; + part: { + type: 'text'; + text: string; + [key: string]: unknown; + }; } /** * Tool call event - Request to execute a tool */ export interface OpenCodeToolCallEvent extends OpenCodeBaseEvent { - type: 'tool-call'; - /** Unique identifier for this tool call */ - call_id?: string; - /** Tool name to invoke */ - name: string; - /** Arguments to pass to the tool */ - args: unknown; + type: 'tool_call'; + part: { + type: 'tool-call'; + name: string; + call_id?: string; + args: unknown; + [key: string]: unknown; + }; } /** * Tool result event - Output from a tool execution */ export interface OpenCodeToolResultEvent extends OpenCodeBaseEvent { - type: 'tool-result'; - /** The tool call ID this result corresponds to */ - call_id?: string; - /** Output from the tool execution */ - output: string; + type: 'tool_result'; + part: { + type: 'tool-result'; + call_id?: string; + output: string; + [key: string]: unknown; + }; } /** * Tool error event - Tool execution failed */ export interface OpenCodeToolErrorEvent extends OpenCodeBaseEvent { - type: 'tool-error'; - /** The tool call ID that failed */ - call_id?: string; - /** Error message describing the failure */ - error: string; + type: 'tool_error'; + part: { + type: 'tool-error'; + call_id?: string; + error: string; + [key: string]: unknown; + }; } /** * Start step event - Begins an agentic loop iteration + * Format: {"type":"step_start","part":{...}} */ export interface OpenCodeStartStepEvent extends OpenCodeBaseEvent { - type: 'start-step'; - /** Step number in the agentic loop */ - step?: number; + type: 'step_start'; + part?: { + type: 'step-start'; + [key: string]: unknown; + }; } /** * Finish step event - Completes an agentic loop iteration + * Format: {"type":"step_finish","part":{"reason":"stop",...}} */ export interface OpenCodeFinishStepEvent extends OpenCodeBaseEvent { - type: 'finish-step'; - /** Step number that completed */ - step?: number; - /** Whether the step completed successfully */ - success?: boolean; - /** Optional result data */ - result?: string; - /** Optional error if step failed */ - error?: string; + type: 'step_finish'; + part?: { + type: 'step-finish'; + reason?: string; + error?: string; + [key: string]: unknown; + }; } /** * Union type of all OpenCode stream events */ export type OpenCodeStreamEvent = - | OpenCodeTextDeltaEvent - | OpenCodeTextEndEvent + | OpenCodeTextEvent | OpenCodeToolCallEvent | OpenCodeToolResultEvent | OpenCodeToolErrorEvent @@ -219,14 +227,12 @@ export class OpencodeProvider extends CliProvider { * * Arguments built: * - 'run' subcommand for executing queries - * - '--format', 'stream-json' for JSONL streaming output - * - '-q' / '--quiet' to suppress spinner and interactive elements - * - '-c', '' for working directory + * - '--format', 'json' for JSON streaming output * - '--model', '' for model selection (if specified) - * - '-' as final arg to read prompt from stdin + * - Message passed via stdin (no positional args needed) * - * The prompt is NOT included in CLI args - it's passed via stdin to avoid - * shell escaping issues with special characters in content. + * The prompt is passed via stdin to avoid shell escaping issues. + * OpenCode will read from stdin when no positional message arguments are provided. * * @param options - Execution options containing model, cwd, etc. * @returns Array of CLI arguments for opencode run @@ -234,27 +240,18 @@ export class OpencodeProvider extends CliProvider { buildCliArgs(options: ExecuteOptions): string[] { const args: string[] = ['run']; - // Add streaming JSON output format for JSONL parsing - args.push('--format', 'stream-json'); - - // Suppress spinner and interactive elements for non-TTY usage - args.push('-q'); - - // Set working directory - if (options.cwd) { - args.push('-c', options.cwd); - } + // Add JSON output format for streaming + args.push('--format', 'json'); // Handle model selection - // Strip 'opencode-' prefix if present, OpenCode uses format like 'anthropic/claude-sonnet-4-5' + // Strip 'opencode-' prefix if present, OpenCode uses native format if (options.model) { const model = stripProviderPrefix(options.model); args.push('--model', model); } - // Use '-' to indicate reading prompt from stdin - // This avoids shell escaping issues with special characters - args.push('-'); + // Note: Working directory is set via subprocess cwd option, not CLI args + // Note: Message is passed via stdin, OpenCode reads from stdin automatically return args; } @@ -314,14 +311,12 @@ export class OpencodeProvider extends CliProvider { * Normalize a raw CLI event to ProviderMessage format * * Maps OpenCode event types to the standard ProviderMessage structure: - * - text-delta -> type: 'assistant', content with type: 'text' - * - text-end -> null (informational, no message needed) - * - tool-call -> type: 'assistant', content with type: 'tool_use' - * - tool-result -> type: 'assistant', content with type: 'tool_result' - * - tool-error -> type: 'error' - * - start-step -> null (informational, no message needed) - * - finish-step with success -> type: 'result', subtype: 'success' - * - finish-step with error -> type: 'error' + * - text -> type: 'assistant', content with type: 'text' + * - step_start -> null (informational, no message needed) + * - step_finish -> type: 'result', subtype: 'success' (or error if failed) + * - tool_call -> type: 'assistant', content with type: 'tool_use' + * - tool_result -> type: 'assistant', content with type: 'tool_result' + * - tool_error -> type: 'error' * * @param event - Raw event from OpenCode CLI JSONL output * @returns Normalized ProviderMessage or null to skip the event @@ -334,24 +329,24 @@ export class OpencodeProvider extends CliProvider { const openCodeEvent = event as OpenCodeStreamEvent; switch (openCodeEvent.type) { - case 'text-delta': { - const textEvent = openCodeEvent as OpenCodeTextDeltaEvent; + case 'text': { + const textEvent = openCodeEvent as OpenCodeTextEvent; - // Skip empty text deltas - if (!textEvent.text) { + // Skip if no text content + if (!textEvent.part?.text) { return null; } const content: ContentBlock[] = [ { type: 'text', - text: textEvent.text, + text: textEvent.part.text, }, ]; return { type: 'assistant', - session_id: textEvent.session_id, + session_id: textEvent.sessionID, message: { role: 'assistant', content, @@ -359,81 +354,20 @@ export class OpencodeProvider extends CliProvider { }; } - case 'text-end': { - // Text end is informational - no message needed - return null; - } - - case 'tool-call': { - const toolEvent = openCodeEvent as OpenCodeToolCallEvent; - - // Generate a tool use ID if not provided - const toolUseId = toolEvent.call_id || generateToolUseId(); - - const content: ContentBlock[] = [ - { - type: 'tool_use', - name: toolEvent.name, - tool_use_id: toolUseId, - input: toolEvent.args, - }, - ]; - - return { - type: 'assistant', - session_id: toolEvent.session_id, - message: { - role: 'assistant', - content, - }, - }; - } - - case 'tool-result': { - const resultEvent = openCodeEvent as OpenCodeToolResultEvent; - - const content: ContentBlock[] = [ - { - type: 'tool_result', - tool_use_id: resultEvent.call_id, - content: resultEvent.output, - }, - ]; - - return { - type: 'assistant', - session_id: resultEvent.session_id, - message: { - role: 'assistant', - content, - }, - }; - } - - case 'tool-error': { - const errorEvent = openCodeEvent as OpenCodeToolErrorEvent; - - return { - type: 'error', - session_id: errorEvent.session_id, - error: errorEvent.error || 'Tool execution failed', - }; - } - - case 'start-step': { + case 'step_start': { // Start step is informational - no message needed return null; } - case 'finish-step': { + case 'step_finish': { const finishEvent = openCodeEvent as OpenCodeFinishStepEvent; // Check if the step failed - if (finishEvent.success === false || finishEvent.error) { + if (finishEvent.part?.error) { return { type: 'error', - session_id: finishEvent.session_id, - error: finishEvent.error || 'Step execution failed', + session_id: finishEvent.sessionID, + error: finishEvent.part.error, }; } @@ -441,8 +375,71 @@ export class OpencodeProvider extends CliProvider { return { type: 'result', subtype: 'success', - session_id: finishEvent.session_id, - result: finishEvent.result, + session_id: finishEvent.sessionID, + }; + } + + case 'tool_call': { + const toolEvent = openCodeEvent as OpenCodeToolCallEvent; + + if (!toolEvent.part) { + return null; + } + + // Generate a tool use ID if not provided + const toolUseId = toolEvent.part.call_id || generateToolUseId(); + + const content: ContentBlock[] = [ + { + type: 'tool_use', + name: toolEvent.part.name, + tool_use_id: toolUseId, + input: toolEvent.part.args, + }, + ]; + + return { + type: 'assistant', + session_id: toolEvent.sessionID, + message: { + role: 'assistant', + content, + }, + }; + } + + case 'tool_result': { + const resultEvent = openCodeEvent as OpenCodeToolResultEvent; + + if (!resultEvent.part) { + return null; + } + + const content: ContentBlock[] = [ + { + type: 'tool_result', + tool_use_id: resultEvent.part.call_id, + content: resultEvent.part.output, + }, + ]; + + return { + type: 'assistant', + session_id: resultEvent.sessionID, + message: { + role: 'assistant', + content, + }, + }; + } + + case 'tool_error': { + const errorEvent = openCodeEvent as OpenCodeToolErrorEvent; + + return { + type: 'error', + session_id: errorEvent.sessionID, + error: errorEvent.part?.error || 'Tool execution failed', }; } diff --git a/apps/ui/src/components/ui/provider-icon.tsx b/apps/ui/src/components/ui/provider-icon.tsx index e21e9ffc..b0683ea6 100644 --- a/apps/ui/src/components/ui/provider-icon.tsx +++ b/apps/ui/src/components/ui/provider-icon.tsx @@ -1,5 +1,4 @@ import type { ComponentType, SVGProps } from 'react'; -import { Cpu } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { AgentModel, ModelProvider } from '@automaker/types'; import { getProviderFromModel } from '@/lib/utils'; @@ -10,6 +9,10 @@ const PROVIDER_ICON_KEYS = { cursor: 'cursor', gemini: 'gemini', grok: 'grok', + opencode: 'opencode', + deepseek: 'deepseek', + qwen: 'qwen', + nova: 'nova', } as const; type ProviderIconKey = keyof typeof PROVIDER_ICON_KEYS; @@ -17,6 +20,8 @@ type ProviderIconKey = keyof typeof PROVIDER_ICON_KEYS; interface ProviderIconDefinition { viewBox: string; path: string; + fillRule?: 'nonzero' | 'evenodd'; + fill?: string; } const PROVIDER_ICON_DEFINITIONS: Record = { @@ -24,15 +29,18 @@ const PROVIDER_ICON_DEFINITIONS: Record viewBox: '0 0 248 248', // Official Claude logo from claude.ai favicon path: 'M52.4285 162.873L98.7844 136.879L99.5485 134.602L98.7844 133.334H96.4921L88.7237 132.862L62.2346 132.153L39.3113 131.207L17.0249 130.026L11.4214 128.844L6.2 121.873L6.7094 118.447L11.4214 115.257L18.171 115.847L33.0711 116.911L55.485 118.447L71.6586 119.392L95.728 121.873H99.5485L100.058 120.337L98.7844 119.392L97.7656 118.447L74.5877 102.732L49.4995 86.1905L36.3823 76.62L29.3779 71.7757L25.8121 67.2858L24.2839 57.3608L30.6515 50.2716L39.3113 50.8623L41.4763 51.4531L50.2636 58.1879L68.9842 72.7209L93.4357 90.6804L97.0015 93.6343L98.4374 92.6652L98.6571 91.9801L97.0015 89.2625L83.757 65.2772L69.621 40.8192L63.2534 30.6579L61.5978 24.632C60.9565 22.1032 60.579 20.0111 60.579 17.4246L67.8381 7.49965L71.9133 6.19995L81.7193 7.49965L85.7946 11.0443L91.9074 24.9865L101.714 46.8451L116.996 76.62L121.453 85.4816L123.873 93.6343L124.764 96.1155H126.292V94.6976L127.566 77.9197L129.858 57.3608L132.15 30.8942L132.915 23.4505L136.608 14.4708L143.994 9.62643L149.725 12.344L154.437 19.0788L153.8 23.4505L150.998 41.6463L145.522 70.1215L141.957 89.2625H143.994L146.414 86.7813L156.093 74.0206L172.266 53.698L179.398 45.6635L187.803 36.802L193.152 32.5484H203.34L210.726 43.6549L207.415 55.1159L196.972 68.3492L188.312 79.5739L175.896 96.2095L168.191 109.585L168.882 110.689L170.738 110.53L198.755 104.504L213.91 101.787L231.994 98.7149L240.144 102.496L241.036 106.395L237.852 114.311L218.495 119.037L195.826 123.645L162.07 131.592L161.696 131.893L162.137 132.547L177.36 133.925L183.855 134.279H199.774L229.447 136.524L237.215 141.605L241.8 147.867L241.036 152.711L229.065 158.737L213.019 154.956L175.45 145.977L162.587 142.787H160.805V143.85L171.502 154.366L191.242 172.089L215.82 195.011L217.094 200.682L213.91 205.172L210.599 204.699L188.949 188.394L180.544 181.069L161.696 165.118H160.422V166.772L164.752 173.152L187.803 207.771L188.949 218.405L187.294 221.832L181.308 223.959L174.813 222.777L161.187 203.754L147.305 182.486L136.098 163.345L134.745 164.2L128.075 235.42L125.019 239.082L117.887 241.8L111.902 237.31L108.718 229.984L111.902 215.452L115.722 196.547L118.779 181.541L121.58 162.873L123.291 156.636L123.14 156.219L121.773 156.449L107.699 175.752L86.304 204.699L69.3663 222.777L65.291 224.431L58.2867 220.768L58.9235 214.27L62.8713 208.48L86.304 178.705L100.44 160.155L109.551 149.507L109.462 147.967L108.959 147.924L46.6977 188.512L35.6182 189.93L30.7788 185.44L31.4156 178.115L33.7079 175.752L52.4285 162.873Z', + fill: '#d97757', }, openai: { viewBox: '0 0 158.7128 157.296', path: 'M60.8734,57.2556v-14.9432c0-1.2586.4722-2.2029,1.5728-2.8314l30.0443-17.3023c4.0899-2.3593,8.9662-3.4599,13.9988-3.4599,18.8759,0,30.8307,14.6289,30.8307,30.2006,0,1.1007,0,2.3593-.158,3.6178l-31.1446-18.2467c-1.8872-1.1006-3.7754-1.1006-5.6629,0l-39.4812,22.9651ZM131.0276,115.4561v-35.7074c0-2.2028-.9446-3.7756-2.8318-4.8763l-39.481-22.9651,12.8982-7.3934c1.1007-.6285,2.0453-.6285,3.1458,0l30.0441,17.3024c8.6523,5.0341,14.4708,15.7296,14.4708,26.1107,0,11.9539-7.0769,22.965-18.2461,27.527v.0021ZM51.593,83.9964l-12.8982-7.5497c-1.1007-.6285-1.5728-1.5728-1.5728-2.8314v-34.6048c0-16.8303,12.8982-29.5722,30.3585-29.5722,6.607,0,12.7403,2.2029,17.9324,6.1349l-30.987,17.9324c-1.8871,1.1007-2.8314,2.6735-2.8314,4.8764v45.6159l-.0014-.0015ZM79.3562,100.0403l-18.4829-10.3811v-22.0209l18.4829-10.3811,18.4812,10.3811v22.0209l-18.4812,10.3811ZM91.2319,147.8591c-6.607,0-12.7403-2.2031-17.9324-6.1344l30.9866-17.9333c1.8872-1.1005,2.8318-2.6728,2.8318-4.8759v-45.616l13.0564,7.5498c1.1005.6285,1.5723,1.5728,1.5723,2.8314v34.6051c0,16.8297-13.0564,29.5723-30.5147,29.5723v.001ZM53.9522,112.7822l-30.0443-17.3024c-8.652-5.0343-14.471-15.7296-14.471-26.1107,0-12.1119,7.2356-22.9652,18.403-27.5272v35.8634c0,2.2028.9443,3.7756,2.8314,4.8763l39.3248,22.8068-12.8982,7.3938c-1.1007.6287-2.045.6287-3.1456,0ZM52.2229,138.5791c-17.7745,0-30.8306-13.3713-30.8306-29.8871,0-1.2585.1578-2.5169.3143-3.7754l30.987,17.9323c1.8871,1.1005,3.7757,1.1005,5.6628,0l39.4811-22.807v14.9435c0,1.2585-.4721,2.2021-1.5728,2.8308l-30.0443,17.3025c-4.0898,2.359-8.9662,3.4605-13.9989,3.4605h.0014ZM91.2319,157.296c19.0327,0,34.9188-13.5272,38.5383-31.4594,17.6164-4.562,28.9425-21.0779,28.9425-37.908,0-11.0112-4.719-21.7066-13.2133-29.4143.7867-3.3035,1.2595-6.607,1.2595-9.909,0-22.4929-18.2471-39.3247-39.3251-39.3247-4.2461,0-8.3363.6285-12.4262,2.045-7.0792-6.9213-16.8318-11.3254-27.5271-11.3254-19.0331,0-34.9191,13.5268-38.5384,31.4591C11.3255,36.0212,0,52.5373,0,69.3675c0,11.0112,4.7184,21.7065,13.2125,29.4142-.7865,3.3035-1.2586,6.6067-1.2586,9.9092,0,22.4923,18.2466,39.3241,39.3248,39.3241,4.2462,0,8.3362-.6277,12.426-2.0441,7.0776,6.921,16.8302,11.3251,27.5271,11.3251Z', + fill: '#74aa9c', }, cursor: { viewBox: '0 0 512 512', // Official Cursor logo - hexagonal shape with triangular wedge path: 'M415.035 156.35l-151.503-87.4695c-4.865-2.8094-10.868-2.8094-15.733 0l-151.4969 87.4695c-4.0897 2.362-6.6146 6.729-6.6146 11.459v176.383c0 4.73 2.5249 9.097 6.6146 11.458l151.5039 87.47c4.865 2.809 10.868 2.809 15.733 0l151.504-87.47c4.089-2.361 6.614-6.728 6.614-11.458v-176.383c0-4.73-2.525-9.097-6.614-11.459zm-9.516 18.528l-146.255 253.32c-.988 1.707-3.599 1.01-3.599-.967v-165.872c0-3.314-1.771-6.379-4.644-8.044l-143.645-82.932c-1.707-.988-1.01-3.599.968-3.599h292.509c4.154 0 6.75 4.503 4.673 8.101h-.007z', + fill: '#5E9EFF', }, gemini: { viewBox: '0 0 192 192', @@ -44,6 +52,28 @@ const PROVIDER_ICON_DEFINITIONS: Record // Official Grok/xAI logo - stylized symbol from grok.com path: 'M213.235 306.019l178.976-180.002v.169l51.695-51.763c-.924 1.32-1.86 2.605-2.785 3.89-39.281 54.164-58.46 80.649-43.07 146.922l-.09-.101c10.61 45.11-.744 95.137-37.398 131.836-46.216 46.306-120.167 56.611-181.063 14.928l42.462-19.675c38.863 15.278 81.392 8.57 111.947-22.03 30.566-30.6 37.432-75.159 22.065-112.252-2.92-7.025-11.67-8.795-17.792-4.263l-124.947 92.341zm-25.786 22.437l-.033.034L68.094 435.217c7.565-10.429 16.957-20.294 26.327-30.149 26.428-27.803 52.653-55.359 36.654-94.302-21.422-52.112-8.952-113.177 30.724-152.898 41.243-41.254 101.98-51.661 152.706-30.758 11.23 4.172 21.016 10.114 28.638 15.639l-42.359 19.584c-39.44-16.563-84.629-5.299-112.207 22.313-37.298 37.308-44.84 102.003-1.128 143.81z', }, + opencode: { + viewBox: '0 0 512 512', + // Official OpenCode favicon - geometric icon from opencode.ai + path: 'M384 416H128V96H384V416ZM320 160H192V352H320V160Z', + fillRule: 'evenodd', + fill: 'white', + }, + deepseek: { + viewBox: '0 0 24 24', + // Official DeepSeek logo - whale icon from lobehub/lobe-icons + path: 'M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z', + }, + qwen: { + viewBox: '0 0 24 24', + // Official Qwen logo - geometric star from lobehub/lobe-icons + path: 'M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z', + }, + nova: { + viewBox: '0 0 33 32', + // Official Amazon Nova logo from lobehub/lobe-icons + path: 'm17.865 23.28 1.533 1.543c.07.07.092.175.055.267l-2.398 6.118A1.24 1.24 0 0 1 15.9 32c-.51 0-.969-.315-1.155-.793l-3.451-8.804-5.582 5.617a.246.246 0 0 1-.35 0l-1.407-1.415a.25.25 0 0 1 0-.352l6.89-6.932a1.3 1.3 0 0 1 .834-.398 1.25 1.25 0 0 1 1.232.79l2.992 7.63 1.557-3.977a.248.248 0 0 1 .408-.085zm8.224-19.3-5.583 5.617-3.45-8.805a1.24 1.24 0 0 0-1.43-.762c-.414.092-.744.407-.899.805l-2.38 6.072a.25.25 0 0 0 .055.267l1.533 1.543c.127.127.34.082.407-.085L15.9 4.655l2.991 7.629a1.24 1.24 0 0 0 2.035.425l6.922-6.965a.25.25 0 0 0 0-.352L26.44 3.977a.246.246 0 0 0-.35 0zM8.578 17.566l-3.953-1.567 7.582-3.01c.49-.195.815-.685.785-1.24a1.3 1.3 0 0 0-.395-.84l-6.886-6.93a.246.246 0 0 0-.35 0L3.954 5.395a.25.25 0 0 0 0 .353l5.583 5.617-8.75 3.472a1.25 1.25 0 0 0 0 2.325l6.079 2.412a.24.24 0 0 0 .266-.055l1.533-1.542a.25.25 0 0 0-.085-.41zm22.434-2.73-6.08-2.412a.24.24 0 0 0-.265.055l-1.533 1.542a.25.25 0 0 0 .084.41L27.172 16l-7.583 3.01a1.255 1.255 0 0 0-.785 1.24c.018.317.172.614.395.84l6.89 6.931a.246.246 0 0 0 .35 0l1.406-1.415a.25.25 0 0 0 0-.352l-5.582-5.617 8.75-3.472a1.25 1.25 0 0 0 0-2.325z', + }, }; export interface ProviderIconProps extends Omit, 'viewBox'> { @@ -72,7 +102,11 @@ export function ProviderIcon({ provider, title, className, ...props }: ProviderI {...rest} > {title && {title}} - + ); } @@ -97,8 +131,140 @@ export function GrokIcon(props: Omit) { return ; } -export function OpenCodeIcon({ className, ...props }: { className?: string }) { - return ; +export function OpenCodeIcon(props: Omit) { + return ; +} + +export function DeepSeekIcon({ + className, + title, + ...props +}: { + className?: string; + title?: string; +}) { + const hasAccessibleLabel = Boolean(title); + + return ( + + {title && {title}} + + + ); +} + +export function QwenIcon({ className, title, ...props }: { className?: string; title?: string }) { + const hasAccessibleLabel = Boolean(title); + + return ( + + {title && {title}} + + + + + + + + + ); +} + +export function NovaIcon({ className, title, ...props }: { className?: string; title?: string }) { + const hasAccessibleLabel = Boolean(title); + + return ( + + {title && {title}} + + + ); +} + +export function MistralIcon({ + className, + title, + ...props +}: { + className?: string; + title?: string; +}) { + const hasAccessibleLabel = Boolean(title); + + return ( + + {title && {title}} + + + + + + + ); +} + +export function MetaIcon({ className, title, ...props }: { className?: string; title?: string }) { + const hasAccessibleLabel = Boolean(title); + + return ( + + {title && {title}} + + + ); } export const PROVIDER_ICON_COMPONENTS: Record< @@ -106,7 +272,7 @@ export const PROVIDER_ICON_COMPONENTS: Record< ComponentType<{ className?: string }> > = { claude: AnthropicIcon, - cursor: CursorIcon, // Default for Cursor provider (will be overridden by getProviderIconForModel) + cursor: CursorIcon, codex: OpenAIIcon, opencode: OpenCodeIcon, }; @@ -120,6 +286,11 @@ function getUnderlyingModelIcon(model?: AgentModel | string): ProviderIconKey { const modelStr = typeof model === 'string' ? model.toLowerCase() : model; + // Check for OpenCode models (opencode/, amazon-bedrock/, opencode-*) + if (modelStr.includes('opencode') || modelStr.includes('amazon-bedrock')) { + return 'opencode'; + } + // Check for Cursor-specific models with underlying providers if (modelStr.includes('sonnet') || modelStr.includes('opus') || modelStr.includes('claude')) { return 'anthropic'; @@ -141,6 +312,7 @@ function getUnderlyingModelIcon(model?: AgentModel | string): ProviderIconKey { const provider = getProviderFromModel(model); if (provider === 'codex') return 'openai'; if (provider === 'cursor') return 'cursor'; + if (provider === 'opencode') return 'opencode'; return 'anthropic'; } @@ -155,6 +327,7 @@ export function getProviderIconForModel( cursor: CursorIcon, gemini: GeminiIcon, grok: GrokIcon, + opencode: OpenCodeIcon, }; return iconMap[iconKey] || AnthropicIcon; diff --git a/apps/ui/src/components/views/settings-view/cli-status/opencode-cli-status.tsx b/apps/ui/src/components/views/settings-view/cli-status/opencode-cli-status.tsx index a68dbcb7..961ff866 100644 --- a/apps/ui/src/components/views/settings-view/cli-status/opencode-cli-status.tsx +++ b/apps/ui/src/components/views/settings-view/cli-status/opencode-cli-status.tsx @@ -1,7 +1,8 @@ import { Button } from '@/components/ui/button'; -import { CheckCircle2, AlertCircle, RefreshCw, XCircle, Bot } from 'lucide-react'; +import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { CliStatus } from '../shared/types'; +import { OpenCodeIcon } from '@/components/ui/provider-icon'; export type OpencodeAuthMethod = | 'api_key_env' // ANTHROPIC_API_KEY or other provider env vars @@ -169,7 +170,7 @@ export function OpencodeCliStatus({
- +

OpenCode CLI

diff --git a/apps/ui/src/components/views/settings-view/config/navigation.ts b/apps/ui/src/components/views/settings-view/config/navigation.ts index 62bb9daf..f7f2b9f6 100644 --- a/apps/ui/src/components/views/settings-view/config/navigation.ts +++ b/apps/ui/src/components/views/settings-view/config/navigation.ts @@ -14,9 +14,8 @@ import { MessageSquareText, User, Shield, - Cpu, } from 'lucide-react'; -import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon'; +import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon } from '@/components/ui/provider-icon'; import type { SettingsViewId } from '../hooks/use-settings-view'; export interface NavigationItem { @@ -48,7 +47,7 @@ export const GLOBAL_NAV_GROUPS: NavigationGroup[] = [ { id: 'claude-provider', label: 'Claude', icon: AnthropicIcon }, { id: 'cursor-provider', label: 'Cursor', icon: CursorIcon }, { id: 'codex-provider', label: 'Codex', icon: OpenAIIcon }, - { id: 'opencode-provider', label: 'OpenCode', icon: Cpu }, + { id: 'opencode-provider', label: 'OpenCode', icon: OpenCodeIcon }, ], }, { id: 'mcp-servers', label: 'MCP Servers', icon: Plug }, diff --git a/apps/ui/src/components/views/settings-view/providers/opencode-model-configuration.tsx b/apps/ui/src/components/views/settings-view/providers/opencode-model-configuration.tsx index 1c762018..524f10e5 100644 --- a/apps/ui/src/components/views/settings-view/providers/opencode-model-configuration.tsx +++ b/apps/ui/src/components/views/settings-view/providers/opencode-model-configuration.tsx @@ -8,11 +8,18 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Terminal, Cloud, Cpu, Brain, Sparkles, Zap } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { OpencodeModelId, OpencodeProvider, OpencodeModelConfig } from '@automaker/types'; import { OPENCODE_MODELS, OPENCODE_MODEL_CONFIG_MAP } from '@automaker/types'; -import { AnthropicIcon } from '@/components/ui/provider-icon'; +import { + OpenCodeIcon, + DeepSeekIcon, + QwenIcon, + NovaIcon, + AnthropicIcon, + MistralIcon, + MetaIcon, +} from '@/components/ui/provider-icon'; import type { ComponentType } from 'react'; interface OpencodeModelConfigurationProps { @@ -29,21 +36,21 @@ interface OpencodeModelConfigurationProps { function getProviderIcon(provider: OpencodeProvider): ComponentType<{ className?: string }> { switch (provider) { case 'opencode': - return Terminal; + return OpenCodeIcon; case 'amazon-bedrock-anthropic': return AnthropicIcon; case 'amazon-bedrock-deepseek': - return Brain; + return DeepSeekIcon; case 'amazon-bedrock-amazon': - return Cloud; + return NovaIcon; case 'amazon-bedrock-meta': - return Cpu; + return MetaIcon; case 'amazon-bedrock-mistral': - return Sparkles; + return MistralIcon; case 'amazon-bedrock-qwen': - return Zap; + return QwenIcon; default: - return Terminal; + return OpenCodeIcon; } } @@ -113,7 +120,7 @@ export function OpencodeModelConfiguration({
- +

Model Configuration diff --git a/libs/model-resolver/src/resolver.ts b/libs/model-resolver/src/resolver.ts index 96848f57..29259f9e 100644 --- a/libs/model-resolver/src/resolver.ts +++ b/libs/model-resolver/src/resolver.ts @@ -15,6 +15,7 @@ import { DEFAULT_MODELS, PROVIDER_PREFIXES, isCursorModel, + isOpencodeModel, stripProviderPrefix, type PhaseModelEntry, type ThinkingLevel, @@ -68,6 +69,13 @@ export function resolveModelString( return modelKey; } + // OpenCode model - pass through unchanged + // Supports: opencode/big-pickle, opencode-sonnet, amazon-bedrock/anthropic.claude-* + if (isOpencodeModel(modelKey)) { + console.log(`[ModelResolver] Using OpenCode model: ${modelKey}`); + return modelKey; + } + // Full Claude model string - pass through unchanged if (modelKey.includes('claude-')) { console.log(`[ModelResolver] Using full Claude model string: ${modelKey}`);