Apply PR #26246: use keymap state for layer visibility

This commit is contained in:
opencode-agent[bot]
2026-05-13 15:24:34 +00:00
11 changed files with 275 additions and 246 deletions

View File

@@ -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: {
<LocalProvider>
<PromptStashProvider>
<DialogProvider>
<CommandPaletteProvider>
<FrecencyProvider>
<PromptHistoryProvider>
<PromptRefProvider>
<EditorContextProvider>
<App onSnapshot={input.onSnapshot} />
</EditorContextProvider>
</PromptRefProvider>
</PromptHistoryProvider>
</FrecencyProvider>
</CommandPaletteProvider>
<FrecencyProvider>
<PromptHistoryProvider>
<PromptRefProvider>
<EditorContextProvider>
<App onSnapshot={input.onSnapshot} />
</EditorContextProvider>
</PromptRefProvider>
</PromptHistoryProvider>
</FrecencyProvider>
</DialogProvider>
</PromptStashProvider>
</LocalProvider>
@@ -269,7 +277,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
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<string[]> }) {
const appCommands = createMemo(() =>
[
{
name: "command.palette.show",
name: COMMAND_PALETTE_COMMAND,
title: "Show command palette",
category: "System",
hidden: true,
run: () => {
command.show()
dialog.replace(() => <CommandPaletteDialog />)
},
},
{
@@ -846,21 +853,18 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
}))
useBindings(() => ({
enabled: command.matcher,
opencodeMode: OPENCODE_BASE_MODE,
bindings: tuiConfig.keybinds.gather(
"app",
Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
? appBindingCommands
: appBindingCommands.filter(
(c) => !c.startsWith("session.cycle_recent") && !c.startsWith("session.quick_switch"),
),
: appBindingCommands.filter((c) => !c.startsWith("session.cycle_recent") && !c.startsWith("session.quick_switch")),
),
}))
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 === ""
@@ -869,7 +873,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
}))
event.on(TuiEvent.CommandExecute.type, (evt) => {
command.run(evt.properties.command)
keymap.dispatchCommand(evt.properties.command)
})
event.on(TuiEvent.ToastShow.type, (evt) => {

View File

@@ -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<OpenTuiKeymap["getCommandEntries"]>[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<string>
const list = () => {
if (ref?.filter) return options()
return [
...options()
.filter((option) => option.suggested)
.map((option) => ({
...option,
value: `suggested:${option.value}`,
category: "Suggested",
})),
...options(),
]
}
return <DialogSelect ref={(value) => (ref = value)} title="Commands" options={list()} />
}

View File

@@ -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)
}

View File

@@ -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")
@@ -630,7 +634,7 @@ export function Prompt(props: PromptProps) {
}))
useBindings(() => ({
enabled: command.matcher,
opencodeMode: OPENCODE_BASE_MODE,
bindings: tuiConfig.keybinds.gather("prompt.palette", [
"prompt.submit",
"prompt.editor",

View File

@@ -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<readonly SlashEntry[]>
suspend(enabled: boolean): void
readonly suspended: boolean
matcher: ReturnType<typeof reactiveMatcherFromSignal>
}
const COMMAND_PALETTE_DIALOG = "command.palette.show"
const ctx = createContext<CommandPaletteContext>()
type PaletteCommandEntry = ReturnType<OpenTuiKeymap["getCommandEntries"]>[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<SlashEntry[]>(() =>
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(() => <CommandPaletteDialog run={run} />)
},
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 <ctx.Provider value={value}>{props.children}</ctx.Provider>
}
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<string>
const list = () => {
if (ref?.filter) return options()
return [
...options()
.filter((option) => option.suggested)
.map((option) => ({
...option,
value: `suggested:${option.value}`,
category: "Suggested",
})),
...options(),
]
}
return <DialogSelect ref={(value) => (ref = value)} title="Commands" options={list()} />
}
export function useCommandSlashes(): Accessor<readonly SlashEntry[]> {
return useCommandPalette().slashes
}

View File

@@ -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<typeof useKeymap>
type OpencodeModeStack = ReturnType<typeof createOpencodeModeStack>
type CommandSlashEntry = {
display: string
description?: string
aliases?: string[]
onSelect: () => void
}
type CommandEntry = ReturnType<OpenTuiKeymap["getCommandEntries"]>[number]
const modeStacks = new WeakMap<OpenTuiKeymap, OpencodeModeStack>()
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<string> {
export function useLeaderActive(): Accessor<boolean> {
return useKeymapSelector((keymap: OpenTuiKeymap) => keymap.getPendingSequence()[0]?.tokenName === LEADER_TOKEN)
}
export function useCommandSlashes(): Accessor<readonly CommandSlashEntry[]> {
const keymap = useOpencodeKeymap()
const entries = useKeymapSelector((keymap: OpenTuiKeymap) =>
keymap
.getCommandEntries({
visibility: "reachable",
namespace: "palette",
})
.filter(isVisiblePaletteCommand),
)
return createMemo<CommandSlashEntry[]>(() =>
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),
}
}),
)
}

View File

@@ -87,9 +87,8 @@ 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 { PathFormatterProvider, usePathFormatter } from "../../context/path-format"
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useOpencodeKeymap } from "../../keymap"
addDefaultParsers(parsers.parsers)
@@ -100,23 +99,6 @@ const GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW = "go_upsell_account_rate_limit_don
const GO_UPSELL_WINDOW = 86_400_000 // 24 hrs
const GO_UPSELL_PROVIDERS = new Set(["opencode", "opencode-go"])
function goUpsellKeys(action: SessionRetry.Retryable["action"]) {
if (!action) return
if (!GO_UPSELL_PROVIDERS.has(action.provider)) return
if (action.reason === "free_tier_limit") {
return {
lastSeenAt: GO_UPSELL_FREE_TIER_LAST_SEEN_AT,
dontShow: GO_UPSELL_FREE_TIER_DONT_SHOW,
}
}
if (action.reason === "account_rate_limit") {
return {
lastSeenAt: GO_UPSELL_ACCOUNT_RATE_LIMIT_LAST_SEEN_AT,
dontShow: GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW,
}
}
}
const sessionBindingCommands = [
"session.share",
"session.rename",
@@ -153,6 +135,23 @@ const sessionBindingCommands = [
"session.child.previous",
] as const
function goUpsellKeys(action: SessionRetry.Retryable["action"]) {
if (!action) return
if (!GO_UPSELL_PROVIDERS.has(action.provider)) return
if (action.reason === "free_tier_limit") {
return {
lastSeenAt: GO_UPSELL_FREE_TIER_LAST_SEEN_AT,
dontShow: GO_UPSELL_FREE_TIER_DONT_SHOW,
}
}
if (action.reason === "account_rate_limit") {
return {
lastSeenAt: GO_UPSELL_ACCOUNT_RATE_LIMIT_LAST_SEEN_AT,
dontShow: GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW,
}
}
}
const context = createContext<{
width: number
sessionID: string
@@ -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() {
<Switch>
<Match when={message.id === revert()?.messageID}>
{(function () {
const command = useCommandPalette()
const redoShortcut = useCommandShortcut("session.redo")
const [hover, setHover] = createSignal(false)
const dialog = useDialog()
@@ -1132,12 +1130,12 @@ export function Session() {
const confirmed = await DialogConfirm.show(
dialog,
"Confirm Redo",
"Are you sure you want to restore the reverted messages?",
)
if (confirmed) {
command.run("session.redo")
}
"Are you sure you want to restore the reverted messages?",
)
if (confirmed) {
keymap.dispatchCommand("session.redo")
}
}
return (
<box

View File

@@ -13,11 +13,10 @@ import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import { Locale } from "@/util/locale"
import { ShellID } from "@/tool/shell/id"
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 { usePathFormatter } from "../../context/path-format"
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keymap"
type PermissionStage = "permission" | "always" | "reject"
@@ -448,9 +447,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 +540,10 @@ function Prompt<const T extends Record<string, string>>(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",

View File

@@ -5,9 +5,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()
@@ -118,9 +117,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
pick(opt.label)
}
const dialog = useDialog()
useBindings(() => ({
opencodeMode: OPENCODE_BASE_MODE,
enabled: store.editing && !confirm(),
commands: [
{
@@ -201,7 +199,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",

View File

@@ -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() {
<box
onMouseOver={() => setHover("parent")}
onMouseOut={() => setHover(null)}
onMouseUp={() => command.run("session.parent")}
onMouseUp={() => keymap.dispatchCommand("session.parent")}
backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel}
>
<text fg={theme.text}>
@@ -108,7 +107,7 @@ export function SubagentFooter() {
<box
onMouseOver={() => setHover("prev")}
onMouseOut={() => setHover(null)}
onMouseUp={() => command.run("session.child.previous")}
onMouseUp={() => keymap.dispatchCommand("session.child.previous")}
backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel}
>
<text fg={theme.text}>
@@ -118,7 +117,7 @@ export function SubagentFooter() {
<box
onMouseOver={() => setHover("next")}
onMouseOut={() => setHover(null)}
onMouseUp={() => command.run("session.child.next")}
onMouseUp={() => keymap.dispatchCommand("session.child.next")}
backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel}
>
<text fg={theme.text}>

View File

@@ -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() {