Ver código fonte

keep session navigation active in prompt modes (#29464)

Sebastian 2 meses atrás
pai
commit
94f2ed1b84

+ 9 - 2
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -79,8 +79,7 @@ import {
 import type { EventSource } from "./context/sdk"
 import { DialogVariant } from "./component/dialog-variant"
 
-const appBindingCommands = [
-  "command.palette.show",
+const appGlobalBindingCommands = [
   "session.list",
   "session.new",
   "session.quick_switch.1",
@@ -92,6 +91,10 @@ const appBindingCommands = [
   "session.quick_switch.7",
   "session.quick_switch.8",
   "session.quick_switch.9",
+] as const
+
+const appBindingCommands = [
+  "command.palette.show",
   "model.list",
   "model.cycle_recent",
   "model.cycle_recent_reverse",
@@ -929,6 +932,10 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
     bindings: tuiConfig.keybinds.gather("app", appBindingCommands),
   }))
 
+  useBindings(() => ({
+    bindings: tuiConfig.keybinds.gather("app.global", appGlobalBindingCommands),
+  }))
+
   useBindings(() => ({
     mode: OPENCODE_BASE_MODE,
     enabled: () => {

+ 20 - 6
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

@@ -134,12 +134,6 @@ const sessionBindingCommands = [
   "session.toggle.actions",
   "session.toggle.scrollbar",
   "session.toggle.generic_tool_output",
-  "session.page.up",
-  "session.page.down",
-  "session.line.up",
-  "session.line.down",
-  "session.half.page.up",
-  "session.half.page.down",
   "session.first",
   "session.last",
   "session.messages_last_user",
@@ -154,6 +148,17 @@ const sessionBindingCommands = [
   "session.child.previous",
 ] as const
 
+const sessionGlobalBindingCommands = [
+  "session.page.up",
+  "session.page.down",
+  "session.line.up",
+  "session.line.down",
+  "session.half.page.up",
+  "session.half.page.down",
+] as const
+
+const sessionGlobalUnfocusedBindingCommands = ["session.first", "session.last"] as const
+
 const context = createContext<{
   width: number
   sessionID: string
@@ -1067,6 +1072,15 @@ export function Session() {
     commands: sessionCommands(),
   }))
 
+  useBindings(() => ({
+    bindings: tuiConfig.keybinds.gather("session.global", sessionGlobalBindingCommands),
+  }))
+
+  useBindings(() => ({
+    enabled: () => renderer.currentFocusedEditor === null,
+    bindings: tuiConfig.keybinds.gather("session.global.unfocused", sessionGlobalUnfocusedBindingCommands),
+  }))
+
   useBindings(() => ({
     mode: OPENCODE_BASE_MODE,
     bindings: tuiConfig.keybinds.gather("session", sessionBindingCommands),

+ 83 - 1
packages/opencode/test/cli/tui/keymap.test.tsx

@@ -4,7 +4,12 @@ import { testRender, useRenderer } from "@opentui/solid"
 import { expect, test } from "bun:test"
 import { onCleanup } from "solid-js"
 import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
-import { OpencodeKeymapProvider, registerOpencodeKeymap } from "@/cli/cmd/tui/keymap"
+import {
+  getOpencodeModeStack,
+  OPENCODE_BASE_MODE,
+  OpencodeKeymapProvider,
+  registerOpencodeKeymap,
+} from "@/cli/cmd/tui/keymap"
 
 test("legacy page key aliases compile as page keys", async () => {
   const sequences: Record<string, string[][]> = {}
@@ -52,3 +57,80 @@ test("legacy page key aliases compile as page keys", async () => {
     app.renderer.destroy()
   }
 })
+
+test("mode-less bindings stay active when opencode mode changes", async () => {
+  const counts: Record<string, Record<string, number>> = {}
+
+  function Harness() {
+    const renderer = useRenderer()
+    const keymap = createDefaultOpenTuiKeymap(renderer)
+    const config = createTuiResolvedConfig()
+    const offKeymap = registerOpencodeKeymap(keymap, renderer, config)
+    const offGlobal = keymap.registerLayer({
+      commands: [
+        { name: "session.list", run() {} },
+        { name: "session.new", run() {} },
+        { name: "session.page.up", run() {} },
+        { name: "session.first", run() {} },
+      ],
+      bindings: config.keybinds.gather("test.global", [
+        "session.list",
+        "session.new",
+        "session.page.up",
+        "session.first",
+      ]),
+    })
+    const offBase = keymap.registerLayer({
+      mode: OPENCODE_BASE_MODE,
+      commands: [{ name: "model.list", run() {} }],
+      bindings: config.keybinds.gather("test.base", ["model.list"]),
+    })
+    const activeCounts = () =>
+      Object.fromEntries(
+        Array.from(
+          keymap.getCommandBindings({
+            visibility: "active",
+            commands: ["session.list", "session.new", "session.page.up", "session.first", "model.list"],
+          }),
+          ([command, bindings]) => [command, bindings.length],
+        ),
+      )
+
+    counts.base = activeCounts()
+    const popQuestion = getOpencodeModeStack(keymap).push("question")
+    counts.question = activeCounts()
+    popQuestion()
+    const popAutocomplete = getOpencodeModeStack(keymap).push("autocomplete")
+    counts.autocomplete = activeCounts()
+    popAutocomplete()
+
+    onCleanup(() => {
+      offBase()
+      offGlobal()
+      offKeymap()
+    })
+
+    return (
+      <OpencodeKeymapProvider keymap={keymap}>
+        <box />
+      </OpencodeKeymapProvider>
+    )
+  }
+
+  const app = await testRender(() => <Harness />)
+  try {
+    expect(counts).toEqual({
+      base: { "session.list": 1, "session.new": 1, "session.page.up": 2, "session.first": 2, "model.list": 1 },
+      question: { "session.list": 1, "session.new": 1, "session.page.up": 2, "session.first": 2, "model.list": 0 },
+      autocomplete: {
+        "session.list": 1,
+        "session.new": 1,
+        "session.page.up": 2,
+        "session.first": 2,
+        "model.list": 0,
+      },
+    })
+  } finally {
+    app.renderer.destroy()
+  }
+})