diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index b540f15726..60886839ae 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -66,8 +66,16 @@ import { createTuiApi } from "@/cli/cmd/tui/plugin/api"
import type { RouteMap } from "@/cli/cmd/tui/plugin/api"
import { createTuiAttention } from "@/cli/cmd/tui/attention"
import { FormatError, FormatUnknownError } from "@/cli/error"
-import { CommandPaletteProvider, useCommandPalette } from "./context/command-palette"
-import { OpencodeKeymapProvider, registerOpencodeKeymap, useBindings, useOpencodeKeymap } from "./keymap"
+import { CommandPaletteDialog } from "./component/command-palette"
+import {
+ COMMAND_PALETTE_COMMAND,
+ OPENCODE_BASE_MODE,
+ OpencodeKeymapProvider,
+ createOpencodeModeStack,
+ registerOpencodeKeymap,
+ useBindings,
+ useOpencodeKeymap,
+} from "./keymap"
import type { EventSource } from "./context/sdk"
import { DialogVariant } from "./component/dialog-variant"
@@ -180,6 +188,7 @@ export function tui(input: {
}
const onBeforeExit = async () => {
offKeymap()
+ modeStack.dispose()
await TuiPluginRuntime.dispose()
TuiAudio.dispose()
}
@@ -190,6 +199,7 @@ export function tui(input: {
const mode = (await renderer.waitForThemeMode(1000)) ?? "dark"
const keymap = createDefaultOpenTuiKeymap(renderer)
+ const modeStack = createOpencodeModeStack(keymap)
const offKeymap = registerOpencodeKeymap(keymap, renderer, input.config)
await render(() => {
@@ -229,17 +239,15 @@ export function tui(input: {
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -269,7 +277,6 @@ function App(props: { onSnapshot?: () => Promise }) {
const dialog = useDialog()
const local = useLocal()
const kv = useKV()
- const command = useCommandPalette()
const keymap = useOpencodeKeymap()
const event = useEvent()
const sdk = useSDK()
@@ -448,12 +455,12 @@ function App(props: { onSnapshot?: () => Promise }) {
const appCommands = createMemo(() =>
[
{
- name: "command.palette.show",
+ name: COMMAND_PALETTE_COMMAND,
title: "Show command palette",
category: "System",
hidden: true,
run: () => {
- command.show()
+ dialog.replace(() => )
},
},
{
@@ -836,7 +843,7 @@ function App(props: { onSnapshot?: () => Promise }) {
}))
useBindings(() => ({
- enabled: command.matcher,
+ opencodeMode: OPENCODE_BASE_MODE,
bindings: tuiConfig.keybinds.gather(
"app",
Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
@@ -848,9 +855,8 @@ function App(props: { onSnapshot?: () => Promise }) {
}))
useBindings(() => ({
+ opencodeMode: OPENCODE_BASE_MODE,
enabled: () => {
- const ok = command.matcher.get()
- if (!ok) return false
const current = promptRef.current
if (!current?.focused) return true
return current.current.input === ""
@@ -859,7 +865,7 @@ function App(props: { onSnapshot?: () => Promise }) {
}))
event.on(TuiEvent.CommandExecute.type, (evt) => {
- command.run(evt.properties.command)
+ keymap.dispatchCommand(evt.properties.command)
})
event.on(TuiEvent.ToastShow.type, (evt) => {
diff --git a/packages/opencode/src/cli/cmd/tui/component/command-palette.tsx b/packages/opencode/src/cli/cmd/tui/component/command-palette.tsx
new file mode 100644
index 0000000000..ba2b1ebdc7
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/component/command-palette.tsx
@@ -0,0 +1,80 @@
+import { createMemo } from "solid-js"
+import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
+import { type DialogContext } from "@tui/ui/dialog"
+import {
+ COMMAND_PALETTE_COMMAND,
+ formatKeyBindings,
+ type OpenTuiKeymap,
+ useKeymapSelector,
+ useOpencodeKeymap,
+} from "../keymap"
+import { useTuiConfig } from "../context/tui-config"
+
+type PaletteCommandEntry = ReturnType[number]
+
+function isVisiblePaletteCommand(entry: PaletteCommandEntry) {
+ return entry.command.hidden !== true && entry.command.name !== COMMAND_PALETTE_COMMAND
+}
+
+function isSuggestedPaletteCommand(entry: PaletteCommandEntry) {
+ const suggested = entry.command.suggested
+ if (typeof suggested === "boolean") return suggested
+ if (typeof suggested === "function") return suggested() === true
+ return false
+}
+
+export function CommandPaletteDialog() {
+ const config = useTuiConfig()
+ const keymap = useOpencodeKeymap()
+ const entries = useKeymapSelector((keymap: OpenTuiKeymap) => {
+ const query = {
+ namespace: "palette",
+ }
+ const reachable = keymap
+ .getCommandEntries({
+ ...query,
+ visibility: "reachable",
+ })
+ .filter(isVisiblePaletteCommand)
+ const registeredBindings = keymap.getCommandBindings({
+ visibility: "registered",
+ commands: reachable.map((entry) => entry.command.name),
+ })
+
+ return reachable.map((entry) => ({
+ ...entry,
+ bindings: registeredBindings.get(entry.command.name) ?? entry.bindings,
+ }))
+ })
+ const options = createMemo(() =>
+ entries().map((entry) => ({
+ title: typeof entry.command.title === "string" ? entry.command.title : entry.command.name,
+ description: typeof entry.command.desc === "string" ? entry.command.desc : undefined,
+ category: typeof entry.command.category === "string" ? entry.command.category : undefined,
+ footer: formatKeyBindings(entry.bindings, config),
+ value: entry.command.name,
+ suggested: isSuggestedPaletteCommand(entry),
+ onSelect: (dialog: DialogContext) => {
+ dialog.clear()
+ keymap.dispatchCommand(entry.command.name)
+ },
+ })),
+ )
+
+ let ref: DialogSelectRef
+ const list = () => {
+ if (ref?.filter) return options()
+ return [
+ ...options()
+ .filter((option) => option.suggested)
+ .map((option) => ({
+ ...option,
+ value: `suggested:${option.value}`,
+ category: "Suggested",
+ })),
+ ...options(),
+ ]
+ }
+
+ return (ref = value)} title="Commands" options={list()} />
+}
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
index 3f7604653c..b9e5aad74e 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
@@ -12,12 +12,11 @@ import { getScrollAcceleration } from "../../util/scroll"
import { useTuiConfig } from "../../context/tui-config"
import { useTheme, selectedForeground } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border"
-import { useCommandPalette } from "../../context/command-palette"
import { useTerminalDimensions } from "@opentui/solid"
import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history"
import { useFrecency } from "./frecency"
-import { useBindings } from "../../keymap"
+import { useBindings, useCommandSlashes, useOpencodeModeStack } from "../../keymap"
import { Reference } from "@/reference/reference"
import type { Config } from "@/config/config"
import { displayCharAt, mentionTriggerIndex } from "@/cli/cmd/prompt-display"
@@ -85,7 +84,8 @@ export function Autocomplete(props: {
const editor = useEditorContext()
const sdk = useSDK()
const sync = useSync()
- const command = useCommandPalette()
+ const slashes = useCommandSlashes()
+ const modeStack = useOpencodeModeStack()
const { theme } = useTheme()
const dimensions = useTerminalDimensions()
const frecency = useFrecency()
@@ -99,6 +99,12 @@ export function Autocomplete(props: {
const [positionTick, setPositionTick] = createSignal(0)
+ createEffect(() => {
+ if (!store.visible) return
+ const popMode = modeStack.push("autocomplete")
+ onCleanup(popMode)
+ })
+
createEffect(() => {
if (store.visible) {
let lastPos = { x: 0, y: 0, width: 0 }
@@ -365,7 +371,6 @@ export function Autocomplete(props: {
const { filename, part } = createFilePart(item, lineRange)
const index = store.visible === "@" ? store.index : props.input().cursorOffset
- command.suspend(false)
setStore("visible", false)
setStore("index", index)
insertPart(filename, part)
@@ -536,7 +541,7 @@ export function Autocomplete(props: {
)
const commands = createMemo((): AutocompleteOption[] => {
- const results: AutocompleteOption[] = [...command.slashes()]
+ const results: AutocompleteOption[] = [...slashes()]
for (const serverCommand of sync.data.command) {
if (serverCommand.source === "skill") continue
@@ -727,7 +732,6 @@ export function Autocomplete(props: {
}))
function show(mode: "@" | "/") {
- command.suspend(true)
setStore({
visible: mode,
index: props.input().cursorOffset,
@@ -744,7 +748,6 @@ export function Autocomplete(props: {
draft.input = props.input().plainText
})
}
- command.suspend(false)
setStore("visible", false)
}
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 f6da08d6c0..a4cfc563f5 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -59,8 +59,13 @@ import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable"
import { useArgs } from "@tui/context/args"
import { Flag } from "@opencode-ai/core/flag/flag"
import { type WorkspaceStatus } from "../workspace-label"
-import { useCommandPalette } from "../../context/command-palette"
-import { useBindings, useCommandShortcut, useLeaderActive, useOpencodeKeymap } from "../../keymap"
+import {
+ OPENCODE_BASE_MODE,
+ useBindings,
+ useCommandShortcut,
+ useLeaderActive,
+ useOpencodeKeymap,
+} from "../../keymap"
import { useTuiConfig } from "../../context/tui-config"
export type PromptProps = {
@@ -152,7 +157,6 @@ export function Prompt(props: PromptProps) {
const status = createMemo(() => sync.data.session_status?.[props.sessionID ?? ""] ?? { type: "idle" })
const history = usePromptHistory()
const stash = usePromptStash()
- const command = useCommandPalette()
const keymap = useOpencodeKeymap()
const agentShortcut = useCommandShortcut("agent.cycle")
const paletteShortcut = useCommandShortcut("command.palette.show")
@@ -640,7 +644,7 @@ export function Prompt(props: PromptProps) {
}))
useBindings(() => ({
- enabled: command.matcher,
+ opencodeMode: OPENCODE_BASE_MODE,
bindings: tuiConfig.keybinds.gather("prompt.palette", [
"prompt.submit",
"prompt.editor",
diff --git a/packages/opencode/src/cli/cmd/tui/context/command-palette.tsx b/packages/opencode/src/cli/cmd/tui/context/command-palette.tsx
deleted file mode 100644
index 07cca99074..0000000000
--- a/packages/opencode/src/cli/cmd/tui/context/command-palette.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import { createContext, createMemo, createSignal, useContext, type Accessor, type ParentProps } from "solid-js"
-import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
-import { useDialog, type DialogContext } from "@tui/ui/dialog"
-import {
- formatKeyBindings,
- reactiveMatcherFromSignal,
- type OpenTuiKeymap,
- useKeymapSelector,
- useOpencodeKeymap,
-} from "../keymap"
-import { useTuiConfig } from "./tui-config"
-
-type SlashEntry = {
- display: string
- description?: string
- aliases?: string[]
- onSelect: () => void
-}
-
-type CommandPaletteContext = {
- run(command: string): void
- show(): void
- slashes: Accessor
- suspend(enabled: boolean): void
- readonly suspended: boolean
- matcher: ReturnType
-}
-
-const COMMAND_PALETTE_DIALOG = "command.palette.show"
-const ctx = createContext()
-type PaletteCommandEntry = ReturnType[number]
-
-function isVisiblePaletteCommand(entry: PaletteCommandEntry) {
- return entry.command.hidden !== true && entry.command.name !== COMMAND_PALETTE_DIALOG
-}
-
-function isSuggestedPaletteCommand(entry: PaletteCommandEntry) {
- const suggested = entry.command.suggested
- if (typeof suggested === "boolean") return suggested
- if (typeof suggested === "function") return suggested() === true
- return false
-}
-
-export function CommandPaletteProvider(props: ParentProps) {
- const dialog = useDialog()
- const keymap = useOpencodeKeymap()
- const [suspendCount, setSuspendCount] = createSignal(0)
- const entries = useKeymapSelector((keymap: OpenTuiKeymap) =>
- keymap
- .getCommandEntries({
- visibility: "reachable",
- namespace: "palette",
- })
- .filter(isVisiblePaletteCommand),
- )
-
- const run = (command: string) => {
- keymap.dispatchCommand(command)
- }
-
- const slashes = createMemo(() =>
- entries().flatMap((entry) => {
- const slashName = entry.command.slashName
- if (typeof slashName !== "string" || !slashName) return []
- const slashAliases = entry.command.slashAliases
- return {
- display: `/${slashName}`,
- description:
- typeof entry.command.desc === "string"
- ? entry.command.desc
- : typeof entry.command.title === "string"
- ? entry.command.title
- : undefined,
- aliases: Array.isArray(slashAliases)
- ? slashAliases.filter((alias): alias is string => typeof alias === "string").map((alias) => `/${alias}`)
- : undefined,
- onSelect: () => run(entry.command.name),
- }
- }),
- )
-
- const value: CommandPaletteContext = {
- run,
- show() {
- dialog.replace(() => )
- },
- slashes,
- suspend(enabled: boolean) {
- setSuspendCount((count) => Math.max(0, count + (enabled ? 1 : -1)))
- },
- get suspended() {
- return suspendCount() > 0 || dialog.stack.length > 0
- },
- matcher: reactiveMatcherFromSignal(() => suspendCount() === 0 && dialog.stack.length === 0),
- }
-
- return {props.children}
-}
-
-export function useCommandPalette() {
- const value = useContext(ctx)
- if (!value) throw new Error("CommandPalette context must be used within a CommandPaletteProvider")
- return value
-}
-
-function CommandPaletteDialog(props: { run(command: string): void }) {
- const config = useTuiConfig()
- const entries = useKeymapSelector((keymap: OpenTuiKeymap) => {
- const query = {
- namespace: "palette",
- }
- const reachable = keymap
- .getCommandEntries({
- ...query,
- visibility: "reachable",
- })
- .filter(isVisiblePaletteCommand)
- const registeredBindings = keymap.getCommandBindings({
- visibility: "registered",
- commands: reachable.map((entry) => entry.command.name),
- })
-
- return reachable.map((entry) => ({
- ...entry,
- bindings: registeredBindings.get(entry.command.name) ?? entry.bindings,
- }))
- })
- const options = createMemo(() =>
- entries().map((entry) => ({
- title: typeof entry.command.title === "string" ? entry.command.title : entry.command.name,
- description: typeof entry.command.desc === "string" ? entry.command.desc : undefined,
- category: typeof entry.command.category === "string" ? entry.command.category : undefined,
- footer: formatKeyBindings(entry.bindings, config),
- value: entry.command.name,
- suggested: isSuggestedPaletteCommand(entry),
- onSelect: (dialog: DialogContext) => {
- dialog.clear()
- props.run(entry.command.name)
- },
- })),
- )
-
- let ref: DialogSelectRef
- const list = () => {
- if (ref?.filter) return options()
- return [
- ...options()
- .filter((option) => option.suggested)
- .map((option) => ({
- ...option,
- value: `suggested:${option.value}`,
- category: "Suggested",
- })),
- ...options(),
- ]
- }
-
- return (ref = value)} title="Commands" options={list()} />
-}
-
-export function useCommandSlashes(): Accessor {
- return useCommandPalette().slashes
-}
diff --git a/packages/opencode/src/cli/cmd/tui/keymap.tsx b/packages/opencode/src/cli/cmd/tui/keymap.tsx
index 289bb901d6..f68a88711f 100644
--- a/packages/opencode/src/cli/cmd/tui/keymap.tsx
+++ b/packages/opencode/src/cli/cmd/tui/keymap.tsx
@@ -7,24 +7,92 @@ import {
} from "@opentui/keymap/extras"
import {
KeymapProvider,
- reactiveMatcherFromSignal,
useKeymap,
useKeymapSelector,
useBindings,
} from "@opentui/keymap/solid"
-import type { Accessor } from "solid-js"
+import { createMemo, type Accessor } from "solid-js"
import type { TuiConfig } from "./config/tui"
import { useTuiConfig } from "./context/tui-config"
import { TuiKeybind } from "./config/keybind"
export const LEADER_TOKEN = "leader"
+export const OPENCODE_BASE_MODE = "base"
+export const COMMAND_PALETTE_COMMAND = "command.palette.show"
+
+const OPENCODE_MODE_KEY = "opencode.mode"
export const OpencodeKeymapProvider = KeymapProvider
export const useOpencodeKeymap = useKeymap
-export { reactiveMatcherFromSignal, useBindings, useKeymapSelector }
+export { useBindings, useKeymapSelector }
export type OpenTuiKeymap = ReturnType
+type OpencodeModeStack = ReturnType
+type CommandSlashEntry = {
+ display: string
+ description?: string
+ aliases?: string[]
+ onSelect: () => void
+}
+type CommandEntry = ReturnType[number]
+
+const modeStacks = new WeakMap()
+
+function isVisiblePaletteCommand(entry: CommandEntry) {
+ return entry.command.hidden !== true && entry.command.name !== COMMAND_PALETTE_COMMAND
+}
+
+export function createOpencodeModeStack(keymap: OpenTuiKeymap) {
+ keymap.setData(OPENCODE_MODE_KEY, OPENCODE_BASE_MODE)
+
+ const offFields = keymap.registerLayerFields({
+ opencodeMode(value, ctx) {
+ ctx.require(OPENCODE_MODE_KEY, value)
+ },
+ })
+
+ const stack: { id: symbol; mode: string }[] = []
+ let disposed = false
+
+ const update = () => {
+ keymap.setData(OPENCODE_MODE_KEY, stack.at(-1)?.mode ?? OPENCODE_BASE_MODE)
+ }
+
+ const stackApi = {
+ push(mode: string) {
+ if (disposed) return () => {}
+ const id = Symbol(mode)
+ let active = true
+ stack.push({ id, mode })
+ update()
+
+ return () => {
+ if (!active) return
+ active = false
+ const index = stack.findIndex((item) => item.id === id)
+ if (index !== -1) stack.splice(index, 1)
+ update()
+ }
+ },
+ dispose() {
+ if (disposed) return
+ disposed = true
+ stack.length = 0
+ offFields()
+ keymap.setData(OPENCODE_MODE_KEY, undefined)
+ },
+ }
+
+ modeStacks.set(keymap, stackApi)
+ return stackApi
+}
+
+export function useOpencodeModeStack() {
+ const value = modeStacks.get(useOpencodeKeymap())
+ if (!value) throw new Error("Opencode mode stack is not registered for this keymap")
+ return value
+}
const KEY_ALIASES = {
enter: "return",
@@ -164,3 +232,36 @@ export function useCommandShortcut(command: string): Accessor {
export function useLeaderActive(): Accessor {
return useKeymapSelector((keymap: OpenTuiKeymap) => keymap.getPendingSequence()[0]?.tokenName === LEADER_TOKEN)
}
+
+export function useCommandSlashes(): Accessor {
+ const keymap = useOpencodeKeymap()
+ const entries = useKeymapSelector((keymap: OpenTuiKeymap) =>
+ keymap
+ .getCommandEntries({
+ visibility: "reachable",
+ namespace: "palette",
+ })
+ .filter(isVisiblePaletteCommand),
+ )
+
+ return createMemo(() =>
+ entries().flatMap((entry) => {
+ const slashName = entry.command.slashName
+ if (typeof slashName !== "string" || !slashName) return []
+ const slashAliases = entry.command.slashAliases
+ return {
+ display: `/${slashName}`,
+ description:
+ typeof entry.command.desc === "string"
+ ? entry.command.desc
+ : typeof entry.command.title === "string"
+ ? entry.command.title
+ : undefined,
+ aliases: Array.isArray(slashAliases)
+ ? slashAliases.filter((alias): alias is string => typeof alias === "string").map((alias) => `/${alias}`)
+ : undefined,
+ onSelect: () => keymap.dispatchCommand(entry.command.name),
+ }
+ }),
+ )
+}
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 70b5570ad5..1dfc67cb95 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -87,8 +87,7 @@ import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
import { DialogRetryAction } from "../../component/dialog-retry-action"
import { SessionRetry } from "@/session/retry"
import { getRevertDiffFiles } from "../../util/revert-diff"
-import { useCommandPalette } from "../../context/command-palette"
-import { useBindings, useCommandShortcut } from "../../keymap"
+import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useOpencodeKeymap } from "../../keymap"
import { PathFormatterProvider, usePathFormatter } from "../../context/path-format"
addDefaultParsers(parsers.parsers)
@@ -306,7 +305,7 @@ export function Session() {
seeded = true
r.set(route.prompt)
}
- const command = useCommandPalette()
+ const keymap = useOpencodeKeymap()
const dialog = useDialog()
const renderer = useRenderer()
@@ -1047,7 +1046,7 @@ export function Session() {
}))
useBindings(() => ({
- enabled: command.matcher,
+ opencodeMode: OPENCODE_BASE_MODE,
bindings: tuiConfig.keybinds.gather("session", sessionBindingCommands),
}))
@@ -1123,7 +1122,6 @@ export function Session() {
{(function () {
- const command = useCommandPalette()
const redoShortcut = useCommandShortcut("session.redo")
const [hover, setHover] = createSignal(false)
const dialog = useDialog()
@@ -1135,7 +1133,7 @@ export function Session() {
"Are you sure you want to restore the reverted messages?",
)
if (confirmed) {
- command.run("session.redo")
+ keymap.dispatchCommand("session.redo")
}
}
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 5b40c3c318..039adad482 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
@@ -16,7 +16,7 @@ import { webSearchProviderLabel } from "@/tool/websearch"
import { useDialog } from "../../ui/dialog"
import { getScrollAcceleration } from "../../util/scroll"
import { useTuiConfig } from "../../context/tui-config"
-import { useBindings, useCommandShortcut } from "../../keymap"
+import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keymap"
import { usePathFormatter } from "../../context/path-format"
type PermissionStage = "permission" | "always" | "reject"
@@ -448,9 +448,8 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
const tuiConfig = useTuiConfig()
const dimensions = useTerminalDimensions()
const narrow = createMemo(() => dimensions().width < 80)
- const dialog = useDialog()
useBindings(() => ({
- enabled: dialog.stack.length === 0,
+ opencodeMode: OPENCODE_BASE_MODE,
commands: [
{
name: "app.exit",
@@ -542,11 +541,10 @@ function Prompt>(props: {
expanded: false,
})
const narrow = createMemo(() => dimensions().width < 80)
- const dialog = useDialog()
const fullscreenHint = useCommandShortcut("permission.prompt.fullscreen")
useBindings(() => ({
- enabled: dialog.stack.length === 0,
+ opencodeMode: OPENCODE_BASE_MODE,
commands: [
{
name: "app.exit",
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
index 46fc220bdc..2fbd3baa40 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
@@ -6,9 +6,8 @@ import { selectedForeground, tint, useTheme } from "../../context/theme"
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
import { useSDK } from "../../context/sdk"
import { SplitBorder } from "../../component/border"
-import { useDialog } from "../../ui/dialog"
import { useTuiConfig } from "../../context/tui-config"
-import { useBindings } from "../../keymap"
+import { OPENCODE_BASE_MODE, useBindings } from "../../keymap"
export function QuestionPrompt(props: { request: QuestionRequest }) {
const sdk = useSDK()
@@ -120,9 +119,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
pick(opt.label)
}
- const dialog = useDialog()
-
useBindings(() => ({
+ opencodeMode: OPENCODE_BASE_MODE,
enabled: store.editing && !confirm(),
commands: [
{
@@ -203,7 +201,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
const max = Math.min(total, 9)
return {
- enabled: dialog.stack.length === 0 && !store.editing,
+ opencodeMode: OPENCODE_BASE_MODE,
+ enabled: !store.editing,
commands: [
{
name: "app.exit",
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx
index f4a458b63d..e1be4286f1 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx
@@ -6,8 +6,7 @@ import { SplitBorder } from "@tui/component/border"
import type { AssistantMessage } from "@opencode-ai/sdk/v2"
import { Locale } from "@/util/locale"
import { useTerminalDimensions } from "@opentui/solid"
-import { useCommandPalette } from "../../context/command-palette"
-import { useCommandShortcut } from "../../keymap"
+import { useCommandShortcut, useOpencodeKeymap } from "../../keymap"
export function SubagentFooter() {
const route = useRouteData("session")
@@ -56,7 +55,7 @@ export function SubagentFooter() {
})
const { theme } = useTheme()
- const command = useCommandPalette()
+ const keymap = useOpencodeKeymap()
const parentShortcut = useCommandShortcut("session.parent")
const previousShortcut = useCommandShortcut("session.child.previous")
const nextShortcut = useCommandShortcut("session.child.next")
@@ -98,7 +97,7 @@ export function SubagentFooter() {
setHover("parent")}
onMouseOut={() => setHover(null)}
- onMouseUp={() => command.run("session.parent")}
+ onMouseUp={() => keymap.dispatchCommand("session.parent")}
backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel}
>
@@ -108,7 +107,7 @@ export function SubagentFooter() {
setHover("prev")}
onMouseOut={() => setHover(null)}
- onMouseUp={() => command.run("session.child.previous")}
+ onMouseUp={() => keymap.dispatchCommand("session.child.previous")}
backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel}
>
@@ -118,7 +117,7 @@ export function SubagentFooter() {
setHover("next")}
onMouseOut={() => setHover(null)}
- onMouseUp={() => command.run("session.child.next")}
+ onMouseUp={() => keymap.dispatchCommand("session.child.next")}
backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel}
>
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx
index 3c74ded5fd..73d61c3ba6 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx
@@ -1,12 +1,12 @@
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
-import { batch, createContext, Show, useContext, type JSX, type ParentProps } from "solid-js"
+import { batch, createContext, createEffect, onCleanup, Show, useContext, type JSX, type ParentProps } from "solid-js"
import { useTheme } from "@tui/context/theme"
import { MouseButton, Renderable, RGBA } from "@opentui/core"
import { createStore } from "solid-js/store"
import { useToast } from "./toast"
import { Flag } from "@opencode-ai/core/flag/flag"
import * as Selection from "@tui/util/selection"
-import { useBindings } from "../keymap"
+import { useBindings, useOpencodeModeStack } from "../keymap"
export function Dialog(
props: ParentProps<{
@@ -73,6 +73,13 @@ function init() {
})
const renderer = useRenderer()
+ const modeStack = useOpencodeModeStack()
+
+ createEffect(() => {
+ if (store.stack.length === 0) return
+ const popMode = modeStack.push("modal")
+ onCleanup(popMode)
+ })
let focus: Renderable | null
function refocus() {