diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index f63f6cb1a8a..5d904cd8608 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -169,6 +169,9 @@ function App() {
const local = useLocal()
const kv = useKV()
const command = useCommandDialog()
+ const [shortcutsHidden, setShortcutsHidden] = createSignal(kv.get("shortcuts_hidden", false))
+ const [headerHidden, setHeaderHidden] = createSignal(kv.get("header_hidden", false))
+ const [footerHidden, setFooterHidden] = createSignal(kv.get("footer_hidden", false))
const sdk = useSDK()
const toast = useToast()
const { theme, mode, setMode } = useTheme()
@@ -450,6 +453,45 @@ function App() {
dialog.clear()
},
},
+ {
+ title: shortcutsHidden() ? "Show shortcuts" : "Hide shortcuts",
+ value: "app.shortcuts.toggle",
+ category: "System",
+ onSelect: (dialog) => {
+ setShortcutsHidden((prev) => {
+ const next = !prev
+ kv.set("shortcuts_hidden", next)
+ return next
+ })
+ dialog.clear()
+ },
+ },
+ {
+ title: headerHidden() ? "Show header" : "Hide header",
+ value: "app.header.toggle",
+ category: "System",
+ onSelect: (dialog) => {
+ setHeaderHidden((prev) => {
+ const next = !prev
+ kv.set("header_hidden", next)
+ return next
+ })
+ dialog.clear()
+ },
+ },
+ {
+ title: footerHidden() ? "Show footer" : "Hide footer",
+ value: "app.footer.toggle",
+ category: "System",
+ onSelect: (dialog) => {
+ setFooterHidden((prev) => {
+ const next = !prev
+ kv.set("footer_hidden", next)
+ return next
+ })
+ dialog.clear()
+ },
+ },
{
title: "Suspend terminal",
value: "terminal.suspend",
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..2eb5480bd78 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -27,6 +27,7 @@ import { useDialog } from "@tui/ui/dialog"
import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
import { DialogAlert } from "../../ui/dialog-alert"
import { useToast } from "../../ui/toast"
+import { useKV } from "../../context/kv"
export type PromptProps = {
sessionID?: string
@@ -35,6 +36,7 @@ export type PromptProps = {
ref?: (ref: PromptRef) => void
hint?: JSX.Element
showPlaceholder?: boolean
+ footerVisible?: boolean
}
export type PromptRef = {
@@ -117,6 +119,12 @@ export function Prompt(props: PromptProps) {
const dialog = useDialog()
const toast = useToast()
const status = createMemo(() => sync.data.session_status?.[props.sessionID ?? ""] ?? { type: "idle" })
+ const kv = useKV()
+ const permissions = createMemo(() => {
+ if (!props.sessionID) return []
+ return sync.data.permission[props.sessionID] ?? []
+ })
+ const shortcutsVisible = createMemo(() => !kv.get("shortcuts_hidden", false))
const history = usePromptHistory()
const command = useCommandDialog()
const renderer = useRenderer()
@@ -881,17 +889,50 @@ 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}
+
+
+
+
+
+ 0}>
+
+ ◉ {permissions().length} Permission
+ {permissions().length > 1 ? "s" : ""} pending
+
+
+
+
+
+ 0 ? theme.primary : theme.text}>
+ esc{" "}
+ 0 ? theme.primary : theme.textMuted }}>
+ {store.interrupt > 0 ? "again to interrupt" : "interrupt"}
+
+
+
+
+
+
+
+ {keybind.print("agent_cycle")} switch agent
+
+
+ {keybind.print("command_list")} commands
+
+
+
+
@@ -922,103 +963,117 @@ export function Prompt(props: PromptProps) {
}
/>
-
- }>
-
-
- {/* @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)
+
+
+
+ esc exit shell mode
+
+
+
+
+
+ }>
+
+
+ {/* @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(() => {
+ clearInterval(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"}
-
-
-
-
-
-
-
-
+
+
+
+
{keybind.print("agent_cycle")} switch agent
{keybind.print("command_list")} commands
-
-
+
+
+
+
+
+
- esc exit shell mode
+ {keybind.print("agent_cycle")} switch agent
-
-
-
-
-
+
+ {keybind.print("command_list")} commands
+
+
+
+
+
+
>
)
diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx
index ecdf93c43df..475ead74ba4 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx
@@ -73,6 +73,7 @@ export function Home() {
promptRef.set(r)
}}
hint={Hint}
+ footerVisible={false}
/>
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 029a012f8e4..96ff764a4bf 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -136,6 +136,8 @@ export function Session() {
return false
})
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
+ const headerVisible = createMemo(() => !sidebarVisible() && !kv.get("header_hidden", false))
+ const footerVisible = createMemo(() => !sidebarVisible() && !kv.get("footer_hidden", false))
const scrollAcceleration = createMemo(() => {
const tui = sync.data.config.tui
@@ -959,9 +961,16 @@ export function Session() {
}}
>
-
+
-
+
-
+