diff options
| author | Dax Raad <[email protected]> | 2025-12-24 12:38:10 -0500 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-12-24 12:38:10 -0500 |
| commit | 99633cb2996e7b99c4e3a9bfa3f95cbd97f9f8c5 (patch) | |
| tree | 3618908a09169f60e81be1d1ad3fd69934506df0 | |
| parent | f822331eb8f42f29d2bba5d2c97037a052fea89f (diff) | |
| download | opencode-99633cb2996e7b99c4e3a9bfa3f95cbd97f9f8c5.tar.gz opencode-99633cb2996e7b99c4e3a9bfa3f95cbd97f9f8c5.zip | |
Revert "feat: better styling for small screens (short and/or not wide) (#5968)"
This reverts commit ac371d2987762e9b0b7627d7f1ee0ea2b5cab11a.
4 files changed, 193 insertions, 258 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 8972ba36e..9494b81cb 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -16,7 +16,7 @@ import { usePromptStash } from "./stash" import { DialogStash } from "../dialog-stash" import { type AutocompleteRef, Autocomplete } from "./autocomplete" import { useCommandDialog } from "../dialog-command" -import { useRenderer, useTerminalDimensions } from "@opentui/solid" +import { useRenderer } from "@opentui/solid" import { Editor } from "@tui/util/editor" import { useExit } from "../../context/exit" import { Clipboard } from "../../util/clipboard" @@ -123,9 +123,6 @@ export function Prompt(props: PromptProps) { const stash = usePromptStash() const command = useCommandDialog() const renderer = useRenderer() - const dimensions = useTerminalDimensions() - const tall = createMemo(() => dimensions().height > 40) - const wide = createMemo(() => dimensions().width > 120) const { theme, syntax } = useTheme() function promptModelWarning() { @@ -949,21 +946,19 @@ export function Prompt(props: PromptProps) { cursorColor={theme.text} syntaxStyle={syntax()} /> - <Show when={tall()}> - <box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}> - <text fg={highlight()}> - {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} - </text> - <Show when={store.mode === "normal"}> - <box flexDirection="row" gap={1}> - <text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}> - {local.model.parsed().model} - </text> - <text fg={theme.textMuted}>{local.model.parsed().provider}</text> - </box> - </Show> - </box> - </Show> + <box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}> + <text fg={highlight()}> + {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} + </text> + <Show when={store.mode === "normal"}> + <box flexDirection="row" gap={1}> + <text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}> + {local.model.parsed().model} + </text> + <text fg={theme.textMuted}>{local.model.parsed().provider}</text> + </box> + </Show> + </box> </box> </box> <box @@ -993,123 +988,101 @@ export function Prompt(props: PromptProps) { /> </box> <box flexDirection="row" justifyContent="space-between"> - <Switch> - <Match when={status().type !== "idle"}> - <box - flexDirection="row" - gap={1} - flexGrow={1} - justifyContent={status().type === "retry" ? "space-between" : "flex-start"} - > - <box flexShrink={0} flexDirection="row" gap={1}> - {/* @ts-ignore // SpinnerOptions doesn't support marginLeft */} - <spinner marginLeft={1} color={spinnerDef().color} frames={spinnerDef().frames} interval={40} /> - <box flexDirection="row" gap={1} flexShrink={0}> - {(() => { - const retry = createMemo(() => { - const s = status() - if (s.type !== "retry") return - return s - }) - const message = createMemo(() => { - const r = retry() - if (!r) return - if (r.message.includes("exceeded your current quota") && r.message.includes("gemini")) - return "gemini is way too hot right now" - if (r.message.length > 80) return r.message.slice(0, 80) + "..." - return r.message - }) - const isTruncated = createMemo(() => { - const r = retry() - if (!r) return false - return r.message.length > 120 - }) - const [seconds, setSeconds] = createSignal(0) - onMount(() => { - const timer = setInterval(() => { - const next = retry()?.next - if (next) setSeconds(Math.round((next - Date.now()) / 1000)) - }, 1000) - - onCleanup(() => { - clearTimeout(timer) - }) + <Show when={status().type !== "idle"} fallback={<text />}> + <box + flexDirection="row" + gap={1} + flexGrow={1} + justifyContent={status().type === "retry" ? "space-between" : "flex-start"} + > + <box flexShrink={0} flexDirection="row" gap={1}> + {/* @ts-ignore // SpinnerOptions doesn't support marginLeft */} + <spinner marginLeft={1} color={spinnerDef().color} frames={spinnerDef().frames} interval={40} /> + <box flexDirection="row" gap={1} flexShrink={0}> + {(() => { + const retry = createMemo(() => { + const s = status() + if (s.type !== "retry") return + return s + }) + const message = createMemo(() => { + const r = retry() + if (!r) return + if (r.message.includes("exceeded your current quota") && r.message.includes("gemini")) + return "gemini is way too hot right now" + if (r.message.length > 80) return r.message.slice(0, 80) + "..." + return r.message + }) + const isTruncated = createMemo(() => { + const r = retry() + if (!r) return false + return r.message.length > 120 + }) + const [seconds, setSeconds] = createSignal(0) + onMount(() => { + const timer = setInterval(() => { + const next = retry()?.next + if (next) setSeconds(Math.round((next - Date.now()) / 1000)) + }, 1000) + + onCleanup(() => { + clearInterval(timer) }) - const handleMessageClick = () => { - const r = retry() - if (!r) return - if (isTruncated()) { - DialogAlert.show(dialog, "Retry Error", r.message) - } + }) + const handleMessageClick = () => { + const r = retry() + if (!r) return + if (isTruncated()) { + DialogAlert.show(dialog, "Retry Error", r.message) } + } - const retryText = () => { - const r = retry() - if (!r) return "" - const baseMessage = message() - const truncatedHint = isTruncated() ? " (click to expand)" : "" - const retryInfo = ` [retrying ${seconds() > 0 ? `in ${seconds()}s ` : ""}attempt #${r.attempt}]` - return baseMessage + truncatedHint + retryInfo - } + const retryText = () => { + const r = retry() + if (!r) return "" + const baseMessage = message() + const truncatedHint = isTruncated() ? " (click to expand)" : "" + const retryInfo = ` [retrying ${seconds() > 0 ? `in ${seconds()}s ` : ""}attempt #${r.attempt}]` + return baseMessage + truncatedHint + retryInfo + } - return ( - <Show when={retry()}> - <box onMouseUp={handleMessageClick}> - <text fg={theme.error}>{retryText()}</text> - </box> - </Show> - ) - })()} - </box> + return ( + <Show when={retry()}> + <box onMouseUp={handleMessageClick}> + <text fg={theme.error}>{retryText()}</text> + </box> + </Show> + ) + })()} </box> - <text fg={store.interrupt > 0 ? theme.primary : theme.text}> - esc{" "} - <span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}> - {store.interrupt > 0 ? "again to interrupt" : "interrupt"} - </span> - </text> </box> - </Match> - <Match when={!tall()}> - <box flexDirection="row" gap={1}> - <text fg={highlight()}> - {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} - </text> - <Show when={store.mode === "normal"}> - <box flexDirection="row" gap={1}> - <text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}> - {local.model.parsed().model} - </text> - <text fg={theme.textMuted}>{local.model.parsed().provider}</text> - </box> - </Show> - </box> - </Match> - </Switch> - <box gap={2} flexDirection="row" marginLeft="auto"> - <Switch> - <Match when={store.mode === "normal"}> - <Show when={wide()}> + <text fg={store.interrupt > 0 ? theme.primary : theme.text}> + esc{" "} + <span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}> + {store.interrupt > 0 ? "again to interrupt" : "interrupt"} + </span> + </text> + </box> + </Show> + <Show when={status().type !== "retry"}> + <box gap={2} flexDirection="row"> + <Switch> + <Match when={store.mode === "normal"}> <text fg={theme.text}> {keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>switch agent</span> </text> - </Show> - <Show when={!wide()}> <text fg={theme.text}> - {keybind.print("sidebar_toggle")} <span style={{ fg: theme.textMuted }}>sidebar</span> + {keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span> </text> - </Show> - <text fg={theme.text}> - {keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span> - </text> - </Match> - <Match when={store.mode === "shell"}> - <text fg={theme.text}> - esc <span style={{ fg: theme.textMuted }}>exit shell mode</span> - </text> - </Match> - </Switch> - </box> + </Match> + <Match when={store.mode === "shell"}> + <text fg={theme.text}> + esc <span style={{ fg: theme.textMuted }}>exit shell mode</span> + </text> + </Match> + </Switch> + </box> + </Show> </box> </box> </> diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index cf6abef47..098ee83cc 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -1,140 +1,124 @@ import { type Accessor, createMemo, Match, Show, Switch } from "solid-js" import { useRouteData } from "@tui/context/route" import { useSync } from "@tui/context/sync" +import { pipe, sumBy } from "remeda" import { useTheme } from "@tui/context/theme" -import { EmptyBorder } from "@tui/component/border" -import type { Session } from "@opencode-ai/sdk/v2" +import { SplitBorder, EmptyBorder } from "@tui/component/border" +import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2" +import { useDirectory } from "../../context/directory" import { useKeybind } from "../../context/keybind" -import { useTerminalDimensions } from "@opentui/solid" -const Title = (props: { session: Accessor<Session>; truncate?: boolean }) => { +const Title = (props: { session: Accessor<Session> }) => { const { theme } = useTheme() return ( - <text fg={theme.text} wrapMode={props.truncate ? "none" : undefined} flexShrink={props.truncate ? 1 : 0}> + <text fg={theme.text}> <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() + return ( + <Show when={props.context()}> + <text fg={theme.textMuted} wrapMode="none" flexShrink={0}> + {props.context()} ({props.cost()}) + </text> + </Show> + ) +} + export function Header() { const route = useRouteData("session") const sync = useSync() const session = createMemo(() => sync.session.get(route.sessionID)!) + const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") - const showShare = createMemo(() => shareEnabled() && !session()?.share?.url) + + const cost = createMemo(() => { + const total = pipe( + messages(), + sumBy((x) => (x.role === "assistant" ? x.cost : 0)), + ) + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(total) + }) + + const context = createMemo(() => { + const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage + if (!last) return + const total = + last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write + const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID] + let result = total.toLocaleString() + if (model?.limit.context) { + result += " " + Math.round((total / model.limit.context) * 100) + "%" + } + return result + }) const { theme } = useTheme() const keybind = useKeybind() - const dimensions = useTerminalDimensions() - const tall = createMemo(() => dimensions().height > 40) return ( <box flexShrink={0}> <box - height={1} - border={["left"]} - borderColor={theme.border} - customBorderChars={{ - ...EmptyBorder, - vertical: theme.backgroundPanel.a !== 0 ? "╻" : " ", - }} - > - <box - height={1} - border={["top"]} - borderColor={theme.backgroundPanel} - customBorderChars={ - theme.backgroundPanel.a !== 0 - ? { - ...EmptyBorder, - horizontal: "▄", - } - : { - ...EmptyBorder, - horizontal: " ", - } - } - /> - </box> - <box + paddingTop={1} + paddingBottom={1} + paddingLeft={2} + paddingRight={1} + {...SplitBorder} border={["left"]} borderColor={theme.border} - customBorderChars={{ - ...EmptyBorder, - vertical: "┃", - bottomLeft: "╹", - }} + flexShrink={0} + backgroundColor={theme.backgroundPanel} > - <box - paddingTop={tall() ? 1 : 0} - paddingBottom={tall() ? 1 : 0} - paddingLeft={2} - paddingRight={1} - flexShrink={0} - flexGrow={1} - backgroundColor={theme.backgroundPanel} - > - <Switch> - <Match when={session()?.parentID}> - <box flexDirection="row" gap={2}> - <text fg={theme.text}> - <b>Subagent session</b> - </text> - <text fg={theme.text}> - Parent <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent")}</span> - </text> - <text fg={theme.text}> - Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span> - </text> - <text fg={theme.text}> - Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span> - </text> - <box flexGrow={1} flexShrink={1} /> - <Show when={showShare()}> - <text fg={theme.textMuted} wrapMode="none" flexShrink={0}> - /share{" "} - </text> - </Show> - </box> - </Match> - <Match when={true}> + <Switch> + <Match when={session()?.parentID}> + <box flexDirection="row" gap={2}> + <text fg={theme.text}> + <b>Subagent session</b> + </text> + <text fg={theme.text}> + Parent <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent")}</span> + </text> + <text fg={theme.text}> + Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span> + </text> + <text fg={theme.text}> + Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span> + </text> + <box flexGrow={1} flexShrink={1} /> + <ContextInfo context={context} cost={cost} /> + </box> + </Match> + <Match when={true}> + <box flexDirection="row" justifyContent="space-between" gap={1}> + <Title session={session} /> + <ContextInfo context={context} cost={cost} /> + </box> + <Show when={shareEnabled()}> <box flexDirection="row" justifyContent="space-between" gap={1}> - <Title session={session} truncate={!tall()} /> - <Show when={showShare()}> - <text fg={theme.textMuted} wrapMode="none" flexShrink={0}> - /share{" "} - </text> - </Show> + <box flexGrow={1} flexShrink={1}> + <Switch> + <Match when={session().share?.url}> + <text fg={theme.textMuted} wrapMode="word"> + {session().share!.url} + </text> + </Match> + <Match when={true}> + <text fg={theme.text} wrapMode="word"> + /share <span style={{ fg: theme.textMuted }}>copy link</span> + </text> + </Match> + </Switch> + </box> </box> - </Match> - </Switch> - </box> - </box> - <box - height={1} - border={["left"]} - borderColor={theme.border} - customBorderChars={{ - ...EmptyBorder, - vertical: theme.backgroundPanel.a !== 0 ? "╹" : " ", - }} - > - <box - height={1} - border={["bottom"]} - borderColor={theme.backgroundPanel} - customBorderChars={ - theme.backgroundPanel.a !== 0 - ? { - ...EmptyBorder, - horizontal: "▀", - } - : { - ...EmptyBorder, - horizontal: " ", - } - } - /> + </Show> + </Match> + </Switch> </box> </box> ) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index c685d8c66..818b96da4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -22,7 +22,6 @@ import { ScrollBoxRenderable, addDefaultParsers, MacOSScrollAccel, - RGBA, type ScrollAcceleration, } from "@opentui/core" import { Prompt, type PromptRef } from "@tui/component/prompt" @@ -131,15 +130,13 @@ export function Session() { const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word") const wide = createMemo(() => dimensions().width > 120) - const tall = createMemo(() => dimensions().height > 40) const sidebarVisible = createMemo(() => { if (session()?.parentID) return false if (sidebar() === "show") return true if (sidebar() === "auto" && wide()) return true return false }) - const sidebarOverlay = createMemo(() => sidebarVisible() && !wide()) - const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() && !sidebarOverlay() ? 42 : 0) - 4) + const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4) const scrollAcceleration = createMemo(() => { const tui = sync.data.config.tui @@ -965,7 +962,7 @@ export function Session() { <box flexDirection="row"> <box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}> <Show when={session()}> - <Show when={!sidebarVisible() || sidebarOverlay()}> + <Show when={!sidebarVisible()}> <Header /> </Show> <scrollbox @@ -1095,33 +1092,15 @@ export function Session() { sessionID={route.sessionID} /> </box> - <Show when={(!sidebarVisible() || sidebarOverlay()) && tall()}> + <Show when={!sidebarVisible()}> <Footer /> </Show> </Show> <Toast /> </box> - <Show when={sidebarVisible() && !sidebarOverlay()}> + <Show when={sidebarVisible()}> <Sidebar sessionID={route.sessionID} /> </Show> - <Show when={sidebarOverlay()}> - <box - position="absolute" - left={0} - top={0} - width={dimensions().width} - height={dimensions().height} - backgroundColor={RGBA.fromInts(0, 0, 0, 150)} - zIndex={100} - flexDirection="row" - justifyContent="flex-end" - onMouseUp={() => setSidebar("hide")} - > - <box onMouseUp={(e) => e.stopPropagation()}> - <Sidebar sessionID={route.sessionID} /> - </box> - </box> - </Show> </box> </context.Provider> ) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 22f2ecc0d..a9ed042d1 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -73,7 +73,6 @@ export function Sidebar(props: { sessionID: string }) { <box backgroundColor={theme.backgroundPanel} width={42} - height="100%" paddingTop={1} paddingBottom={1} paddingLeft={2} |
