From 2e6dd1ae6719aff906a6ac2dc680170b960ee514 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:40:02 +0200 Subject: [PATCH 1/4] gui: add Claude Code workspace view (section rail + main pane) Adds a workspace layout for the Claude Code tab following the Providers workspace DNA. The left rail lists the five page sections (Settings, Get started, Background helper model, Model interception, Available models); the main pane shows the selected section. Section bodies are shared between classic and workspace views, so all controls and the save action behave identically. Classic view stays available behind a toggle persisted in localStorage (ocx-claudecode-view). New i18n key for the Settings section in all six locales. --- gui/src/App.tsx | 2 +- gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/zh.ts | 1 + gui/src/pages/ClaudeCode.tsx | 335 ++++++++++++++---------- gui/src/styles-claudecode-workspace.css | 144 ++++++++++ gui/src/styles.css | 1 + 10 files changed, 352 insertions(+), 136 deletions(-) create mode 100644 gui/src/styles-claudecode-workspace.css 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..09663916b 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,163 @@ export default function ClaudeCode({ apiBase }: { apiBase: string }) { const manualEnv = buildManualEnv(state); - return ( - <> -

{t("claude.pageTitle")}

-

{t("claude.subtitle")}

+ const settingsSection = ( +
+
+
+ {t("claude.enabledLabel")} + {t("claude.enabledHint")} +
+ setState({ ...state, enabled })} /> +
- {status && {status}} +
+
+ {t("claude.authMode")} + {t("claude.authModeHint")} +
+ { + const v = e.target.value; + setState({ ...state, fastMode: v === "auto" ? null : v === "on" }); + }} + className="text-label font-medium" + aria-label={t("claude.fastMode")} + style={{ padding: "5px 10px", borderRadius: "var(--radius-xs)", border: "1px solid var(--border)", background: "var(--surface)", color: "var(--text)" }} + > + + + + +
+
+
+ {t("claude.autoContext")} + {t("claude.autoContextDesc")} + {state.maxContextTokens !== null && {t("claude.autoContextInert")}} +
+ setState({ ...state, autoContext })} /> +
+ + {state.autoContext && (
- {t("claude.authMode")} - {t("claude.authModeHint")} + {t("claude.autoCompactWindow")} + {t("claude.autoCompactWindowDesc")} + {state.autoCompactWindow !== null && {t("claude.autoCompactWindowWarn")}}
{ - const v = e.target.value; - setState({ ...state, fastMode: v === "auto" ? null : v === "on" }); - }} - className="text-label font-medium" - aria-label={t("claude.fastMode")} - style={{ padding: "5px 10px", borderRadius: "var(--radius-xs)", border: "1px solid var(--border)", background: "var(--surface)", color: "var(--text)" }} - > - - - - -
- -
-
- {t("claude.autoContext")} - {t("claude.autoContextDesc")} - {state.maxContextTokens !== null && {t("claude.autoContextInert")}} -
- setState({ ...state, autoContext })} /> +
+
+ {t("claude.injectAgents")} + {t("claude.injectAgentsDesc")}
+ setState({ ...state, injectAgents })} /> +
- {state.autoContext && ( -
-
- {t("claude.autoCompactWindow")} - {t("claude.autoCompactWindowDesc")} - {state.autoCompactWindow !== null && {t("claude.autoCompactWindowWarn")}} + {(["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]: value === "inherit" - ? undefined - : { ...override, backend: value === "auto" ? undefined : value as SidecarBackend }, - })} - label={t("dash.sidecarBackend")} - portal - /> - setState({ ...state, [key]: { ...override, model: e.target.value } })} - placeholder={t("claude.sidecarModelPlaceholder")} - disabled={!override} - aria-label={t("dash.sidecarModel")} - style={{ minWidth: 210 }} - /> -
+
+ 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 +432,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 +452,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 +469,61 @@ 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..091a09552 --- /dev/null +++ b/gui/src/styles-claudecode-workspace.css @@ -0,0 +1,144 @@ +/* ============================================================================ + 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-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; +} + +.claudecode-workspace-rail-title { + font-size: var(--text-body); + font-weight: var(--weight-semibold); + color: var(--text); +} + +.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); + outline: 1px solid 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; +} + +/* ── 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.css b/gui/src/styles.css index ec6f21ac4..d38167320 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 */ From f7a05797e1e70053531e3661d241d4db1f3cf31c Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Thu, 23 Jul 2026 00:32:30 +0200 Subject: [PATCH 2/4] fix(gui): fix rail row border clipping (outline -> inset shadow) --- gui/src/styles-claudecode-workspace.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/styles-claudecode-workspace.css b/gui/src/styles-claudecode-workspace.css index 091a09552..bd412be95 100644 --- a/gui/src/styles-claudecode-workspace.css +++ b/gui/src/styles-claudecode-workspace.css @@ -77,7 +77,7 @@ .claudecode-workspace-rail-row--selected { background: var(--surface-2); - outline: 1px solid var(--border); + box-shadow: inset 0 0 0 1px var(--border); } .claudecode-workspace-rail-name { From 81c6f6fadb7613a6fc02b0939bcce410a7949909 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:18:12 +0200 Subject: [PATCH 3/4] fix(gui): polish Claude workspace rail and shared select focus Drop the duplicate rail title, align section content with the Settings card top, center toggle knobs, and remove the left inset accent bar from Select menus. --- gui/src/pages/ClaudeCode.tsx | 3 --- gui/src/styles-claudecode-workspace.css | 18 +++++------------- gui/src/styles-combos-workspace.css | 2 +- gui/src/styles.css | 8 +++----- 4 files changed, 9 insertions(+), 22 deletions(-) diff --git a/gui/src/pages/ClaudeCode.tsx b/gui/src/pages/ClaudeCode.tsx index 09663916b..4c82f58af 100644 --- a/gui/src/pages/ClaudeCode.tsx +++ b/gui/src/pages/ClaudeCode.tsx @@ -489,9 +489,6 @@ export default function ClaudeCode({ apiBase, viewMode }: { apiBase: string; vie {status && {status}}