Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/todo-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export function TodoItem(props: TodoItemProps) {
style={{
fg: props.status === "in_progress" ? theme.warning : theme.textMuted,
}}
selectionBg={theme.selection}
selectionFg={theme.selectionForeground}
>
[{props.status === "completed" ? "✓" : props.status === "in_progress" ? "•" : " "}]{" "}
</text>
Expand All @@ -24,6 +26,8 @@ export function TodoItem(props: TodoItemProps) {
style={{
fg: props.status === "in_progress" ? theme.warning : theme.textMuted,
}}
selectionBg={theme.selection}
selectionFg={theme.selectionForeground}
>
{props.content}
</text>
Expand Down
31 changes: 29 additions & 2 deletions packages/opencode/src/cli/cmd/tui/context/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ type ThemeColors = {
text: RGBA
textMuted: RGBA
selectedListItemText: RGBA
selection: RGBA
selectionForeground: RGBA
background: RGBA
backgroundPanel: RGBA
backgroundElement: RGBA
Expand Down Expand Up @@ -131,9 +133,11 @@ type ColorValue = HexColor | RefName | Variant | RGBA
type ThemeJson = {
$schema?: string
defs?: Record<string, HexColor | RefName>
theme: Omit<Record<keyof ThemeColors, ColorValue>, "selectedListItemText" | "backgroundMenu"> & {
theme: Omit<Record<keyof ThemeColors, ColorValue>, "selectedListItemText" | "backgroundMenu" | "selection" | "selectionForeground"> & {
selectedListItemText?: ColorValue
backgroundMenu?: ColorValue
selection?: ColorValue
selectionForeground?: ColorValue
thinkingOpacity?: number
}
}
Expand Down Expand Up @@ -197,9 +201,10 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
return resolveColor(c[mode])
}

const optionalKeys = ["selectedListItemText", "backgroundMenu", "selection", "selectionForeground", "thinkingOpacity"]
const resolved = Object.fromEntries(
Object.entries(theme.theme)
.filter(([key]) => key !== "selectedListItemText" && key !== "backgroundMenu" && key !== "thinkingOpacity")
.filter(([key]) => !optionalKeys.includes(key))
.map(([key, value]) => {
return [key, resolveColor(value as ColorValue)]
}),
Expand All @@ -222,6 +227,24 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
resolved.backgroundMenu = resolved.backgroundElement
}

// Handle selection colors - optional with smart fallbacks for good contrast
if (theme.theme.selection !== undefined) {
resolved.selection = resolveColor(theme.theme.selection)
} else {
// Default selection background to primary color for visibility
resolved.selection = resolved.primary
}

if (theme.theme.selectionForeground !== undefined) {
resolved.selectionForeground = resolveColor(theme.theme.selectionForeground)
} else {
// Calculate contrasting foreground based on selection background luminance
const sel = resolved.selection!
const luminance = 0.299 * sel.r + 0.587 * sel.g + 0.114 * sel.b
// Use white text on dark backgrounds, black text on light backgrounds
resolved.selectionForeground = luminance > 0.5 ? RGBA.fromInts(0, 0, 0) : RGBA.fromInts(255, 255, 255)
}

// Handle thinkingOpacity - optional with default of 0.6
const thinkingOpacity = theme.theme.thinkingOpacity ?? 0.6

Expand Down Expand Up @@ -479,6 +502,10 @@ function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJs
textMuted,
selectedListItemText: bg,

// Selection colors - use cyan background with contrasting foreground
selection: ansiColors.cyan,
selectionForeground: isDark ? RGBA.fromInts(0, 0, 0) : RGBA.fromInts(255, 255, 255),

// Background colors
background: bg,
backgroundPanel: grays[2],
Expand Down
8 changes: 8 additions & 0 deletions packages/opencode/src/cli/cmd/tui/context/theme/orng.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@
"dark": "#0a0a0a",
"light": "#ffffff"
},
"selection": {
"dark": "darkOrange",
"light": "lightStep10"
},
"selectionForeground": {
"dark": "darkStep1",
"light": "#ffffff"
},
"background": {
"dark": "darkStep1",
"light": "lightStep1"
Expand Down
48 changes: 28 additions & 20 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -999,16 +999,16 @@ export function Session() {
paddingLeft={2}
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
>
<text fg={theme.textMuted}>{revert()!.reverted.length} message reverted</text>
<text fg={theme.textMuted}>
<text fg={theme.textMuted} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>{revert()!.reverted.length} message reverted</text>
<text fg={theme.textMuted} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>
<span style={{ fg: theme.text }}>{keybind.print("messages_redo")}</span> or /redo to
restore
</text>
<Show when={revert()!.diffFiles?.length}>
<box marginTop={1}>
<For each={revert()!.diffFiles}>
{(file) => (
<text fg={theme.text}>
<text fg={theme.text} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>
{file.filename}
<Show when={file.additions > 0}>
<span style={{ fg: theme.diffAdded }}> +{file.additions}</span>
Expand Down Expand Up @@ -1164,7 +1164,7 @@ function UserMessage(props: {
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
flexShrink={0}
>
<text fg={theme.text}>{text()?.text}</text>
<text fg={theme.text} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>{text()?.text}</text>
<Show when={files().length}>
<box flexDirection="row" paddingBottom={metadataVisible() ? 1 : 0} paddingTop={1} gap={1} flexWrap="wrap">
<For each={files()}>
Expand All @@ -1175,7 +1175,7 @@ function UserMessage(props: {
return theme.secondary
})
return (
<text fg={theme.text}>
<text fg={theme.text} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>
<span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
</text>
Expand Down Expand Up @@ -1262,13 +1262,13 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.error}
>
<text fg={theme.textMuted}>{props.message.error?.data.message}</text>
<text fg={theme.textMuted} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>{props.message.error?.data.message}</text>
</box>
</Show>
<Switch>
<Match when={props.last || final() || props.message.error?.name === "MessageAbortedError"}>
<box paddingLeft={3}>
<text marginTop={1}>
<text marginTop={1} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>
<span
style={{
fg:
Expand Down Expand Up @@ -1328,6 +1328,8 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
content={"_Thinking:_ " + content()}
conceal={ctx.conceal()}
fg={theme.textMuted}
selectionBg={theme.selection}
selectionFg={theme.selectionForeground}
/>
</box>
</Show>
Expand All @@ -1347,6 +1349,8 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
streaming={true}
content={props.part.text.trim()}
conceal={ctx.conceal()}
selectionBg={theme.selection}
selectionFg={theme.selectionForeground}
/>
</Match>
<Match when={!Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
Expand All @@ -1358,6 +1362,8 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
content={props.part.text.trim()}
conceal={ctx.conceal()}
fg={theme.text}
selectionBg={theme.selection}
selectionFg={theme.selectionForeground}
/>
</Match>
</Switch>
Expand Down Expand Up @@ -1474,7 +1480,7 @@ function GenericTool(props: ToolProps<any>) {
function ToolTitle(props: { fallback: string; when: any; icon: string; children: JSX.Element }) {
const { theme } = useTheme()
return (
<text paddingLeft={3} fg={props.when ? theme.textMuted : theme.text}>
<text paddingLeft={3} fg={props.when ? theme.textMuted : theme.text} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>
<Show fallback={<>~ {props.fallback}</>} when={props.when}>
<span style={{ bold: true }}>{props.icon}</span> {props.children}
</Show>
Expand Down Expand Up @@ -1543,13 +1549,13 @@ function InlineTool(props: {
}
}}
>
<text paddingLeft={3} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
<text paddingLeft={3} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
<span style={{ fg: props.iconColor }}>{props.icon}</span> {props.children}
</Show>
</text>
<Show when={error() && !denied()}>
<text fg={theme.error}>{error()}</text>
<text fg={theme.error} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>{error()}</text>
</Show>
</box>
)
Expand Down Expand Up @@ -1578,12 +1584,12 @@ function BlockTool(props: { title: string; children: JSX.Element; onClick?: () =
props.onClick?.()
}}
>
<text paddingLeft={3} fg={theme.textMuted}>
<text paddingLeft={3} fg={theme.textMuted} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>
{props.title}
</text>
{props.children}
<Show when={error()}>
<text fg={theme.error}>{error()}</text>
<text fg={theme.error} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>{error()}</text>
</Show>
</box>
)
Expand Down Expand Up @@ -1635,10 +1641,10 @@ function Bash(props: ToolProps<typeof BashTool>) {
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
>
<box gap={1}>
<text fg={theme.text}>$ {props.input.command}</text>
<text fg={theme.text}>{limited()}</text>
<text fg={theme.text} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>$ {props.input.command}</text>
<text fg={theme.text} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>{limited()}</text>
<Show when={overflow()}>
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
<text fg={theme.textMuted} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
</Show>
</box>
</BlockTool>
Expand Down Expand Up @@ -1675,12 +1681,14 @@ function Write(props: ToolProps<typeof WriteTool>) {
filetype={filetype(props.input.filePath!)}
syntaxStyle={syntax()}
content={code()}
selectionBg={theme.selection}
selectionFg={theme.selectionForeground}
/>
</line_number>
<Show when={diagnostics().length}>
<For each={diagnostics()}>
{(diagnostic) => (
<text fg={theme.error}>
<text fg={theme.error} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>
Error [{diagnostic.range.start.line}:{diagnostic.range.start.character}]: {diagnostic.message}
</text>
)}
Expand Down Expand Up @@ -1806,17 +1814,17 @@ function Task(props: ToolProps<typeof TaskTool>) {
part={props.part}
>
<box>
<text style={{ fg: theme.textMuted }}>
<text style={{ fg: theme.textMuted }} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>
{props.input.description} ({props.metadata.summary?.length} toolcalls)
</text>
<Show when={current()}>
<text style={{ fg: current()!.state.status === "error" ? theme.error : theme.textMuted }}>
<text style={{ fg: current()!.state.status === "error" ? theme.error : theme.textMuted }} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>
└ {Locale.titlecase(current()!.tool)}{" "}
{current()!.state.status === "completed" ? current()!.state.title : ""}
</text>
</Show>
</box>
<text fg={theme.text}>
<text fg={theme.text} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>
{keybind.print("session_child_cycle")}
<span style={{ fg: theme.textMuted }}> view subagents</span>
</text>
Expand Down Expand Up @@ -1888,7 +1896,7 @@ function Edit(props: ToolProps<typeof EditTool>) {
<box>
<For each={diagnostics()}>
{(diagnostic) => (
<text fg={theme.error}>
<text fg={theme.error} selectionBg={theme.selection} selectionFg={theme.selectionForeground}>
Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}]{" "}
{diagnostic.message}
</text>
Expand Down
Loading