diff --git a/gui/src/App.tsx b/gui/src/App.tsx index 0f3daa799..6d522dde7 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -285,7 +285,7 @@ export default function App() { {page === "storage" && } {page === "codex-auth" && } {page === "api" && } - {page === "claude" && } + {page === "claude" && } diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index f184e271f..56cbd3929 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -771,6 +771,7 @@ export const de = { "claude.networkError": "Netzwerkfehler — läuft der Proxy?", "claude.toggleAria": "Claude-Verbindung umschalten", "claude.none": "Keine", + "claude.workspace.settings": "Einstellungen", "common.close": "Schließen", "common.ok": "OK", "app.logoAria": "opencodex-Logo", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 10eb519fc..4cec75042 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1079,6 +1079,7 @@ export const en = { "claude.networkError": "Network error — is the proxy running?", "claude.toggleAria": "Toggle Claude connection", "claude.none": "None", + "claude.workspace.settings": "Settings", // Combos workspace "cws.loading": "Loading combos…", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 69267e3bb..df2b3a914 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -1034,6 +1034,7 @@ export const ja: Record = { "claude.networkError": "ネットワークエラー — プロキシは起動していますか?", "claude.toggleAria": "Claude 接続を切り替え", "claude.none": "なし", + "claude.workspace.settings": "設定", // Combos workspace "cws.loading": "コンボを読み込み中…", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index cb12c0134..0d43a44a2 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -793,6 +793,7 @@ export const ko: Record = { "claude.networkError": "네트워크 오류 — 프록시가 실행 중인가요?", "claude.toggleAria": "Claude 인바운드 켜기/끄기", "claude.none": "없음", + "claude.workspace.settings": "설정", "common.close": "닫기", "common.ok": "확인", "app.logoAria": "opencodex 로고", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 54793f1bd..54ef22123 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -1079,6 +1079,7 @@ export const ru: Record = { "claude.networkError": "Ошибка сети — запущен ли прокси?", "claude.toggleAria": "Переключить подключение Claude", "claude.none": "Нет", + "claude.workspace.settings": "Настройки", // Combos workspace "cws.loading": "Загрузка комбо…", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 740580a8f..a8e71b60d 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -793,6 +793,7 @@ export const zh: Record = { "claude.networkError": "网络错误 — 代理是否在运行?", "claude.toggleAria": "切换 Claude 连接", "claude.none": "无", + "claude.workspace.settings": "设置", "common.close": "关闭", "common.ok": "确定", "app.logoAria": "opencodex 徽标", diff --git a/gui/src/pages/ClaudeCode.tsx b/gui/src/pages/ClaudeCode.tsx index ca2d48ca3..838b9ca52 100644 --- a/gui/src/pages/ClaudeCode.tsx +++ b/gui/src/pages/ClaudeCode.tsx @@ -4,6 +4,7 @@ import { IconPlus, IconX } from "../icons"; import { Trans } from "../i18n/provider"; import { useT } from "../i18n/shared"; import { modelLabel } from "../model-display"; +import { readViewMode, type ViewMode } from "../view-mode"; import { reconcileAutoConnectState } from "./claude-autoconnect"; import { buildManualEnv, type SidecarBackend, type SidecarOverride } from "./claude-manual-env"; @@ -137,13 +138,15 @@ export function SmallFastModelSetting({ ); } -export default function ClaudeCode({ apiBase }: { apiBase: string }) { +export default function ClaudeCode({ apiBase, viewMode }: { apiBase: string; viewMode?: ViewMode }) { const t = useT(); const [state, setState] = useState(null); const [rows, setRows] = useState([]); const [status, setStatus] = useState(""); const [ok, setOk] = useState(false); const [loading, setLoading] = useState(true); + const [selectedSection, setSelectedSection] = useState("settings"); + const workspaceView = (viewMode ?? readViewMode()) === "workspace"; const load = useCallback(async () => { try { @@ -236,161 +239,166 @@ export default function ClaudeCode({ apiBase }: { apiBase: string }) { const manualEnv = buildManualEnv(state); - return ( + const settingsSection = ( <> -

{t("claude.pageTitle")}

-

{t("claude.subtitle")}

- - {status && {status}} - + {workspaceView &&
{t("claude.workspace.settings")}
}
-
-
- {t("claude.enabledLabel")} - {t("claude.enabledHint")} -
- setState({ ...state, enabled })} /> +
+
+ {t("claude.enabledLabel")} + {t("claude.enabledHint")}
+ setState({ ...state, enabled })} /> +
-
-
- {t("claude.authMode")} - {t("claude.authModeHint")} -
- setState({ ...state, authMode: v as ClaudeCodeState["authMode"] })} + label={t("claude.authMode")} + style={{ minWidth: 220 }} + portal /> +
-
-
- {t("claude.fastMode")} - {t("claude.fastModeDesc")} -
- -
+ setState({ ...state, systemEnv })} + /> -
-
- {t("claude.autoContext")} - {t("claude.autoContextDesc")} - {state.maxContextTokens !== null && {t("claude.autoContextInert")}} -
- setState({ ...state, autoContext })} /> +
+
+ {t("claude.fastMode")} + {t("claude.fastModeDesc")}
+ +
- {state.autoContext && ( -
-
- {t("claude.autoCompactWindow")} - {t("claude.autoCompactWindowDesc")} - {state.autoCompactWindow !== null && {t("claude.autoCompactWindowWarn")}} -
- setState({ ...state, autoCompactWindow: v === "" ? null : Number(v) })} + label={t("claude.autoCompactWindow")} + style={{ minWidth: 130 }} + portal + />
+ )} - {(["webSearchSidecar", "visionSidecar"] as const).map(key => { - const override = state[key]; - const titleKey = key === "webSearchSidecar" ? "claude.webSearchSidecar" : "claude.visionSidecar"; - const hintKey = key === "webSearchSidecar" ? "claude.webSearchSidecarHint" : "claude.visionSidecarHint"; - return ( -
-
- {t(titleKey)} - {t(hintKey)} -
-
- setState({ ...state, [key]: { ...override, model: e.target.value } })} - placeholder={t("claude.sidecarModelPlaceholder")} - disabled={!override} - aria-label={t("dash.sidecarModel")} - style={{ minWidth: 210 }} - /> -
-
- ); - })} +
+
+ {t("claude.injectAgents")} + {t("claude.injectAgentsDesc")} +
+ setState({ ...state, injectAgents })} />
+ {(["webSearchSidecar", "visionSidecar"] as const).map(key => { + const override = state[key]; + const titleKey = key === "webSearchSidecar" ? "claude.webSearchSidecar" : "claude.visionSidecar"; + const hintKey = key === "webSearchSidecar" ? "claude.webSearchSidecarHint" : "claude.visionSidecarHint"; + return ( +
+
+ {t(titleKey)} + {t(hintKey)} +
+
+ setState({ ...state, [key]: { ...override, model: e.target.value } })} + placeholder={t("claude.sidecarModelPlaceholder")} + disabled={!override} + aria-label={t("dash.sidecarModel")} + style={{ minWidth: 210 }} + /> +
+
+ ); + })} +
+ + ); + + const quickstartSection = ( + <>
{t("claude.quickstart")}

ocx claude
- {/* Advanced manual setup: collapsed by default (audit 080 UX-1). */}
{t("claude.manualEnv")}
{manualEnv}
+ + ); - setState({ ...state, smallFastModel })} - /> + const smallFastSection = ( + setState({ ...state, smallFastModel })} + /> + ); + const modelMapSection = ( + <>
{t("claude.modelMap")} {rows.length}

{t("claude.modelMapHint")}

@@ -427,15 +435,18 @@ export default function ClaudeCode({ apiBase }: { apiBase: string }) {
- +
+ + ); + const aliasesSection = ( + <>
{t("claude.aliases")} {state.aliases.length}

{t("claude.aliasesHint")}

{state.aliases.length === 0 ? (
{t("claude.none")}
) : ( - // Grouped by provider (audit 080 UX-2): one scroll area, group labels first.
{Array.from( state.aliases.reduce((groups, a) => { @@ -444,11 +455,11 @@ export default function ClaudeCode({ apiBase }: { apiBase: string }) { (groups.get(provider) ?? groups.set(provider, []).get(provider)!).push(a); return groups; }, new Map()), - ).map(([provider, rows]) => ( + ).map(([provider, aliasRows]) => (
-
{provider} · {rows.length}
+
{provider} · {aliasRows.length}
- {rows.map(a => ( + {aliasRows.map(a => (
{a.id} {a.display_name} @@ -461,4 +472,58 @@ export default function ClaudeCode({ apiBase }: { apiBase: string }) { )} ); + + const sections = [ + { id: "settings", label: t("claude.workspace.settings"), body: settingsSection }, + { id: "quickstart", label: t("claude.quickstart"), body: quickstartSection }, + { id: "smallFast", label: t("claude.smallFastModel"), body: smallFastSection }, + { id: "modelMap", label: t("claude.modelMap"), body: modelMapSection }, + { id: "aliases", label: t("claude.aliases"), body: aliasesSection }, + ]; + + if (workspaceView) { + const selected = sections.find(s => s.id === selectedSection) ?? sections[0]; + return ( +
+
+

{t("claude.pageTitle")}

+
+

{t("claude.subtitle")}

+ {status && {status}} +
+ +
+
{selected.body}
+
+
+
+ ); + } + + return ( + <> +

{t("claude.pageTitle")}

+

{t("claude.subtitle")}

+ {status && {status}} + {settingsSection} + {quickstartSection} + {smallFastSection} + {modelMapSection} + {aliasesSection} + + ); } diff --git a/gui/src/styles-claudecode-workspace.css b/gui/src/styles-claudecode-workspace.css new file mode 100644 index 000000000..2e6e256c6 --- /dev/null +++ b/gui/src/styles-claudecode-workspace.css @@ -0,0 +1,136 @@ +/* ============================================================================ + claudecode-workspace — isolated stylesheet (mirrors providers-workspace layout DNA) + Namespace: claudecode-workspace- | widgets: ccw- + Uses only design tokens from styles.css. No gradients. + ============================================================================ */ + +.main-inner:has(.claudecode-workspace-shell) { + max-width: 1200px; +} + +.claudecode-workspace-shell { + width: 100%; + min-width: 0; +} + +.claudecode-workspace-root { + display: grid; + grid-template-columns: minmax(240px, 280px) minmax(0, 1fr); + gap: var(--space-4); + width: 100%; + max-width: 100%; + min-height: 480px; +} + +/* ── Rail ─────────────────────────────────────────────── */ + +.claudecode-workspace-rail { + display: flex; + flex-direction: column; + gap: var(--space-3); + border-right: 1px solid var(--border); + padding-right: var(--space-3); + min-width: 0; +} + +.claudecode-workspace-rail-list { + display: flex; + flex-direction: column; + gap: 2px; + min-height: 0; +} + +.claudecode-workspace-rail-row { + display: flex; + flex-direction: column; + gap: var(--space-0-5); + width: 100%; + min-height: 46px; + appearance: none; + background: none; + border: none; + border-radius: var(--radius-sm); + padding: var(--space-1-5) var(--space-2); + cursor: pointer; + text-align: left; + color: inherit; + font: inherit; + overflow: hidden; +} + +.claudecode-workspace-rail-row:hover { + background: var(--surface-2); +} + +.claudecode-workspace-rail-row--selected { + background: var(--surface-2); + box-shadow: inset 0 0 0 1px var(--border); +} + +.claudecode-workspace-rail-name { + font-size: var(--text-body); + font-weight: var(--weight-medium); + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.claudecode-workspace-rail-meta { + font-size: var(--text-caption); + color: var(--faint); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ── Main pane ────────────────────────────────────────── */ + +.claudecode-workspace-main { + display: flex; + flex-direction: column; + gap: var(--space-4); + min-width: 0; +} + +.ccw-detail-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + flex-wrap: wrap; +} + +.ccw-detail-title { + font-size: var(--text-h2); + font-weight: var(--weight-semibold); + color: var(--text); + margin: 0; +} + +.ccw-body { + display: flex; + flex-direction: column; + gap: var(--space-3); + min-width: 0; +} + +/* Match Settings: section titles must not push content below the Settings card top. */ +.ccw-body > .h-section { + margin-top: 0; +} + +/* ── Responsive ───────────────────────────────────────── */ + +@media (max-width: 768px) { + .claudecode-workspace-root { + grid-template-columns: 1fr; + } + + .claudecode-workspace-rail { + border-right: none; + border-bottom: 1px solid var(--border); + padding-right: 0; + padding-bottom: var(--space-3); + } +} diff --git a/gui/src/styles-combos-workspace.css b/gui/src/styles-combos-workspace.css index 649898ecd..452a0c1bd 100644 --- a/gui/src/styles-combos-workspace.css +++ b/gui/src/styles-combos-workspace.css @@ -117,7 +117,7 @@ .combos-workspace-rail-row.combos-workspace-rail-row--selected { background: var(--accent-soft); - box-shadow: inset 3px 0 0 var(--accent); + box-shadow: inset 0 0 0 1px var(--border); } .combos-workspace-rail-row:focus-visible { diff --git a/gui/src/styles.css b/gui/src/styles.css index ec6f21ac4..9d8982e25 100644 --- a/gui/src/styles.css +++ b/gui/src/styles.css @@ -17,6 +17,7 @@ @import "./styles-dashboard-workspace.css"; @import "./styles-usage-workspace.css"; @import "./styles-apikeys-workspace.css"; +@import "./styles-claudecode-workspace.css"; :root { /* default: follow the OS; [data-theme] below pins color-scheme so light-dark() obeys it */ @@ -670,11 +671,11 @@ select.input { appearance: none; } /* Keyboard / aria-activedescendant focus — visible without relying on :hover. */ .select-option-active { background: var(--hover); - box-shadow: inset 2px 0 0 var(--accent); } +/* Selected + keyboard-highlighted: full inset ring (not a left stripe). */ .select-option.active.select-option-active { background: var(--accent-soft); - box-shadow: inset 2px 0 0 var(--accent); + box-shadow: inset 0 0 0 1px var(--accent); } /* ---- switch ---- */ @@ -815,7 +816,7 @@ dialog.modal-overlay::backdrop { .bar-amber { background: linear-gradient(90deg, var(--green), var(--amber)); } .toggle { width: var(--toggle-w); height: var(--toggle-h); border-radius: var(--radius-pill); background: var(--toggle-off-bg); border: 1px solid var(--border); cursor: pointer; position: relative; transition: background var(--motion-normal), border-color var(--motion-normal); flex-shrink: 0; } .toggle.on { background: var(--toggle-on-bg); border-color: var(--toggle-on-bg); } -.toggle-knob { width: 16px; height: 16px; background: light-dark(#ffffff, #ececec); border-radius: var(--radius-round); position: absolute; top: 1px; left: 1px; transition: left var(--motion-normal), background var(--motion-normal); } +.toggle-knob { width: 16px; height: 16px; background: light-dark(#ffffff, #ececec); border-radius: var(--radius-round); position: absolute; top: 50%; left: 1px; transform: translateY(-50%); transition: left var(--motion-normal), background var(--motion-normal); } .toggle.on .toggle-knob { left: 17px; background: var(--toggle-dot-color); } .toggle:disabled { cursor: not-allowed; opacity: 0.55; } .codex-auto-switch-card { gap: 16px; flex-wrap: wrap; } @@ -1167,9 +1168,9 @@ button.prov-account-row.active { cursor: default; } .toggle { position: relative; display: inline-block; width: var(--toggle-w); height: var(--toggle-h); flex-shrink: 0; } .toggle input { opacity: 0; width: 0; height: 0; position: absolute; } .toggle .slider { position: absolute; inset: 0; background: var(--toggle-off-bg); border-radius: var(--radius-pill); cursor: pointer; transition: background var(--motion-normal) ease; } -.toggle .slider::after { content: ""; position: absolute; top: 3px; left: 3px; width: var(--toggle-dot); height: var(--toggle-dot); background: var(--toggle-dot-color); border-radius: var(--radius-round); transition: transform var(--motion-normal) ease; } +.toggle .slider::after { content: ""; position: absolute; top: 50%; left: 3px; width: var(--toggle-dot); height: var(--toggle-dot); background: var(--toggle-dot-color); border-radius: var(--radius-round); transform: translateY(-50%); transition: transform var(--motion-normal) ease; } .toggle input:checked + .slider { background: var(--toggle-on-bg); } -.toggle input:checked + .slider::after { transform: translateX(calc(var(--toggle-w) - var(--toggle-dot) - 6px)); } +.toggle input:checked + .slider::after { transform: translate(calc(var(--toggle-w) - var(--toggle-dot) - 6px), -50%); } .toggle input:focus-visible + .slider { outline: 2px solid var(--accent-ring); outline-offset: 2px; } .toggle input:disabled + .slider { opacity: 0.5; cursor: not-allowed; }