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 71937e179fa..47940d0e234 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -14,7 +14,7 @@ import { Keybind } from "@/util/keybind"
import { usePromptHistory, type PromptInfo } from "./history"
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
import { useCommandDialog } from "../dialog-command"
-import { useRenderer } from "@opentui/solid"
+import { useRenderer, useTerminalDimensions } from "@opentui/solid"
import { Editor } from "@tui/util/editor"
import { useExit } from "../../context/exit"
import { Clipboard } from "../../util/clipboard"
@@ -120,6 +120,9 @@ export function Prompt(props: PromptProps) {
const history = usePromptHistory()
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() {
@@ -881,19 +884,21 @@ export function Prompt(props: PromptProps) {
cursorColor={theme.text}
syntaxStyle={syntax()}
/>
-
-
- {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
-
-
-
-
- {local.model.parsed().model}
-
- {local.model.parsed().provider}
-
-
-
+
+
+
+ {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
+
+
+
+
+ {local.model.parsed().model}
+
+ {local.model.parsed().provider}
+
+
+
+
- }>
-
-
- {/* @ts-ignore // SpinnerOptions doesn't support marginLeft */}
-
-
- {(() => {
- 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)
+
+
+
+
+ {/* @ts-ignore // SpinnerOptions doesn't support marginLeft */}
+
+
+ {(() => {
+ const retry = createMemo(() => {
+ const s = status()
+ if (s.type !== "retry") return
+ return s
})
- })
- const handleMessageClick = () => {
- const r = retry()
- if (!r) return
- if (isTruncated()) {
- DialogAlert.show(dialog, "Retry Error", r.message)
+ 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)
+ })
+ })
+ 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 (
-
-
- {retryText()}
-
-
- )
- })()}
+ return (
+
+
+ {retryText()}
+
+
+ )
+ })()}
+
+ 0 ? theme.primary : theme.text}>
+ esc{" "}
+ 0 ? theme.primary : theme.textMuted }}>
+ {store.interrupt > 0 ? "again to interrupt" : "interrupt"}
+
+
- 0 ? theme.primary : theme.text}>
- esc{" "}
- 0 ? theme.primary : theme.textMuted }}>
- {store.interrupt > 0 ? "again to interrupt" : "interrupt"}
-
-
-
-
-
-
-
-
+
+
+
+
+ {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
+
+
+
+
+ {local.model.parsed().model}
+
+ {local.model.parsed().provider}
+
+
+
+
+
+
+
+
+
{keybind.print("agent_cycle")} switch agent
+
+
- {keybind.print("command_list")} commands
-
-
-
-
- esc exit shell mode
+ {keybind.print("sidebar_toggle")} sidebar
-
-
-
-
+
+
+ {keybind.print("command_list")} commands
+
+
+
+
+ esc exit shell mode
+
+
+
+
>
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 098ee83cce8..cf6abef4755 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
@@ -1,124 +1,140 @@
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 { SplitBorder, EmptyBorder } from "@tui/component/border"
-import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
-import { useDirectory } from "../../context/directory"
+import { EmptyBorder } from "@tui/component/border"
+import type { Session } from "@opencode-ai/sdk/v2"
import { useKeybind } from "../../context/keybind"
+import { useTerminalDimensions } from "@opentui/solid"
-const Title = (props: { session: Accessor }) => {
+const Title = (props: { session: Accessor; truncate?: boolean }) => {
const { theme } = useTheme()
return (
-
+
# {props.session().title}
)
}
-const ContextInfo = (props: { context: Accessor; cost: Accessor }) => {
- const { theme } = useTheme()
- return (
-
-
- {props.context()} ({props.cost()})
-
-
- )
-}
-
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 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 showShare = createMemo(() => shareEnabled() && !session()?.share?.url)
const { theme } = useTheme()
const keybind = useKeybind()
+ const dimensions = useTerminalDimensions()
+ const tall = createMemo(() => dimensions().height > 40)
return (
+
+
+
-
-
-
-
- Subagent session
-
-
- Parent {keybind.print("session_parent")}
-
-
- Prev {keybind.print("session_child_cycle_reverse")}
-
-
- Next {keybind.print("session_child_cycle")}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ Subagent session
+
+
+ Parent {keybind.print("session_parent")}
+
+
+ Prev {keybind.print("session_child_cycle_reverse")}
+
+
+ Next {keybind.print("session_child_cycle")}
+
+
+
+
+ /share{" "}
+
+
+
+
+
-
-
-
-
- {session().share!.url}
-
-
-
-
- /share copy link
-
-
-
-
+
+
+
+ /share{" "}
+
+
-
-
-
+
+
+
+
+
+
)
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 da697e632ff..826fa2acf8e 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -22,6 +22,7 @@ import {
ScrollBoxRenderable,
addDefaultParsers,
MacOSScrollAccel,
+ RGBA,
type ScrollAcceleration,
} from "@opentui/core"
import { Prompt, type PromptRef } from "@tui/component/prompt"
@@ -129,13 +130,15 @@ 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 contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
+ const sidebarOverlay = createMemo(() => sidebarVisible() && !wide())
+ const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() && !sidebarOverlay() ? 42 : 0) - 4)
const scrollAcceleration = createMemo(() => {
const tui = sync.data.config.tui
@@ -961,7 +964,7 @@ export function Session() {
-
+
-
+
-
+
+
+ setSidebar("hide")}
+ >
+ e.stopPropagation()}>
+
+
+
+
)
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 6c74e04fac7..c29057770f5 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
@@ -62,6 +62,7 @@ export function Sidebar(props: { sessionID: string }) {