mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 18:16:08 +00:00
feat(cli): add browser automation commands
This commit is contained in:
@@ -139,6 +139,11 @@ export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) {
|
||||
Export <code>CLIPROXY_API_KEY</code> before launching native Codex.
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
CCS-managed browser tooling belongs to <code>Settings > Browser</code>. Do not edit
|
||||
the <code>ccs_browser</code> entry from the generic MCP card unless you are
|
||||
intentionally overriding the managed path in raw TOML.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ const EMPTY_MCP_SERVER_DRAFT: CodexMcpServerEntry = {
|
||||
toolTimeoutSec: null,
|
||||
enabledTools: [],
|
||||
disabledTools: [],
|
||||
isCcsManaged: false,
|
||||
managementSurface: null,
|
||||
};
|
||||
|
||||
function toCsv(value: string[]) {
|
||||
@@ -70,9 +72,16 @@ function McpServerEditor({
|
||||
}: McpServerEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [draft, setDraft] = useState<CodexMcpServerEntry>(initialDraft);
|
||||
const reservedManagedBrowserDraft = isNew && draft.name.trim() === 'ccs_browser';
|
||||
|
||||
return (
|
||||
<>
|
||||
{reservedManagedBrowserDraft ? (
|
||||
<div className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm text-amber-900 dark:text-amber-200">
|
||||
<strong>ccs_browser</strong> is reserved for the CCS-managed browser tooling path.
|
||||
Configure it from <code>Settings > Browser</code> instead of creating it here.
|
||||
</div>
|
||||
) : null}
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Input
|
||||
value={draft.name}
|
||||
@@ -216,7 +225,9 @@ function McpServerEditor({
|
||||
disabledTools: draft.disabledTools,
|
||||
})
|
||||
}
|
||||
disabled={disabled || saving || draft.name.trim().length === 0}
|
||||
disabled={
|
||||
disabled || saving || draft.name.trim().length === 0 || reservedManagedBrowserDraft
|
||||
}
|
||||
>
|
||||
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
{/* TODO i18n: missing key codex.saveMcpServer */}
|
||||
@@ -244,6 +255,8 @@ export function CodexMcpServersCard({
|
||||
);
|
||||
const draftSeed = selectedEntry ?? EMPTY_MCP_SERVER_DRAFT;
|
||||
const draftKey = JSON.stringify(draftSeed);
|
||||
const selectedEntryIsManagedBrowser =
|
||||
selectedEntry?.isCcsManaged && selectedEntry.managementSurface === 'browser-settings';
|
||||
|
||||
return (
|
||||
<CodexConfigCardShell
|
||||
@@ -255,6 +268,12 @@ export function CodexMcpServersCard({
|
||||
description="Manage the safe MCP transport fields. Keep auth headers and bearer tokens in raw TOML."
|
||||
disabledReason={disabledReason}
|
||||
>
|
||||
{selectedEntryIsManagedBrowser ? (
|
||||
<div className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm text-amber-900 dark:text-amber-200">
|
||||
<strong>{selectedEntry?.name}</strong> is CCS-managed. Configure browser tooling from{' '}
|
||||
<code>Settings > Browser</code>; the generic MCP editor is read-only for this entry.
|
||||
</div>
|
||||
) : null}
|
||||
<Select value={selectedName} onValueChange={setSelectedName} disabled={disabled}>
|
||||
<SelectTrigger>
|
||||
{/* TODO i18n: missing key codex.selectMcpServer */}
|
||||
@@ -264,7 +283,7 @@ export function CodexMcpServersCard({
|
||||
<SelectItem value="new">{t('codex.createNewMcpServer')}</SelectItem>
|
||||
{entries.map((entry) => (
|
||||
<SelectItem key={entry.name} value={entry.name}>
|
||||
{entry.name}
|
||||
{entry.isCcsManaged ? `${entry.name} (CCS managed)` : entry.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -273,9 +292,9 @@ export function CodexMcpServersCard({
|
||||
key={draftKey}
|
||||
initialDraft={draftSeed}
|
||||
isNew={selectedName === 'new'}
|
||||
disabled={disabled}
|
||||
disabled={disabled || Boolean(selectedEntryIsManagedBrowser)}
|
||||
saving={saving}
|
||||
canDelete={selectedEntry !== null}
|
||||
canDelete={selectedEntry !== null && !selectedEntryIsManagedBrowser}
|
||||
onDelete={async () => {
|
||||
if (!selectedEntry) return;
|
||||
await onDelete(selectedEntry.name);
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
UpsertAiProviderEntryInput,
|
||||
} from '../../../src/cliproxy/ai-providers';
|
||||
import type { ProviderEntitlementEvidence } from '../../../src/cliproxy/provider-entitlement-types';
|
||||
import type { BrowserRuntimeEnv } from '../../../src/utils/browser/chrome-reuse';
|
||||
|
||||
export const API_BASE_URL = '/api';
|
||||
export const API_CONFLICT_ERROR_CODE = 'CONFLICT';
|
||||
@@ -233,6 +234,67 @@ export interface UpdateImageAnalysisSettingsPayload {
|
||||
profileBackends?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface BrowserSettingsConfig {
|
||||
claude: {
|
||||
enabled: boolean;
|
||||
userDataDir: string;
|
||||
devtoolsPort: number;
|
||||
};
|
||||
codex: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BrowserLaunchCommands {
|
||||
darwin: string;
|
||||
linux: string;
|
||||
win32: string;
|
||||
}
|
||||
|
||||
export interface ClaudeBrowserStatus {
|
||||
enabled: boolean;
|
||||
source: 'config' | 'CCS_BROWSER_USER_DATA_DIR' | 'CCS_BROWSER_PROFILE_DIR';
|
||||
overrideActive: boolean;
|
||||
state: 'disabled' | 'path_missing' | 'browser_not_running' | 'endpoint_unreachable' | 'ready';
|
||||
title: string;
|
||||
detail: string;
|
||||
nextStep: string;
|
||||
effectiveUserDataDir: string;
|
||||
recommendedUserDataDir: string;
|
||||
devtoolsPort: number;
|
||||
managedMcpServerName: string;
|
||||
managedMcpServerPath: string;
|
||||
launchCommands: BrowserLaunchCommands;
|
||||
runtimeEnv?: BrowserRuntimeEnv;
|
||||
}
|
||||
|
||||
export interface CodexBrowserStatus {
|
||||
enabled: boolean;
|
||||
state: 'disabled' | 'enabled' | 'unsupported_build';
|
||||
title: string;
|
||||
detail: string;
|
||||
nextStep: string;
|
||||
serverName: string;
|
||||
supportsConfigOverrides: boolean;
|
||||
binaryPath: string | null;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface BrowserStatusPayload {
|
||||
claude: ClaudeBrowserStatus;
|
||||
codex: CodexBrowserStatus;
|
||||
}
|
||||
|
||||
export interface BrowserDashboardPayload {
|
||||
config: BrowserSettingsConfig;
|
||||
status: BrowserStatusPayload;
|
||||
}
|
||||
|
||||
export interface UpdateBrowserSettingsPayload {
|
||||
claude?: Partial<BrowserSettingsConfig['claude']>;
|
||||
codex?: Partial<BrowserSettingsConfig['codex']>;
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
name: string;
|
||||
settingsPath: string;
|
||||
@@ -1090,6 +1152,15 @@ export const api = {
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
},
|
||||
browser: {
|
||||
get: () => request<BrowserDashboardPayload>('/browser'),
|
||||
getStatus: () => request<BrowserStatusPayload>('/browser/status'),
|
||||
update: (data: UpdateBrowserSettingsPayload) =>
|
||||
request<{ success: boolean; browser: BrowserDashboardPayload }>('/browser', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
},
|
||||
logs: {
|
||||
getConfig: () => request<{ logging: LogsConfig }>('/logs/config'),
|
||||
updateConfig: (data: UpdateLogsConfigPayload) =>
|
||||
|
||||
@@ -45,6 +45,8 @@ export interface CodexMcpServerEntry {
|
||||
toolTimeoutSec: number | null;
|
||||
enabledTools: string[];
|
||||
disabledTools: string[];
|
||||
isCcsManaged: boolean;
|
||||
managementSurface: 'browser-settings' | null;
|
||||
}
|
||||
|
||||
export interface CodexFeatureCatalogEntry {
|
||||
@@ -239,6 +241,8 @@ export function readCodexMcpServers(config: Record<string, unknown> | null): Cod
|
||||
toolTimeoutSec: asNumber(server.tool_timeout_sec),
|
||||
enabledTools: asStringArray(server.enabled_tools),
|
||||
disabledTools: asStringArray(server.disabled_tools),
|
||||
isCcsManaged: name === 'ccs_browser',
|
||||
managementSurface: name === 'ccs_browser' ? 'browser-settings' : null,
|
||||
};
|
||||
})
|
||||
.filter((entry): entry is CodexMcpServerEntry => entry !== null)
|
||||
|
||||
@@ -873,6 +873,7 @@ const resources = {
|
||||
configFileNotFound: 'Config file not found',
|
||||
},
|
||||
settingsTabs: {
|
||||
browser: 'Browser',
|
||||
web: 'Web',
|
||||
image: 'Image',
|
||||
channels: 'Channels',
|
||||
@@ -2424,6 +2425,61 @@ const resources = {
|
||||
description: 'Configure image analysis settings.',
|
||||
loading: 'Loading image settings...',
|
||||
},
|
||||
browserSection: {
|
||||
title: 'Browser',
|
||||
description: 'Primary setup surface for managed browser automation in CCS.',
|
||||
primaryTitle: 'Settings > Browser is the primary browser setup surface.',
|
||||
primaryDescription:
|
||||
'Configure Claude Browser Attach and Codex Browser Tools here, then use the guidance below to launch or verify each lane.',
|
||||
readiness: 'Readiness',
|
||||
nextStep: 'Next step',
|
||||
actions: {
|
||||
saveClaude: 'Save Claude settings',
|
||||
saveCodex: 'Save Codex settings',
|
||||
testConnection: 'Test connection',
|
||||
copyLaunchCommand: 'Copy launch command',
|
||||
},
|
||||
messages: {
|
||||
statusRefreshed: 'Browser status refreshed',
|
||||
launchCommandCopied: 'Launch command copied',
|
||||
},
|
||||
claude: {
|
||||
title: 'Claude Browser Attach',
|
||||
description:
|
||||
'Attach Claude-target launches to a managed Chrome session with remote debugging enabled.',
|
||||
enabledLabel: 'Enable Claude Browser Attach',
|
||||
enabledDescription:
|
||||
'When enabled, CCS prepares the managed browser MCP runtime for Claude-target launches.',
|
||||
userDataDir: 'Chrome user-data directory',
|
||||
userDataDirHint:
|
||||
'Use a dedicated Chrome profile directory so attach-mode state stays isolated from your daily browser.',
|
||||
devtoolsPort: 'DevTools port',
|
||||
devtoolsPortHint: 'Use the same port when you launch Chrome in attach mode.',
|
||||
devtoolsPortInvalid: 'Enter a valid port between 1 and 65535.',
|
||||
effectivePath: 'Effective attach path',
|
||||
recommendedPath: 'Recommended path',
|
||||
managedRuntime: 'Managed browser runtime',
|
||||
overrideMessage:
|
||||
'An environment override is currently active from {{source}}. This dashboard remains the source of truth once that override is removed.',
|
||||
launchGuidance: 'Launch guidance',
|
||||
launchGuidanceHint:
|
||||
'Start Chrome with remote debugging enabled, then rerun Test connection if needed.',
|
||||
},
|
||||
codex: {
|
||||
title: 'Codex Browser Tools',
|
||||
description:
|
||||
'Controls the managed Playwright MCP override path for Codex-target launches.',
|
||||
enabledLabel: 'Enable Codex Browser Tools',
|
||||
enabledDescription:
|
||||
'Keep this on to make the Browser page the first-class setup surface for Codex browser tooling.',
|
||||
serverName: 'Managed server name',
|
||||
overrideSupport: 'Config override support',
|
||||
overrideSupported: 'Supported',
|
||||
overrideUnsupported: 'Not supported',
|
||||
binary: 'Detected Codex binary',
|
||||
notDetected: 'Not detected',
|
||||
},
|
||||
},
|
||||
},
|
||||
codexPage: {
|
||||
title: 'Codex',
|
||||
@@ -3284,6 +3340,7 @@ const resources = {
|
||||
configFileNotFound: '未找到配置文件',
|
||||
},
|
||||
settingsTabs: {
|
||||
browser: '浏览器',
|
||||
web: '网页',
|
||||
image: '图片',
|
||||
channels: '频道',
|
||||
@@ -4781,6 +4838,55 @@ const resources = {
|
||||
description: '配置图片分析设置。',
|
||||
loading: '加载图片设置中...',
|
||||
},
|
||||
browserSection: {
|
||||
title: '浏览器',
|
||||
description: 'CCS 中托管浏览器自动化的主设置入口。',
|
||||
primaryTitle: 'Settings > Browser 是主要的浏览器设置界面。',
|
||||
primaryDescription:
|
||||
'在这里配置 Claude Browser Attach 和 Codex Browser Tools,然后按下方指引启动或验证每条路径。',
|
||||
readiness: '就绪状态',
|
||||
nextStep: '下一步',
|
||||
actions: {
|
||||
saveClaude: '保存 Claude 设置',
|
||||
saveCodex: '保存 Codex 设置',
|
||||
testConnection: '测试连接',
|
||||
copyLaunchCommand: '复制启动命令',
|
||||
},
|
||||
messages: {
|
||||
statusRefreshed: '浏览器状态已刷新',
|
||||
launchCommandCopied: '启动命令已复制',
|
||||
},
|
||||
claude: {
|
||||
title: 'Claude Browser Attach',
|
||||
description: '将 Claude 目标会话附加到启用了远程调试的 Chrome 会话。',
|
||||
enabledLabel: '启用 Claude Browser Attach',
|
||||
enabledDescription: '启用后,CCS 会为 Claude 目标会话准备托管浏览器 MCP 运行时。',
|
||||
userDataDir: 'Chrome 用户数据目录',
|
||||
userDataDirHint: '建议使用单独的 Chrome 配置目录,避免自动化状态污染日常浏览器。',
|
||||
devtoolsPort: 'DevTools 端口',
|
||||
devtoolsPortHint: '必须与 Chrome attach 模式实际启动时使用的端口一致。',
|
||||
devtoolsPortInvalid: '请输入 1 到 65535 之间的有效端口。',
|
||||
effectivePath: '当前生效的 attach 路径',
|
||||
recommendedPath: '推荐路径',
|
||||
managedRuntime: '托管浏览器运行时',
|
||||
overrideMessage:
|
||||
'当前存在来自 {{source}} 的环境变量覆盖。移除该覆盖后,Dashboard 配置将重新成为唯一来源。',
|
||||
launchGuidance: '启动指引',
|
||||
launchGuidanceHint: '使用远程调试启动 Chrome,如有需要再重新执行 Test connection。',
|
||||
},
|
||||
codex: {
|
||||
title: 'Codex Browser Tools',
|
||||
description: '控制 Codex 目标会话中的托管 Playwright MCP 浏览器路径。',
|
||||
enabledLabel: '启用 Codex Browser Tools',
|
||||
enabledDescription: '保持开启可让 Browser 页面成为 Codex 浏览器工具的主设置入口。',
|
||||
serverName: '托管服务名称',
|
||||
overrideSupport: '配置覆盖支持',
|
||||
overrideSupported: '支持',
|
||||
overrideUnsupported: '不支持',
|
||||
binary: '检测到的 Codex 可执行文件',
|
||||
notDetected: '未检测到',
|
||||
},
|
||||
},
|
||||
},
|
||||
codexPage: {
|
||||
title: 'Codex',
|
||||
@@ -5716,6 +5822,7 @@ const resources = {
|
||||
configFileNotFound: 'Không tìm thấy tập tin cấu hình',
|
||||
},
|
||||
settingsTabs: {
|
||||
browser: 'Trình duyệt',
|
||||
web: 'Web',
|
||||
image: 'Hình ảnh',
|
||||
channels: 'Kênh',
|
||||
@@ -7246,6 +7353,62 @@ const resources = {
|
||||
description: 'Cấu hình cài đặt phân tích hình ảnh.',
|
||||
loading: 'Đang tải cài đặt hình ảnh...',
|
||||
},
|
||||
browserSection: {
|
||||
title: 'Trình duyệt',
|
||||
description: 'Bề mặt thiết lập chính cho tự động hóa trình duyệt được CCS quản lý.',
|
||||
primaryTitle: 'Settings > Browser là nơi thiết lập trình duyệt chính.',
|
||||
primaryDescription:
|
||||
'Cấu hình Claude Browser Attach và Codex Browser Tools tại đây, rồi dùng hướng dẫn bên dưới để khởi chạy hoặc kiểm tra từng luồng.',
|
||||
readiness: 'Trạng thái sẵn sàng',
|
||||
nextStep: 'Bước tiếp theo',
|
||||
actions: {
|
||||
saveClaude: 'Lưu cấu hình Claude',
|
||||
saveCodex: 'Lưu cấu hình Codex',
|
||||
testConnection: 'Kiểm tra kết nối',
|
||||
copyLaunchCommand: 'Sao chép lệnh khởi chạy',
|
||||
},
|
||||
messages: {
|
||||
statusRefreshed: 'Đã làm mới trạng thái trình duyệt',
|
||||
launchCommandCopied: 'Đã sao chép lệnh khởi chạy',
|
||||
},
|
||||
claude: {
|
||||
title: 'Claude Browser Attach',
|
||||
description:
|
||||
'Gắn các phiên chạy Claude-target vào một phiên Chrome có bật remote debugging.',
|
||||
enabledLabel: 'Bật Claude Browser Attach',
|
||||
enabledDescription:
|
||||
'Khi bật, CCS sẽ chuẩn bị runtime MCP trình duyệt được quản lý cho các phiên chạy Claude-target.',
|
||||
userDataDir: 'Thư mục dữ liệu người dùng Chrome',
|
||||
userDataDirHint:
|
||||
'Nên dùng thư mục profile Chrome riêng để trạng thái attach được tách khỏi trình duyệt hằng ngày.',
|
||||
devtoolsPort: 'Cổng DevTools',
|
||||
devtoolsPortHint:
|
||||
'Phải khớp với cổng thực tế dùng khi khởi chạy Chrome trong attach mode.',
|
||||
devtoolsPortInvalid: 'Nhập cổng hợp lệ trong khoảng 1 đến 65535.',
|
||||
effectivePath: 'Đường dẫn attach đang có hiệu lực',
|
||||
recommendedPath: 'Đường dẫn khuyến nghị',
|
||||
managedRuntime: 'Runtime trình duyệt được quản lý',
|
||||
overrideMessage:
|
||||
'Hiện có override môi trường từ {{source}}. Khi bỏ override này, Dashboard sẽ lại là nguồn cấu hình chính.',
|
||||
launchGuidance: 'Hướng dẫn khởi chạy',
|
||||
launchGuidanceHint:
|
||||
'Khởi chạy Chrome với remote debugging, rồi chạy lại Test connection nếu cần.',
|
||||
},
|
||||
codex: {
|
||||
title: 'Codex Browser Tools',
|
||||
description:
|
||||
'Điều khiển đường dẫn trình duyệt Playwright MCP được quản lý cho các phiên chạy Codex-target.',
|
||||
enabledLabel: 'Bật Codex Browser Tools',
|
||||
enabledDescription:
|
||||
'Giữ bật để trang Browser là nơi cấu hình chính cho công cụ trình duyệt của Codex.',
|
||||
serverName: 'Tên dịch vụ được quản lý',
|
||||
overrideSupport: 'Hỗ trợ config override',
|
||||
overrideSupported: 'Được hỗ trợ',
|
||||
overrideUnsupported: 'Không được hỗ trợ',
|
||||
binary: 'Binary Codex được phát hiện',
|
||||
notDetected: 'Không phát hiện',
|
||||
},
|
||||
},
|
||||
},
|
||||
codexPage: {
|
||||
title: 'Codex',
|
||||
@@ -8180,6 +8343,7 @@ const resources = {
|
||||
configFileNotFound: '設定ファイルが見つかりません',
|
||||
},
|
||||
settingsTabs: {
|
||||
browser: 'ブラウザ',
|
||||
web: 'Web検索',
|
||||
image: '画像',
|
||||
channels: 'チャンネル',
|
||||
@@ -9611,6 +9775,62 @@ const resources = {
|
||||
description: '画像分析の設定。',
|
||||
loading: '画像設定を読み込み中...',
|
||||
},
|
||||
browserSection: {
|
||||
title: 'ブラウザ',
|
||||
description: 'CCS が管理するブラウザ自動化の主要な設定画面です。',
|
||||
primaryTitle: 'Settings > Browser がブラウザ設定の主入口です。',
|
||||
primaryDescription:
|
||||
'ここで Claude Browser Attach と Codex Browser Tools を設定し、各レーンの起動や確認は下の案内を使ってください。',
|
||||
readiness: '準備状況',
|
||||
nextStep: '次の手順',
|
||||
actions: {
|
||||
saveClaude: 'Claude 設定を保存',
|
||||
saveCodex: 'Codex 設定を保存',
|
||||
testConnection: '接続を確認',
|
||||
copyLaunchCommand: '起動コマンドをコピー',
|
||||
},
|
||||
messages: {
|
||||
statusRefreshed: 'ブラウザ状態を更新しました',
|
||||
launchCommandCopied: '起動コマンドをコピーしました',
|
||||
},
|
||||
claude: {
|
||||
title: 'Claude Browser Attach',
|
||||
description:
|
||||
'リモートデバッグを有効にした Chrome セッションへ Claude-target の起動を接続します。',
|
||||
enabledLabel: 'Claude Browser Attach を有効にする',
|
||||
enabledDescription:
|
||||
'有効にすると、CCS は Claude-target 起動向けに管理済みブラウザ MCP ランタイムを準備します。',
|
||||
userDataDir: 'Chrome ユーザーデータディレクトリ',
|
||||
userDataDirHint:
|
||||
'日常利用のブラウザと状態を分離するため、専用の Chrome プロファイルディレクトリを使うことを推奨します。',
|
||||
devtoolsPort: 'DevTools ポート',
|
||||
devtoolsPortHint:
|
||||
'Chrome を attach モードで起動したときに使う実際のポートと一致させてください。',
|
||||
devtoolsPortInvalid: '1 から 65535 の有効なポート番号を入力してください。',
|
||||
effectivePath: '現在有効な attach パス',
|
||||
recommendedPath: '推奨パス',
|
||||
managedRuntime: '管理済みブラウザランタイム',
|
||||
overrideMessage:
|
||||
'{{source}} から環境変数オーバーライドが有効です。これを外すと Dashboard の設定が再び主ソースになります。',
|
||||
launchGuidance: '起動ガイダンス',
|
||||
launchGuidanceHint:
|
||||
'リモートデバッグ付きで Chrome を起動し、必要なら Test connection を再実行してください。',
|
||||
},
|
||||
codex: {
|
||||
title: 'Codex Browser Tools',
|
||||
description:
|
||||
'Codex-target 起動向けの管理済み Playwright MCP ブラウザパスを制御します。',
|
||||
enabledLabel: 'Codex Browser Tools を有効にする',
|
||||
enabledDescription:
|
||||
'有効のままにすると、Browser ページが Codex ブラウザツールの主設定画面になります。',
|
||||
serverName: '管理対象サーバー名',
|
||||
overrideSupport: 'config override 対応',
|
||||
overrideSupported: '対応',
|
||||
overrideUnsupported: '非対応',
|
||||
binary: '検出された Codex バイナリ',
|
||||
notDetected: '未検出',
|
||||
},
|
||||
},
|
||||
},
|
||||
setupWizard: {
|
||||
title: 'クイックセットアップウィザード',
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Brain,
|
||||
Archive,
|
||||
MessageSquare,
|
||||
Monitor,
|
||||
} from 'lucide-react';
|
||||
import type { SettingsTab } from '../types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -25,6 +26,7 @@ interface TabNavigationProps {
|
||||
export function TabNavigation({ activeTab, onTabChange }: TabNavigationProps) {
|
||||
const { t } = useTranslation();
|
||||
const tabs = [
|
||||
{ value: 'browser' as const, label: t('settingsTabs.browser'), icon: Monitor },
|
||||
{ value: 'websearch' as const, label: t('settingsTabs.web'), icon: Globe },
|
||||
{ value: 'image' as const, label: t('settingsTabs.image'), icon: ImageIcon },
|
||||
{ value: 'channels' as const, label: t('settingsTabs.channels'), icon: MessageSquare },
|
||||
@@ -37,7 +39,7 @@ export function TabNavigation({ activeTab, onTabChange }: TabNavigationProps) {
|
||||
|
||||
return (
|
||||
<Tabs value={activeTab} onValueChange={(v) => onTabChange(v as SettingsTab)}>
|
||||
<TabsList className="grid w-full grid-cols-8">
|
||||
<TabsList className="grid w-full grid-cols-9">
|
||||
{tabs.map(({ value, label, icon: Icon }) => (
|
||||
<TabsTrigger key={value} value={value} className="gap-1.5 px-2 text-xs">
|
||||
<Icon className="h-3.5 w-3.5 shrink-0" />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
export {
|
||||
useBrowserConfig,
|
||||
useSettingsContext,
|
||||
useSettingsActions,
|
||||
useSettingsTab,
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { SettingsContext } from '../settings-context';
|
||||
import type {
|
||||
BrowserConfig,
|
||||
BrowserStatus,
|
||||
WebSearchConfig,
|
||||
GlobalEnvConfig,
|
||||
CliproxyServerConfig,
|
||||
@@ -24,6 +26,41 @@ export function useSettingsContext() {
|
||||
export function useSettingsActions() {
|
||||
const { dispatch } = useSettingsContext();
|
||||
|
||||
const setBrowserConfig = useCallback(
|
||||
(config: BrowserConfig | null) => dispatch({ type: 'SET_BROWSER_CONFIG', payload: config }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setBrowserStatus = useCallback(
|
||||
(status: BrowserStatus | null) => dispatch({ type: 'SET_BROWSER_STATUS', payload: status }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setBrowserLoading = useCallback(
|
||||
(loading: boolean) => dispatch({ type: 'SET_BROWSER_LOADING', payload: loading }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setBrowserStatusLoading = useCallback(
|
||||
(loading: boolean) => dispatch({ type: 'SET_BROWSER_STATUS_LOADING', payload: loading }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setBrowserSaving = useCallback(
|
||||
(saving: boolean) => dispatch({ type: 'SET_BROWSER_SAVING', payload: saving }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setBrowserError = useCallback(
|
||||
(error: string | null) => dispatch({ type: 'SET_BROWSER_ERROR', payload: error }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setBrowserSuccess = useCallback(
|
||||
(success: boolean) => dispatch({ type: 'SET_BROWSER_SUCCESS', payload: success }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setWebSearchConfig = useCallback(
|
||||
(config: WebSearchConfig | null) => dispatch({ type: 'SET_WEBSEARCH_CONFIG', payload: config }),
|
||||
[dispatch]
|
||||
@@ -133,6 +170,13 @@ export function useSettingsActions() {
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
setBrowserConfig,
|
||||
setBrowserStatus,
|
||||
setBrowserLoading,
|
||||
setBrowserStatusLoading,
|
||||
setBrowserSaving,
|
||||
setBrowserError,
|
||||
setBrowserSuccess,
|
||||
setWebSearchConfig,
|
||||
setWebSearchStatus,
|
||||
setWebSearchLoading,
|
||||
@@ -156,6 +200,13 @@ export function useSettingsActions() {
|
||||
setRawConfigLoading,
|
||||
}),
|
||||
[
|
||||
setBrowserConfig,
|
||||
setBrowserStatus,
|
||||
setBrowserLoading,
|
||||
setBrowserStatusLoading,
|
||||
setBrowserSaving,
|
||||
setBrowserError,
|
||||
setBrowserSuccess,
|
||||
setWebSearchConfig,
|
||||
setWebSearchStatus,
|
||||
setWebSearchLoading,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export { useSettingsContext, useSettingsActions } from './context-hooks';
|
||||
export { useSettingsTab } from './use-settings-tab';
|
||||
export { useBrowserConfig } from './use-browser-config';
|
||||
export { useWebSearchConfig } from './use-websearch-config';
|
||||
export { useGlobalEnvConfig } from './use-globalenv-config';
|
||||
export { useProxyConfig } from './use-proxy-config';
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useCallback } from 'react';
|
||||
import { api } from '@/lib/api-client';
|
||||
import { useSettingsActions, useSettingsContext } from './context-hooks';
|
||||
import type { BrowserConfig, BrowserSavePayload } from '../types';
|
||||
|
||||
function mergeConfig(current: BrowserConfig, updates: BrowserSavePayload): BrowserConfig {
|
||||
return {
|
||||
claude: {
|
||||
...current.claude,
|
||||
...updates.claude,
|
||||
},
|
||||
codex: {
|
||||
...current.codex,
|
||||
...updates.codex,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useBrowserConfig() {
|
||||
const { state } = useSettingsContext();
|
||||
const actions = useSettingsActions();
|
||||
|
||||
const fetchConfig = useCallback(async () => {
|
||||
try {
|
||||
actions.setBrowserLoading(true);
|
||||
actions.setBrowserError(null);
|
||||
const payload = await api.browser.get();
|
||||
actions.setBrowserConfig(payload.config);
|
||||
actions.setBrowserStatus(payload.status);
|
||||
} catch (err) {
|
||||
actions.setBrowserError((err as Error).message);
|
||||
} finally {
|
||||
actions.setBrowserLoading(false);
|
||||
actions.setBrowserStatusLoading(false);
|
||||
}
|
||||
}, [actions]);
|
||||
|
||||
const fetchStatus = useCallback(async () => {
|
||||
try {
|
||||
actions.setBrowserStatusLoading(true);
|
||||
actions.setBrowserError(null);
|
||||
const status = await api.browser.getStatus();
|
||||
actions.setBrowserStatus(status);
|
||||
return status;
|
||||
} catch (err) {
|
||||
actions.setBrowserError((err as Error).message);
|
||||
return null;
|
||||
} finally {
|
||||
actions.setBrowserStatusLoading(false);
|
||||
}
|
||||
}, [actions]);
|
||||
|
||||
const saveConfig = useCallback(
|
||||
async (updates: BrowserSavePayload) => {
|
||||
const currentConfig = state.browserConfig;
|
||||
if (!currentConfig) return false;
|
||||
|
||||
const optimisticConfig = mergeConfig(currentConfig, updates);
|
||||
actions.setBrowserConfig(optimisticConfig);
|
||||
|
||||
try {
|
||||
actions.setBrowserSaving(true);
|
||||
actions.setBrowserError(null);
|
||||
const response = await api.browser.update(updates);
|
||||
actions.setBrowserConfig(response.browser.config);
|
||||
actions.setBrowserStatus(response.browser.status);
|
||||
actions.setBrowserSuccess(true);
|
||||
window.setTimeout(() => actions.setBrowserSuccess(false), 1500);
|
||||
return true;
|
||||
} catch (err) {
|
||||
actions.setBrowserConfig(currentConfig);
|
||||
actions.setBrowserError((err as Error).message);
|
||||
return false;
|
||||
} finally {
|
||||
actions.setBrowserSaving(false);
|
||||
}
|
||||
},
|
||||
[actions, state.browserConfig]
|
||||
);
|
||||
|
||||
return {
|
||||
config: state.browserConfig,
|
||||
status: state.browserStatus,
|
||||
loading: state.browserLoading,
|
||||
statusLoading: state.browserStatusLoading,
|
||||
saving: state.browserSaving,
|
||||
error: state.browserError,
|
||||
success: state.browserSuccess,
|
||||
fetchConfig,
|
||||
fetchStatus,
|
||||
saveConfig,
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type ComponentType,
|
||||
} from 'react';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RefreshCw, FileCode, Copy, Check, GripVertical, AlertCircle } from 'lucide-react';
|
||||
import { CodeEditor } from '@/components/shared/code-editor';
|
||||
@@ -48,6 +49,7 @@ function lazyWithRetry<T extends ComponentType<unknown>>(importFn: () => Promise
|
||||
|
||||
// Lazy-loaded sections with retry capability
|
||||
const WebSearchSection = lazyWithRetry(() => import('./sections/websearch'));
|
||||
const BrowserSection = lazyWithRetry(() => import('./sections/browser'));
|
||||
const ImageAnalysisSection = lazyWithRetry(() => import('./sections/image-analysis'));
|
||||
const ChannelsSection = lazyWithRetry(() => import('./sections/channels'));
|
||||
const GlobalEnvSection = lazyWithRetry(() => import('./sections/globalenv-section'));
|
||||
@@ -99,10 +101,36 @@ class SectionErrorBoundary extends Component<
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSettingsTab(tabParam: string | null | undefined): SettingsTab {
|
||||
switch (tabParam?.toLowerCase()) {
|
||||
case 'browser':
|
||||
return 'browser';
|
||||
case 'imageanalysis':
|
||||
case 'image':
|
||||
return 'image';
|
||||
case 'channels':
|
||||
return 'channels';
|
||||
case 'globalenv':
|
||||
return 'globalenv';
|
||||
case 'proxy':
|
||||
return 'proxy';
|
||||
case 'auth':
|
||||
return 'auth';
|
||||
case 'thinking':
|
||||
return 'thinking';
|
||||
case 'backups':
|
||||
return 'backups';
|
||||
default:
|
||||
return 'websearch';
|
||||
}
|
||||
}
|
||||
|
||||
// Inner component that uses context
|
||||
function SettingsPageInner() {
|
||||
const { t } = useTranslation();
|
||||
const { activeTab, setActiveTab } = useSettingsTab();
|
||||
const { setActiveTab } = useSettingsTab();
|
||||
const [searchParams] = useSearchParams();
|
||||
const activeTab = resolveSettingsTab(searchParams.get('tab'));
|
||||
const {
|
||||
rawConfig,
|
||||
loading: rawConfigLoading,
|
||||
@@ -131,6 +159,7 @@ function SettingsPageInner() {
|
||||
</div>
|
||||
<SectionErrorBoundary>
|
||||
<Suspense fallback={<SectionSkeleton />}>
|
||||
{activeTab === 'browser' && <BrowserSection />}
|
||||
{activeTab === 'websearch' && <WebSearchSection />}
|
||||
{activeTab === 'image' && <ImageAnalysisSection />}
|
||||
{activeTab === 'channels' && <ChannelsSection />}
|
||||
@@ -156,6 +185,7 @@ function SettingsPageInner() {
|
||||
{/* Tab Content */}
|
||||
<SectionErrorBoundary>
|
||||
<Suspense fallback={<SectionSkeleton />}>
|
||||
{activeTab === 'browser' && <BrowserSection />}
|
||||
{activeTab === 'websearch' && <WebSearchSection />}
|
||||
{activeTab === 'image' && <ImageAnalysisSection />}
|
||||
{activeTab === 'channels' && <ChannelsSection />}
|
||||
|
||||
@@ -0,0 +1,577 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Copy,
|
||||
Monitor,
|
||||
RefreshCw,
|
||||
SearchCheck,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useBrowserConfig, useRawConfig } from '../../hooks';
|
||||
import type { BrowserConfig } from '../../types';
|
||||
|
||||
function getPlatformKey(): 'darwin' | 'linux' | 'win32' {
|
||||
const platform = navigator.platform.toLowerCase();
|
||||
if (platform.includes('mac')) return 'darwin';
|
||||
if (platform.includes('win')) return 'win32';
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
function parsePortDraft(value: string): number | null {
|
||||
if (!/^\d+$/.test(value.trim())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const port = Number.parseInt(value.trim(), 10);
|
||||
if (port < 1 || port > 65535) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
function statusTone(state: string) {
|
||||
if (state === 'ready' || state === 'enabled') {
|
||||
return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
|
||||
}
|
||||
if (state === 'disabled') {
|
||||
return 'border-slate-500/20 bg-slate-500/10 text-slate-700 dark:text-slate-300';
|
||||
}
|
||||
return 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300';
|
||||
}
|
||||
|
||||
function stateLabel(state: string) {
|
||||
return state.replaceAll('_', ' ');
|
||||
}
|
||||
|
||||
function buildLaunchCommand(
|
||||
userDataDir: string,
|
||||
devtoolsPort: number,
|
||||
platform: 'darwin' | 'linux' | 'win32'
|
||||
): string {
|
||||
const quotedPath = JSON.stringify(userDataDir);
|
||||
if (platform === 'darwin') {
|
||||
return `open -na "Google Chrome" --args --remote-debugging-port=${devtoolsPort} --user-data-dir=${quotedPath}`;
|
||||
}
|
||||
if (platform === 'win32') {
|
||||
return `chrome.exe --remote-debugging-port=${devtoolsPort} --user-data-dir=${quotedPath}`;
|
||||
}
|
||||
return `google-chrome --remote-debugging-port=${devtoolsPort} --user-data-dir=${quotedPath}`;
|
||||
}
|
||||
|
||||
function updateClaudeDraft(
|
||||
source: BrowserConfig,
|
||||
updates: Partial<BrowserConfig['claude']>
|
||||
): BrowserConfig {
|
||||
return {
|
||||
...source,
|
||||
claude: {
|
||||
...source.claude,
|
||||
...updates,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function updateCodexDraft(
|
||||
source: BrowserConfig,
|
||||
updates: Partial<BrowserConfig['codex']>
|
||||
): BrowserConfig {
|
||||
return {
|
||||
...source,
|
||||
codex: {
|
||||
...source.codex,
|
||||
...updates,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function BrowserSection() {
|
||||
const { t } = useTranslation();
|
||||
const { fetchRawConfig } = useRawConfig();
|
||||
const {
|
||||
config,
|
||||
status,
|
||||
loading,
|
||||
statusLoading,
|
||||
saving,
|
||||
error,
|
||||
success,
|
||||
fetchConfig,
|
||||
fetchStatus,
|
||||
saveConfig,
|
||||
} = useBrowserConfig();
|
||||
|
||||
const [draft, setDraft] = useState<BrowserConfig | null>(null);
|
||||
const [claudePortDraft, setClaudePortDraft] = useState<string | null>(null);
|
||||
const [actionMessage, setActionMessage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchConfig();
|
||||
}, [fetchConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!actionMessage && !success) return;
|
||||
const timer = window.setTimeout(() => setActionMessage(null), 2500);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [actionMessage, success]);
|
||||
|
||||
const effectiveConfig = draft ?? config;
|
||||
const preferredLaunchCommand = useMemo(() => {
|
||||
if (!effectiveConfig) return '';
|
||||
return buildLaunchCommand(
|
||||
effectiveConfig.claude.userDataDir,
|
||||
effectiveConfig.claude.devtoolsPort,
|
||||
getPlatformKey()
|
||||
);
|
||||
}, [effectiveConfig]);
|
||||
|
||||
const displayedClaudePort =
|
||||
claudePortDraft ?? String(effectiveConfig?.claude.devtoolsPort ?? 9222);
|
||||
const claudePort = parsePortDraft(displayedClaudePort);
|
||||
const claudePortInvalid = displayedClaudePort.trim().length > 0 && claudePort === null;
|
||||
|
||||
const hasClaudeChanges =
|
||||
config !== null &&
|
||||
effectiveConfig !== null &&
|
||||
(config.claude.enabled !== effectiveConfig.claude.enabled ||
|
||||
config.claude.userDataDir !== effectiveConfig.claude.userDataDir ||
|
||||
config.claude.devtoolsPort !== claudePort);
|
||||
const hasCodexChanges =
|
||||
config !== null &&
|
||||
effectiveConfig !== null &&
|
||||
config.codex.enabled !== effectiveConfig.codex.enabled;
|
||||
|
||||
const refreshAll = useCallback(async () => {
|
||||
setActionMessage(null);
|
||||
setDraft(null);
|
||||
setClaudePortDraft(null);
|
||||
await Promise.all([fetchConfig(), fetchRawConfig()]);
|
||||
}, [fetchConfig, fetchRawConfig]);
|
||||
|
||||
const refreshStatus = useCallback(async () => {
|
||||
const nextStatus = await fetchStatus();
|
||||
if (nextStatus) {
|
||||
setActionMessage(t('settingsPage.browserSection.messages.statusRefreshed'));
|
||||
}
|
||||
}, [fetchStatus, t]);
|
||||
|
||||
const saveClaudeSettings = useCallback(async () => {
|
||||
if (!effectiveConfig || claudePort === null) return;
|
||||
|
||||
const saved = await saveConfig({
|
||||
claude: {
|
||||
enabled: effectiveConfig.claude.enabled,
|
||||
userDataDir: effectiveConfig.claude.userDataDir.trim(),
|
||||
devtoolsPort: claudePort,
|
||||
},
|
||||
});
|
||||
|
||||
if (saved) {
|
||||
await fetchRawConfig();
|
||||
setActionMessage(null);
|
||||
setDraft(null);
|
||||
setClaudePortDraft(null);
|
||||
}
|
||||
}, [claudePort, effectiveConfig, fetchRawConfig, saveConfig]);
|
||||
|
||||
const saveCodexSettings = useCallback(async () => {
|
||||
if (!effectiveConfig) return;
|
||||
|
||||
const saved = await saveConfig({
|
||||
codex: {
|
||||
enabled: effectiveConfig.codex.enabled,
|
||||
},
|
||||
});
|
||||
|
||||
if (saved) {
|
||||
await fetchRawConfig();
|
||||
setActionMessage(null);
|
||||
setDraft(null);
|
||||
setClaudePortDraft(null);
|
||||
}
|
||||
}, [effectiveConfig, fetchRawConfig, saveConfig]);
|
||||
|
||||
const copyLaunchCommand = useCallback(async () => {
|
||||
if (!preferredLaunchCommand) return;
|
||||
await navigator.clipboard.writeText(preferredLaunchCommand);
|
||||
setActionMessage(t('settingsPage.browserSection.messages.launchCommandCopied'));
|
||||
}, [preferredLaunchCommand, t]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="flex items-center gap-3 text-muted-foreground">
|
||||
<RefreshCw className="h-5 w-5 animate-spin" />
|
||||
<span>{t('settings.loading')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!config || !status || !effectiveConfig) {
|
||||
return (
|
||||
<div className="p-5">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{error ?? t('settingsPage.browserSection.description')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="mt-4">
|
||||
<Button variant="outline" size="sm" onClick={refreshAll}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{t('sharedPage.retry')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-5 right-5 top-20 z-10 transition-all duration-200 ease-out',
|
||||
error || success || actionMessage
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'pointer-events-none -translate-y-2 opacity-0'
|
||||
)}
|
||||
>
|
||||
{error && (
|
||||
<Alert variant="destructive" className="py-2 shadow-lg">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{!error && (success || actionMessage) && (
|
||||
<div className="flex items-center gap-2 rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-emerald-700 shadow-lg dark:border-emerald-900/50 dark:bg-emerald-950/80 dark:text-emerald-300">
|
||||
<CheckCircle2 className="h-4 w-4 shrink-0" />
|
||||
<span className="text-sm font-medium">
|
||||
{actionMessage ?? t('commonToast.settingsSaved')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="space-y-5 p-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Monitor className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold">{t('settingsPage.browserSection.title')}</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('settingsPage.browserSection.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={refreshAll} disabled={saving || loading}>
|
||||
<RefreshCw className={cn('mr-2 h-4 w-4', statusLoading && 'animate-spin')} />
|
||||
{t('settings.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<Wrench className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<span className="font-medium">{t('settingsPage.browserSection.primaryTitle')}</span>{' '}
|
||||
{t('settingsPage.browserSection.primaryDescription')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="gap-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle>{t('settingsPage.browserSection.claude.title')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('settingsPage.browserSection.claude.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="outline" className={statusTone(status.claude.state)}>
|
||||
{stateLabel(status.claude.state)}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-[1fr_auto] md:items-center">
|
||||
<div>
|
||||
<Label htmlFor="browser-claude-enabled">
|
||||
{t('settingsPage.browserSection.claude.enabledLabel')}
|
||||
</Label>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{t('settingsPage.browserSection.claude.enabledDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="browser-claude-enabled"
|
||||
checked={effectiveConfig.claude.enabled}
|
||||
onCheckedChange={(next) =>
|
||||
setDraft((current) =>
|
||||
updateClaudeDraft(current ?? effectiveConfig, { enabled: next })
|
||||
)
|
||||
}
|
||||
aria-label={t('settingsPage.browserSection.claude.enabledLabel')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="browser-claude-user-data-dir">
|
||||
{t('settingsPage.browserSection.claude.userDataDir')}
|
||||
</Label>
|
||||
<Input
|
||||
id="browser-claude-user-data-dir"
|
||||
value={effectiveConfig.claude.userDataDir}
|
||||
onChange={(event) =>
|
||||
setDraft((current) =>
|
||||
updateClaudeDraft(current ?? effectiveConfig, {
|
||||
userDataDir: event.target.value,
|
||||
})
|
||||
)
|
||||
}
|
||||
placeholder={status.claude.recommendedUserDataDir}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('settingsPage.browserSection.claude.userDataDirHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="browser-claude-devtools-port">
|
||||
{t('settingsPage.browserSection.claude.devtoolsPort')}
|
||||
</Label>
|
||||
<Input
|
||||
id="browser-claude-devtools-port"
|
||||
value={displayedClaudePort}
|
||||
onChange={(event) => setClaudePortDraft(event.target.value)}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
<p
|
||||
className={cn(
|
||||
'text-xs text-muted-foreground',
|
||||
claudePortInvalid && 'text-destructive'
|
||||
)}
|
||||
>
|
||||
{claudePortInvalid
|
||||
? t('settingsPage.browserSection.claude.devtoolsPortInvalid')
|
||||
: t('settingsPage.browserSection.claude.devtoolsPortHint')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 rounded-lg border bg-muted/30 p-4 md:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('settingsPage.browserSection.readiness')}
|
||||
</p>
|
||||
<p className="mt-1 font-medium">{status.claude.title}</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{status.claude.detail}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('settingsPage.browserSection.nextStep')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm">{status.claude.nextStep}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 text-sm md:grid-cols-2">
|
||||
<div className="rounded-lg border p-3">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('settingsPage.browserSection.claude.effectivePath')}
|
||||
</p>
|
||||
<p className="mt-1 break-all font-mono text-xs">
|
||||
{status.claude.effectiveUserDataDir}
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{t('settingsPage.browserSection.claude.recommendedPath')}:{' '}
|
||||
{status.claude.recommendedUserDataDir}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border p-3">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('settingsPage.browserSection.claude.managedRuntime')}
|
||||
</p>
|
||||
<p className="mt-1">{status.claude.managedMcpServerName}</p>
|
||||
<p className="mt-2 break-all font-mono text-xs text-muted-foreground">
|
||||
{status.claude.managedMcpServerPath}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status.claude.overrideActive ? (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{t('settingsPage.browserSection.claude.overrideMessage', {
|
||||
source: status.claude.source,
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-2 rounded-lg border p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
{t('settingsPage.browserSection.claude.launchGuidance')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('settingsPage.browserSection.claude.launchGuidanceHint')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={copyLaunchCommand}
|
||||
disabled={!preferredLaunchCommand}
|
||||
>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
{t('settingsPage.browserSection.actions.copyLaunchCommand')}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="overflow-x-auto rounded-md bg-muted p-3 text-xs">
|
||||
<code>{preferredLaunchCommand}</code>
|
||||
</pre>
|
||||
{status.claude.runtimeEnv?.CCS_BROWSER_DEVTOOLS_HTTP_URL ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
DevTools: {status.claude.runtimeEnv.CCS_BROWSER_DEVTOOLS_HTTP_URL}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={saveClaudeSettings}
|
||||
disabled={saving || claudePortInvalid || !hasClaudeChanges}
|
||||
>
|
||||
{saving ? (
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{t('settingsPage.browserSection.actions.saveClaude')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={refreshStatus}
|
||||
disabled={saving || statusLoading || hasClaudeChanges || claudePortInvalid}
|
||||
>
|
||||
<SearchCheck className="mr-2 h-4 w-4" />
|
||||
{t('settingsPage.browserSection.actions.testConnection')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="gap-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle>{t('settingsPage.browserSection.codex.title')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('settingsPage.browserSection.codex.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="outline" className={statusTone(status.codex.state)}>
|
||||
{stateLabel(status.codex.state)}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-[1fr_auto] md:items-center">
|
||||
<div>
|
||||
<Label htmlFor="browser-codex-enabled">
|
||||
{t('settingsPage.browserSection.codex.enabledLabel')}
|
||||
</Label>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{t('settingsPage.browserSection.codex.enabledDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="browser-codex-enabled"
|
||||
checked={effectiveConfig.codex.enabled}
|
||||
onCheckedChange={(next) =>
|
||||
setDraft((current) =>
|
||||
updateCodexDraft(current ?? effectiveConfig, { enabled: next })
|
||||
)
|
||||
}
|
||||
aria-label={t('settingsPage.browserSection.codex.enabledLabel')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 rounded-lg border bg-muted/30 p-4 md:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('settingsPage.browserSection.readiness')}
|
||||
</p>
|
||||
<p className="mt-1 font-medium">{status.codex.title}</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{status.codex.detail}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('settingsPage.browserSection.nextStep')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm">{status.codex.nextStep}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 text-sm md:grid-cols-3">
|
||||
<div className="rounded-lg border p-3">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('settingsPage.browserSection.codex.serverName')}
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-xs">{status.codex.serverName}</p>
|
||||
</div>
|
||||
<div className="rounded-lg border p-3">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('settingsPage.browserSection.codex.overrideSupport')}
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
{status.codex.supportsConfigOverrides
|
||||
? t('settingsPage.browserSection.codex.overrideSupported')
|
||||
: t('settingsPage.browserSection.codex.overrideUnsupported')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border p-3">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('settingsPage.browserSection.codex.binary')}
|
||||
</p>
|
||||
<p className="mt-1 break-all font-mono text-xs">
|
||||
{status.codex.binaryPath ?? t('settingsPage.browserSection.codex.notDetected')}
|
||||
</p>
|
||||
{status.codex.version ? (
|
||||
<p className="mt-2 text-xs text-muted-foreground">{status.codex.version}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={saveCodexSettings} disabled={saving || !hasCodexChanges}>
|
||||
{saving ? (
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{t('settingsPage.browserSection.actions.saveCodex')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import { createContext, type Dispatch } from 'react';
|
||||
import type {
|
||||
BrowserConfig,
|
||||
BrowserStatus,
|
||||
WebSearchConfig,
|
||||
GlobalEnvConfig,
|
||||
CliproxyServerConfig,
|
||||
@@ -15,6 +17,14 @@ import type {
|
||||
// === State ===
|
||||
|
||||
export interface SettingsState {
|
||||
// Browser state
|
||||
browserConfig: BrowserConfig | null;
|
||||
browserStatus: BrowserStatus | null;
|
||||
browserLoading: boolean;
|
||||
browserStatusLoading: boolean;
|
||||
browserSaving: boolean;
|
||||
browserError: string | null;
|
||||
browserSuccess: boolean;
|
||||
// WebSearch state
|
||||
webSearchConfig: WebSearchConfig | null;
|
||||
webSearchStatus: WebSearchStatus | null;
|
||||
@@ -43,6 +53,13 @@ export interface SettingsState {
|
||||
}
|
||||
|
||||
export const initialSettingsState: SettingsState = {
|
||||
browserConfig: null,
|
||||
browserStatus: null,
|
||||
browserLoading: true,
|
||||
browserStatusLoading: true,
|
||||
browserSaving: false,
|
||||
browserError: null,
|
||||
browserSuccess: false,
|
||||
webSearchConfig: null,
|
||||
webSearchStatus: null,
|
||||
webSearchLoading: true,
|
||||
@@ -69,6 +86,13 @@ export const initialSettingsState: SettingsState = {
|
||||
// === Actions ===
|
||||
|
||||
export type SettingsAction =
|
||||
| { type: 'SET_BROWSER_CONFIG'; payload: BrowserConfig | null }
|
||||
| { type: 'SET_BROWSER_STATUS'; payload: BrowserStatus | null }
|
||||
| { type: 'SET_BROWSER_LOADING'; payload: boolean }
|
||||
| { type: 'SET_BROWSER_STATUS_LOADING'; payload: boolean }
|
||||
| { type: 'SET_BROWSER_SAVING'; payload: boolean }
|
||||
| { type: 'SET_BROWSER_ERROR'; payload: string | null }
|
||||
| { type: 'SET_BROWSER_SUCCESS'; payload: boolean }
|
||||
| { type: 'SET_WEBSEARCH_CONFIG'; payload: WebSearchConfig | null }
|
||||
| { type: 'SET_WEBSEARCH_STATUS'; payload: WebSearchStatus | null }
|
||||
| { type: 'SET_WEBSEARCH_LOADING'; payload: boolean }
|
||||
@@ -93,6 +117,20 @@ export type SettingsAction =
|
||||
|
||||
export function settingsReducer(state: SettingsState, action: SettingsAction): SettingsState {
|
||||
switch (action.type) {
|
||||
case 'SET_BROWSER_CONFIG':
|
||||
return { ...state, browserConfig: action.payload };
|
||||
case 'SET_BROWSER_STATUS':
|
||||
return { ...state, browserStatus: action.payload };
|
||||
case 'SET_BROWSER_LOADING':
|
||||
return { ...state, browserLoading: action.payload };
|
||||
case 'SET_BROWSER_STATUS_LOADING':
|
||||
return { ...state, browserStatusLoading: action.payload };
|
||||
case 'SET_BROWSER_SAVING':
|
||||
return { ...state, browserSaving: action.payload };
|
||||
case 'SET_BROWSER_ERROR':
|
||||
return { ...state, browserError: action.payload };
|
||||
case 'SET_BROWSER_SUCCESS':
|
||||
return { ...state, browserSuccess: action.payload };
|
||||
case 'SET_WEBSEARCH_CONFIG':
|
||||
return { ...state, webSearchConfig: action.payload };
|
||||
case 'SET_WEBSEARCH_STATUS':
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
* Type definitions for WebSearch, GlobalEnv, and Proxy configurations
|
||||
*/
|
||||
|
||||
import type { CliproxyServerConfig, RemoteProxyStatus } from '@/lib/api-client';
|
||||
import type {
|
||||
BrowserSettingsConfig,
|
||||
BrowserStatusPayload,
|
||||
CliproxyServerConfig,
|
||||
RemoteProxyStatus,
|
||||
UpdateBrowserSettingsPayload,
|
||||
} from '@/lib/api-client';
|
||||
|
||||
// === WebSearch Types ===
|
||||
|
||||
@@ -162,6 +168,7 @@ export interface OfficialChannelsStatus {
|
||||
// === Tab Types ===
|
||||
|
||||
export type SettingsTab =
|
||||
| 'browser'
|
||||
| 'websearch'
|
||||
| 'image'
|
||||
| 'channels'
|
||||
@@ -192,3 +199,6 @@ export interface ThinkingConfig {
|
||||
// === Re-exports from api-client ===
|
||||
|
||||
export type { CliproxyServerConfig, RemoteProxyStatus };
|
||||
export type BrowserConfig = BrowserSettingsConfig;
|
||||
export type BrowserStatus = BrowserStatusPayload;
|
||||
export type BrowserSavePayload = UpdateBrowserSettingsPayload;
|
||||
|
||||
Reference in New Issue
Block a user