hangwin--mcp-chrome
369 行
12 KiB
Vue
369 行
12 KiB
Vue
<template>
|
|
<div
|
|
v-if="open"
|
|
class="fixed top-12 left-4 right-4 z-50 py-2 max-w-[calc(100%-2rem)]"
|
|
:style="{
|
|
backgroundColor: 'var(--ac-surface, #ffffff)',
|
|
border: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
|
|
borderRadius: 'var(--ac-radius-inner, 8px)',
|
|
boxShadow: 'var(--ac-shadow-float, 0 4px 20px -2px rgba(0,0,0,0.1))',
|
|
}"
|
|
>
|
|
<!-- Projects Section -->
|
|
<div
|
|
class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider"
|
|
:style="{ color: 'var(--ac-text-subtle, #a8a29e)' }"
|
|
>
|
|
Projects
|
|
</div>
|
|
|
|
<!-- Project List -->
|
|
<div class="max-h-[200px] overflow-y-auto ac-scroll">
|
|
<button
|
|
v-for="p in projects"
|
|
:key="p.id"
|
|
class="w-full px-3 py-2 text-left text-sm flex items-center justify-between ac-menu-item"
|
|
:style="{
|
|
color:
|
|
selectedProjectId === p.id ? 'var(--ac-accent, #c87941)' : 'var(--ac-text, #1a1a1a)',
|
|
}"
|
|
@click="$emit('project:select', p.id)"
|
|
>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="truncate">{{ p.name }}</div>
|
|
<div
|
|
class="text-[10px] truncate"
|
|
:style="{
|
|
fontFamily: 'var(--ac-font-mono, monospace)',
|
|
color: 'var(--ac-text-subtle, #a8a29e)',
|
|
}"
|
|
>
|
|
{{ p.rootPath }}
|
|
</div>
|
|
</div>
|
|
<svg
|
|
v-if="selectedProjectId === p.id"
|
|
class="w-4 h-4 flex-shrink-0 ml-2"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M5 13l4 4L19 7"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- New Project -->
|
|
<button
|
|
class="w-full px-3 py-2 text-left text-sm ac-menu-item"
|
|
:style="{ color: 'var(--ac-link, #3b82f6)' }"
|
|
:disabled="isPicking"
|
|
@click="$emit('project:new')"
|
|
>
|
|
{{ isPicking ? 'Selecting...' : '+ New Project' }}
|
|
</button>
|
|
|
|
<!-- Divider -->
|
|
<div
|
|
class="my-2"
|
|
:style="{ borderTop: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)' }"
|
|
/>
|
|
|
|
<!-- CLI & Model Settings -->
|
|
<div
|
|
class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider"
|
|
:style="{ color: 'var(--ac-text-subtle, #a8a29e)' }"
|
|
>
|
|
Settings
|
|
</div>
|
|
|
|
<!-- CLI Selection -->
|
|
<div class="px-3 py-2 flex items-center gap-2">
|
|
<span class="text-xs w-12" :style="{ color: 'var(--ac-text-muted, #6e6e6e)' }"> CLI </span>
|
|
<select
|
|
:value="selectedCli"
|
|
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="handleCliChange"
|
|
>
|
|
<option value="">Auto</option>
|
|
<option v-for="e in engines" :key="e.name" :value="e.name">
|
|
{{ e.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Model Selection -->
|
|
<div class="px-3 py-2 flex items-center gap-2">
|
|
<span class="text-xs w-12" :style="{ color: 'var(--ac-text-muted, #6e6e6e)' }"> Model </span>
|
|
<select
|
|
:value="normalizedModel"
|
|
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)',
|
|
}"
|
|
:disabled="isModelDisabled"
|
|
@change="handleModelChange"
|
|
>
|
|
<option value="">Default</option>
|
|
<option v-for="m in availableModels" :key="m.id" :value="m.id">
|
|
{{ m.name }}
|
|
</option>
|
|
</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>
|
|
<label
|
|
class="flex items-center gap-2 cursor-pointer"
|
|
title="Use Claude Code Router for API routing"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
:checked="useCcr"
|
|
class="w-4 h-4 rounded"
|
|
:style="{
|
|
accentColor: 'var(--ac-accent, #c87941)',
|
|
}"
|
|
@change="handleCcrChange"
|
|
/>
|
|
<span class="text-xs" :style="{ color: 'var(--ac-text, #1a1a1a)' }">
|
|
Use Claude Code Router
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Chrome MCP Option - only shown when Claude or Codex CLI is selected -->
|
|
<div v-if="showChromeMcpOption" class="px-3 py-2 flex items-center gap-2">
|
|
<span class="text-xs w-12" :style="{ color: 'var(--ac-text-muted, #6e6e6e)' }"> MCP </span>
|
|
<label
|
|
class="flex items-center gap-2 cursor-pointer"
|
|
title="Enable local Chrome MCP server integration"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
:checked="enableChromeMcp"
|
|
class="w-4 h-4 rounded"
|
|
:style="{
|
|
accentColor: 'var(--ac-accent, #c87941)',
|
|
}"
|
|
@change="handleChromeMcpChange"
|
|
/>
|
|
<span class="text-xs" :style="{ color: 'var(--ac-text, #1a1a1a)' }">
|
|
Enable Chrome MCP Server
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Save Button -->
|
|
<div class="px-3 py-2">
|
|
<button
|
|
class="w-full px-3 py-1.5 text-xs rounded transition-colors hover:opacity-90 cursor-pointer"
|
|
:style="{
|
|
backgroundColor: 'var(--ac-accent, #c87941)',
|
|
color: 'var(--ac-accent-contrast, #ffffff)',
|
|
borderRadius: 'var(--ac-radius-button, 8px)',
|
|
}"
|
|
:disabled="isSaving"
|
|
@click="handleSave"
|
|
>
|
|
{{ isSaving ? 'Saving...' : 'Save Settings' }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Error -->
|
|
<div v-if="error" class="px-3 py-1 text-[10px]" :style="{ color: 'var(--ac-danger, #dc2626)' }">
|
|
{{ error }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed } from 'vue';
|
|
import type { AgentProject, AgentEngineInfo, CodexReasoningEffort } from 'chrome-mcp-shared';
|
|
import {
|
|
getModelsForCli,
|
|
getDefaultModelForCli,
|
|
getCodexReasoningEfforts,
|
|
type ModelDefinition,
|
|
} from '@/common/agent-models';
|
|
|
|
const props = defineProps<{
|
|
open: boolean;
|
|
projects: AgentProject[];
|
|
selectedProjectId: string;
|
|
selectedCli: string;
|
|
model: string;
|
|
reasoningEffort: CodexReasoningEffort;
|
|
useCcr: boolean;
|
|
enableChromeMcp: boolean;
|
|
engines: AgentEngineInfo[];
|
|
isPicking: boolean;
|
|
isSaving: boolean;
|
|
error: string | null;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
'project:select': [projectId: string];
|
|
'project:new': [];
|
|
'cli:update': [cli: string];
|
|
'model:update': [model: string];
|
|
'reasoning-effort:update': [effort: CodexReasoningEffort];
|
|
'ccr:update': [useCcr: boolean];
|
|
'chrome-mcp:update': [enableChromeMcp: boolean];
|
|
save: [];
|
|
}>();
|
|
|
|
// Get available models based on selected CLI
|
|
const availableModels = computed<ModelDefinition[]>(() => {
|
|
return getModelsForCli(props.selectedCli);
|
|
});
|
|
|
|
// Normalize model value: ensure it exists in available models or fallback to empty
|
|
const normalizedModel = computed(() => {
|
|
const trimmedModel = props.model.trim();
|
|
if (!trimmedModel) return '';
|
|
// No CLI selected = model disabled, show empty (server will use default)
|
|
if (!props.selectedCli) return '';
|
|
const models = availableModels.value;
|
|
// If CLI selected but no models defined, fallback to empty
|
|
if (models.length === 0) return '';
|
|
// Check if current model is valid for selected CLI
|
|
const isValid = models.some((m) => m.id === trimmedModel);
|
|
return isValid ? trimmedModel : '';
|
|
});
|
|
|
|
// Check if Model select should be disabled
|
|
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';
|
|
});
|
|
|
|
// Show Chrome MCP option when Claude, Codex, or Auto (empty) CLI is selected
|
|
// Auto typically defaults to Claude, and users should be able to manage this project-level setting
|
|
const showChromeMcpOption = computed(() => {
|
|
return !props.selectedCli || props.selectedCli === 'claude' || props.selectedCli === 'codex';
|
|
});
|
|
|
|
// Handle CLI change - auto-select default model for the CLI
|
|
function handleCliChange(event: Event): void {
|
|
const cli = (event.target as HTMLSelectElement).value;
|
|
emit('cli:update', cli);
|
|
|
|
// Auto-select default model when CLI changes
|
|
if (cli) {
|
|
const defaultModel = getDefaultModelForCli(cli);
|
|
// Validate default model exists in available models
|
|
const models = getModelsForCli(cli);
|
|
const isValidDefault = models.some((m) => m.id === defaultModel);
|
|
emit('model:update', isValidDefault ? defaultModel : (models[0]?.id ?? ''));
|
|
} else {
|
|
emit('model:update', '');
|
|
}
|
|
|
|
// Reset CCR when switching away from Claude
|
|
if (cli !== 'claude') {
|
|
emit('ccr:update', false);
|
|
}
|
|
}
|
|
|
|
function handleCcrChange(event: Event): void {
|
|
emit('ccr:update', (event.target as HTMLInputElement).checked);
|
|
}
|
|
|
|
function handleChromeMcpChange(event: Event): void {
|
|
emit('chrome-mcp:update', (event.target as HTMLInputElement).checked);
|
|
}
|
|
|
|
function handleModelChange(event: Event): void {
|
|
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 handleSave(): void {
|
|
emit('save');
|
|
}
|
|
</script>
|