From c0ba62b97220dcffaffe4bf09ac501d6e1f5cbd1 Mon Sep 17 00:00:00 2001 From: James Long Date: Mon, 20 Jul 2026 16:17:09 -0400 Subject: [PATCH] refactor(tui): expose theme colors as properties (#37995) --- packages/tui/src/app.tsx | 2 +- .../tui/src/component/devtools-sidebar.tsx | 16 +- packages/tui/src/component/dialog-debug.tsx | 14 +- .../tui/src/component/dialog-integration.tsx | 38 +-- packages/tui/src/component/dialog-mcp.tsx | 22 +- .../tui/src/component/dialog-move-session.tsx | 22 +- packages/tui/src/component/dialog-pair.tsx | 28 +- .../component/dialog-project-copy-name.tsx | 21 +- .../tui/src/component/dialog-retry-action.tsx | 22 +- .../dialog-session-delete-failed.tsx | 14 +- packages/tui/src/component/dialog-skill.tsx | 12 +- packages/tui/src/component/dialog-status.tsx | 22 +- .../dialog-workspace-file-changes.tsx | 24 +- packages/tui/src/component/logo.tsx | 6 +- .../src/component/plugin-route-missing.tsx | 6 +- packages/tui/src/component/prompt/index.tsx | 80 +++--- packages/tui/src/component/reconnecting.tsx | 4 +- packages/tui/src/component/spinner.tsx | 2 +- .../tui/src/component/startup-loading.tsx | 4 +- packages/tui/src/context/local.tsx | 5 +- .../tui/src/feature-plugins/home/footer.tsx | 14 +- .../src/feature-plugins/sidebar/context.tsx | 10 +- .../src/feature-plugins/sidebar/footer.tsx | 2 +- .../tui/src/feature-plugins/sidebar/lsp.tsx | 4 +- .../tui/src/feature-plugins/sidebar/mcp.tsx | 22 +- .../feature-plugins/system/diff-viewer-ui.tsx | 4 +- .../tui/src/feature-plugins/system/scrap.tsx | 8 +- .../tui/src/routes/session/composer/index.tsx | 18 +- .../src/routes/session/composer/shell-tab.tsx | 8 +- .../routes/session/composer/subagents-tab.tsx | 27 +- packages/tui/src/routes/session/footer.tsx | 22 +- packages/tui/src/routes/session/form.tsx | 148 +++++----- packages/tui/src/routes/session/index.tsx | 258 +++++++++--------- .../tui/src/routes/session/permission.tsx | 113 ++++---- packages/tui/src/routes/session/sidebar.tsx | 10 +- .../src/routes/session/subagent-footer.tsx | 32 ++- packages/tui/src/theme/v2/component.ts | 156 ++--------- packages/tui/src/ui/dialog-alert.tsx | 10 +- packages/tui/src/ui/dialog-confirm.tsx | 10 +- packages/tui/src/ui/dialog-export-options.tsx | 106 +++++-- packages/tui/src/ui/dialog-export-result.tsx | 15 +- packages/tui/src/ui/dialog-help.tsx | 10 +- packages/tui/src/ui/dialog-prompt.tsx | 20 +- packages/tui/src/ui/dialog.tsx | 2 +- packages/tui/src/ui/toast.tsx | 8 +- packages/tui/test/theme/v2/component.test.ts | 92 +++---- 46 files changed, 742 insertions(+), 751 deletions(-) diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 0ebebad798..f0ec2a59b4 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -1088,7 +1088,7 @@ function App(props: { pair?: DialogPairCredentials; started: number }) { width={dimensions().width} height={dimensions().height} flexDirection="column" - backgroundColor={themeV2.background()} + backgroundColor={themeV2.background.default} onMouseDown={(evt) => { if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return if (evt.button !== MouseButton.RIGHT) return diff --git a/packages/tui/src/component/devtools-sidebar.tsx b/packages/tui/src/component/devtools-sidebar.tsx index f3ef08cc73..cca1965232 100644 --- a/packages/tui/src/component/devtools-sidebar.tsx +++ b/packages/tui/src/component/devtools-sidebar.tsx @@ -17,26 +17,26 @@ export function DevToolsSidebar() { paddingBottom={1} paddingLeft={2} paddingRight={2} - backgroundColor={themeV2.background()} + backgroundColor={themeV2.background.default} > - + Theme - Mode + Mode setModeHovered(canSwitchMode())} onMouseOut={() => setModeHovered(false)} onMouseUp={canSwitchMode() ? () => setMode(nextMode()) : undefined} > - {mode()} + {mode()} @@ -44,16 +44,16 @@ export function DevToolsSidebar() { {(group) => ( - + {group.title} {(entry) => ( - {entry.key} + {entry.key} - {String(entry.value)} + {String(entry.value)} )} diff --git a/packages/tui/src/component/dialog-debug.tsx b/packages/tui/src/component/dialog-debug.tsx index deab786726..99e0fd0f71 100644 --- a/packages/tui/src/component/dialog-debug.tsx +++ b/packages/tui/src/component/dialog-debug.tsx @@ -54,10 +54,10 @@ export function DialogDebug() { return ( - + Debug - dialog.clear()}> + dialog.clear()}> esc @@ -67,10 +67,10 @@ export function DialogDebug() { {(entry) => ( - + {entry.label.padEnd(10)} - + {entry.value} @@ -78,12 +78,12 @@ export function DialogDebug() { - Share this when reporting an issue. + Share this when reporting an issue. - + {copied() ? "✓ copied" : "copy"}{" "} - enter + enter diff --git a/packages/tui/src/component/dialog-integration.tsx b/packages/tui/src/component/dialog-integration.tsx index 0024bf3443..a2c6a531ab 100644 --- a/packages/tui/src/component/dialog-integration.tsx +++ b/packages/tui/src/component/dialog-integration.tsx @@ -74,7 +74,7 @@ export function DialogIntegration(props: { onConnected?: OnIntegrationConnected footer: connectionSummary(integration) || undefined, category: integration.id in INTEGRATION_PRIORITY ? "Popular" : "Services", disabled: methods.length === 0, - gutter: connected ? () => : undefined, + gutter: connected ? () => : undefined, onSelect: () => credentialConnections(integration).length ? manageConnections(integration, methods, dialog, props.onConnected) @@ -89,12 +89,12 @@ export function DialogIntegration(props: { onConnected?: OnIntegrationConnected options={options()} emptyView={ - No integrations available + No integrations available } noMatchView={ - No integrations found + No integrations found } /> @@ -295,24 +295,24 @@ function CommandView(props: { title: string; output: string; message: string }) return ( - + {props.title} - dialog.clear()}> + dialog.clear()}> esc close - {props.output.trim()} + {props.output.trim()} - {props.message} + {props.message} ) @@ -346,7 +346,7 @@ function KeyMethod(props: { .catch((cause) => setError(message(cause))) }} description={() => ( - {(value) => {value()}} + {(value) => {value()}} )} /> ) @@ -536,9 +536,9 @@ function OAuthCode(props: { }} description={() => ( - {props.attempt.instructions} - - {(value) => {value()}} + {props.attempt.instructions} + + {(value) => {value()}} )} /> @@ -551,27 +551,27 @@ function OAuthView(props: { title: string; url?: string; instructions?: string; return ( - + {props.title} - dialog.clear()}> + dialog.clear()}> esc {(url) => ( - + - {(instructions) => {instructions()}} + {(instructions) => {instructions()}} )} - {props.message} + {props.message} - - c copy + + c copy diff --git a/packages/tui/src/component/dialog-mcp.tsx b/packages/tui/src/component/dialog-mcp.tsx index 606e47a48e..e1ecd011f4 100644 --- a/packages/tui/src/component/dialog-mcp.tsx +++ b/packages/tui/src/component/dialog-mcp.tsx @@ -21,11 +21,11 @@ function statusError(status: McpServer["status"]) { function Status(props: { enabled: boolean; loading: boolean }) { const { themeV2 } = useTheme().contextual("elevated") - if (props.loading) return ⋯ Loading + if (props.loading) return ⋯ Loading if (props.enabled) { - return ✓ Enabled + return ✓ Enabled } - return ○ Disabled + return ○ Disabled } export function DialogMcp() { @@ -110,7 +110,7 @@ export function DialogMcp() { ]} footer={ - enter to view error + enter to view error } /> @@ -171,16 +171,16 @@ function DialogMcpError(props: { server: McpServer; onBack: () => void }) { return ( - + MCP server: {props.server.name} - + esc back - ✗ Failed + ✗ Failed void }) { scrollbarOptions={{ visible: false }} scrollAcceleration={getScrollAcceleration(config)} > - + {error()} - ↑↓ scroll - + ↑↓ scroll + {copied() ? "✓ copied" : "c copy details"} diff --git a/packages/tui/src/component/dialog-move-session.tsx b/packages/tui/src/component/dialog-move-session.tsx index 3348b6c2c8..6f2f3e6ba6 100644 --- a/packages/tui/src/component/dialog-move-session.tsx +++ b/packages/tui/src/component/dialog-move-session.tsx @@ -172,18 +172,18 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { return { title, titleView: isRemoving ? ( - Deleting {item.location} + Deleting {item.location} ) : deleting ? ( - + Press {shortcuts.get("dialog.move_session.delete")} again to confirm ) : suffix ? ( <> {visible.slice(0, split)} - {visible.slice(split)} + {visible.slice(split)} ) : undefined, - bg: deleting ? themeV2.background.action.destructive() : undefined, + bg: deleting ? themeV2.background.action.destructive.default : undefined, value: { type: "directory", directory: item.location, @@ -316,7 +316,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { title="Move session" titleView={ - + Move session @@ -329,25 +329,25 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { emptyView={ showError() ? ( - + Could not load project directories - {errorMessage(loadError())} - Close and reopen Move session to try again. + {errorMessage(loadError())} + Close and reopen Move session to try again. ) : directories.loading || loadedProject.loading ? ( - Loading project directories… + Loading project directories… ) : ( - No project directories available + No project directories available ) } noMatchView={ - No project directories found + No project directories found } locked={showError() || directories.loading || loadedProject.loading || Boolean(removing())} diff --git a/packages/tui/src/component/dialog-pair.tsx b/packages/tui/src/component/dialog-pair.tsx index d26c248a6d..0a5cb0d75d 100644 --- a/packages/tui/src/component/dialog-pair.tsx +++ b/packages/tui/src/component/dialog-pair.tsx @@ -47,17 +47,17 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) { - URLs - {(url) => {url}} + URLs + {(url) => {url}} - Username - {value.username} + Username + {value.username} - Password + Password setPasswordHover(true)} onMouseOut={() => setPasswordHover(false)} @@ -67,7 +67,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) { ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname))}> - + Run `opencode service set hostname 0.0.0.0` to access the service remotely. @@ -78,7 +78,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) { flexShrink={0} alignItems={horizontal() ? "flex-end" : "center"} > - {renderUnicodeCompact(JSON.stringify(value), { border: 1 })} + {renderUnicodeCompact(JSON.stringify(value), { border: 1 })} ) @@ -87,17 +87,17 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) { return ( - + Pair - dialog.clear()}> + dialog.clear()}> esc Loading server information…}> + Loading server information…}> = 36} fallback={ @@ -116,11 +116,11 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) { > {(error) => ( - + Could not load server information - {errorMessage(error())} - Close and reopen Pair to try again. + {errorMessage(error())} + Close and reopen Pair to try again. )} diff --git a/packages/tui/src/component/dialog-project-copy-name.tsx b/packages/tui/src/component/dialog-project-copy-name.tsx index ef51e5a37a..63a15bcace 100644 --- a/packages/tui/src/component/dialog-project-copy-name.tsx +++ b/packages/tui/src/component/dialog-project-copy-name.tsx @@ -47,10 +47,10 @@ export function DialogProjectCopyName(props: { onConfirm: (name: string) => void return ( - + Name project copy - dialog.clear()}> + dialog.clear()}> esc @@ -61,18 +61,17 @@ export function DialogProjectCopyName(props: { onConfirm: (name: string) => void }} onSubmit={confirm} placeholder="Project copy name" - placeholderColor={themeV2.text.subdued()} - textColor={themeV2.text.formfield()} - focusedTextColor={themeV2.text.formfield()} - cursorColor={themeV2.text.formfield()} + placeholderColor={themeV2.text.subdued} + textColor={themeV2.text.formfield.default} + focusedTextColor={themeV2.text.formfield.default} + cursorColor={themeV2.text.formfield.default} /> - - enter submit + + enter submit - - {shortcuts.get("dialog.project_copy.generate")}{" "} - generate one + + {shortcuts.get("dialog.project_copy.generate")} generate one diff --git a/packages/tui/src/component/dialog-retry-action.tsx b/packages/tui/src/component/dialog-retry-action.tsx index 01b5226fa1..24c4b7cdcf 100644 --- a/packages/tui/src/component/dialog-retry-action.tsx +++ b/packages/tui/src/component/dialog-retry-action.tsx @@ -40,7 +40,7 @@ export function DialogRetryAction(props: DialogRetryActionProps) { const dialog = useDialog() const { themeV2 } = useTheme().contextual("elevated") const showGoTreatment = () => props.link === GO_URL - const textBg = () => (showGoTreatment() ? panelOverlay(themeV2.background()) : undefined) + const textBg = () => (showGoTreatment() ? panelOverlay(themeV2.background.default) : undefined) const [selected, setSelected] = createSignal<"dismiss" | "action">("action") Keymap.createLayer(() => ({ @@ -85,26 +85,26 @@ export function DialogRetryAction(props: DialogRetryActionProps) { ) : null} - + {props.title} - dialog.clear()}> + dialog.clear()}> esc - + {props.message} {props.link ? ( showGoTreatment() ? ( - + ) : ( - + ) ) : ( @@ -115,13 +115,13 @@ export function DialogRetryAction(props: DialogRetryActionProps) { paddingLeft={2} paddingRight={2} backgroundColor={ - selected() === "dismiss" ? themeV2.background.action("focused") : RGBA.fromInts(0, 0, 0, 0) + selected() === "dismiss" ? themeV2.background.action.primary.focused : RGBA.fromInts(0, 0, 0, 0) } onMouseOver={() => setSelected("dismiss")} onMouseUp={() => dismiss(props, dialog)} > @@ -131,12 +131,14 @@ export function DialogRetryAction(props: DialogRetryActionProps) { setSelected("action")} onMouseUp={() => runAction(props, dialog)} > diff --git a/packages/tui/src/component/dialog-session-delete-failed.tsx b/packages/tui/src/component/dialog-session-delete-failed.tsx index b70a17d44e..1f67121804 100644 --- a/packages/tui/src/component/dialog-session-delete-failed.tsx +++ b/packages/tui/src/component/dialog-session-delete-failed.tsx @@ -64,17 +64,17 @@ export function DialogSessionDeleteFailed(props: { return ( - + Failed to Delete Session - dialog.clear()}> + dialog.clear()}> esc - + {`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`} - + Choose how you want to recover this broken workspace session. @@ -86,7 +86,7 @@ export function DialogSessionDeleteFailed(props: { paddingRight={1} paddingTop={1} paddingBottom={1} - backgroundColor={item.id === store.active ? themeV2.background.action("focused") : undefined} + backgroundColor={item.id === store.active ? themeV2.background.action.primary.focused : undefined} onMouseUp={() => { setStore("active", item.id) void confirm() @@ -94,12 +94,12 @@ export function DialogSessionDeleteFailed(props: { > {item.title} {item.description} diff --git a/packages/tui/src/component/dialog-skill.tsx b/packages/tui/src/component/dialog-skill.tsx index 8db0009100..d853e55fff 100644 --- a/packages/tui/src/component/dialog-skill.tsx +++ b/packages/tui/src/component/dialog-skill.tsx @@ -63,29 +63,29 @@ export function DialogSkill(props: DialogSkillProps) { - No skills available + No skills available } > - + Could not load skills - {errorMessage(loadError())} - Close and reopen Skills to try again. + {errorMessage(loadError())} + Close and reopen Skills to try again. - Loading skills… + Loading skills… } noMatchView={ - No skills found + No skills found } /> diff --git a/packages/tui/src/component/dialog-status.tsx b/packages/tui/src/component/dialog-status.tsx index 7a0d71897d..2e983465be 100644 --- a/packages/tui/src/component/dialog-status.tsx +++ b/packages/tui/src/component/dialog-status.tsx @@ -13,25 +13,25 @@ export function DialogStatus() { const mcp = createMemo(() => data.location.mcp.server.list() ?? []) const color = (status: string) => { - if (status === "connected") return themeV2.text.feedback.success() - if (status === "failed") return themeV2.text.feedback.error() - if (status === "needs_auth") return themeV2.text.feedback.warning() - if (status === "needs_client_registration") return themeV2.text.feedback.error() - return themeV2.text.subdued() + if (status === "connected") return themeV2.text.feedback.success.default + if (status === "failed") return themeV2.text.feedback.error.default + if (status === "needs_auth") return themeV2.text.feedback.warning.default + if (status === "needs_client_registration") return themeV2.text.feedback.error.default + return themeV2.text.subdued } return ( - + Status - dialog.clear()}> + dialog.clear()}> esc - 0} fallback={No MCP servers}> + 0} fallback={No MCP servers}> - + {mcp().length} MCP server{mcp().length === 1 ? "" : "s"} @@ -40,9 +40,9 @@ export function DialogStatus() { - + {item.name}{" "} - + Connected {(val) => val().error} diff --git a/packages/tui/src/component/dialog-workspace-file-changes.tsx b/packages/tui/src/component/dialog-workspace-file-changes.tsx index f2c344065c..4d79dd26c6 100644 --- a/packages/tui/src/component/dialog-workspace-file-changes.tsx +++ b/packages/tui/src/component/dialog-workspace-file-changes.tsx @@ -72,21 +72,21 @@ export function DialogWorkspaceFileChanges(props: { return ( - + {props.title ?? "File Changes Found"} - dialog.clear()}> + dialog.clear()}> esc - + {props.message ?? "Do you want to move these changes with the session?"} @@ -95,18 +95,16 @@ export function DialogWorkspaceFileChanges(props: { - {statusLabel(item.status)} + {statusLabel(item.status)} - + {" "} - {item.additions ? ( - +{item.additions} - ) : null} + {item.additions ? +{item.additions} : null} {item.deletions ? ( - -{item.deletions} + -{item.deletions} ) : null} @@ -120,14 +118,16 @@ export function DialogWorkspaceFileChanges(props: { { setStore("active", item) props.onSelect(item) dialog.clear() }} > - {item} + + {item} + )} diff --git a/packages/tui/src/component/logo.tsx b/packages/tui/src/component/logo.tsx index 0acaa44882..2146582059 100644 --- a/packages/tui/src/component/logo.tsx +++ b/packages/tui/src/component/logo.tsx @@ -8,7 +8,7 @@ export function Logo() { const { themeV2 } = useTheme() const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => { - const shadow = tint(themeV2.background(), fg, 0.25) + const shadow = tint(themeV2.background.default, fg, 0.25) const attrs = bold ? TextAttributes.BOLD : undefined return Array.from(line).map((char) => { if (char === "_") { @@ -52,8 +52,8 @@ export function Logo() { {(line, index) => ( - {renderLine(line, themeV2.text.subdued(), false)} - {renderLine(logo.right[index()], themeV2.text(), true)} + {renderLine(line, themeV2.text.subdued, false)} + {renderLine(logo.right[index()], themeV2.text.default, true)} )} diff --git a/packages/tui/src/component/plugin-route-missing.tsx b/packages/tui/src/component/plugin-route-missing.tsx index 2cbe40446b..5b36dcbdaf 100644 --- a/packages/tui/src/component/plugin-route-missing.tsx +++ b/packages/tui/src/component/plugin-route-missing.tsx @@ -5,16 +5,16 @@ export function PluginRouteMissing(props: { id: string; name: string; onHome: () return ( - + Unknown plugin route: {props.id}/{props.name} - go home + go home ) diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index 1aed9227da..d6b39d85d4 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -297,8 +297,8 @@ export function Prompt(props: PromptProps) { createEffect(() => { if (!input || input.isDestroyed) return - if (props.disabled) input.cursorColor = themeV2.background.surface.offset() - if (!props.disabled) input.cursorColor = themeV2.text() + if (props.disabled) input.cursorColor = themeV2.background.surface.offset + if (!props.disabled) input.cursorColor = themeV2.text.default }) const usage = createMemo(() => { @@ -1304,10 +1304,10 @@ export function Prompt(props: PromptProps) { } const highlight = createMemo(() => { - if (leader()) return themeV2.border() - if (store.mode === "shell") return themeV2.background.action() + if (leader()) return themeV2.border.default + if (store.mode === "shell") return themeV2.background.action.primary.default const agent = local.agent.current() - if (!agent) return themeV2.border() + if (!agent) return themeV2.border.default return local.agent.color(agent.id) }) @@ -1324,7 +1324,7 @@ export function Prompt(props: PromptProps) { () => !!local.agent.current() && store.mode === "normal" && showVariant(), animationsEnabled, ) - const borderHighlight = createMemo(() => tint(themeV2.border(), highlight(), agentMetaAlpha())) + const borderHighlight = createMemo(() => tint(themeV2.border.default, highlight(), agentMetaAlpha())) const placeholderText = createMemo(() => { if (props.showPlaceholder === false) return undefined @@ -1344,7 +1344,7 @@ export function Prompt(props: PromptProps) { const spinnerDef = createMemo(() => { const agent = status() === "running" ? local.agent.current() : local.agent.current() - const color = agent ? local.agent.color(agent.id) : themeV2.border() + const color = agent ? local.agent.color(agent.id) : themeV2.border.default return { frames: createFrames({ color, @@ -1364,7 +1364,7 @@ export function Prompt(props: PromptProps) { }) const maxHeight = createMemo(() => Math.max(6, Math.floor(dimensions().height / 3))) - const promptBg = createMemo(() => themeV2.raise(themeV2.background.surface.offset())) + const promptBg = createMemo(() => themeV2.raise(themeV2.background.surface.offset)) return ( <> @@ -1390,9 +1390,9 @@ export function Prompt(props: PromptProps) {