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() { }} > - + - +
- +