From d52cb5c06266dcecde3ca3f3ea957fadeda81faa Mon Sep 17 00:00:00 2001 From: hangerye Date: Wed, 17 Dec 2025 20:56:31 +0800 Subject: [PATCH] feat: add theme --- app/chrome-extension/common/agent-models.ts | 96 +++--- .../sidepanel/components/AgentChat.vue | 166 +++++++++- .../components/agent-chat/AgentChatShell.vue | 6 +- .../components/agent-chat/AgentComposer.vue | 172 ++++++++++- .../agent-chat/AgentProjectMenu.vue | 72 ++++- .../agent-chat/AgentSessionMenu.vue | 98 +----- .../agent-chat/AgentSessionSettingsPanel.vue | 164 ++++++++-- .../agent-chat/AgentSettingsMenu.vue | 3 + .../components/agent-chat/AgentTopBar.vue | 23 +- .../timeline/TimelineNarrativeStep.vue | 12 +- .../timeline/TimelineToolCallStep.vue | 6 +- .../timeline/TimelineToolResultCardStep.vue | 14 +- .../sidepanel/composables/useAgentProjects.ts | 36 +++ .../sidepanel/composables/useAgentTheme.ts | 20 +- .../sidepanel/composables/useAgentThreads.ts | 3 +- .../sidepanel/styles/agent-chat.css | 292 ++++++++++++++++++ app/native-server/src/agent/engines/codex.ts | 1 + packages/shared/src/agent-types.ts | 10 + 18 files changed, 998 insertions(+), 196 deletions(-) diff --git a/app/chrome-extension/common/agent-models.ts b/app/chrome-extension/common/agent-models.ts index 4115e09..85b81d7 100644 --- a/app/chrome-extension/common/agent-models.ts +++ b/app/chrome-extension/common/agent-models.ts @@ -5,6 +5,8 @@ * Based on the pattern from Claudable (other/cweb). */ +import type { CodexReasoningEffort } from 'chrome-mcp-shared'; + // ============================================================ // Types // ============================================================ @@ -14,6 +16,8 @@ export interface ModelDefinition { name: string; description?: string; supportsImages?: boolean; + /** Supported reasoning effort levels for Codex models */ + supportedReasoningEfforts?: readonly CodexReasoningEffort[]; } export type AgentCliType = 'claude' | 'codex' | 'cursor' | 'qwen' | 'glm'; @@ -46,65 +50,55 @@ export const CLAUDE_MODELS: ModelDefinition[] = [ export const CLAUDE_DEFAULT_MODEL = 'claude-sonnet-4-5-20250929'; // ============================================================ -// Codex Models (aligned with other/cweb) +// Codex Models // ============================================================ +/** Standard reasoning efforts supported by all models */ +const CODEX_STANDARD_EFFORTS: readonly CodexReasoningEffort[] = ['low', 'medium', 'high']; +/** Extended reasoning efforts (includes xhigh) - only for gpt-5.2 and gpt-5.1-codex-max */ +const CODEX_EXTENDED_EFFORTS: readonly CodexReasoningEffort[] = ['low', 'medium', 'high', 'xhigh']; + export const CODEX_MODELS: ModelDefinition[] = [ { - id: 'gpt-5', - name: 'GPT-5', - description: 'OpenAI flagship reasoning model', + id: 'gpt-5.1', + name: 'GPT-5.1', + description: 'OpenAI high-quality reasoning model', + supportedReasoningEfforts: CODEX_STANDARD_EFFORTS, }, { - id: 'gpt-4o', - name: 'GPT-4o', - description: 'General-purpose model with multimodal support', - supportsImages: true, + id: 'gpt-5.2', + name: 'GPT-5.2', + description: 'OpenAI flagship reasoning model with extended effort support', + supportedReasoningEfforts: CODEX_EXTENDED_EFFORTS, }, { - id: 'gpt-4o-mini', - name: 'GPT-4o Mini', - description: 'Cost-efficient GPT-4o variant', - supportsImages: true, + id: 'gpt-5.1-codex', + name: 'GPT-5.1 Codex', + description: 'Coding-optimized model for agent workflows', + supportedReasoningEfforts: CODEX_STANDARD_EFFORTS, }, { - id: 'o1-preview', - name: 'o1 Preview', - description: 'OpenAI o1 preview model focused on agent use-cases', + id: 'gpt-5.1-codex-max', + name: 'GPT-5.1 Codex Max', + description: 'Highest quality coding model with extended effort support', + supportedReasoningEfforts: CODEX_EXTENDED_EFFORTS, }, { - id: 'o1-mini', - name: 'o1 Mini', - description: 'Lightweight o1 model for faster iterations', - }, - { - id: 'o3', - name: 'o3', - description: 'OpenAI o3 reasoning model', - }, - { - id: 'claude-3.5-sonnet', - name: 'Claude 3.5 Sonnet (via Codex)', - description: 'Anthropic Claude via Codex router', - }, - { - id: 'claude-3-haiku', - name: 'Claude 3 Haiku (via Codex)', - description: 'Anthropic Haiku model routed through Codex', + id: 'gpt-5.1-codex-mini', + name: 'GPT-5.1 Codex Mini', + description: 'Fast, cost-efficient coding model', + supportedReasoningEfforts: CODEX_STANDARD_EFFORTS, }, ]; -export const CODEX_DEFAULT_MODEL = 'gpt-5'; +export const CODEX_DEFAULT_MODEL = 'gpt-5.1'; -// Codex model alias normalization (aligned with other/cweb) +// Codex model alias normalization const CODEX_ALIAS_MAP: Record = { - gpt5: 'gpt-5', - gpt_5: 'gpt-5', - 'gpt-5.0': 'gpt-5', - 'gpt-4o-mini-high': 'gpt-4o-mini', - 'gpt-4o-mini-low': 'gpt-4o-mini', - 'claude-sonnet-3.5': 'claude-3.5-sonnet', - 'claude35-sonnet': 'claude-3.5-sonnet', + gpt5: 'gpt-5.1', + gpt_5: 'gpt-5.1', + 'gpt-5': 'gpt-5.1', + 'gpt-5.0': 'gpt-5.1', }; const CODEX_KNOWN_IDS = new Set(CODEX_MODELS.map((model) => model.id)); @@ -139,6 +133,24 @@ export function normalizeCodexModelId(model?: string | null): string { return CODEX_DEFAULT_MODEL; } +/** + * Get supported reasoning efforts for a Codex model. + * Returns standard efforts (low/medium/high) for unknown models. + */ +export function getCodexReasoningEfforts(modelId?: string | null): readonly CodexReasoningEffort[] { + const normalized = normalizeCodexModelId(modelId); + const model = CODEX_MODELS.find((m) => m.id === normalized); + return model?.supportedReasoningEfforts ?? CODEX_STANDARD_EFFORTS; +} + +/** + * Check if a model supports xhigh reasoning effort. + */ +export function supportsXhighEffort(modelId?: string | null): boolean { + const efforts = getCodexReasoningEfforts(modelId); + return efforts.includes('xhigh'); +} + // ============================================================ // Cursor Models // ============================================================ diff --git a/app/chrome-extension/entrypoints/sidepanel/components/AgentChat.vue b/app/chrome-extension/entrypoints/sidepanel/components/AgentChat.vue index 2850cd5..66cccb9 100644 --- a/app/chrome-extension/entrypoints/sidepanel/components/AgentChat.vue +++ b/app/chrome-extension/entrypoints/sidepanel/components/AgentChat.vue @@ -29,11 +29,20 @@ :can-cancel="!!chat.currentRequestId.value" :can-send="chat.canSend.value" placeholder="Ask Claude to write code..." + :engine-name="currentEngineName" + :selected-model="currentSessionModel" + :available-models="currentAvailableModels" + :reasoning-effort="currentReasoningEffort" + :available-reasoning-efforts="currentAvailableReasoningEfforts" @update:model-value="chat.input.value = $event" @submit="handleSend" @cancel="chat.cancelCurrentRequest()" @attachment:add="handleAttachmentAdd" @attachment:remove="attachments.removeAttachment" + @model:change="handleComposerModelChange" + @reasoning-effort:change="handleComposerReasoningEffortChange" + @session:settings="handleComposerOpenSettings" + @session:reset="handleComposerReset" /> @@ -52,6 +61,7 @@ :selected-project-id="projects.selectedProjectId.value" :selected-cli="selectedCli" :model="model" + :reasoning-effort="reasoningEffort" :use-ccr="useCcr" :project-root-override="projects.projectRootOverride.value" :engines="server.engines.value" @@ -62,6 +72,7 @@ @project:new="handleNewProject" @cli:update="selectedCli = $event" @model:update="model = $event" + @reasoning-effort:update="reasoningEffort = $event" @ccr:update="useCcr = $event" @root:update="projects.projectRootOverride.value = $event" @save="handleSaveSettings" @@ -78,8 +89,6 @@ @session:new="handleNewSession" @session:delete="handleDeleteSession" @session:rename="handleRenameSession" - @session:settings="handleOpenSessionSettings" - @session:reset="handleResetSession" /> import { ref, computed, onMounted, onUnmounted, watch } from 'vue'; -import type { AgentStoredMessage, AgentMessage } from 'chrome-mcp-shared'; +import type { AgentStoredMessage, AgentMessage, CodexReasoningEffort } from 'chrome-mcp-shared'; // Composables import { @@ -132,11 +141,16 @@ import { import type { SessionSettings } from './agent-chat/AgentSessionSettingsPanel.vue'; // Model utilities -import { getModelsForCli } from '@/common/agent-models'; +import { + getModelsForCli, + getCodexReasoningEfforts, + getDefaultModelForCli, +} from '@/common/agent-models'; // Local UI state const selectedCli = ref(''); const model = ref(''); +const reasoningEffort = ref('medium'); const useCcr = ref(false); const isSavingPreference = ref(false); @@ -156,6 +170,20 @@ function getNormalizedModel(): string { const isValid = models.some((m) => m.id === trimmedModel); return isValid ? trimmedModel : ''; } + +/** + * Get normalized reasoning effort that is valid for the current model. + * Used when creating/updating codex sessions. + */ +function getNormalizedReasoningEffort(): CodexReasoningEffort { + if (selectedCli.value !== 'codex') return 'medium'; + const effectiveModel = getNormalizedModel() || getDefaultModelForCli('codex'); + const supported = getCodexReasoningEfforts(effectiveModel); + return supported.includes(reasoningEffort.value) + ? reasoningEffort.value + : (supported[supported.length - 1] as CodexReasoningEffort); +} + const isPickingDirectory = ref(false); const projectMenuOpen = ref(false); const sessionMenuOpen = ref(false); @@ -232,6 +260,35 @@ const connectionState = computed(() => { return 'disconnected'; }); +// Computed values for AgentComposer +const currentEngineName = computed(() => sessions.selectedSession.value?.engineName ?? ''); + +const currentSessionModel = computed(() => { + const session = sessions.selectedSession.value; + if (!session) return ''; + // Use session model if set, otherwise use default for the engine + return session.model || getDefaultModelForCli(session.engineName); +}); + +const currentAvailableModels = computed(() => { + const session = sessions.selectedSession.value; + if (!session) return []; + return getModelsForCli(session.engineName); +}); + +const currentReasoningEffort = computed(() => { + const session = sessions.selectedSession.value; + if (!session || session.engineName !== 'codex') return 'medium' as CodexReasoningEffort; + return session.optionsConfig?.codexConfig?.reasoningEffort ?? 'medium'; +}); + +const currentAvailableReasoningEfforts = computed(() => { + const session = sessions.selectedSession.value; + if (!session || session.engineName !== 'codex') return [] as readonly CodexReasoningEffort[]; + const effectiveModel = currentSessionModel.value || getDefaultModelForCli('codex'); + return getCodexReasoningEfforts(effectiveModel); +}); + // Load chat history for a specific session async function loadSessionHistory(sessionId: string): Promise { const serverPort = server.serverPort.value; @@ -322,9 +379,23 @@ async function handleNewSession(): Promise { const projectId = projects.selectedProjectId.value; if (!projectId) return; + const engineName = + (selectedCli.value as 'claude' | 'codex' | 'cursor' | 'qwen' | 'glm') || 'claude'; + + // Include codex config if using codex engine + const optionsConfig = + engineName === 'codex' + ? { + codexConfig: { + reasoningEffort: getNormalizedReasoningEffort(), + }, + } + : undefined; + const session = await sessions.createSession(projectId, { - engineName: (selectedCli.value as 'claude' | 'codex' | 'cursor' | 'qwen' | 'glm') || 'claude', + engineName, name: `Session ${sessions.sessions.value.length + 1}`, + optionsConfig, }); if (session) { @@ -369,6 +440,47 @@ async function handleResetSession(sessionId: string): Promise { } } +// Composer direct model/reasoning effort change handlers +async function handleComposerModelChange(modelId: string): Promise { + const sessionId = sessions.selectedSessionId.value; + if (!sessionId) return; + + await sessions.updateSession(sessionId, { model: modelId || null }); +} + +async function handleComposerReasoningEffortChange(effort: CodexReasoningEffort): Promise { + const sessionId = sessions.selectedSessionId.value; + const session = sessions.selectedSession.value; + if (!sessionId || !session) return; + + const existingOptions = session.optionsConfig ?? {}; + const existingCodexConfig = existingOptions.codexConfig ?? {}; + await sessions.updateSession(sessionId, { + optionsConfig: { + ...existingOptions, + codexConfig: { + ...existingCodexConfig, + reasoningEffort: effort, + }, + }, + }); +} + +// Composer session settings/reset handlers (without sessionId parameter) +function handleComposerOpenSettings(): void { + const sessionId = sessions.selectedSessionId.value; + if (sessionId) { + handleOpenSessionSettings(sessionId); + } +} + +async function handleComposerReset(): Promise { + const sessionId = sessions.selectedSessionId.value; + if (sessionId) { + await handleResetSession(sessionId); + } +} + function handleCloseSessionSettings(): void { sessionSettingsOpen.value = false; currentManagementInfo.value = null; @@ -384,6 +496,7 @@ async function handleSaveSessionSettings(settings: SessionSettings): Promise { selectedCli.value = project.preferredCli ?? ''; model.value = project.selectedModel ?? ''; useCcr.value = project.useCcr ?? false; + + // Ensure a default session exists for the new project + const engineName = + (selectedCli.value as 'claude' | 'codex' | 'cursor' | 'qwen' | 'glm') || 'claude'; + await sessions.ensureDefaultSession(project.id, engineName); + + // Reconnect SSE and load session history + if (sessions.selectedSessionId.value) { + server.openEventSource(); + await loadSessionHistory(sessions.selectedSessionId.value); + } } } } finally { @@ -432,7 +556,11 @@ async function handleNewProject(): Promise { } async function handleSaveSettings(): Promise { - if (!projects.selectedProject.value) return; + const project = projects.selectedProject.value; + if (!project) return; + + // Capture previous CLI to detect changes + const previousCli = project.preferredCli ?? ''; isSavingPreference.value = true; try { @@ -445,6 +573,32 @@ async function handleSaveSettings(): Promise { // Sync local state with normalized values model.value = normalizedModel; useCcr.value = normalizedCcr; + + // If CLI changed, create a new empty session with the new CLI + const cliChanged = previousCli !== selectedCli.value; + if (cliChanged && selectedCli.value) { + const engineName = selectedCli.value as 'claude' | 'codex' | 'cursor' | 'qwen' | 'glm'; + + // Include codex config if using codex engine + const optionsConfig = + engineName === 'codex' + ? { + codexConfig: { + reasoningEffort: getNormalizedReasoningEffort(), + }, + } + : undefined; + + const session = await sessions.createSession(project.id, { + engineName, + name: `Session ${sessions.sessions.value.length + 1}`, + optionsConfig, + }); + + if (session) { + chat.setMessages([]); + } + } } finally { isSavingPreference.value = false; closeMenus(); diff --git a/app/chrome-extension/entrypoints/sidepanel/components/agent-chat/AgentChatShell.vue b/app/chrome-extension/entrypoints/sidepanel/components/agent-chat/AgentChatShell.vue index d1a5ddb..fa15267 100644 --- a/app/chrome-extension/entrypoints/sidepanel/components/agent-chat/AgentChatShell.vue +++ b/app/chrome-extension/entrypoints/sidepanel/components/agent-chat/AgentChatShell.vue @@ -1,9 +1,5 @@