Skip to content
Open
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
7 changes: 7 additions & 0 deletions packages/opencode/src/cli/cmd/tui/context/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,11 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
const syntax = createMemo(() => generateSyntax(values()))
const subtleSyntax = createMemo(() => generateSubtleSyntax(values()))

const selectionBg = createMemo(() =>
tint(values().backgroundPanel, values().primary, store.mode === "light" ? 0.2 : 0.3),
)
const selectionFg = createMemo(() => values().text)

return {
theme: new Proxy(values(), {
get(_target, prop) {
Expand All @@ -373,6 +378,8 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
},
syntax,
subtleSyntax,
selectionBg,
selectionFg,
mode() {
return store.mode
},
Expand Down
8 changes: 4 additions & 4 deletions packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ import { useKeybind } from "../../context/keybind"
import { useTerminalDimensions } from "@opentui/solid"

const Title = (props: { session: Accessor<Session> }) => {
const { theme } = useTheme()
const { theme, selectionBg, selectionFg } = useTheme()
return (
<text fg={theme.text}>
<text fg={theme.text} selectionBg={selectionBg()} selectionFg={selectionFg()}>
<span style={{ bold: true }}>#</span> <span style={{ bold: true }}>{props.session().title}</span>
</text>
)
}

const ContextInfo = (props: { context: Accessor<string | undefined>; cost: Accessor<string> }) => {
const { theme } = useTheme()
const { theme, selectionBg, selectionFg } = useTheme()
return (
<Show when={props.context()}>
<text fg={theme.textMuted} wrapMode="none" flexShrink={0}>
<text fg={theme.textMuted} selectionBg={selectionBg()} selectionFg={selectionFg()} wrapMode="none" flexShrink={0}>
{props.context()} ({props.cost()})
</text>
</Show>
Expand Down
42 changes: 29 additions & 13 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export function Session() {
const { navigate } = useRoute()
const sync = useSync()
const kv = useKV()
const { theme } = useTheme()
const { theme, selectionBg, selectionFg } = useTheme()
const promptRef = usePromptRef()
const session = createMemo(() => sync.session.get(route.sessionID))
const children = createMemo(() => {
Expand Down Expand Up @@ -1031,16 +1031,18 @@ 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={selectionBg()} selectionFg={selectionFg()}>
{revert()!.reverted.length} message reverted
</text>
<text fg={theme.textMuted} selectionBg={selectionBg()} selectionFg={selectionFg()}>
<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={selectionBg()} selectionFg={selectionFg()}>
{file.filename}
<Show when={file.additions > 0}>
<span style={{ fg: theme.diffAdded }}> +{file.additions}</span>
Expand Down Expand Up @@ -1164,7 +1166,7 @@ function UserMessage(props: {
const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0])
const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
const sync = useSync()
const { theme } = useTheme()
const { theme, selectionBg, selectionFg } = useTheme()
const [hover, setHover] = createSignal(false)
const queued = createMemo(() => props.pending && props.message.id > props.pending)
const color = createMemo(() => local.agent.color(props.message.agent))
Expand Down Expand Up @@ -1197,7 +1199,9 @@ function UserMessage(props: {
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
flexShrink={0}
>
<text fg={theme.text}>{text()?.text}</text>
<text fg={theme.text} selectionBg={selectionBg()} selectionFg={selectionFg()}>
{text()?.text}
</text>
<Show when={files().length}>
<box flexDirection="row" paddingBottom={metadataVisible() ? 1 : 0} paddingTop={1} gap={1} flexWrap="wrap">
<For each={files()}>
Expand Down Expand Up @@ -1335,7 +1339,7 @@ const PART_MAPPING = {
}

function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: AssistantMessage }) {
const { theme, subtleSyntax } = useTheme()
const { theme, subtleSyntax, selectionBg, selectionFg } = useTheme()
const ctx = use()
const content = createMemo(() => {
// Filter out redacted reasoning chunks from OpenRouter
Expand All @@ -1361,6 +1365,8 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
content={"_Thinking:_ " + content()}
conceal={ctx.conceal()}
fg={theme.textMuted}
selectionBg={selectionBg()}
selectionFg={selectionFg()}
/>
</box>
</Show>
Expand All @@ -1369,7 +1375,7 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass

function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) {
const ctx = use()
const { theme, syntax } = useTheme()
const { theme, syntax, selectionBg, selectionFg } = useTheme()
return (
<Show when={props.part.text.trim()}>
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
Expand All @@ -1391,6 +1397,8 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
content={props.part.text.trim()}
conceal={ctx.conceal()}
fg={theme.text}
selectionBg={selectionBg()}
selectionFg={selectionFg()}
/>
</Match>
</Switch>
Expand Down Expand Up @@ -1909,7 +1917,7 @@ function Task(props: ToolProps<typeof TaskTool>) {

function Edit(props: ToolProps<typeof EditTool>) {
const ctx = use()
const { theme, syntax } = useTheme()
const { theme, syntax, selectionBg, selectionFg } = useTheme()

const view = createMemo(() => {
const diffStyle = ctx.sync.data.config.tui?.diff_style
Expand Down Expand Up @@ -1942,6 +1950,8 @@ function Edit(props: ToolProps<typeof EditTool>) {
width="100%"
wrapMode={ctx.diffWrapMode()}
fg={theme.text}
selectionBg={selectionBg()}
selectionFg={selectionFg()}
addedBg={theme.diffAddedBg}
removedBg={theme.diffRemovedBg}
contextBg={theme.diffContextBg}
Expand Down Expand Up @@ -1978,7 +1988,7 @@ function Edit(props: ToolProps<typeof EditTool>) {

function ApplyPatch(props: ToolProps<typeof ApplyPatchTool>) {
const ctx = use()
const { theme, syntax } = useTheme()
const { theme, syntax, selectionBg, selectionFg } = useTheme()

const files = createMemo(() => props.metadata.files ?? [])

Expand All @@ -2000,6 +2010,8 @@ function ApplyPatch(props: ToolProps<typeof ApplyPatchTool>) {
width="100%"
wrapMode={ctx.diffWrapMode()}
fg={theme.text}
selectionBg={selectionBg()}
selectionFg={selectionFg()}
addedBg={theme.diffAddedBg}
removedBg={theme.diffRemovedBg}
contextBg={theme.diffContextBg}
Expand Down Expand Up @@ -2072,7 +2084,7 @@ function TodoWrite(props: ToolProps<typeof TodoWriteTool>) {
}

function Question(props: ToolProps<typeof QuestionTool>) {
const { theme } = useTheme()
const { theme, selectionBg, selectionFg } = useTheme()
const count = createMemo(() => props.input.questions?.length ?? 0)

function format(answer?: string[]) {
Expand All @@ -2088,8 +2100,12 @@ function Question(props: ToolProps<typeof QuestionTool>) {
<For each={props.input.questions ?? []}>
{(q, i) => (
<box flexDirection="column">
<text fg={theme.textMuted}>{q.question}</text>
<text fg={theme.text}>{format(props.metadata.answers?.[i()])}</text>
<text fg={theme.textMuted} selectionBg={selectionBg()} selectionFg={selectionFg()}>
{q.question}
</text>
<text fg={theme.text} selectionBg={selectionBg()} selectionFg={selectionFg()}>
{format(props.metadata.answers?.[i()])}
</text>
</box>
)}
</For>
Expand Down
45 changes: 29 additions & 16 deletions packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { TodoItem } from "../../component/todo-item"

export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
const sync = useSync()
const { theme } = useTheme()
const { theme, selectionBg, selectionFg } = useTheme()
const session = createMemo(() => sync.session.get(props.sessionID)!)
const diff = createMemo(() => sync.data.session_diff[props.sessionID] ?? [])
const todo = createMemo(() => sync.data.todo[props.sessionID] ?? [])
Expand Down Expand Up @@ -83,20 +83,28 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
<scrollbox flexGrow={1}>
<box flexShrink={0} gap={1} paddingRight={1}>
<box paddingRight={1}>
<text fg={theme.text}>
<text fg={theme.text} selectionBg={selectionBg()} selectionFg={selectionFg()}>
<b>{session().title}</b>
</text>
<Show when={session().share?.url}>
<text fg={theme.textMuted}>{session().share!.url}</text>
<text fg={theme.textMuted} selectionBg={selectionBg()} selectionFg={selectionFg()}>
{session().share!.url}
</text>
</Show>
</box>
<box>
<text fg={theme.text}>
<text fg={theme.text} selectionBg={selectionBg()} selectionFg={selectionFg()}>
<b>Context</b>
</text>
<text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text>
<text fg={theme.textMuted}>{context()?.percentage ?? 0}% used</text>
<text fg={theme.textMuted}>{cost()} spent</text>
<text fg={theme.textMuted} selectionBg={selectionBg()} selectionFg={selectionFg()}>
{context()?.tokens ?? 0} tokens
</text>
<text fg={theme.textMuted} selectionBg={selectionBg()} selectionFg={selectionFg()}>
{context()?.percentage ?? 0}% used
</text>
<text fg={theme.textMuted} selectionBg={selectionBg()} selectionFg={selectionFg()}>
{cost()} spent
</text>
</box>
<Show when={mcpEntries().length > 0}>
<box>
Expand All @@ -108,7 +116,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
<Show when={mcpEntries().length > 2}>
<text fg={theme.text}>{expanded.mcp ? "▼" : "▶"}</text>
</Show>
<text fg={theme.text}>
<text fg={theme.text} selectionBg={selectionBg()} selectionFg={selectionFg()}>
<b>MCP</b>
<Show when={!expanded.mcp}>
<span style={{ fg: theme.textMuted }}>
Expand Down Expand Up @@ -139,7 +147,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
>
</text>
<text fg={theme.text} wrapMode="word">
<text fg={theme.text} selectionBg={selectionBg()} selectionFg={selectionFg()} wrapMode="word">
{key}{" "}
<span style={{ fg: theme.textMuted }}>
<Switch fallback={item.status}>
Expand Down Expand Up @@ -168,13 +176,13 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
<Show when={sync.data.lsp.length > 2}>
<text fg={theme.text}>{expanded.lsp ? "▼" : "▶"}</text>
</Show>
<text fg={theme.text}>
<text fg={theme.text} selectionBg={selectionBg()} selectionFg={selectionFg()}>
<b>LSP</b>
</text>
</box>
<Show when={sync.data.lsp.length <= 2 || expanded.lsp}>
<Show when={sync.data.lsp.length === 0}>
<text fg={theme.textMuted}>
<text fg={theme.textMuted} selectionBg={selectionBg()} selectionFg={selectionFg()}>
{sync.data.config.lsp === false
? "LSPs have been disabled in settings"
: "LSPs will activate as files are read"}
Expand All @@ -194,7 +202,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
>
</text>
<text fg={theme.textMuted}>
<text fg={theme.textMuted} selectionBg={selectionBg()} selectionFg={selectionFg()}>
{item.id} {item.root}
</text>
</box>
Expand Down Expand Up @@ -231,7 +239,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
<Show when={diff().length > 2}>
<text fg={theme.text}>{expanded.diff ? "▼" : "▶"}</text>
</Show>
<text fg={theme.text}>
<text fg={theme.text} selectionBg={selectionBg()} selectionFg={selectionFg()}>
<b>Modified Files</b>
</text>
</box>
Expand All @@ -240,7 +248,12 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
{(item) => {
return (
<box flexDirection="row" gap={1} justifyContent="space-between">
<text fg={theme.textMuted} wrapMode="none">
<text
fg={theme.textMuted}
selectionBg={selectionBg()}
selectionFg={selectionFg()}
wrapMode="none"
>
{item.file}
</text>
<box flexDirection="row" gap={1} flexShrink={0}>
Expand Down Expand Up @@ -295,11 +308,11 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
</box>
</box>
</Show>
<text>
<text selectionBg={selectionBg()} selectionFg={selectionFg()}>
<span style={{ fg: theme.textMuted }}>{directory().split("/").slice(0, -1).join("/")}/</span>
<span style={{ fg: theme.text }}>{directory().split("/").at(-1)}</span>
</text>
<text fg={theme.textMuted}>
<text fg={theme.textMuted} selectionBg={selectionBg()} selectionFg={selectionFg()}>
<span style={{ fg: theme.success }}>•</span> <b>Open</b>
<span style={{ fg: theme.text }}>
<b>Code</b>
Expand Down
Loading