diff options
| author | Dax Raad <[email protected]> | 2026-01-13 09:57:43 -0500 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2026-01-13 09:58:08 -0500 |
| commit | c86c2acf4c0e0f9945b3dd83a1a32e1eb9783c86 (patch) | |
| tree | 9ae36a9015fd3554cf5abc6f5b5395bcce4992c9 | |
| parent | 29bf731d47da1cda99de2c9890d525045b1bc8e8 (diff) | |
| download | opencode-c86c2acf4c0e0f9945b3dd83a1a32e1eb9783c86.tar.gz opencode-c86c2acf4c0e0f9945b3dd83a1a32e1eb9783c86.zip | |
add fullscreen view to permission prompt
| -rw-r--r-- | AGENTS.md | 6 | ||||
| -rw-r--r-- | STYLE_GUIDE.md | 21 | ||||
| -rw-r--r-- | packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | 40 | ||||
| -rw-r--r-- | packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx | 212 |
4 files changed, 162 insertions, 117 deletions
@@ -1,4 +1,4 @@ -- To test opencode in the `packages/opencode` directory you can run `bun dev` -- To regenerate the javascript SDK, run ./packages/sdk/js/script/build.ts +- To test opencode in `packages/opencode`, run `bun dev`. +- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`. - ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. -- the default branch in this repo is `dev` +- The default branch in this repo is `dev`. diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index a46ce221f..52d012fcb 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -1,19 +1,16 @@ ## Style Guide -- Try to keep things in one function unless composable or reusable -- AVOID unnecessary destructuring of variables. instead of doing `const { a, b } -= obj` just reference it as obj.a and obj.b. this preserves context -- AVOID `try`/`catch` where possible -- AVOID using `any` type -- PREFER single word variable names where possible -- Use as many bun apis as possible like Bun.file() +- Keep things in one function unless composable or reusable +- Avoid unnecessary destructuring. Instead of `const { a, b } = obj`, use `obj.a` and `obj.b` to preserve context +- Avoid `try`/`catch` where possible +- Avoid using the `any` type +- Prefer single word variable names where possible +- Use Bun APIs when possible, like `Bun.file()` # Avoid let statements -we don't like let statements, especially combined with if/else statements. -prefer const - -This is bad: +We don't like `let` statements, especially combined with if/else statements. +Prefer `const`. Good: @@ -32,7 +29,7 @@ else foo = 2 # Avoid else statements -Prefer early returns or even using `iife` to avoid else statements +Prefer early returns or using an `iife` to avoid else statements. Good: 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 d5e0a0aa2..9ad85d08f 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -563,25 +563,27 @@ export function Prompt(props: PromptProps) { })), }) } else { - sdk.client.session.prompt({ - sessionID, - ...selectedModel, - messageID, - agent: local.agent.current().name, - model: selectedModel, - variant, - parts: [ - { - id: Identifier.ascending("part"), - type: "text", - text: inputText, - }, - ...nonTextParts.map((x) => ({ - id: Identifier.ascending("part"), - ...x, - })), - ], - }) + sdk.client.session + .prompt({ + sessionID, + ...selectedModel, + messageID, + agent: local.agent.current().name, + model: selectedModel, + variant, + parts: [ + { + id: Identifier.ascending("part"), + type: "text", + text: inputText, + }, + ...nonTextParts.map((x) => ({ + id: Identifier.ascending("part"), + ...x, + })), + ], + }) + .catch(() => {}) } history.append({ ...store.prompt, diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index f5b6badb5..c95b42260 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -1,6 +1,6 @@ import { createStore } from "solid-js/store" import { createMemo, For, Match, Show, Switch } from "solid-js" -import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid" +import { Portal, useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" import type { TextareaRenderable } from "@opentui/core" import { useKeybind } from "../../context/keybind" import { useTheme, selectedForeground } from "../../context/theme" @@ -11,6 +11,7 @@ import { useSync } from "../../context/sync" import { useTextareaKeybindings } from "../../component/textarea-keybindings" import path from "path" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" +import { Keybind } from "@/util/keybind" import { Locale } from "@/util/locale" type PermissionStage = "permission" | "always" | "reject" @@ -32,7 +33,9 @@ function filetype(input?: string) { } function EditBody(props: { request: PermissionRequest }) { - const { theme, syntax } = useTheme() + const themeState = useTheme() + const theme = themeState.theme + const syntax = themeState.syntax const sync = useSync() const dimensions = useTerminalDimensions() @@ -54,7 +57,7 @@ function EditBody(props: { request: PermissionRequest }) { <text fg={theme.textMuted}>Edit {normalizePath(filepath())}</text> </box> <Show when={diff()}> - <box maxHeight={Math.floor(dimensions().height / 4)} overflow="scroll"> + <scrollbox height="100%"> <diff diff={diff()} view={view()} @@ -74,7 +77,7 @@ function EditBody(props: { request: PermissionRequest }) { addedLineNumberBg={theme.diffAddedLineNumberBg} removedLineNumberBg={theme.diffRemovedLineNumberBg} /> - </box> + </scrollbox> </Show> </box> ) @@ -172,86 +175,95 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { message: message || undefined, }) }} - onCancel={() => setStore("stage", "permission")} + onCancel={() => { + setStore("stage", "permission") + }} /> </Match> <Match when={store.stage === "permission"}> - <Prompt - title="Permission required" - body={ - <Switch> - <Match when={props.request.permission === "edit"}> - <EditBody request={props.request} /> - </Match> - <Match when={props.request.permission === "read"}> - <TextBody icon="→" title={`Read ` + normalizePath(input().filePath as string)} /> - </Match> - <Match when={props.request.permission === "glob"}> - <TextBody icon="✱" title={`Glob "` + (input().pattern ?? "") + `"`} /> - </Match> - <Match when={props.request.permission === "grep"}> - <TextBody icon="✱" title={`Grep "` + (input().pattern ?? "") + `"`} /> - </Match> - <Match when={props.request.permission === "list"}> - <TextBody icon="→" title={`List ` + normalizePath(input().path as string)} /> - </Match> - <Match when={props.request.permission === "bash"}> - <TextBody - icon="#" - title={(input().description as string) ?? ""} - description={("$ " + input().command) as string} - /> - </Match> - <Match when={props.request.permission === "task"}> - <TextBody - icon="#" - title={`${Locale.titlecase((input().subagent_type as string) ?? "Unknown")} Task`} - description={"◉ " + input().description} - /> - </Match> - <Match when={props.request.permission === "webfetch"}> - <TextBody icon="%" title={`WebFetch ` + (input().url ?? "")} /> - </Match> - <Match when={props.request.permission === "websearch"}> - <TextBody icon="◈" title={`Exa Web Search "` + (input().query ?? "") + `"`} /> - </Match> - <Match when={props.request.permission === "codesearch"}> - <TextBody icon="◇" title={`Exa Code Search "` + (input().query ?? "") + `"`} /> - </Match> - <Match when={props.request.permission === "external_directory"}> - <TextBody icon="←" title={`Access external directory ` + normalizePath(input().path as string)} /> - </Match> - <Match when={props.request.permission === "doom_loop"}> - <TextBody icon="⟳" title="Continue after repeated failures" /> - </Match> - <Match when={true}> - <TextBody icon="⚙" title={`Call tool ` + props.request.permission} /> - </Match> - </Switch> - } - options={{ once: "Allow once", always: "Allow always", reject: "Reject" }} - escapeKey="reject" - onSelect={(option) => { - if (option === "always") { - setStore("stage", "always") - return - } - if (option === "reject") { - if (session()?.parentID) { - setStore("stage", "reject") - return + {(() => { + const body = ( + <Prompt + title="Permission required" + body={ + <Switch> + <Match when={props.request.permission === "edit"}> + <EditBody request={props.request} /> + </Match> + <Match when={props.request.permission === "read"}> + <TextBody icon="→" title={`Read ` + normalizePath(input().filePath as string)} /> + </Match> + <Match when={props.request.permission === "glob"}> + <TextBody icon="✱" title={`Glob "` + (input().pattern ?? "") + `"`} /> + </Match> + <Match when={props.request.permission === "grep"}> + <TextBody icon="✱" title={`Grep "` + (input().pattern ?? "") + `"`} /> + </Match> + <Match when={props.request.permission === "list"}> + <TextBody icon="→" title={`List ` + normalizePath(input().path as string)} /> + </Match> + <Match when={props.request.permission === "bash"}> + <TextBody + icon="#" + title={(input().description as string) ?? ""} + description={("$ " + input().command) as string} + /> + </Match> + <Match when={props.request.permission === "task"}> + <TextBody + icon="#" + title={`${Locale.titlecase((input().subagent_type as string) ?? "Unknown")} Task`} + description={"◉ " + input().description} + /> + </Match> + <Match when={props.request.permission === "webfetch"}> + <TextBody icon="%" title={`WebFetch ` + (input().url ?? "")} /> + </Match> + <Match when={props.request.permission === "websearch"}> + <TextBody icon="◈" title={`Exa Web Search "` + (input().query ?? "") + `"`} /> + </Match> + <Match when={props.request.permission === "codesearch"}> + <TextBody icon="◇" title={`Exa Code Search "` + (input().query ?? "") + `"`} /> + </Match> + <Match when={props.request.permission === "external_directory"}> + <TextBody icon="←" title={`Access external directory ` + normalizePath(input().path as string)} /> + </Match> + <Match when={props.request.permission === "doom_loop"}> + <TextBody icon="⟳" title="Continue after repeated failures" /> + </Match> + <Match when={true}> + <TextBody icon="⚙" title={`Call tool ` + props.request.permission} /> + </Match> + </Switch> } - sdk.client.permission.reply({ - reply: "reject", - requestID: props.request.id, - }) - } - sdk.client.permission.reply({ - reply: "once", - requestID: props.request.id, - }) - }} - /> + options={{ once: "Allow once", always: "Allow always", reject: "Reject" }} + escapeKey="reject" + fullscreen + onSelect={(option) => { + if (option === "always") { + setStore("stage", "always") + return + } + if (option === "reject") { + if (session()?.parentID) { + setStore("stage", "reject") + return + } + sdk.client.permission.reply({ + reply: "reject", + requestID: props.request.id, + }) + } + sdk.client.permission.reply({ + reply: "once", + requestID: props.request.id, + }) + }} + /> + ) + + return body + })()} </Match> </Switch> ) @@ -327,14 +339,18 @@ function Prompt<const T extends Record<string, string>>(props: { body: JSX.Element options: T escapeKey?: keyof T + fullscreen?: boolean onSelect: (option: keyof T) => void }) { const { theme } = useTheme() const keybind = useKeybind() + const dimensions = useTerminalDimensions() const keys = Object.keys(props.options) as (keyof T)[] const [store, setStore] = createStore({ selected: keys[0], + expanded: false, }) + const diffKey = Keybind.parse("ctrl+f")[0] useKeyboard((evt) => { if (evt.name === "left" || evt.name == "h") { @@ -360,17 +376,36 @@ function Prompt<const T extends Record<string, string>>(props: { evt.preventDefault() props.onSelect(props.escapeKey) } + + if (props.fullscreen && diffKey && Keybind.match(diffKey, keybind.parse(evt))) { + evt.preventDefault() + evt.stopPropagation() + setStore("expanded", (v) => !v) + } }) - return ( + const hint = createMemo(() => (store.expanded ? "minimize" : "fullscreen")) + const renderer = useRenderer() + + const content = () => ( <box backgroundColor={theme.backgroundPanel} border={["left"]} borderColor={theme.warning} customBorderChars={SplitBorder.customBorderChars} + {...(store.expanded + ? { top: dimensions().height * -1 + 1, bottom: 1, left: 2, right: 2, position: "absolute" } + : { + top: 0, + maxHeight: 15, + bottom: 0, + left: 0, + right: 0, + position: "relative", + })} > - <box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}> - <box flexDirection="row" gap={1} paddingLeft={1}> + <box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1} flexGrow={1}> + <box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}> <text fg={theme.warning}>{"△"}</text> <text fg={theme.text}>{props.title}</text> </box> @@ -403,6 +438,11 @@ function Prompt<const T extends Record<string, string>>(props: { </For> </box> <box flexDirection="row" gap={2}> + <Show when={props.fullscreen}> + <text fg={theme.text}> + {"ctrl+f"} <span style={{ fg: theme.textMuted }}>{hint()}</span> + </text> + </Show> <text fg={theme.text}> {"⇆"} <span style={{ fg: theme.textMuted }}>select</span> </text> @@ -413,4 +453,10 @@ function Prompt<const T extends Record<string, string>>(props: { </box> </box> ) + + return ( + <Show when={!store.expanded} fallback={<Portal>{content()}</Portal>}> + {content()} + </Show> + ) } |
