mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
Merge branch 'v0.10.0rc' of github.com:AutoMaker-Org/automaker into v0.10.0rc
This commit is contained in:
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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', '<cwd>' for working directory
|
||||
* - '--format', 'json' for JSON streaming output
|
||||
* - '--model', '<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,90 +354,105 @@ 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) {
|
||||
// Check if the step failed (either has error field or reason is 'error')
|
||||
if (finishEvent.part?.error || finishEvent.part?.reason === 'error') {
|
||||
return {
|
||||
type: 'error',
|
||||
session_id: finishEvent.session_id,
|
||||
error: finishEvent.error || 'Step execution failed',
|
||||
session_id: finishEvent.sessionID,
|
||||
error: finishEvent.part?.error || 'Step execution failed',
|
||||
};
|
||||
}
|
||||
|
||||
// Successful completion
|
||||
const result: { type: 'result'; subtype: 'success'; session_id?: string; result?: string } =
|
||||
{
|
||||
type: 'result',
|
||||
subtype: 'success',
|
||||
};
|
||||
|
||||
if (finishEvent.sessionID) {
|
||||
result.session_id = finishEvent.sessionID;
|
||||
}
|
||||
|
||||
// Safely handle arbitrary result payloads from CLI: ensure we assign a string.
|
||||
const rawResult =
|
||||
(finishEvent.part && (finishEvent.part as Record<string, unknown>).result) ?? undefined;
|
||||
if (rawResult !== undefined) {
|
||||
result.result = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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: 'result',
|
||||
subtype: 'success',
|
||||
session_id: finishEvent.session_id,
|
||||
result: finishEvent.result,
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -168,41 +168,23 @@ describe('opencode-provider.ts', () => {
|
||||
it('should build correct args with run subcommand', () => {
|
||||
const args = provider.buildCliArgs({
|
||||
prompt: 'Hello',
|
||||
model: 'opencode/big-pickle',
|
||||
cwd: '/tmp/project',
|
||||
});
|
||||
|
||||
expect(args[0]).toBe('run');
|
||||
});
|
||||
|
||||
it('should include --format stream-json for streaming output', () => {
|
||||
it('should include --format json for streaming output', () => {
|
||||
const args = provider.buildCliArgs({
|
||||
prompt: 'Hello',
|
||||
model: 'opencode/big-pickle',
|
||||
cwd: '/tmp/project',
|
||||
});
|
||||
|
||||
const formatIndex = args.indexOf('--format');
|
||||
expect(formatIndex).toBeGreaterThan(-1);
|
||||
expect(args[formatIndex + 1]).toBe('stream-json');
|
||||
});
|
||||
|
||||
it('should include -q flag for quiet mode', () => {
|
||||
const args = provider.buildCliArgs({
|
||||
prompt: 'Hello',
|
||||
cwd: '/tmp/project',
|
||||
});
|
||||
|
||||
expect(args).toContain('-q');
|
||||
});
|
||||
|
||||
it('should include working directory with -c flag', () => {
|
||||
const args = provider.buildCliArgs({
|
||||
prompt: 'Hello',
|
||||
cwd: '/tmp/my-project',
|
||||
});
|
||||
|
||||
const cwdIndex = args.indexOf('-c');
|
||||
expect(cwdIndex).toBeGreaterThan(-1);
|
||||
expect(args[cwdIndex + 1]).toBe('/tmp/my-project');
|
||||
expect(args[formatIndex + 1]).toBe('json');
|
||||
});
|
||||
|
||||
it('should include model with --model flag', () => {
|
||||
@@ -228,30 +210,24 @@ describe('opencode-provider.ts', () => {
|
||||
expect(args[modelIndex + 1]).toBe('anthropic/claude-sonnet-4-5');
|
||||
});
|
||||
|
||||
it('should include dash as final arg for stdin prompt', () => {
|
||||
const args = provider.buildCliArgs({
|
||||
prompt: 'Hello',
|
||||
cwd: '/tmp/project',
|
||||
});
|
||||
|
||||
expect(args[args.length - 1]).toBe('-');
|
||||
});
|
||||
|
||||
it('should handle missing cwd', () => {
|
||||
const args = provider.buildCliArgs({
|
||||
prompt: 'Hello',
|
||||
model: 'opencode/big-pickle',
|
||||
});
|
||||
|
||||
expect(args).not.toContain('-c');
|
||||
});
|
||||
|
||||
it('should handle missing model', () => {
|
||||
it('should handle model from opencode provider', () => {
|
||||
const args = provider.buildCliArgs({
|
||||
prompt: 'Hello',
|
||||
model: 'opencode/big-pickle',
|
||||
cwd: '/tmp/project',
|
||||
});
|
||||
|
||||
expect(args).not.toContain('--model');
|
||||
expect(args).toContain('--model');
|
||||
expect(args).toContain('opencode/big-pickle');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -260,12 +236,15 @@ describe('opencode-provider.ts', () => {
|
||||
// ==========================================================================
|
||||
|
||||
describe('normalizeEvent', () => {
|
||||
describe('text-delta events', () => {
|
||||
it('should convert text-delta to assistant message with text content', () => {
|
||||
describe('text events (new OpenCode format)', () => {
|
||||
it('should convert text to assistant message with text content', () => {
|
||||
const event = {
|
||||
type: 'text-delta',
|
||||
text: 'Hello, world!',
|
||||
session_id: 'test-session',
|
||||
type: 'text',
|
||||
part: {
|
||||
type: 'text',
|
||||
text: 'Hello, world!',
|
||||
},
|
||||
sessionID: 'test-session',
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -285,10 +264,13 @@ describe('opencode-provider.ts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for empty text-delta', () => {
|
||||
it('should return null for empty text', () => {
|
||||
const event = {
|
||||
type: 'text-delta',
|
||||
text: '',
|
||||
type: 'text',
|
||||
part: {
|
||||
type: 'text',
|
||||
text: '',
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -296,9 +278,10 @@ describe('opencode-provider.ts', () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for text-delta with undefined text', () => {
|
||||
it('should return null for text with undefined text', () => {
|
||||
const event = {
|
||||
type: 'text-delta',
|
||||
type: 'text',
|
||||
part: {},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -307,27 +290,17 @@ describe('opencode-provider.ts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('text-end events', () => {
|
||||
it('should return null for text-end events (informational)', () => {
|
||||
describe('tool_call events', () => {
|
||||
it('should convert tool_call to assistant message with tool_use content', () => {
|
||||
const event = {
|
||||
type: 'text-end',
|
||||
session_id: 'test-session',
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('tool-call events', () => {
|
||||
it('should convert tool-call to assistant message with tool_use content', () => {
|
||||
const event = {
|
||||
type: 'tool-call',
|
||||
call_id: 'call-123',
|
||||
name: 'Read',
|
||||
args: { file_path: '/tmp/test.txt' },
|
||||
session_id: 'test-session',
|
||||
type: 'tool_call',
|
||||
part: {
|
||||
type: 'tool-call',
|
||||
call_id: 'call-123',
|
||||
name: 'Read',
|
||||
args: { file_path: '/tmp/test.txt' },
|
||||
},
|
||||
sessionID: 'test-session',
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -351,9 +324,12 @@ describe('opencode-provider.ts', () => {
|
||||
|
||||
it('should generate tool_use_id when call_id is missing', () => {
|
||||
const event = {
|
||||
type: 'tool-call',
|
||||
name: 'Write',
|
||||
args: { content: 'test' },
|
||||
type: 'tool_call',
|
||||
part: {
|
||||
type: 'tool-call',
|
||||
name: 'Write',
|
||||
args: { content: 'test' },
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -363,21 +339,27 @@ describe('opencode-provider.ts', () => {
|
||||
|
||||
// Second call should increment
|
||||
const result2 = provider.normalizeEvent({
|
||||
type: 'tool-call',
|
||||
name: 'Edit',
|
||||
args: {},
|
||||
type: 'tool_call',
|
||||
part: {
|
||||
type: 'tool-call',
|
||||
name: 'Edit',
|
||||
args: {},
|
||||
},
|
||||
});
|
||||
expect(result2?.message?.content[0].tool_use_id).toBe('opencode-tool-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tool-result events', () => {
|
||||
it('should convert tool-result to assistant message with tool_result content', () => {
|
||||
describe('tool_result events', () => {
|
||||
it('should convert tool_result to assistant message with tool_result content', () => {
|
||||
const event = {
|
||||
type: 'tool-result',
|
||||
call_id: 'call-123',
|
||||
output: 'File contents here',
|
||||
session_id: 'test-session',
|
||||
type: 'tool_result',
|
||||
part: {
|
||||
type: 'tool-result',
|
||||
call_id: 'call-123',
|
||||
output: 'File contents here',
|
||||
},
|
||||
sessionID: 'test-session',
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -398,10 +380,13 @@ describe('opencode-provider.ts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle tool-result without call_id', () => {
|
||||
it('should handle tool_result without call_id', () => {
|
||||
const event = {
|
||||
type: 'tool-result',
|
||||
output: 'Result without ID',
|
||||
type: 'tool_result',
|
||||
part: {
|
||||
type: 'tool-result',
|
||||
output: 'Result without ID',
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -411,13 +396,16 @@ describe('opencode-provider.ts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('tool-error events', () => {
|
||||
it('should convert tool-error to error message', () => {
|
||||
describe('tool_error events', () => {
|
||||
it('should convert tool_error to error message', () => {
|
||||
const event = {
|
||||
type: 'tool-error',
|
||||
call_id: 'call-123',
|
||||
error: 'File not found',
|
||||
session_id: 'test-session',
|
||||
type: 'tool_error',
|
||||
part: {
|
||||
type: 'tool-error',
|
||||
call_id: 'call-123',
|
||||
error: 'File not found',
|
||||
},
|
||||
sessionID: 'test-session',
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -431,8 +419,11 @@ describe('opencode-provider.ts', () => {
|
||||
|
||||
it('should provide default error message when error is missing', () => {
|
||||
const event = {
|
||||
type: 'tool-error',
|
||||
call_id: 'call-123',
|
||||
type: 'tool_error',
|
||||
part: {
|
||||
type: 'tool-error',
|
||||
call_id: 'call-123',
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -442,12 +433,14 @@ describe('opencode-provider.ts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('start-step events', () => {
|
||||
it('should return null for start-step events (informational)', () => {
|
||||
describe('step_start events', () => {
|
||||
it('should return null for step_start events (informational)', () => {
|
||||
const event = {
|
||||
type: 'start-step',
|
||||
step: 1,
|
||||
session_id: 'test-session',
|
||||
type: 'step_start',
|
||||
part: {
|
||||
type: 'step-start',
|
||||
},
|
||||
sessionID: 'test-session',
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -456,14 +449,16 @@ describe('opencode-provider.ts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('finish-step events', () => {
|
||||
it('should convert successful finish-step to result message', () => {
|
||||
describe('step_finish events', () => {
|
||||
it('should convert successful step_finish to result message', () => {
|
||||
const event = {
|
||||
type: 'finish-step',
|
||||
step: 1,
|
||||
success: true,
|
||||
result: 'Task completed successfully',
|
||||
session_id: 'test-session',
|
||||
type: 'step_finish',
|
||||
part: {
|
||||
type: 'step-finish',
|
||||
reason: 'stop',
|
||||
result: 'Task completed successfully',
|
||||
},
|
||||
sessionID: 'test-session',
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -476,13 +471,15 @@ describe('opencode-provider.ts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert finish-step with success=false to error message', () => {
|
||||
it('should convert step_finish with error to error message', () => {
|
||||
const event = {
|
||||
type: 'finish-step',
|
||||
step: 1,
|
||||
success: false,
|
||||
error: 'Something went wrong',
|
||||
session_id: 'test-session',
|
||||
type: 'step_finish',
|
||||
part: {
|
||||
type: 'step-finish',
|
||||
reason: 'error',
|
||||
error: 'Something went wrong',
|
||||
},
|
||||
sessionID: 'test-session',
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -494,11 +491,13 @@ describe('opencode-provider.ts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert finish-step with error property to error message', () => {
|
||||
it('should convert step_finish with error property to error message', () => {
|
||||
const event = {
|
||||
type: 'finish-step',
|
||||
step: 1,
|
||||
error: 'Process failed',
|
||||
type: 'step_finish',
|
||||
part: {
|
||||
type: 'step-finish',
|
||||
error: 'Process failed',
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -509,9 +508,11 @@ describe('opencode-provider.ts', () => {
|
||||
|
||||
it('should provide default error message for failed step without error text', () => {
|
||||
const event = {
|
||||
type: 'finish-step',
|
||||
step: 1,
|
||||
success: false,
|
||||
type: 'step_finish',
|
||||
part: {
|
||||
type: 'step-finish',
|
||||
reason: 'error',
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -520,11 +521,14 @@ describe('opencode-provider.ts', () => {
|
||||
expect(result?.error).toBe('Step execution failed');
|
||||
});
|
||||
|
||||
it('should treat finish-step without success flag as success', () => {
|
||||
it('should treat step_finish with reason=stop as success', () => {
|
||||
const event = {
|
||||
type: 'finish-step',
|
||||
step: 1,
|
||||
result: 'Done',
|
||||
type: 'step_finish',
|
||||
part: {
|
||||
type: 'step-finish',
|
||||
reason: 'stop',
|
||||
result: 'Done',
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -586,13 +590,12 @@ describe('opencode-provider.ts', () => {
|
||||
return mockedProvider;
|
||||
}
|
||||
|
||||
it('should stream text-delta events as assistant messages', async () => {
|
||||
it('should stream text events as assistant messages', async () => {
|
||||
const mockedProvider = setupMockedProvider();
|
||||
|
||||
const mockEvents = [
|
||||
{ type: 'text-delta', text: 'Hello ' },
|
||||
{ type: 'text-delta', text: 'World!' },
|
||||
{ type: 'text-end' },
|
||||
{ type: 'text', part: { type: 'text', text: 'Hello ' } },
|
||||
{ type: 'text', part: { type: 'text', text: 'World!' } },
|
||||
];
|
||||
|
||||
vi.mocked(spawnJSONLProcess).mockReturnValue(
|
||||
@@ -611,7 +614,6 @@ describe('opencode-provider.ts', () => {
|
||||
})
|
||||
);
|
||||
|
||||
// text-end should be filtered out (returns null)
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0].type).toBe('assistant');
|
||||
expect(results[0].message?.content[0].text).toBe('Hello ');
|
||||
@@ -623,15 +625,21 @@ describe('opencode-provider.ts', () => {
|
||||
|
||||
const mockEvents = [
|
||||
{
|
||||
type: 'tool-call',
|
||||
call_id: 'tool-1',
|
||||
name: 'Read',
|
||||
args: { file_path: '/tmp/test.txt' },
|
||||
type: 'tool_call',
|
||||
part: {
|
||||
type: 'tool-call',
|
||||
call_id: 'tool-1',
|
||||
name: 'Read',
|
||||
args: { file_path: '/tmp/test.txt' },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tool-result',
|
||||
call_id: 'tool-1',
|
||||
output: 'File contents',
|
||||
type: 'tool_result',
|
||||
part: {
|
||||
type: 'tool-result',
|
||||
call_id: 'tool-1',
|
||||
output: 'File contents',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -718,10 +726,7 @@ describe('opencode-provider.ts', () => {
|
||||
const call = vi.mocked(spawnJSONLProcess).mock.calls[0][0];
|
||||
expect(call.args).toContain('run');
|
||||
expect(call.args).toContain('--format');
|
||||
expect(call.args).toContain('stream-json');
|
||||
expect(call.args).toContain('-q');
|
||||
expect(call.args).toContain('-c');
|
||||
expect(call.args).toContain('/tmp/workspace');
|
||||
expect(call.args).toContain('json');
|
||||
expect(call.args).toContain('--model');
|
||||
expect(call.args).toContain('anthropic/claude-opus-4-5');
|
||||
});
|
||||
@@ -731,9 +736,9 @@ describe('opencode-provider.ts', () => {
|
||||
|
||||
const mockEvents = [
|
||||
{ type: 'unknown-internal-event', data: 'ignored' },
|
||||
{ type: 'text-delta', text: 'Valid text' },
|
||||
{ type: 'text', part: { type: 'text', text: 'Valid text' } },
|
||||
{ type: 'another-unknown', foo: 'bar' },
|
||||
{ type: 'finish-step', result: 'Done' },
|
||||
{ type: 'step_finish', part: { type: 'step-finish', reason: 'stop', result: 'Done' } },
|
||||
];
|
||||
|
||||
vi.mocked(spawnJSONLProcess).mockReturnValue(
|
||||
@@ -747,6 +752,7 @@ describe('opencode-provider.ts', () => {
|
||||
const results = await collectAsyncGenerator<ProviderMessage>(
|
||||
mockedProvider.executeQuery({
|
||||
prompt: 'Test',
|
||||
model: 'opencode/big-pickle',
|
||||
cwd: '/test',
|
||||
})
|
||||
);
|
||||
@@ -1039,10 +1045,22 @@ describe('opencode-provider.ts', () => {
|
||||
const sessionId = 'test-session-123';
|
||||
|
||||
const mockEvents = [
|
||||
{ type: 'text-delta', text: 'Hello ', session_id: sessionId },
|
||||
{ type: 'tool-call', name: 'Read', args: {}, call_id: 'c1', session_id: sessionId },
|
||||
{ type: 'tool-result', call_id: 'c1', output: 'file content', session_id: sessionId },
|
||||
{ type: 'finish-step', result: 'Done', session_id: sessionId },
|
||||
{ type: 'text', part: { type: 'text', text: 'Hello ' }, sessionID: sessionId },
|
||||
{
|
||||
type: 'tool_call',
|
||||
part: { type: 'tool-call', name: 'Read', args: {}, call_id: 'c1' },
|
||||
sessionID: sessionId,
|
||||
},
|
||||
{
|
||||
type: 'tool_result',
|
||||
part: { type: 'tool-result', call_id: 'c1', output: 'file content' },
|
||||
sessionID: sessionId,
|
||||
},
|
||||
{
|
||||
type: 'step_finish',
|
||||
part: { type: 'step-finish', reason: 'stop', result: 'Done' },
|
||||
sessionID: sessionId,
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(spawnJSONLProcess).mockReturnValue(
|
||||
@@ -1056,6 +1074,7 @@ describe('opencode-provider.ts', () => {
|
||||
const results = await collectAsyncGenerator<ProviderMessage>(
|
||||
mockedProvider.executeQuery({
|
||||
prompt: 'Test',
|
||||
model: 'opencode/big-pickle',
|
||||
cwd: '/tmp',
|
||||
})
|
||||
);
|
||||
@@ -1069,12 +1088,15 @@ describe('opencode-provider.ts', () => {
|
||||
});
|
||||
|
||||
describe('normalizeEvent additional edge cases', () => {
|
||||
it('should handle tool-call with empty args object', () => {
|
||||
it('should handle tool_call with empty args object', () => {
|
||||
const event = {
|
||||
type: 'tool-call',
|
||||
call_id: 'call-123',
|
||||
name: 'Glob',
|
||||
args: {},
|
||||
type: 'tool_call',
|
||||
part: {
|
||||
type: 'tool-call',
|
||||
call_id: 'call-123',
|
||||
name: 'Glob',
|
||||
args: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -1083,12 +1105,15 @@ describe('opencode-provider.ts', () => {
|
||||
expect(result?.message?.content[0].input).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle tool-call with null args', () => {
|
||||
it('should handle tool_call with null args', () => {
|
||||
const event = {
|
||||
type: 'tool-call',
|
||||
call_id: 'call-123',
|
||||
name: 'Glob',
|
||||
args: null,
|
||||
type: 'tool_call',
|
||||
part: {
|
||||
type: 'tool-call',
|
||||
call_id: 'call-123',
|
||||
name: 'Glob',
|
||||
args: null,
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -1097,18 +1122,21 @@ describe('opencode-provider.ts', () => {
|
||||
expect(result?.message?.content[0].input).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle tool-call with complex nested args', () => {
|
||||
it('should handle tool_call with complex nested args', () => {
|
||||
const event = {
|
||||
type: 'tool-call',
|
||||
call_id: 'call-123',
|
||||
name: 'Edit',
|
||||
args: {
|
||||
file_path: '/tmp/test.ts',
|
||||
changes: [
|
||||
{ line: 10, old: 'foo', new: 'bar' },
|
||||
{ line: 20, old: 'baz', new: 'qux' },
|
||||
],
|
||||
options: { replace_all: true },
|
||||
type: 'tool_call',
|
||||
part: {
|
||||
type: 'tool-call',
|
||||
call_id: 'call-123',
|
||||
name: 'Edit',
|
||||
args: {
|
||||
file_path: '/tmp/test.ts',
|
||||
changes: [
|
||||
{ line: 10, old: 'foo', new: 'bar' },
|
||||
{ line: 20, old: 'baz', new: 'qux' },
|
||||
],
|
||||
options: { replace_all: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1125,11 +1153,14 @@ describe('opencode-provider.ts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle tool-result with empty output', () => {
|
||||
it('should handle tool_result with empty output', () => {
|
||||
const event = {
|
||||
type: 'tool-result',
|
||||
call_id: 'call-123',
|
||||
output: '',
|
||||
type: 'tool_result',
|
||||
part: {
|
||||
type: 'tool-result',
|
||||
call_id: 'call-123',
|
||||
output: '',
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -1138,10 +1169,13 @@ describe('opencode-provider.ts', () => {
|
||||
expect(result?.message?.content[0].content).toBe('');
|
||||
});
|
||||
|
||||
it('should handle text-delta with whitespace-only text', () => {
|
||||
it('should handle text with whitespace-only text', () => {
|
||||
const event = {
|
||||
type: 'text-delta',
|
||||
text: ' ',
|
||||
type: 'text',
|
||||
part: {
|
||||
type: 'text',
|
||||
text: ' ',
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -1151,10 +1185,13 @@ describe('opencode-provider.ts', () => {
|
||||
expect(result?.message?.content[0].text).toBe(' ');
|
||||
});
|
||||
|
||||
it('should handle text-delta with newlines', () => {
|
||||
it('should handle text with newlines', () => {
|
||||
const event = {
|
||||
type: 'text-delta',
|
||||
text: 'Line 1\nLine 2\nLine 3',
|
||||
type: 'text',
|
||||
part: {
|
||||
type: 'text',
|
||||
text: 'Line 1\nLine 2\nLine 3',
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -1162,12 +1199,15 @@ describe('opencode-provider.ts', () => {
|
||||
expect(result?.message?.content[0].text).toBe('Line 1\nLine 2\nLine 3');
|
||||
});
|
||||
|
||||
it('should handle finish-step with both result and error (error takes precedence)', () => {
|
||||
it('should handle step_finish with both result and error (error takes precedence)', () => {
|
||||
const event = {
|
||||
type: 'finish-step',
|
||||
result: 'Some result',
|
||||
error: 'But also an error',
|
||||
success: false,
|
||||
type: 'step_finish',
|
||||
part: {
|
||||
type: 'step-finish',
|
||||
reason: 'stop',
|
||||
result: 'Some result',
|
||||
error: 'But also an error',
|
||||
},
|
||||
};
|
||||
|
||||
const result = provider.normalizeEvent(event);
|
||||
@@ -1231,13 +1271,14 @@ describe('opencode-provider.ts', () => {
|
||||
const longPrompt = 'a'.repeat(10000);
|
||||
const args = provider.buildCliArgs({
|
||||
prompt: longPrompt,
|
||||
model: 'opencode/big-pickle',
|
||||
cwd: '/tmp',
|
||||
});
|
||||
|
||||
// The prompt is NOT in args (it's passed via stdin)
|
||||
// Just verify the args structure is correct
|
||||
expect(args).toContain('run');
|
||||
expect(args).toContain('-');
|
||||
expect(args).not.toContain('-');
|
||||
expect(args.join(' ')).not.toContain(longPrompt);
|
||||
});
|
||||
|
||||
@@ -1245,22 +1286,25 @@ describe('opencode-provider.ts', () => {
|
||||
const specialPrompt = 'Test $HOME $(rm -rf /) `command` "quotes" \'single\'';
|
||||
const args = provider.buildCliArgs({
|
||||
prompt: specialPrompt,
|
||||
model: 'opencode/big-pickle',
|
||||
cwd: '/tmp',
|
||||
});
|
||||
|
||||
// Special chars in prompt should not affect args (prompt is via stdin)
|
||||
expect(args).toContain('run');
|
||||
expect(args).toContain('-');
|
||||
expect(args).not.toContain('-');
|
||||
});
|
||||
|
||||
it('should handle cwd with spaces', () => {
|
||||
const args = provider.buildCliArgs({
|
||||
prompt: 'Test',
|
||||
model: 'opencode/big-pickle',
|
||||
cwd: '/tmp/path with spaces/project',
|
||||
});
|
||||
|
||||
const cwdIndex = args.indexOf('-c');
|
||||
expect(args[cwdIndex + 1]).toBe('/tmp/path with spaces/project');
|
||||
// cwd is set at subprocess level, not via CLI args
|
||||
expect(args).not.toContain('-c');
|
||||
expect(args).not.toContain('/tmp/path with spaces/project');
|
||||
});
|
||||
|
||||
it('should handle model with unusual characters', () => {
|
||||
|
||||
@@ -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<ProviderIconKey, ProviderIconDefinition> = {
|
||||
@@ -24,15 +29,18 @@ const PROVIDER_ICON_DEFINITIONS: Record<ProviderIconKey, ProviderIconDefinition>
|
||||
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<ProviderIconKey, ProviderIconDefinition>
|
||||
// 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: '#6366F1',
|
||||
},
|
||||
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<SVGProps<SVGSVGElement>, 'viewBox'> {
|
||||
@@ -72,7 +102,11 @@ export function ProviderIcon({ provider, title, className, ...props }: ProviderI
|
||||
{...rest}
|
||||
>
|
||||
{title && <title>{title}</title>}
|
||||
<path d={definition.path} fill="currentColor" />
|
||||
<path
|
||||
d={definition.path}
|
||||
fill={definition.fill || 'currentColor'}
|
||||
fillRule={definition.fillRule}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -97,8 +131,140 @@ export function GrokIcon(props: Omit<ProviderIconProps, 'provider'>) {
|
||||
return <ProviderIcon provider={PROVIDER_ICON_KEYS.grok} {...props} />;
|
||||
}
|
||||
|
||||
export function OpenCodeIcon({ className, ...props }: { className?: string }) {
|
||||
return <Cpu className={cn('inline-block', className)} {...props} />;
|
||||
export function OpenCodeIcon(props: Omit<ProviderIconProps, 'provider'>) {
|
||||
return <ProviderIcon provider={PROVIDER_ICON_KEYS.opencode} {...props} />;
|
||||
}
|
||||
|
||||
export function DeepSeekIcon({
|
||||
className,
|
||||
title,
|
||||
...props
|
||||
}: {
|
||||
className?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
const hasAccessibleLabel = Boolean(title);
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className={cn('inline-block', className)}
|
||||
role={hasAccessibleLabel ? 'img' : 'presentation'}
|
||||
aria-hidden={!hasAccessibleLabel}
|
||||
focusable="false"
|
||||
{...props}
|
||||
>
|
||||
{title && <title>{title}</title>}
|
||||
<path
|
||||
d="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"
|
||||
fill="#4D6BFE"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function QwenIcon({ className, title, ...props }: { className?: string; title?: string }) {
|
||||
const hasAccessibleLabel = Boolean(title);
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className={cn('inline-block', className)}
|
||||
role={hasAccessibleLabel ? 'img' : 'presentation'}
|
||||
aria-hidden={!hasAccessibleLabel}
|
||||
focusable="false"
|
||||
{...props}
|
||||
>
|
||||
{title && <title>{title}</title>}
|
||||
<defs>
|
||||
<linearGradient id="qwen-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style={{ stopColor: '#6336E7', stopOpacity: 0.84 }} />
|
||||
<stop offset="100%" style={{ stopColor: '#6F69F7', stopOpacity: 0.84 }} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="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"
|
||||
fill="url(#qwen-gradient)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function NovaIcon({ className, title, ...props }: { className?: string; title?: string }) {
|
||||
const hasAccessibleLabel = Boolean(title);
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 33 32"
|
||||
className={cn('inline-block', className)}
|
||||
role={hasAccessibleLabel ? 'img' : 'presentation'}
|
||||
aria-hidden={!hasAccessibleLabel}
|
||||
focusable="false"
|
||||
{...props}
|
||||
>
|
||||
{title && <title>{title}</title>}
|
||||
<path
|
||||
d="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"
|
||||
fill="#FF9900"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function MistralIcon({
|
||||
className,
|
||||
title,
|
||||
...props
|
||||
}: {
|
||||
className?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
const hasAccessibleLabel = Boolean(title);
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className={cn('inline-block', className)}
|
||||
role={hasAccessibleLabel ? 'img' : 'presentation'}
|
||||
aria-hidden={!hasAccessibleLabel}
|
||||
focusable="false"
|
||||
{...props}
|
||||
>
|
||||
{title && <title>{title}</title>}
|
||||
<path d="M3.428 3.4h3.429v3.428H3.428V3.4zm13.714 0h3.43v3.428h-3.43V3.4z" fill="gold" />
|
||||
<path
|
||||
d="M3.428 6.828h6.857v3.429H3.429V6.828zm10.286 0h6.857v3.429h-6.857V6.828z"
|
||||
fill="#FFAF00"
|
||||
/>
|
||||
<path d="M3.428 10.258h17.144v3.428H3.428v-3.428z" fill="#FF8205" />
|
||||
<path
|
||||
d="M3.428 13.686h3.429v3.428H3.428v-3.428zm6.858 0h3.429v3.428h-3.429v-3.428zm6.856 0h3.43v3.428h-3.43v-3.428z"
|
||||
fill="#FA500F"
|
||||
/>
|
||||
<path d="M0 17.114h10.286v3.429H0v-3.429zm13.714 0H24v3.429H13.714v-3.429z" fill="#E10500" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function MetaIcon({ className, title, ...props }: { className?: string; title?: string }) {
|
||||
const hasAccessibleLabel = Boolean(title);
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className={cn('inline-block', className)}
|
||||
role={hasAccessibleLabel ? 'img' : 'presentation'}
|
||||
aria-hidden={!hasAccessibleLabel}
|
||||
focusable="false"
|
||||
{...props}
|
||||
>
|
||||
{title && <title>{title}</title>}
|
||||
<path
|
||||
d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"
|
||||
fill="#1877F2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import {
|
||||
getAncestors,
|
||||
formatAncestorContextForPrompt,
|
||||
@@ -492,23 +493,44 @@ export function AddFeatureDialog({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'grid gap-3',
|
||||
modelSupportsPlanningMode ? 'grid-cols-2' : 'grid-cols-1'
|
||||
)}
|
||||
>
|
||||
{modelSupportsPlanningMode && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">Planning</Label>
|
||||
<div className="grid gap-3 grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label
|
||||
className={cn(
|
||||
'text-xs text-muted-foreground',
|
||||
!modelSupportsPlanningMode && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
Planning
|
||||
</Label>
|
||||
{modelSupportsPlanningMode ? (
|
||||
<PlanningModeSelect
|
||||
mode={planningMode}
|
||||
onModeChange={setPlanningMode}
|
||||
testIdPrefix="add-feature-planning"
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<PlanningModeSelect
|
||||
mode="skip"
|
||||
onModeChange={() => {}}
|
||||
testIdPrefix="add-feature-planning"
|
||||
compact
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Planning modes are only available for Claude Provider</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">Options</Label>
|
||||
<div className="flex flex-col gap-2 pt-1">
|
||||
@@ -526,28 +548,32 @@ export function AddFeatureDialog({
|
||||
Run tests
|
||||
</Label>
|
||||
</div>
|
||||
{modelSupportsPlanningMode && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="add-feature-require-approval"
|
||||
checked={requirePlanApproval}
|
||||
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
|
||||
disabled={planningMode === 'skip' || planningMode === 'lite'}
|
||||
data-testid="add-feature-require-approval-checkbox"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="add-feature-require-approval"
|
||||
className={cn(
|
||||
'text-xs font-normal',
|
||||
planningMode === 'skip' || planningMode === 'lite'
|
||||
? 'cursor-not-allowed text-muted-foreground'
|
||||
: 'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
Require approval
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="add-feature-require-approval"
|
||||
checked={requirePlanApproval}
|
||||
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
|
||||
disabled={
|
||||
!modelSupportsPlanningMode ||
|
||||
planningMode === 'skip' ||
|
||||
planningMode === 'lite'
|
||||
}
|
||||
data-testid="add-feature-require-approval-checkbox"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="add-feature-require-approval"
|
||||
className={cn(
|
||||
'text-xs font-normal',
|
||||
!modelSupportsPlanningMode ||
|
||||
planningMode === 'skip' ||
|
||||
planningMode === 'lite'
|
||||
? 'cursor-not-allowed text-muted-foreground'
|
||||
: 'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
Require approval
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { DependencyTreeDialog } from './dependency-tree-dialog';
|
||||
import { isClaudeModel, supportsReasoningEffort } from '@automaker/types';
|
||||
|
||||
@@ -516,23 +517,44 @@ export function EditFeatureDialog({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'grid gap-3',
|
||||
modelSupportsPlanningMode ? 'grid-cols-2' : 'grid-cols-1'
|
||||
)}
|
||||
>
|
||||
{modelSupportsPlanningMode && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">Planning</Label>
|
||||
<div className="grid gap-3 grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label
|
||||
className={cn(
|
||||
'text-xs text-muted-foreground',
|
||||
!modelSupportsPlanningMode && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
Planning
|
||||
</Label>
|
||||
{modelSupportsPlanningMode ? (
|
||||
<PlanningModeSelect
|
||||
mode={planningMode}
|
||||
onModeChange={setPlanningMode}
|
||||
testIdPrefix="edit-feature-planning"
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<PlanningModeSelect
|
||||
mode="skip"
|
||||
onModeChange={() => {}}
|
||||
testIdPrefix="edit-feature-planning"
|
||||
compact
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Planning modes are only available for Claude Provider</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">Options</Label>
|
||||
<div className="flex flex-col gap-2 pt-1">
|
||||
@@ -552,28 +574,32 @@ export function EditFeatureDialog({
|
||||
Run tests
|
||||
</Label>
|
||||
</div>
|
||||
{modelSupportsPlanningMode && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="edit-feature-require-approval"
|
||||
checked={requirePlanApproval}
|
||||
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
|
||||
disabled={planningMode === 'skip' || planningMode === 'lite'}
|
||||
data-testid="edit-feature-require-approval-checkbox"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="edit-feature-require-approval"
|
||||
className={cn(
|
||||
'text-xs font-normal',
|
||||
planningMode === 'skip' || planningMode === 'lite'
|
||||
? 'cursor-not-allowed text-muted-foreground'
|
||||
: 'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
Require approval
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="edit-feature-require-approval"
|
||||
checked={requirePlanApproval}
|
||||
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
|
||||
disabled={
|
||||
!modelSupportsPlanningMode ||
|
||||
planningMode === 'skip' ||
|
||||
planningMode === 'lite'
|
||||
}
|
||||
data-testid="edit-feature-require-approval-checkbox"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="edit-feature-require-approval"
|
||||
className={cn(
|
||||
'text-xs font-normal',
|
||||
!modelSupportsPlanningMode ||
|
||||
planningMode === 'skip' ||
|
||||
planningMode === 'lite'
|
||||
? 'cursor-not-allowed text-muted-foreground'
|
||||
: 'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
Require approval
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,8 +15,9 @@ import { modelSupportsThinking } from '@/lib/utils';
|
||||
import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store';
|
||||
import { TestingTabContent, PrioritySelect, PlanningModeSelect } from '../shared';
|
||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||
import { isCursorModel, type PhaseModelEntry } from '@automaker/types';
|
||||
import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
|
||||
interface MassEditDialogProps {
|
||||
open: boolean;
|
||||
@@ -167,6 +168,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
const hasAnyApply = Object.values(applyState).some(Boolean);
|
||||
const isCurrentModelCursor = isCursorModel(model);
|
||||
const modelAllowsThinking = !isCurrentModelCursor && modelSupportsThinking(model);
|
||||
const modelSupportsPlanningMode = isClaudeModel(model);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
|
||||
@@ -205,30 +207,64 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
||||
<div className="border-t border-border" />
|
||||
|
||||
{/* Planning Mode */}
|
||||
<FieldWrapper
|
||||
label="Planning Mode"
|
||||
isMixed={mixedValues.planningMode || mixedValues.requirePlanApproval}
|
||||
willApply={applyState.planningMode || applyState.requirePlanApproval}
|
||||
onApplyChange={(apply) =>
|
||||
setApplyState((prev) => ({
|
||||
...prev,
|
||||
planningMode: apply,
|
||||
requirePlanApproval: apply,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<PlanningModeSelect
|
||||
mode={planningMode}
|
||||
onModeChange={(newMode) => {
|
||||
setPlanningMode(newMode);
|
||||
// Auto-suggest approval based on mode, but user can override
|
||||
setRequirePlanApproval(newMode === 'spec' || newMode === 'full');
|
||||
}}
|
||||
requireApproval={requirePlanApproval}
|
||||
onRequireApprovalChange={setRequirePlanApproval}
|
||||
testIdPrefix="mass-edit-planning"
|
||||
/>
|
||||
</FieldWrapper>
|
||||
{modelSupportsPlanningMode ? (
|
||||
<FieldWrapper
|
||||
label="Planning Mode"
|
||||
isMixed={mixedValues.planningMode || mixedValues.requirePlanApproval}
|
||||
willApply={applyState.planningMode || applyState.requirePlanApproval}
|
||||
onApplyChange={(apply) =>
|
||||
setApplyState((prev) => ({
|
||||
...prev,
|
||||
planningMode: apply,
|
||||
requirePlanApproval: apply,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<PlanningModeSelect
|
||||
mode={planningMode}
|
||||
onModeChange={(newMode) => {
|
||||
setPlanningMode(newMode);
|
||||
// Auto-suggest approval based on mode, but user can override
|
||||
setRequirePlanApproval(newMode === 'spec' || newMode === 'full');
|
||||
}}
|
||||
requireApproval={requirePlanApproval}
|
||||
onRequireApprovalChange={setRequirePlanApproval}
|
||||
testIdPrefix="mass-edit-planning"
|
||||
/>
|
||||
</FieldWrapper>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'p-3 rounded-lg border transition-colors border-border bg-muted/20 opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox checked={false} disabled className="opacity-50" />
|
||||
<Label className="text-sm font-medium text-muted-foreground">
|
||||
Planning Mode
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="opacity-50 pointer-events-none">
|
||||
<PlanningModeSelect
|
||||
mode="skip"
|
||||
onModeChange={() => {}}
|
||||
testIdPrefix="mass-edit-planning"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Planning modes are only available for Claude Provider</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* Priority */}
|
||||
<FieldWrapper
|
||||
|
||||
@@ -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({
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Bot className="w-5 h-5 text-brand-500" />
|
||||
<OpenCodeIcon className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">OpenCode CLI</h2>
|
||||
</div>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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({
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Terminal className="w-5 h-5 text-brand-500" />
|
||||
<OpenCodeIcon className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||
Model Configuration
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user