Skip to content

Commit 8a8c630

Browse files
committed
STASH
1 parent 22e64ca commit 8a8c630

9 files changed

Lines changed: 104 additions & 40 deletions

File tree

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,14 @@ import { createTuiApi } from "@/cli/cmd/tui/plugin/api"
6565
import type { RouteMap } from "@/cli/cmd/tui/plugin/api"
6666
import { FormatError, FormatUnknownError } from "@/cli/error"
6767
import { CommandPaletteProvider, useCommandPalette } from "./context/command-palette"
68-
import { OpencodeKeymapProvider, registerOpencodeKeymap, useBindings, useOpencodeKeymap } from "./keymap"
68+
import {
69+
OPENCODE_BASE_MODE,
70+
OpencodeKeymapProvider,
71+
createOpencodeModeStack,
72+
registerOpencodeKeymap,
73+
useBindings,
74+
useOpencodeKeymap,
75+
} from "./keymap"
6976

7077
import type { EventSource } from "./context/sdk"
7178
import { DialogVariant } from "./component/dialog-variant"
@@ -133,6 +140,7 @@ export function tui(input: {
133140

134141
const onBeforeExit = async () => {
135142
offKeymap()
143+
modeStack.dispose()
136144
await TuiPluginRuntime.dispose()
137145
}
138146

@@ -142,6 +150,7 @@ export function tui(input: {
142150
const mode = (await renderer.waitForThemeMode(1000)) ?? "dark"
143151

144152
const keymap = createDefaultOpenTuiKeymap(renderer)
153+
const modeStack = createOpencodeModeStack(keymap)
145154
const offKeymap = registerOpencodeKeymap(keymap, renderer, input.config)
146155

147156
await render(() => {
@@ -747,7 +756,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
747756
}))
748757

749758
useBindings(() => ({
750-
enabled: command.matcher,
759+
opencodeMode: OPENCODE_BASE_MODE,
751760
bindings: sections.global,
752761
}))
753762

packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useTerminalDimensions } from "@opentui/solid"
1717
import { Locale } from "@/util/locale"
1818
import type { PromptInfo } from "./history"
1919
import { useFrecency } from "./frecency"
20-
import { useBindings } from "../../keymap"
20+
import { useBindings, useOpencodeModeStack } from "../../keymap"
2121

2222
function removeLineRange(input: string) {
2323
const hashIndex = input.lastIndexOf("#")
@@ -83,6 +83,7 @@ export function Autocomplete(props: {
8383
const sdk = useSDK()
8484
const sync = useSync()
8585
const command = useCommandPalette()
86+
const modeStack = useOpencodeModeStack()
8687
const { theme } = useTheme()
8788
const dimensions = useTerminalDimensions()
8889
const frecency = useFrecency()
@@ -99,6 +100,12 @@ export function Autocomplete(props: {
99100

100101
const [positionTick, setPositionTick] = createSignal(0)
101102

103+
createEffect(() => {
104+
if (!store.visible) return
105+
const popMode = modeStack.push("autocomplete")
106+
onCleanup(popMode)
107+
})
108+
102109
createEffect(() => {
103110
if (store.visible) {
104111
let lastPos = { x: 0, y: 0, width: 0 }
@@ -284,7 +291,6 @@ export function Autocomplete(props: {
284291
const { filename, part } = createFilePart(item, lineRange)
285292
const index = store.visible === "@" ? store.index : props.input().cursorOffset
286293

287-
command.suspend(false)
288294
setStore("visible", false)
289295
setStore("index", index)
290296
insertPart(filename, part)
@@ -569,7 +575,6 @@ export function Autocomplete(props: {
569575
}))
570576

571577
function show(mode: "@" | "/") {
572-
command.suspend(true)
573578
setStore({
574579
visible: mode,
575580
index: props.input().cursorOffset,
@@ -586,7 +591,6 @@ export function Autocomplete(props: {
586591
draft.input = props.input().plainText
587592
})
588593
}
589-
command.suspend(false)
590594
setStore("visible", false)
591595
}
592596

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,13 @@ import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable"
5959
import { useArgs } from "@tui/context/args"
6060
import { Flag } from "@opencode-ai/core/flag/flag"
6161
import { type WorkspaceStatus } from "../workspace-label"
62-
import { useCommandPalette } from "../../context/command-palette"
63-
import { useBindings, useCommandShortcut, useLeaderActive, useOpencodeKeymap } from "../../keymap"
62+
import {
63+
OPENCODE_BASE_MODE,
64+
useBindings,
65+
useCommandShortcut,
66+
useLeaderActive,
67+
useOpencodeKeymap,
68+
} from "../../keymap"
6469
import { useTuiConfig } from "../../context/tui-config"
6570

6671
export type PromptProps = {
@@ -151,7 +156,6 @@ export function Prompt(props: PromptProps) {
151156
const status = createMemo(() => sync.data.session_status?.[props.sessionID ?? ""] ?? { type: "idle" })
152157
const history = usePromptHistory()
153158
const stash = usePromptStash()
154-
const command = useCommandPalette()
155159
const keymap = useOpencodeKeymap()
156160
const agentShortcut = useCommandShortcut("agent.cycle")
157161
const paletteShortcut = useCommandShortcut("command.palette.show")
@@ -632,7 +636,7 @@ export function Prompt(props: PromptProps) {
632636
}))
633637

634638
useBindings(() => ({
635-
enabled: command.matcher,
639+
opencodeMode: OPENCODE_BASE_MODE,
636640
bindings: keymapConfig.pick("prompt", [
637641
"prompt.submit",
638642
"prompt.editor",

packages/opencode/src/cli/cmd/tui/context/command-palette.tsx

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { createContext, createMemo, createSignal, useContext, type Accessor, type ParentProps } from "solid-js"
1+
import { createContext, createMemo, useContext, type Accessor, type ParentProps } from "solid-js"
22
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
33
import { useDialog, type DialogContext } from "@tui/ui/dialog"
44
import {
55
formatKeyBindings,
6-
reactiveMatcherFromSignal,
76
type OpenTuiKeymap,
87
useKeymapSelector,
98
useOpencodeKeymap,
@@ -21,9 +20,6 @@ type CommandPaletteContext = {
2120
run(command: string): void
2221
show(): void
2322
slashes: Accessor<readonly SlashEntry[]>
24-
suspend(enabled: boolean): void
25-
readonly suspended: boolean
26-
matcher: ReturnType<typeof reactiveMatcherFromSignal>
2723
}
2824

2925
const COMMAND_PALETTE_DIALOG = "command.palette.show"
@@ -44,7 +40,6 @@ function isSuggestedPaletteCommand(entry: PaletteCommandEntry) {
4440
export function CommandPaletteProvider(props: ParentProps) {
4541
const dialog = useDialog()
4642
const keymap = useOpencodeKeymap()
47-
const [suspendCount, setSuspendCount] = createSignal(0)
4843
const entries = useKeymapSelector((keymap: OpenTuiKeymap) =>
4944
keymap
5045
.getCommandEntries({
@@ -85,13 +80,6 @@ export function CommandPaletteProvider(props: ParentProps) {
8580
dialog.replace(() => <CommandPaletteDialog run={run} />)
8681
},
8782
slashes,
88-
suspend(enabled: boolean) {
89-
setSuspendCount((count) => Math.max(0, count + (enabled ? 1 : -1)))
90-
},
91-
get suspended() {
92-
return suspendCount() > 0 || dialog.stack.length > 0
93-
},
94-
matcher: reactiveMatcherFromSignal(() => suspendCount() === 0 && dialog.stack.length === 0),
9583
}
9684

9785
return <ctx.Provider value={value}>{props.children}</ctx.Provider>

packages/opencode/src/cli/cmd/tui/keymap.tsx

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
} from "@opentui/keymap/extras"
77
import {
88
KeymapProvider,
9-
reactiveMatcherFromSignal,
109
useBindings,
1110
useKeymap,
1211
useKeymapSelector,
@@ -16,13 +15,70 @@ import type { TuiConfig } from "./config/tui"
1615
import { useTuiConfig } from "./context/tui-config"
1716

1817
export const LEADER_TOKEN = "leader"
18+
export const OPENCODE_BASE_MODE = "base"
19+
20+
const OPENCODE_MODE_KEY = "opencode.mode"
1921

2022
export const OpencodeKeymapProvider = KeymapProvider
2123
export const useOpencodeKeymap = useKeymap
2224

23-
export { reactiveMatcherFromSignal, useBindings, useKeymapSelector }
25+
export { useBindings, useKeymapSelector }
2426

2527
export type OpenTuiKeymap = ReturnType<typeof useKeymap>
28+
type OpencodeModeStack = ReturnType<typeof createOpencodeModeStack>
29+
30+
const modeStacks = new WeakMap<OpenTuiKeymap, OpencodeModeStack>()
31+
32+
export function createOpencodeModeStack(keymap: OpenTuiKeymap) {
33+
keymap.setData(OPENCODE_MODE_KEY, OPENCODE_BASE_MODE)
34+
35+
const offFields = keymap.registerLayerFields({
36+
opencodeMode(value, ctx) {
37+
ctx.require(OPENCODE_MODE_KEY, value)
38+
},
39+
})
40+
41+
const stack: { id: symbol; mode: string }[] = []
42+
let disposed = false
43+
44+
const update = () => {
45+
keymap.setData(OPENCODE_MODE_KEY, stack.at(-1)?.mode ?? OPENCODE_BASE_MODE)
46+
}
47+
48+
const stackApi = {
49+
push(mode: string) {
50+
if (disposed) return () => {}
51+
const id = Symbol(mode)
52+
let active = true
53+
stack.push({ id, mode })
54+
update()
55+
56+
return () => {
57+
if (!active) return
58+
active = false
59+
const index = stack.findIndex((item) => item.id === id)
60+
if (index !== -1) stack.splice(index, 1)
61+
update()
62+
}
63+
},
64+
dispose() {
65+
if (disposed) return
66+
disposed = true
67+
stack.length = 0
68+
offFields()
69+
keymap.setData(OPENCODE_MODE_KEY, undefined)
70+
},
71+
}
72+
73+
modeStacks.set(keymap, stackApi)
74+
return stackApi
75+
}
76+
77+
export function useOpencodeModeStack() {
78+
const value = modeStacks.get(useOpencodeKeymap())
79+
if (!value) throw new Error("Opencode mode stack is not registered for this keymap")
80+
return value
81+
}
2682

2783
function formatOptions(config: TuiConfig.Resolved) {
2884
return {

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ import { DialogGoUpsell } from "../../component/dialog-go-upsell"
8989
import { SessionRetry } from "@/session/retry"
9090
import { getRevertDiffFiles } from "../../util/revert-diff"
9191
import { useCommandPalette } from "../../context/command-palette"
92-
import { useBindings, useCommandShortcut } from "../../keymap"
92+
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keymap"
9393

9494
addDefaultParsers(parsers.parsers)
9595

@@ -991,7 +991,7 @@ export function Session() {
991991
}))
992992

993993
useBindings(() => ({
994-
enabled: command.matcher,
994+
opencodeMode: OPENCODE_BASE_MODE,
995995
bindings: sections.session,
996996
}))
997997

packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
1313
import { Locale } from "@/util/locale"
1414
import { Global } from "@opencode-ai/core/global"
1515
import { ShellID } from "@/tool/shell/id"
16-
import { useDialog } from "../../ui/dialog"
1716
import { getScrollAcceleration } from "../../util/scroll"
1817
import { useTuiConfig } from "../../context/tui-config"
19-
import { useBindings, useCommandShortcut } from "../../keymap"
18+
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keymap"
2019

2120
type PermissionStage = "permission" | "always" | "reject"
2221

@@ -465,9 +464,8 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
465464
const keymapConfig = tuiConfig.keymap
466465
const dimensions = useTerminalDimensions()
467466
const narrow = createMemo(() => dimensions().width < 80)
468-
const dialog = useDialog()
469467
useBindings(() => ({
470-
enabled: dialog.stack.length === 0,
468+
opencodeMode: OPENCODE_BASE_MODE,
471469
commands: [
472470
{
473471
name: "permission.reject.cancel",
@@ -553,11 +551,10 @@ function Prompt<const T extends Record<string, string>>(props: {
553551
expanded: false,
554552
})
555553
const narrow = createMemo(() => dimensions().width < 80)
556-
const dialog = useDialog()
557554
const fullscreenHint = useCommandShortcut("permission.prompt.fullscreen")
558555

559556
useBindings(() => ({
560-
enabled: dialog.stack.length === 0,
557+
opencodeMode: OPENCODE_BASE_MODE,
561558
commands: [
562559
{
563560
name: "permission.prompt.escape",

packages/opencode/src/cli/cmd/tui/routes/session/question.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ import { selectedForeground, tint, useTheme } from "../../context/theme"
55
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
66
import { useSDK } from "../../context/sdk"
77
import { SplitBorder } from "../../component/border"
8-
import { useDialog } from "../../ui/dialog"
98
import { useTuiConfig } from "../../context/tui-config"
10-
import { useBindings } from "../../keymap"
9+
import { OPENCODE_BASE_MODE, useBindings } from "../../keymap"
1110

1211
export function QuestionPrompt(props: { request: QuestionRequest }) {
1312
const sdk = useSDK()
@@ -122,9 +121,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
122121
pick(opt.label)
123122
}
124123

125-
const dialog = useDialog()
126-
127124
useBindings(() => ({
125+
opencodeMode: OPENCODE_BASE_MODE,
128126
enabled: store.editing && !confirm(),
129127
commands: [
130128
{
@@ -199,7 +197,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
199197
const max = Math.min(total, 9)
200198

201199
return {
202-
enabled: dialog.stack.length === 0 && !store.editing,
200+
opencodeMode: OPENCODE_BASE_MODE,
201+
enabled: !store.editing,
203202
commands: [
204203
{
205204
name: "question.reject",

packages/opencode/src/cli/cmd/tui/ui/dialog.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
2-
import { batch, createContext, Show, useContext, type JSX, type ParentProps } from "solid-js"
2+
import { batch, createContext, createEffect, onCleanup, Show, useContext, type JSX, type ParentProps } from "solid-js"
33
import { useTheme } from "@tui/context/theme"
44
import { MouseButton, Renderable, RGBA } from "@opentui/core"
55
import { createStore } from "solid-js/store"
66
import { useToast } from "./toast"
77
import { Flag } from "@opencode-ai/core/flag/flag"
88
import * as Selection from "@tui/util/selection"
9-
import { useBindings } from "../keymap"
9+
import { useBindings, useOpencodeModeStack } from "../keymap"
1010

1111
export function Dialog(
1212
props: ParentProps<{
@@ -73,6 +73,13 @@ function init() {
7373
})
7474

7575
const renderer = useRenderer()
76+
const modeStack = useOpencodeModeStack()
77+
78+
createEffect(() => {
79+
if (store.stack.length === 0) return
80+
const popMode = modeStack.push("modal")
81+
onCleanup(popMode)
82+
})
7683

7784
let focus: Renderable | null
7885
function refocus() {

0 commit comments

Comments
 (0)