mirror of
https://github.com/hangwin/mcp-chrome.git
synced 2026-09-21 12:43:18 +08:00
feat: add theme
This commit is contained in:
@@ -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<string, string> = {
|
||||
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
|
||||
// ============================================================
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
</template>
|
||||
</AgentChatShell>
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
<AgentSettingsMenu
|
||||
@@ -104,7 +113,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
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<CodexReasoningEffort>('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<void> {
|
||||
const serverPort = server.serverPort.value;
|
||||
@@ -322,9 +379,23 @@ async function handleNewSession(): Promise<void> {
|
||||
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<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Composer direct model/reasoning effort change handlers
|
||||
async function handleComposerModelChange(modelId: string): Promise<void> {
|
||||
const sessionId = sessions.selectedSessionId.value;
|
||||
if (!sessionId) return;
|
||||
|
||||
await sessions.updateSession(sessionId, { model: modelId || null });
|
||||
}
|
||||
|
||||
async function handleComposerReasoningEffortChange(effort: CodexReasoningEffort): Promise<void> {
|
||||
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<void> {
|
||||
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<voi
|
||||
model: settings.model || null,
|
||||
permissionMode: settings.permissionMode || null,
|
||||
systemPromptConfig: settings.systemPromptConfig,
|
||||
optionsConfig: settings.optionsConfig,
|
||||
});
|
||||
sessionSettingsOpen.value = false;
|
||||
currentManagementInfo.value = null;
|
||||
@@ -423,6 +536,17 @@ async function handleNewProject(): Promise<void> {
|
||||
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<void> {
|
||||
}
|
||||
|
||||
async function handleSaveSettings(): Promise<void> {
|
||||
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<void> {
|
||||
// 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();
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
ref="shellRef"
|
||||
class="h-full flex flex-col overflow-hidden relative"
|
||||
:style="{ backgroundColor: 'var(--ac-bg)' }"
|
||||
>
|
||||
<div ref="shellRef" class="h-full flex flex-col overflow-hidden relative">
|
||||
<!-- Header -->
|
||||
<header
|
||||
class="flex-none px-5 py-3 flex items-center justify-between z-20"
|
||||
|
||||
+165
-7
@@ -8,12 +8,13 @@
|
||||
<div
|
||||
v-for="(attachment, index) in attachments"
|
||||
:key="index"
|
||||
class="flex items-center gap-1 px-2 py-0.5 rounded text-[11px]"
|
||||
class="flex items-center gap-1 px-2 py-0.5 text-[11px]"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface)',
|
||||
border: 'var(--ac-border-width) solid var(--ac-border)',
|
||||
color: 'var(--ac-text-muted)',
|
||||
boxShadow: 'var(--ac-shadow-card)',
|
||||
borderRadius: 'var(--ac-radius-button)',
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
@@ -66,9 +67,9 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Attach Button -->
|
||||
<button
|
||||
class="p-1.5 rounded-lg ac-btn"
|
||||
:style="{ color: 'var(--ac-text-subtle)' }"
|
||||
title="Attach image"
|
||||
class="p-1.5 ac-btn"
|
||||
:style="{ color: 'var(--ac-text-subtle)', borderRadius: 'var(--ac-radius-button)' }"
|
||||
data-tooltip="Attach image"
|
||||
@click="$emit('attachment:add')"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -81,6 +82,102 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Model Selector (auto-width) -->
|
||||
<div v-if="availableModels.length > 0" class="relative" data-tooltip="Switch model">
|
||||
<!-- Hidden span to measure text width -->
|
||||
<span
|
||||
ref="modelWidthRef"
|
||||
class="invisible absolute whitespace-nowrap px-1.5 text-[10px]"
|
||||
:style="{ fontFamily: 'var(--ac-font-mono)' }"
|
||||
>
|
||||
{{ selectedModelName }}
|
||||
</span>
|
||||
<select
|
||||
:value="selectedModel"
|
||||
class="py-0.5 text-[10px] border-none bg-transparent cursor-pointer appearance-none pr-4 pl-1.5"
|
||||
:style="{
|
||||
color: 'var(--ac-text-muted)',
|
||||
fontFamily: 'var(--ac-font-mono)',
|
||||
width: modelSelectWidth,
|
||||
borderRadius: 'var(--ac-radius-button)',
|
||||
}"
|
||||
@change="handleModelChange"
|
||||
>
|
||||
<option v-for="m in availableModels" :key="m.id" :value="m.id">
|
||||
{{ m.name }}
|
||||
</option>
|
||||
</select>
|
||||
<!-- Dropdown arrow -->
|
||||
<svg
|
||||
class="absolute right-0 top-1/2 -translate-y-1/2 w-3 h-3 pointer-events-none"
|
||||
:style="{ color: 'var(--ac-text-subtle)' }"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Reasoning Effort (Codex only) -->
|
||||
<select
|
||||
v-if="
|
||||
isCodexEngine && availableReasoningEfforts && availableReasoningEfforts.length > 0
|
||||
"
|
||||
:value="reasoningEffort"
|
||||
class="px-1.5 py-0.5 text-[10px] border-none bg-transparent cursor-pointer"
|
||||
:style="{
|
||||
color: 'var(--ac-text-muted)',
|
||||
fontFamily: 'var(--ac-font-mono)',
|
||||
borderRadius: 'var(--ac-radius-button)',
|
||||
}"
|
||||
data-tooltip="Reasoning effort"
|
||||
@change="handleReasoningEffortChange"
|
||||
>
|
||||
<option v-for="effort in availableReasoningEfforts" :key="effort" :value="effort">
|
||||
{{ effort }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- Reset Button -->
|
||||
<button
|
||||
class="p-1 ac-btn"
|
||||
:style="{ color: 'var(--ac-text-subtle)', borderRadius: 'var(--ac-radius-button)' }"
|
||||
data-tooltip="Reset conversation"
|
||||
@click="handleReset"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Session Settings Button -->
|
||||
<button
|
||||
class="p-1 ac-btn"
|
||||
:style="{ color: 'var(--ac-text-subtle)', borderRadius: 'var(--ac-radius-button)' }"
|
||||
data-tooltip="Session settings"
|
||||
@click="handleOpenSettings"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Status Text -->
|
||||
<div class="text-[11px] ml-1 flex items-center gap-1" :style="{ color: statusColor }">
|
||||
<span
|
||||
@@ -97,7 +194,7 @@
|
||||
<!-- Stop Button -->
|
||||
<button
|
||||
v-if="isStreaming && canCancel"
|
||||
class="px-3 py-1.5 text-xs rounded transition-colors"
|
||||
class="px-3 py-1.5 text-xs transition-colors"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-hover-bg)',
|
||||
color: 'var(--ac-text)',
|
||||
@@ -137,8 +234,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import type { AgentAttachment } from 'chrome-mcp-shared';
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import type { AgentAttachment, CodexReasoningEffort } from 'chrome-mcp-shared';
|
||||
import type { ModelDefinition } from '@/common/agent-models';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
@@ -149,8 +247,40 @@ const props = defineProps<{
|
||||
canCancel: boolean;
|
||||
canSend: boolean;
|
||||
placeholder?: string;
|
||||
// Model selection props
|
||||
engineName?: string;
|
||||
selectedModel: string;
|
||||
availableModels: ModelDefinition[];
|
||||
// Codex reasoning effort props
|
||||
reasoningEffort?: CodexReasoningEffort;
|
||||
availableReasoningEfforts?: readonly CodexReasoningEffort[];
|
||||
}>();
|
||||
|
||||
const isCodexEngine = computed(() => props.engineName === 'codex');
|
||||
|
||||
// Model selector auto-width
|
||||
const modelWidthRef = ref<HTMLSpanElement | null>(null);
|
||||
const modelSelectWidth = ref('auto');
|
||||
|
||||
const selectedModelName = computed(() => {
|
||||
const model = props.availableModels.find((m) => m.id === props.selectedModel);
|
||||
return model?.name || props.selectedModel || '';
|
||||
});
|
||||
|
||||
// Update width when model changes
|
||||
watch(
|
||||
[selectedModelName, () => props.availableModels],
|
||||
async () => {
|
||||
await nextTick();
|
||||
if (modelWidthRef.value) {
|
||||
const width = modelWidthRef.value.offsetWidth;
|
||||
// Add extra space for dropdown arrow (16px)
|
||||
modelSelectWidth.value = `${width + 16}px`;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (props.sending) return 'Sending...';
|
||||
if (props.isStreaming) return 'Agent is thinking...';
|
||||
@@ -168,6 +298,10 @@ const emit = defineEmits<{
|
||||
cancel: [];
|
||||
'attachment:add': [];
|
||||
'attachment:remove': [index: number];
|
||||
'model:change': [modelId: string];
|
||||
'reasoning-effort:change': [effort: CodexReasoningEffort];
|
||||
'session:settings': [];
|
||||
'session:reset': [];
|
||||
}>();
|
||||
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null);
|
||||
@@ -185,6 +319,30 @@ function handleSubmit(): void {
|
||||
emit('submit');
|
||||
}
|
||||
|
||||
function handleModelChange(event: Event): void {
|
||||
const modelId = (event.target as HTMLSelectElement).value;
|
||||
emit('model:change', modelId);
|
||||
}
|
||||
|
||||
function handleReasoningEffortChange(event: Event): void {
|
||||
const effort = (event.target as HTMLSelectElement).value as CodexReasoningEffort;
|
||||
emit('reasoning-effort:change', effort);
|
||||
}
|
||||
|
||||
function handleReset(): void {
|
||||
if (
|
||||
confirm(
|
||||
'Reset this conversation? All messages will be deleted and the session will start fresh.',
|
||||
)
|
||||
) {
|
||||
emit('session:reset');
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenSettings(): void {
|
||||
emit('session:settings');
|
||||
}
|
||||
|
||||
// Expose ref for parent focus control
|
||||
defineExpose({
|
||||
focus: () => textareaRef.value?.focus(),
|
||||
|
||||
+70
-2
@@ -125,6 +125,33 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Reasoning Effort (Codex only) -->
|
||||
<div v-if="showReasoningEffortOption" class="px-3 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs w-12" :style="{ color: 'var(--ac-text-muted, #6e6e6e)' }">
|
||||
Effort
|
||||
</span>
|
||||
<select
|
||||
:value="normalizedReasoningEffort"
|
||||
class="flex-1 px-2 py-1 text-xs rounded"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface-muted, #f2f0eb)',
|
||||
border: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
|
||||
color: 'var(--ac-text, #1a1a1a)',
|
||||
borderRadius: 'var(--ac-radius-button, 8px)',
|
||||
}"
|
||||
@change="handleReasoningEffortChange"
|
||||
>
|
||||
<option v-for="effort in availableReasoningEfforts" :key="effort" :value="effort">
|
||||
{{ effort }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="text-[10px] mt-1 ml-14" :style="{ color: 'var(--ac-text-subtle, #a8a29e)' }">
|
||||
Applies to new sessions. Edit existing session in Session Settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- CCR Option (Claude Code Router) - only shown when Claude CLI is selected -->
|
||||
<div v-if="showCcrOption" class="px-3 py-2 flex items-center gap-2">
|
||||
<span class="text-xs w-12" :style="{ color: 'var(--ac-text-muted, #6e6e6e)' }"> CCR </span>
|
||||
@@ -191,10 +218,11 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import type { AgentProject, AgentEngineInfo } from 'chrome-mcp-shared';
|
||||
import type { AgentProject, AgentEngineInfo, CodexReasoningEffort } from 'chrome-mcp-shared';
|
||||
import {
|
||||
getModelsForCli,
|
||||
getDefaultModelForCli,
|
||||
getCodexReasoningEfforts,
|
||||
type ModelDefinition,
|
||||
} from '@/common/agent-models';
|
||||
|
||||
@@ -204,6 +232,7 @@ const props = defineProps<{
|
||||
selectedProjectId: string;
|
||||
selectedCli: string;
|
||||
model: string;
|
||||
reasoningEffort: CodexReasoningEffort;
|
||||
useCcr: boolean;
|
||||
projectRootOverride: string;
|
||||
engines: AgentEngineInfo[];
|
||||
@@ -217,6 +246,7 @@ const emit = defineEmits<{
|
||||
'project:new': [];
|
||||
'cli:update': [cli: string];
|
||||
'model:update': [model: string];
|
||||
'reasoning-effort:update': [effort: CodexReasoningEffort];
|
||||
'ccr:update': [useCcr: boolean];
|
||||
'root:update': [root: string];
|
||||
save: [];
|
||||
@@ -246,6 +276,27 @@ const isModelDisabled = computed(() => {
|
||||
return !props.selectedCli || availableModels.value.length === 0;
|
||||
});
|
||||
|
||||
// Show reasoning effort option only when Codex CLI is selected
|
||||
const showReasoningEffortOption = computed(() => {
|
||||
return props.selectedCli === 'codex';
|
||||
});
|
||||
|
||||
// Get available reasoning efforts based on selected model
|
||||
const availableReasoningEfforts = computed<readonly CodexReasoningEffort[]>(() => {
|
||||
if (!showReasoningEffortOption.value) return [];
|
||||
const effectiveModel = normalizedModel.value || getDefaultModelForCli('codex');
|
||||
return getCodexReasoningEfforts(effectiveModel);
|
||||
});
|
||||
|
||||
// Normalize reasoning effort value - fallback to highest supported
|
||||
const normalizedReasoningEffort = computed(() => {
|
||||
const supported = availableReasoningEfforts.value;
|
||||
if (supported.length === 0) return props.reasoningEffort;
|
||||
if (supported.includes(props.reasoningEffort)) return props.reasoningEffort;
|
||||
// Fallback to highest supported effort (last in the sorted array)
|
||||
return supported[supported.length - 1];
|
||||
});
|
||||
|
||||
// Show CCR option only when Claude CLI is selected
|
||||
const showCcrOption = computed(() => {
|
||||
return props.selectedCli === 'claude';
|
||||
@@ -278,7 +329,24 @@ function handleCcrChange(event: Event): void {
|
||||
}
|
||||
|
||||
function handleModelChange(event: Event): void {
|
||||
emit('model:update', (event.target as HTMLSelectElement).value);
|
||||
const newModel = (event.target as HTMLSelectElement).value;
|
||||
emit('model:update', newModel);
|
||||
|
||||
// When model changes for Codex, validate reasoning effort
|
||||
if (props.selectedCli === 'codex') {
|
||||
const supported = getCodexReasoningEfforts(newModel || getDefaultModelForCli('codex'));
|
||||
if (!supported.includes(props.reasoningEffort)) {
|
||||
// Auto-downgrade to highest supported effort
|
||||
emit('reasoning-effort:update', supported[supported.length - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleReasoningEffortChange(event: Event): void {
|
||||
emit(
|
||||
'reasoning-effort:update',
|
||||
(event.target as HTMLSelectElement).value as CodexReasoningEffort,
|
||||
);
|
||||
}
|
||||
|
||||
function handleRootInput(event: Event): void {
|
||||
|
||||
+16
-82
@@ -9,55 +9,6 @@
|
||||
boxShadow: 'var(--ac-shadow-float, 0 4px 20px -2px rgba(0,0,0,0.1))',
|
||||
}"
|
||||
>
|
||||
<!-- Current Session Actions (only when a session is selected) -->
|
||||
<template v-if="selectedSession">
|
||||
<div
|
||||
class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider"
|
||||
:style="{ color: 'var(--ac-text-subtle, #a8a29e)' }"
|
||||
>
|
||||
Current Session
|
||||
</div>
|
||||
<div class="px-3 py-2 flex flex-col gap-1">
|
||||
<button
|
||||
class="w-full px-2 py-1.5 text-left text-xs rounded ac-menu-item flex items-center gap-2"
|
||||
:style="{ color: 'var(--ac-text, #1a1a1a)' }"
|
||||
@click="handleOpenSettings"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
Session settings...
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1.5 text-left text-xs rounded ac-menu-item flex items-center gap-2"
|
||||
:style="{ color: 'var(--ac-danger, #dc2626)' }"
|
||||
@click="handleResetConversation"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
Reset conversation...
|
||||
</button>
|
||||
</div>
|
||||
<div class="mx-3 my-1 border-t" :style="{ borderColor: 'var(--ac-border, #e5e5e5)' }" />
|
||||
</template>
|
||||
|
||||
<!-- Sessions Section -->
|
||||
<div
|
||||
class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider"
|
||||
@@ -105,10 +56,11 @@
|
||||
ref="renameInputRef"
|
||||
v-model="editingName"
|
||||
type="text"
|
||||
class="w-full px-1 py-0.5 text-sm rounded border"
|
||||
class="w-full px-1 py-0.5 text-sm"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface, #ffffff)',
|
||||
borderColor: 'var(--ac-accent, #c87941)',
|
||||
border: 'var(--ac-border-width, 1px) solid var(--ac-accent, #c87941)',
|
||||
borderRadius: 'var(--ac-radius-button, 8px)',
|
||||
color: 'var(--ac-text, #1a1a1a)',
|
||||
outline: 'none',
|
||||
}"
|
||||
@@ -121,10 +73,11 @@
|
||||
<template v-else>
|
||||
<span>{{ getSessionDisplayName(session) }}</span>
|
||||
<span
|
||||
class="text-[10px] px-1.5 py-0.5 rounded"
|
||||
class="text-[10px] px-1.5 py-0.5"
|
||||
:style="{
|
||||
backgroundColor: getEngineColor(session.engineName),
|
||||
color: '#ffffff',
|
||||
borderRadius: 'var(--ac-radius-button, 8px)',
|
||||
}"
|
||||
>
|
||||
{{ session.engineName }}
|
||||
@@ -151,8 +104,11 @@
|
||||
<!-- Rename Button -->
|
||||
<button
|
||||
v-if="editingSessionId !== session.id"
|
||||
class="p-1 rounded ac-btn"
|
||||
:style="{ color: 'var(--ac-text-muted, #6e6e6e)' }"
|
||||
class="p-1 ac-btn"
|
||||
:style="{
|
||||
color: 'var(--ac-text-muted, #6e6e6e)',
|
||||
borderRadius: 'var(--ac-radius-button)',
|
||||
}"
|
||||
title="Rename session"
|
||||
@click.stop="startRename(session)"
|
||||
>
|
||||
@@ -167,8 +123,11 @@
|
||||
</button>
|
||||
<!-- Delete Button -->
|
||||
<button
|
||||
class="p-1 rounded ac-btn"
|
||||
:style="{ color: 'var(--ac-danger, #dc2626)' }"
|
||||
class="p-1 ac-btn"
|
||||
:style="{
|
||||
color: 'var(--ac-danger, #dc2626)',
|
||||
borderRadius: 'var(--ac-radius-button)',
|
||||
}"
|
||||
title="Delete session"
|
||||
@click.stop="handleDeleteSession(session.id)"
|
||||
>
|
||||
@@ -220,7 +179,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, nextTick } from 'vue';
|
||||
import { ref, nextTick } from 'vue';
|
||||
import type { AgentSession } from 'chrome-mcp-shared';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -237,8 +196,6 @@ const emit = defineEmits<{
|
||||
'session:new': [];
|
||||
'session:delete': [sessionId: string];
|
||||
'session:rename': [sessionId: string, name: string];
|
||||
'session:settings': [sessionId: string];
|
||||
'session:reset': [sessionId: string];
|
||||
}>();
|
||||
|
||||
// Inline rename state
|
||||
@@ -246,11 +203,6 @@ const editingSessionId = ref<string | null>(null);
|
||||
const editingName = ref('');
|
||||
const renameInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// Computed
|
||||
const selectedSession = computed(() => {
|
||||
return props.sessions.find((s) => s.id === props.selectedSessionId) || null;
|
||||
});
|
||||
|
||||
function getEngineColor(engineName: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
claude: '#c87941',
|
||||
@@ -331,22 +283,4 @@ function cancelRename(): void {
|
||||
editingSessionId.value = null;
|
||||
editingName.value = '';
|
||||
}
|
||||
|
||||
// Current session actions
|
||||
function handleOpenSettings(): void {
|
||||
if (props.selectedSessionId) {
|
||||
emit('session:settings', props.selectedSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
function handleResetConversation(): void {
|
||||
if (
|
||||
props.selectedSessionId &&
|
||||
confirm(
|
||||
'Reset this conversation? All messages will be deleted and the session will start fresh.',
|
||||
)
|
||||
) {
|
||||
emit('session:reset', props.selectedSessionId);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+136
-28
@@ -13,21 +13,24 @@
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface, #ffffff)',
|
||||
border: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
|
||||
borderRadius: 'var(--ac-radius-outer, 12px)',
|
||||
borderRadius: 'var(--ac-radius-card, 12px)',
|
||||
boxShadow: 'var(--ac-shadow-float, 0 4px 20px -2px rgba(0,0,0,0.2))',
|
||||
}"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-3 border-b"
|
||||
:style="{ borderColor: 'var(--ac-border, #e5e5e5)' }"
|
||||
class="flex items-center justify-between px-4 py-3"
|
||||
:style="{ borderBottom: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)' }"
|
||||
>
|
||||
<h2 class="text-sm font-semibold" :style="{ color: 'var(--ac-text, #1a1a1a)' }">
|
||||
Session Settings
|
||||
</h2>
|
||||
<button
|
||||
class="p-1 rounded ac-btn"
|
||||
:style="{ color: 'var(--ac-text-muted, #6e6e6e)' }"
|
||||
class="p-1 ac-btn"
|
||||
:style="{
|
||||
color: 'var(--ac-text-muted, #6e6e6e)',
|
||||
borderRadius: 'var(--ac-radius-button)',
|
||||
}"
|
||||
@click="handleClose"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -63,10 +66,11 @@
|
||||
<div class="flex justify-between">
|
||||
<span :style="{ color: 'var(--ac-text-muted, #6e6e6e)' }">Engine</span>
|
||||
<span
|
||||
class="px-1.5 py-0.5 rounded text-[10px]"
|
||||
class="px-1.5 py-0.5 text-[10px]"
|
||||
:style="{
|
||||
backgroundColor: getEngineColor(session?.engineName || ''),
|
||||
color: '#ffffff',
|
||||
borderRadius: 'var(--ac-radius-button, 8px)',
|
||||
}"
|
||||
>
|
||||
{{ session?.engineName || 'Unknown' }}
|
||||
@@ -95,10 +99,11 @@
|
||||
</label>
|
||||
<select
|
||||
v-model="localModel"
|
||||
class="w-full px-2 py-1.5 text-xs rounded border"
|
||||
class="w-full px-2 py-1.5 text-xs"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface, #ffffff)',
|
||||
borderColor: 'var(--ac-border, #e5e5e5)',
|
||||
border: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
|
||||
borderRadius: 'var(--ac-radius-button, 8px)',
|
||||
color: 'var(--ac-text, #1a1a1a)',
|
||||
}"
|
||||
>
|
||||
@@ -109,6 +114,36 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Reasoning Effort (Codex only) -->
|
||||
<div v-if="isCodexEngine" class="space-y-2">
|
||||
<label
|
||||
class="text-[10px] font-bold uppercase tracking-wider"
|
||||
:style="{ color: 'var(--ac-text-subtle, #a8a29e)' }"
|
||||
>
|
||||
Reasoning Effort
|
||||
</label>
|
||||
<select
|
||||
v-model="localReasoningEffort"
|
||||
class="w-full px-2 py-1.5 text-xs"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface, #ffffff)',
|
||||
border: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
|
||||
borderRadius: 'var(--ac-radius-button, 8px)',
|
||||
color: 'var(--ac-text, #1a1a1a)',
|
||||
}"
|
||||
>
|
||||
<option v-for="effort in availableReasoningEfforts" :key="effort" :value="effort">
|
||||
{{ effort }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="text-[10px]" :style="{ color: 'var(--ac-text-subtle, #a8a29e)' }">
|
||||
Controls the reasoning depth. Higher effort = better quality but slower.
|
||||
<span v-if="!availableReasoningEfforts.includes('xhigh')" class="block mt-1">
|
||||
Note: xhigh is only available for gpt-5.2 and gpt-5.1-codex-max models.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Permission Mode (Claude only) -->
|
||||
<div v-if="isClaudeEngine" class="space-y-2">
|
||||
<label
|
||||
@@ -119,10 +154,11 @@
|
||||
</label>
|
||||
<select
|
||||
v-model="localPermissionMode"
|
||||
class="w-full px-2 py-1.5 text-xs rounded border"
|
||||
class="w-full px-2 py-1.5 text-xs"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface, #ffffff)',
|
||||
borderColor: 'var(--ac-border, #e5e5e5)',
|
||||
border: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
|
||||
borderRadius: 'var(--ac-radius-button, 8px)',
|
||||
color: 'var(--ac-text, #1a1a1a)',
|
||||
}"
|
||||
>
|
||||
@@ -165,10 +201,11 @@
|
||||
<textarea
|
||||
v-if="localAppendToPrompt"
|
||||
v-model="localPromptAppend"
|
||||
class="mt-1 w-full px-2 py-1.5 text-xs rounded border resize-none"
|
||||
class="mt-1 w-full px-2 py-1.5 text-xs resize-none"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface, #ffffff)',
|
||||
borderColor: 'var(--ac-border, #e5e5e5)',
|
||||
border: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
|
||||
borderRadius: 'var(--ac-radius-button, 8px)',
|
||||
color: 'var(--ac-text, #1a1a1a)',
|
||||
fontFamily: 'var(--ac-font-mono, monospace)',
|
||||
}"
|
||||
@@ -187,10 +224,11 @@
|
||||
<textarea
|
||||
v-if="localUseCustomPrompt"
|
||||
v-model="localCustomPrompt"
|
||||
class="w-full px-2 py-1.5 text-xs rounded border resize-none"
|
||||
class="w-full px-2 py-1.5 text-xs resize-none"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface, #ffffff)',
|
||||
borderColor: 'var(--ac-border, #e5e5e5)',
|
||||
border: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
|
||||
borderRadius: 'var(--ac-radius-button, 8px)',
|
||||
color: 'var(--ac-text, #1a1a1a)',
|
||||
fontFamily: 'var(--ac-font-mono, monospace)',
|
||||
}"
|
||||
@@ -209,8 +247,11 @@
|
||||
SDK Info
|
||||
</label>
|
||||
<div
|
||||
class="text-[10px] space-y-1 p-2 rounded"
|
||||
:style="{ backgroundColor: 'var(--ac-surface-inset, #f5f5f5)' }"
|
||||
class="text-[10px] space-y-1 p-2"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface-inset, #f5f5f5)',
|
||||
borderRadius: 'var(--ac-radius-inner, 8px)',
|
||||
}"
|
||||
>
|
||||
<div v-if="managementInfo.model" class="flex justify-between">
|
||||
<span :style="{ color: 'var(--ac-text-muted, #6e6e6e)' }">Active Model</span>
|
||||
@@ -243,8 +284,11 @@
|
||||
View tools ({{ managementInfo.tools.length }})
|
||||
</summary>
|
||||
<div
|
||||
class="mt-1 p-2 rounded max-h-32 overflow-y-auto ac-scroll"
|
||||
:style="{ backgroundColor: 'var(--ac-surface-inset, #f5f5f5)' }"
|
||||
class="mt-1 p-2 max-h-32 overflow-y-auto ac-scroll"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface-inset, #f5f5f5)',
|
||||
borderRadius: 'var(--ac-radius-inner, 8px)',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="tool in managementInfo.tools"
|
||||
@@ -262,8 +306,11 @@
|
||||
View MCP servers ({{ managementInfo.mcpServers.length }})
|
||||
</summary>
|
||||
<div
|
||||
class="mt-1 p-2 rounded max-h-32 overflow-y-auto ac-scroll"
|
||||
:style="{ backgroundColor: 'var(--ac-surface-inset, #f5f5f5)' }"
|
||||
class="mt-1 p-2 max-h-32 overflow-y-auto ac-scroll"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface-inset, #f5f5f5)',
|
||||
borderRadius: 'var(--ac-radius-inner, 8px)',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="server in managementInfo.mcpServers"
|
||||
@@ -273,10 +320,11 @@
|
||||
>
|
||||
<span>{{ server.name }}</span>
|
||||
<span
|
||||
class="text-[9px] px-1 rounded"
|
||||
class="text-[9px] px-1"
|
||||
:style="{
|
||||
backgroundColor: server.status === 'connected' ? '#10b981' : '#6b7280',
|
||||
color: '#fff',
|
||||
borderRadius: 'var(--ac-radius-button, 8px)',
|
||||
}"
|
||||
>{{ server.status }}</span
|
||||
>
|
||||
@@ -289,24 +337,26 @@
|
||||
|
||||
<!-- Footer -->
|
||||
<div
|
||||
class="flex items-center justify-end gap-2 px-4 py-3 border-t"
|
||||
:style="{ borderColor: 'var(--ac-border, #e5e5e5)' }"
|
||||
class="flex items-center justify-end gap-2 px-4 py-3"
|
||||
:style="{ borderTop: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)' }"
|
||||
>
|
||||
<button
|
||||
class="px-3 py-1.5 text-xs rounded ac-btn"
|
||||
class="px-3 py-1.5 text-xs ac-btn"
|
||||
:style="{
|
||||
color: 'var(--ac-text-muted, #6e6e6e)',
|
||||
border: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
|
||||
borderRadius: 'var(--ac-radius-button, 8px)',
|
||||
}"
|
||||
@click="handleClose"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-xs rounded ac-btn"
|
||||
class="px-3 py-1.5 text-xs ac-btn"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-accent, #c87941)',
|
||||
color: '#ffffff',
|
||||
color: 'var(--ac-accent-contrast, #ffffff)',
|
||||
borderRadius: 'var(--ac-radius-button, 8px)',
|
||||
}"
|
||||
:disabled="isSaving"
|
||||
@click="handleSave"
|
||||
@@ -320,8 +370,18 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import type { AgentSession, AgentManagementInfo, AgentSystemPromptConfig } from 'chrome-mcp-shared';
|
||||
import { getModelsForCli } from '@/common/agent-models';
|
||||
import type {
|
||||
AgentSession,
|
||||
AgentManagementInfo,
|
||||
AgentSystemPromptConfig,
|
||||
CodexReasoningEffort,
|
||||
AgentSessionOptionsConfig,
|
||||
} from 'chrome-mcp-shared';
|
||||
import {
|
||||
getModelsForCli,
|
||||
getCodexReasoningEfforts,
|
||||
getDefaultModelForCli,
|
||||
} from '@/common/agent-models';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
@@ -340,11 +400,13 @@ export interface SessionSettings {
|
||||
model: string;
|
||||
permissionMode: string;
|
||||
systemPromptConfig: AgentSystemPromptConfig | null;
|
||||
optionsConfig?: AgentSessionOptionsConfig;
|
||||
}
|
||||
|
||||
// Local state
|
||||
const localModel = ref('');
|
||||
const localPermissionMode = ref('');
|
||||
const localReasoningEffort = ref<CodexReasoningEffort>('medium');
|
||||
const localUseCustomPrompt = ref(false);
|
||||
const localCustomPrompt = ref('');
|
||||
const localAppendToPrompt = ref(false);
|
||||
@@ -352,6 +414,22 @@ const localPromptAppend = ref('');
|
||||
|
||||
// Computed
|
||||
const isClaudeEngine = computed(() => props.session?.engineName === 'claude');
|
||||
const isCodexEngine = computed(() => props.session?.engineName === 'codex');
|
||||
|
||||
// Get available reasoning efforts based on selected model
|
||||
const availableReasoningEfforts = computed<readonly CodexReasoningEffort[]>(() => {
|
||||
if (!isCodexEngine.value) return [];
|
||||
const effectiveModel = localModel.value || getDefaultModelForCli('codex');
|
||||
return getCodexReasoningEfforts(effectiveModel);
|
||||
});
|
||||
|
||||
// Normalize reasoning effort when model changes
|
||||
const normalizedReasoningEffort = computed(() => {
|
||||
const supported = availableReasoningEfforts.value;
|
||||
if (supported.length === 0) return localReasoningEffort.value;
|
||||
if (supported.includes(localReasoningEffort.value)) return localReasoningEffort.value;
|
||||
return supported[supported.length - 1]; // fallback to highest supported
|
||||
});
|
||||
|
||||
const availableModels = computed(() => {
|
||||
if (!props.session?.engineName) return [];
|
||||
@@ -366,6 +444,14 @@ watch(
|
||||
localModel.value = session.model || '';
|
||||
localPermissionMode.value = session.permissionMode || '';
|
||||
|
||||
// Initialize reasoning effort from session's codex config
|
||||
const codexConfig = session.optionsConfig?.codexConfig;
|
||||
if (codexConfig?.reasoningEffort) {
|
||||
localReasoningEffort.value = codexConfig.reasoningEffort;
|
||||
} else {
|
||||
localReasoningEffort.value = 'medium';
|
||||
}
|
||||
|
||||
// Parse system prompt config based on type
|
||||
const config = session.systemPromptConfig;
|
||||
if (config) {
|
||||
@@ -391,6 +477,13 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Auto-adjust reasoning effort when model changes
|
||||
watch(localModel, () => {
|
||||
if (isCodexEngine.value) {
|
||||
localReasoningEffort.value = normalizedReasoningEffort.value;
|
||||
}
|
||||
});
|
||||
|
||||
function getEngineColor(engineName: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
claude: '#c87941',
|
||||
@@ -429,10 +522,25 @@ function handleSave(): void {
|
||||
};
|
||||
}
|
||||
|
||||
// Build optionsConfig for Codex engine
|
||||
let optionsConfig: AgentSessionOptionsConfig | undefined;
|
||||
if (isCodexEngine.value) {
|
||||
const existingOptions = props.session?.optionsConfig ?? {};
|
||||
const existingCodexConfig = existingOptions.codexConfig ?? {};
|
||||
optionsConfig = {
|
||||
...existingOptions,
|
||||
codexConfig: {
|
||||
...existingCodexConfig,
|
||||
reasoningEffort: normalizedReasoningEffort.value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const settings: SessionSettings = {
|
||||
model: localModel.value.trim(),
|
||||
permissionMode: localPermissionMode.value,
|
||||
systemPromptConfig,
|
||||
optionsConfig,
|
||||
};
|
||||
emit('save', settings);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@ defineEmits<{
|
||||
|
||||
const themes: { id: AgentThemeId; label: string }[] = [
|
||||
{ id: 'warm-editorial', label: THEME_LABELS['warm-editorial'] },
|
||||
{ id: 'blueprint-architect', label: THEME_LABELS['blueprint-architect'] },
|
||||
{ id: 'zen-journal', label: THEME_LABELS['zen-journal'] },
|
||||
{ id: 'neo-pop', label: THEME_LABELS['neo-pop'] },
|
||||
{ id: 'dark-console', label: THEME_LABELS['dark-console'] },
|
||||
{ id: 'swiss-grid', label: THEME_LABELS['swiss-grid'] },
|
||||
];
|
||||
|
||||
@@ -21,10 +21,11 @@
|
||||
|
||||
<!-- Project Breadcrumb -->
|
||||
<button
|
||||
class="flex items-center gap-1.5 text-xs px-2 py-1 rounded truncate group ac-btn"
|
||||
class="flex items-center gap-1.5 text-xs px-2 py-1 truncate group ac-btn"
|
||||
:style="{
|
||||
fontFamily: 'var(--ac-font-mono)',
|
||||
color: 'var(--ac-text-muted)',
|
||||
borderRadius: 'var(--ac-radius-button)',
|
||||
}"
|
||||
@click="$emit('toggle:projectMenu')"
|
||||
>
|
||||
@@ -47,10 +48,11 @@
|
||||
<!-- Session Breadcrumb -->
|
||||
<div class="h-3 w-[1px] flex-shrink-0" :style="{ backgroundColor: 'var(--ac-border)' }" />
|
||||
<button
|
||||
class="flex items-center gap-1.5 text-xs px-2 py-1 rounded truncate group ac-btn"
|
||||
class="flex items-center gap-1.5 text-xs px-2 py-1 truncate group ac-btn"
|
||||
:style="{
|
||||
fontFamily: 'var(--ac-font-mono)',
|
||||
color: 'var(--ac-text-subtle)',
|
||||
borderRadius: 'var(--ac-radius-button)',
|
||||
}"
|
||||
@click="$emit('toggle:sessionMenu')"
|
||||
>
|
||||
@@ -84,10 +86,10 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Settings Icon -->
|
||||
<!-- Theme & Settings Icon (Color Palette) -->
|
||||
<button
|
||||
class="p-1 rounded ac-btn ac-hover-text"
|
||||
:style="{ color: 'var(--ac-text-subtle)' }"
|
||||
class="p-1 ac-btn ac-hover-text"
|
||||
:style="{ color: 'var(--ac-text-subtle)', borderRadius: 'var(--ac-radius-button)' }"
|
||||
@click="$emit('toggle:settingsMenu')"
|
||||
>
|
||||
<svg
|
||||
@@ -95,11 +97,16 @@
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 15a3 3 0 100-6 3 3 0 000 6z" />
|
||||
<circle cx="13.5" cy="6.5" r=".5" fill="currentColor" />
|
||||
<circle cx="17.5" cy="10.5" r=".5" fill="currentColor" />
|
||||
<circle cx="8.5" cy="7.5" r=".5" fill="currentColor" />
|
||||
<circle cx="6.5" cy="12.5" r=".5" fill="currentColor" />
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"
|
||||
d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
+6
-6
@@ -35,8 +35,8 @@ defineProps<{
|
||||
<style scoped>
|
||||
.markdown-content :deep(pre) {
|
||||
background-color: var(--ac-code-bg);
|
||||
border: 1px solid var(--ac-code-border);
|
||||
border-radius: 6px;
|
||||
border: var(--ac-border-width) solid var(--ac-code-border);
|
||||
border-radius: var(--ac-radius-inner);
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
@@ -74,7 +74,7 @@ defineProps<{
|
||||
}
|
||||
|
||||
.markdown-content :deep(blockquote) {
|
||||
border-left: 3px solid var(--ac-border);
|
||||
border-left: var(--ac-border-width-strong) solid var(--ac-border);
|
||||
padding-left: 1em;
|
||||
margin: 0.5em 0;
|
||||
color: var(--ac-text-muted);
|
||||
@@ -97,7 +97,7 @@ defineProps<{
|
||||
|
||||
.markdown-content :deep(th),
|
||||
.markdown-content :deep(td) {
|
||||
border: 1px solid var(--ac-border);
|
||||
border: var(--ac-border-width) solid var(--ac-border);
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -108,13 +108,13 @@ defineProps<{
|
||||
|
||||
.markdown-content :deep(hr) {
|
||||
border: none;
|
||||
border-top: 1px solid var(--ac-border);
|
||||
border-top: var(--ac-border-width) solid var(--ac-border);
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown-content :deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--ac-radius-inner);
|
||||
}
|
||||
</style>
|
||||
|
||||
+4
-2
@@ -14,11 +14,12 @@
|
||||
<!-- Content based on tool kind -->
|
||||
<code
|
||||
v-if="item.tool.kind === 'grep' || item.tool.kind === 'read'"
|
||||
class="text-xs px-1.5 py-0.5 rounded cursor-pointer ac-chip-hover"
|
||||
class="text-xs px-1.5 py-0.5 cursor-pointer ac-chip-hover"
|
||||
:style="{
|
||||
fontFamily: 'var(--ac-font-mono)',
|
||||
backgroundColor: 'var(--ac-chip-bg)',
|
||||
color: 'var(--ac-chip-text)',
|
||||
borderRadius: 'var(--ac-radius-button)',
|
||||
}"
|
||||
:title="item.tool.filePath || item.tool.pattern"
|
||||
>
|
||||
@@ -40,11 +41,12 @@
|
||||
<!-- Diff Stats Preview (for edit) -->
|
||||
<span
|
||||
v-if="hasDiffStats"
|
||||
class="text-[10px] px-1.5 py-0.5 rounded"
|
||||
class="text-[10px] px-1.5 py-0.5"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-chip-bg)',
|
||||
color: 'var(--ac-text-muted)',
|
||||
fontFamily: 'var(--ac-font-mono)',
|
||||
borderRadius: 'var(--ac-radius-button)',
|
||||
}"
|
||||
>
|
||||
<span v-if="item.tool.diffStats?.addedLines" class="text-green-600 dark:text-green-400">
|
||||
|
||||
+9
-5
@@ -21,11 +21,12 @@
|
||||
<!-- Diff Stats Badge -->
|
||||
<span
|
||||
v-if="hasDiffStats"
|
||||
class="text-[10px] px-1.5 py-0.5 rounded"
|
||||
class="text-[10px] px-1.5 py-0.5"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-chip-bg)',
|
||||
color: 'var(--ac-text-muted)',
|
||||
fontFamily: 'var(--ac-font-mono)',
|
||||
borderRadius: 'var(--ac-radius-button)',
|
||||
}"
|
||||
>
|
||||
<span v-if="item.tool.diffStats?.addedLines" class="text-green-600 dark:text-green-400">
|
||||
@@ -60,7 +61,7 @@
|
||||
<!-- Result Card -->
|
||||
<div
|
||||
v-if="showCard"
|
||||
class="rounded-lg overflow-hidden text-xs leading-5"
|
||||
class="overflow-hidden text-xs leading-5"
|
||||
:style="{
|
||||
fontFamily: 'var(--ac-font-mono)',
|
||||
border: 'var(--ac-border-width) solid var(--ac-code-border)',
|
||||
@@ -71,12 +72,15 @@
|
||||
<!-- File list for edit -->
|
||||
<template v-if="item.tool.kind === 'edit' && item.tool.files?.length">
|
||||
<div
|
||||
v-for="file in item.tool.files.slice(0, 5)"
|
||||
v-for="(file, idx) in item.tool.files.slice(0, 5)"
|
||||
:key="file"
|
||||
class="px-3 py-1 border-b last:border-b-0"
|
||||
class="px-3 py-1"
|
||||
:style="{
|
||||
backgroundColor: 'var(--ac-surface)',
|
||||
borderColor: 'var(--ac-border)',
|
||||
borderBottom:
|
||||
idx === Math.min(item.tool.files.length, 5) - 1
|
||||
? 'none'
|
||||
: 'var(--ac-border-width) solid var(--ac-border)',
|
||||
color: 'var(--ac-text-muted)',
|
||||
}"
|
||||
>
|
||||
|
||||
@@ -16,6 +16,18 @@ interface PathValidationResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize path for comparison (handle trailing slashes and separators).
|
||||
*/
|
||||
function normalizePathForComparison(path: string): string {
|
||||
// Remove trailing slashes and normalize separators
|
||||
return path
|
||||
.trim()
|
||||
.replace(/[/\\]+$/, '')
|
||||
.replace(/\\/g, '/')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export interface UseAgentProjectsOptions {
|
||||
getServerPort: () => number | null;
|
||||
ensureServer: () => Promise<boolean>;
|
||||
@@ -410,6 +422,30 @@ export function useAgentProjects(options: UseAgentProjectsOptions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if project with same path already exists
|
||||
const normalizedPath = normalizePathForComparison(validation.absolute);
|
||||
const existingProject = projects.value.find(
|
||||
(p) => normalizePathForComparison(p.rootPath) === normalizedPath,
|
||||
);
|
||||
|
||||
if (existingProject) {
|
||||
// Project already exists - select it instead of creating a new one
|
||||
const shouldSwitch = confirm(
|
||||
`目录 "${validation.absolute}" 已存在对应的项目:${existingProject.name}\n\n` +
|
||||
`是否切换到该项目?\n\n` +
|
||||
`A project already exists for "${validation.absolute}": ${existingProject.name}\n` +
|
||||
`Switch to that project?`,
|
||||
);
|
||||
if (shouldSwitch) {
|
||||
selectedProjectId.value = existingProject.id;
|
||||
await saveSelectedProjectId();
|
||||
await loadChatHistory(existingProject.id);
|
||||
return existingProject;
|
||||
}
|
||||
// User declined to switch, return null to indicate no action taken
|
||||
return null;
|
||||
}
|
||||
|
||||
// If directory doesn't exist, ask user for confirmation
|
||||
let allowCreate = false;
|
||||
if (validation.needsCreation) {
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
import { ref, type Ref } from 'vue';
|
||||
|
||||
/** Available theme identifiers */
|
||||
export type AgentThemeId = 'warm-editorial' | 'dark-console' | 'swiss-grid';
|
||||
export type AgentThemeId =
|
||||
| 'warm-editorial'
|
||||
| 'blueprint-architect'
|
||||
| 'zen-journal'
|
||||
| 'neo-pop'
|
||||
| 'dark-console'
|
||||
| 'swiss-grid';
|
||||
|
||||
/** Storage key for persisting theme preference */
|
||||
const STORAGE_KEY_THEME = 'agentTheme';
|
||||
@@ -14,11 +20,21 @@ const STORAGE_KEY_THEME = 'agentTheme';
|
||||
const DEFAULT_THEME: AgentThemeId = 'warm-editorial';
|
||||
|
||||
/** Valid theme IDs for validation */
|
||||
const VALID_THEMES: AgentThemeId[] = ['warm-editorial', 'dark-console', 'swiss-grid'];
|
||||
const VALID_THEMES: AgentThemeId[] = [
|
||||
'warm-editorial',
|
||||
'blueprint-architect',
|
||||
'zen-journal',
|
||||
'neo-pop',
|
||||
'dark-console',
|
||||
'swiss-grid',
|
||||
];
|
||||
|
||||
/** Theme display names for UI */
|
||||
export const THEME_LABELS: Record<AgentThemeId, string> = {
|
||||
'warm-editorial': 'Editorial',
|
||||
'blueprint-architect': 'Blueprint',
|
||||
'zen-journal': 'Zen',
|
||||
'neo-pop': 'Neo-Pop',
|
||||
'dark-console': 'Console',
|
||||
'swiss-grid': 'Swiss',
|
||||
};
|
||||
|
||||
@@ -538,10 +538,11 @@ function buildThreads(
|
||||
const items = [...g.items].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
||||
|
||||
// Add streaming status item if running
|
||||
// Use stable ID without Date.now() to prevent component remount on each render
|
||||
if (state === 'running') {
|
||||
items.push({
|
||||
kind: 'status',
|
||||
id: `status:streaming:${requestId ?? 'current'}:${Date.now()}`,
|
||||
id: `status:streaming:${requestId ?? 'current'}`,
|
||||
requestId,
|
||||
createdAt: new Date().toISOString(),
|
||||
status: 'running',
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
*
|
||||
* Themes:
|
||||
* - warm-editorial (default): Warm, editorial style from agent-ux.html
|
||||
* - blueprint-architect: Blueprint grid with technical aesthetic
|
||||
* - zen-journal: Calm journal / graphite accent (Muji style)
|
||||
* - neo-pop: Thick borders + hard shadow (Brutalist)
|
||||
* - dark-console: Dark terminal/console style
|
||||
* - swiss-grid: High-contrast brutalist/swiss style
|
||||
*/
|
||||
@@ -168,6 +171,236 @@
|
||||
--ac-border-width: 1px;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
BLUEPRINT ARCHITECT
|
||||
======================================== */
|
||||
.agent-theme[data-agent-theme='blueprint-architect'] {
|
||||
--ac-font-body: var(--ac-font-grotesk);
|
||||
--ac-font-heading: var(--ac-font-grotesk);
|
||||
--ac-font-code: var(--ac-font-mono);
|
||||
|
||||
--ac-bg: #f7fbff;
|
||||
--ac-bg-pattern:
|
||||
linear-gradient(to right, rgba(37, 99, 235, 0.14) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(37, 99, 235, 0.14) 1px, transparent 1px);
|
||||
--ac-bg-pattern-size: 24px 24px;
|
||||
|
||||
--ac-header-bg: rgba(247, 251, 255, 0.86);
|
||||
--ac-header-border: rgba(37, 99, 235, 0.25);
|
||||
|
||||
--ac-surface: rgba(255, 255, 255, 0.92);
|
||||
--ac-surface-muted: rgba(239, 246, 255, 0.9);
|
||||
--ac-surface-inset: rgba(239, 246, 255, 0.9);
|
||||
|
||||
--ac-text: #0b1220;
|
||||
--ac-text-muted: #1f2a44;
|
||||
--ac-text-subtle: #475569;
|
||||
--ac-text-inverse: #ffffff;
|
||||
--ac-text-placeholder: #64748b;
|
||||
|
||||
--ac-border: rgba(37, 99, 235, 0.25);
|
||||
--ac-border-strong: rgba(37, 99, 235, 0.45);
|
||||
|
||||
--ac-hover-bg: rgba(37, 99, 235, 0.08);
|
||||
--ac-hover-bg-subtle: rgba(37, 99, 235, 0.05);
|
||||
|
||||
--ac-accent: #2563eb;
|
||||
--ac-accent-hover: #1d4ed8;
|
||||
--ac-accent-subtle: rgba(37, 99, 235, 0.12);
|
||||
--ac-accent-contrast: #ffffff;
|
||||
|
||||
--ac-accent-2: #0ea5e9;
|
||||
--ac-link: var(--ac-accent);
|
||||
--ac-link-hover: var(--ac-accent-hover);
|
||||
|
||||
--ac-selection-bg: rgba(37, 99, 235, 0.16);
|
||||
--ac-selection-text: #0b1220;
|
||||
|
||||
--ac-shadow-card: 0 1px 3px rgba(2, 6, 23, 0.12);
|
||||
--ac-shadow-float: 0 10px 28px -10px rgba(2, 6, 23, 0.22);
|
||||
--ac-focus-ring: rgba(37, 99, 235, 0.4);
|
||||
|
||||
--ac-timeline-line: rgba(37, 99, 235, 0.35);
|
||||
--ac-timeline-node: rgba(37, 99, 235, 0.35);
|
||||
--ac-timeline-node-hover: rgba(37, 99, 235, 0.55);
|
||||
--ac-timeline-node-active: var(--ac-accent);
|
||||
--ac-timeline-node-active-border: var(--ac-accent);
|
||||
--ac-timeline-node-pulse-shadow:
|
||||
0 0 0 2px rgba(37, 99, 235, 0.25), 0 0 12px rgba(37, 99, 235, 0.2);
|
||||
|
||||
--ac-chip-bg: rgba(239, 246, 255, 0.9);
|
||||
--ac-chip-text: #0b1220;
|
||||
--ac-chip-border: rgba(37, 99, 235, 0.25);
|
||||
|
||||
--ac-code-bg: rgba(255, 255, 255, 0.92);
|
||||
--ac-code-text: #0b1220;
|
||||
--ac-code-border: rgba(37, 99, 235, 0.25);
|
||||
|
||||
--ac-diff-add-bg: rgba(34, 197, 94, 0.12);
|
||||
--ac-diff-add-text: #15803d;
|
||||
--ac-diff-add-border: rgba(34, 197, 94, 0.4);
|
||||
--ac-diff-del-bg: rgba(239, 68, 68, 0.12);
|
||||
--ac-diff-del-text: #b91c1c;
|
||||
--ac-diff-del-border: rgba(239, 68, 68, 0.4);
|
||||
|
||||
--ac-scrollbar-thumb: rgba(37, 99, 235, 0.2);
|
||||
--ac-scrollbar-thumb-hover: rgba(37, 99, 235, 0.35);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
ZEN JOURNAL
|
||||
======================================== */
|
||||
.agent-theme[data-agent-theme='zen-journal'] {
|
||||
--ac-font-body: var(--ac-font-serif);
|
||||
--ac-font-heading: var(--ac-font-serif);
|
||||
--ac-font-code: var(--ac-font-mono);
|
||||
|
||||
--ac-bg: #fafaf9;
|
||||
--ac-bg-pattern: linear-gradient(to bottom, rgba(120, 113, 108, 0.07) 1px, transparent 1px);
|
||||
--ac-bg-pattern-size: 100% 28px;
|
||||
|
||||
--ac-header-bg: rgba(250, 250, 249, 0.92);
|
||||
--ac-header-border: rgba(231, 229, 228, 0.9);
|
||||
|
||||
--ac-surface: rgba(255, 255, 255, 0.92);
|
||||
--ac-surface-muted: rgba(245, 245, 244, 0.92);
|
||||
--ac-surface-inset: rgba(245, 245, 244, 0.92);
|
||||
|
||||
--ac-text: #1c1917;
|
||||
--ac-text-muted: #44403c;
|
||||
--ac-text-subtle: #78716c;
|
||||
--ac-text-inverse: #ffffff;
|
||||
--ac-text-placeholder: #a8a29e;
|
||||
|
||||
--ac-border: #e7e5e4;
|
||||
--ac-border-strong: #d6d3d1;
|
||||
|
||||
--ac-hover-bg: rgba(120, 113, 108, 0.08);
|
||||
--ac-hover-bg-subtle: rgba(120, 113, 108, 0.05);
|
||||
|
||||
--ac-accent: #57534e;
|
||||
--ac-accent-hover: #44403c;
|
||||
--ac-accent-subtle: rgba(87, 83, 78, 0.12);
|
||||
--ac-accent-contrast: #ffffff;
|
||||
--ac-accent-2: var(--ac-accent);
|
||||
|
||||
--ac-link: var(--ac-accent);
|
||||
--ac-link-hover: var(--ac-accent-hover);
|
||||
|
||||
--ac-selection-bg: rgba(87, 83, 78, 0.18);
|
||||
--ac-selection-text: #1c1917;
|
||||
|
||||
--ac-shadow-card: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
--ac-shadow-float: 0 14px 34px -18px rgba(0, 0, 0, 0.18);
|
||||
--ac-focus-ring: rgba(87, 83, 78, 0.35);
|
||||
|
||||
--ac-timeline-line: #e7e5e4;
|
||||
--ac-timeline-node: #d6d3d1;
|
||||
--ac-timeline-node-hover: #a8a29e;
|
||||
--ac-timeline-node-active: var(--ac-accent);
|
||||
--ac-timeline-node-active-border: var(--ac-accent);
|
||||
--ac-timeline-node-pulse-shadow:
|
||||
0 0 0 2px rgba(87, 83, 78, 0.25), 0 0 12px rgba(87, 83, 78, 0.2);
|
||||
|
||||
--ac-chip-bg: rgba(245, 245, 244, 0.92);
|
||||
--ac-chip-text: #1c1917;
|
||||
--ac-chip-border: #e7e5e4;
|
||||
|
||||
--ac-code-bg: rgba(255, 255, 255, 0.92);
|
||||
--ac-code-text: #1c1917;
|
||||
--ac-code-border: #e7e5e4;
|
||||
|
||||
--ac-diff-add-bg: rgba(34, 197, 94, 0.1);
|
||||
--ac-diff-add-text: #15803d;
|
||||
--ac-diff-add-border: rgba(34, 197, 94, 0.35);
|
||||
--ac-diff-del-bg: rgba(239, 68, 68, 0.1);
|
||||
--ac-diff-del-text: #b91c1c;
|
||||
--ac-diff-del-border: rgba(239, 68, 68, 0.35);
|
||||
|
||||
--ac-scrollbar-thumb: rgba(120, 113, 108, 0.15);
|
||||
--ac-scrollbar-thumb-hover: rgba(120, 113, 108, 0.25);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
NEO POP
|
||||
======================================== */
|
||||
.agent-theme[data-agent-theme='neo-pop'] {
|
||||
--ac-font-body: var(--ac-font-sans);
|
||||
--ac-font-heading: var(--ac-font-grotesk);
|
||||
--ac-font-code: var(--ac-font-mono);
|
||||
|
||||
--ac-border-width: 4px;
|
||||
--ac-border-width-strong: 4px;
|
||||
--ac-radius-card: 0px;
|
||||
--ac-radius-inner: 0px;
|
||||
--ac-radius-button: 0px;
|
||||
|
||||
--ac-bg: #fff7ed;
|
||||
--ac-bg-pattern: radial-gradient(rgba(17, 24, 39, 0.12) 1px, transparent 1px);
|
||||
--ac-bg-pattern-size: 18px 18px;
|
||||
|
||||
--ac-header-bg: rgba(255, 247, 237, 0.92);
|
||||
--ac-header-border: #111827;
|
||||
|
||||
--ac-surface: #ffffff;
|
||||
--ac-surface-muted: #ffedd5;
|
||||
--ac-surface-inset: #ffffff;
|
||||
|
||||
--ac-text: #111827;
|
||||
--ac-text-muted: #374151;
|
||||
--ac-text-subtle: #6b7280;
|
||||
--ac-text-inverse: #ffffff;
|
||||
--ac-text-placeholder: #9ca3af;
|
||||
|
||||
--ac-border: #111827;
|
||||
--ac-border-strong: #111827;
|
||||
|
||||
--ac-hover-bg: rgba(17, 24, 39, 0.06);
|
||||
--ac-hover-bg-subtle: rgba(17, 24, 39, 0.04);
|
||||
|
||||
--ac-accent: #ff3d7f;
|
||||
--ac-accent-hover: #ff1f6a;
|
||||
--ac-accent-subtle: rgba(255, 61, 127, 0.14);
|
||||
--ac-accent-contrast: #ffffff;
|
||||
|
||||
--ac-accent-2: #22d3ee;
|
||||
--ac-link: var(--ac-accent-2);
|
||||
--ac-link-hover: #06b6d4;
|
||||
|
||||
--ac-selection-bg: rgba(255, 61, 127, 0.25);
|
||||
--ac-selection-text: #111827;
|
||||
|
||||
--ac-shadow-card: 6px 6px 0 0 var(--ac-border);
|
||||
--ac-shadow-float: 8px 8px 0 0 var(--ac-border);
|
||||
--ac-focus-ring: rgba(17, 24, 39, 0.35);
|
||||
|
||||
--ac-timeline-line-width: 4px;
|
||||
--ac-timeline-line: #111827;
|
||||
--ac-timeline-node: #111827;
|
||||
--ac-timeline-node-hover: #374151;
|
||||
--ac-timeline-node-active: var(--ac-accent);
|
||||
--ac-timeline-node-active-border: #111827;
|
||||
--ac-timeline-node-pulse-shadow: 0 0 0 2px rgba(17, 24, 39, 1);
|
||||
|
||||
--ac-chip-bg: #ffffff;
|
||||
--ac-chip-text: #111827;
|
||||
--ac-chip-border: #111827;
|
||||
|
||||
--ac-code-bg: #ffffff;
|
||||
--ac-code-text: #111827;
|
||||
--ac-code-border: #111827;
|
||||
|
||||
--ac-diff-add-bg: rgba(34, 197, 94, 0.18);
|
||||
--ac-diff-add-text: #15803d;
|
||||
--ac-diff-add-border: #111827;
|
||||
--ac-diff-del-bg: rgba(239, 68, 68, 0.18);
|
||||
--ac-diff-del-text: #b91c1c;
|
||||
--ac-diff-del-border: #111827;
|
||||
|
||||
--ac-scrollbar-thumb: rgba(17, 24, 39, 0.25);
|
||||
--ac-scrollbar-thumb-hover: rgba(17, 24, 39, 0.4);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
DARK CONSOLE
|
||||
======================================== */
|
||||
@@ -525,4 +758,63 @@
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Tooltip System - CSS-only tooltips using data-tooltip attribute
|
||||
============================================================ */
|
||||
.agent-theme [data-tooltip] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.agent-theme [data-tooltip]::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
font-family: var(--ac-font-sans);
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
color: var(--ac-text-inverse);
|
||||
background-color: var(--ac-text);
|
||||
border-radius: var(--ac-radius-button);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity 150ms ease,
|
||||
visibility 150ms ease;
|
||||
pointer-events: none;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.agent-theme [data-tooltip]:hover::after {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Tooltip arrow */
|
||||
.agent-theme [data-tooltip]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: calc(100% + 2px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 4px solid transparent;
|
||||
border-top-color: var(--ac-text);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity 150ms ease,
|
||||
visibility 150ms ease;
|
||||
pointer-events: none;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.agent-theme [data-tooltip]:hover::before {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,6 +667,7 @@ Work directly in the current directory. Do not create subdirectories unless spec
|
||||
pushConfig('sandbox_mode', config.sandboxMode);
|
||||
pushConfig('max_turns', config.maxTurns);
|
||||
pushConfig('max_thinking_tokens', config.maxThinkingTokens);
|
||||
pushConfig('reasoning_effort', config.reasoningEffort);
|
||||
args.push('-c', `instructions=${JSON.stringify(config.autoInstructions)}`);
|
||||
|
||||
return args;
|
||||
|
||||
@@ -283,6 +283,13 @@ export interface AgentStoredMessage {
|
||||
*/
|
||||
export type CodexSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
|
||||
|
||||
/**
|
||||
* Reasoning effort for Codex models.
|
||||
* - low/medium/high: supported by all models
|
||||
* - xhigh: only supported by gpt-5.2 and gpt-5.1-codex-max
|
||||
*/
|
||||
export type CodexReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh';
|
||||
|
||||
/**
|
||||
* Configuration options for Codex Engine.
|
||||
* These can be overridden per-session via session settings.
|
||||
@@ -302,6 +309,8 @@ export interface CodexEngineConfig {
|
||||
maxTurns: number;
|
||||
/** Maximum thinking tokens. Default: 4096 */
|
||||
maxThinkingTokens: number;
|
||||
/** Reasoning effort for supported models. Default: 'medium' */
|
||||
reasoningEffort: CodexReasoningEffort;
|
||||
/** Auto instructions for autonomous behavior. Default: AUTO_INSTRUCTIONS */
|
||||
autoInstructions: string;
|
||||
/** Append project context (file listing) to prompt. Default: true */
|
||||
@@ -331,6 +340,7 @@ export const DEFAULT_CODEX_CONFIG: CodexEngineConfig = {
|
||||
sandboxMode: 'danger-full-access',
|
||||
maxTurns: 20,
|
||||
maxThinkingTokens: 4096,
|
||||
reasoningEffort: 'medium',
|
||||
autoInstructions: CODEX_AUTO_INSTRUCTIONS,
|
||||
appendProjectContext: true,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user