summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorWill Marella <[email protected]>2025-12-22 12:06:00 -0500
committerGitHub <[email protected]>2025-12-22 11:06:00 -0600
commitaf214d35cb24fe20d4f967f07b639c7de39c830c (patch)
treeff91ef0effd671e435543ed346a819c7b875edeb
parent3f0afd7cf6438c37213d16c178d082a0146bb693 (diff)
downloadopencode-af214d35cb24fe20d4f967f07b639c7de39c830c.tar.gz
opencode-af214d35cb24fe20d4f967f07b639c7de39c830c.zip
Add keybindable commands to navigate between user messages (#5078)
Co-authored-by: Will@Cambridge <[email protected]> Co-authored-by: Will@Cambridge <[email protected]>
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/index.tsx62
-rw-r--r--packages/opencode/src/config/config.ts2
-rw-r--r--packages/sdk/js/src/gen/types.gen.ts8
-rw-r--r--packages/web/src/content/docs/keybinds.mdx2
4 files changed, 74 insertions, 0 deletions
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 4ad4dc0ca..3b1c58966 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -201,6 +201,52 @@ export function Session() {
let prompt: PromptRef
const keybind = useKeybind()
+ // Helper: Find next visible message boundary in direction
+ const findNextVisibleMessage = (direction: "next" | "prev"): string | null => {
+ const children = scroll.getChildren()
+ const messagesList = messages()
+ const scrollTop = scroll.y
+
+ // Get visible messages sorted by position, filtering for valid non-synthetic, non-ignored content
+ const visibleMessages = children
+ .filter((c) => {
+ if (!c.id) return false
+ const message = messagesList.find((m) => m.id === c.id)
+ if (!message) return false
+
+ // Check if message has valid non-synthetic, non-ignored text parts
+ const parts = sync.data.part[message.id]
+ if (!parts || !Array.isArray(parts)) return false
+
+ return parts.some((part) => part && part.type === "text" && !part.synthetic && !part.ignored)
+ })
+ .sort((a, b) => a.y - b.y)
+
+ if (visibleMessages.length === 0) return null
+
+ if (direction === "next") {
+ // Find first message below current position
+ return visibleMessages.find((c) => c.y > scrollTop + 10)?.id ?? null
+ }
+ // Find last message above current position
+ return [...visibleMessages].reverse().find((c) => c.y < scrollTop - 10)?.id ?? null
+ }
+
+ // Helper: Scroll to message in direction or fallback to page scroll
+ const scrollToMessage = (direction: "next" | "prev", dialog: ReturnType<typeof useDialog>) => {
+ const targetID = findNextVisibleMessage(direction)
+
+ if (!targetID) {
+ scroll.scrollBy(direction === "next" ? scroll.height : -scroll.height)
+ dialog.clear()
+ return
+ }
+
+ const child = scroll.getChildren().find((c) => c.id === targetID)
+ if (child) scroll.scrollBy(child.y - scroll.y - 1)
+ dialog.clear()
+ }
+
useKeyboard((evt) => {
if (dialog.stack.length > 0) return
@@ -635,6 +681,22 @@ export function Session() {
},
},
{
+ title: "Next message",
+ value: "session.message.next",
+ keybind: "messages_next",
+ category: "Session",
+ disabled: true,
+ onSelect: (dialog) => scrollToMessage("next", dialog),
+ },
+ {
+ title: "Previous message",
+ value: "session.message.previous",
+ keybind: "messages_previous",
+ category: "Session",
+ disabled: true,
+ onSelect: (dialog) => scrollToMessage("prev", dialog),
+ },
+ {
title: "Copy last assistant message",
value: "messages.copy",
keybind: "messages_copy",
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 031bdd31b..daf81f434 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -456,6 +456,8 @@ export namespace Config {
.describe("Scroll messages down by half page"),
messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
+ messages_next: z.string().optional().default("none").describe("Navigate to next message"),
+ messages_previous: z.string().optional().default("none").describe("Navigate to previous message"),
messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"),
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts
index 964112d81..06993d3f9 100644
--- a/packages/sdk/js/src/gen/types.gen.ts
+++ b/packages/sdk/js/src/gen/types.gen.ts
@@ -859,6 +859,14 @@ export type KeybindsConfig = {
*/
messages_last?: string
/**
+ * Navigate to next message
+ */
+ messages_next?: string
+ /**
+ * Navigate to previous message
+ */
+ messages_previous?: string
+ /**
* Navigate to last user message
*/
messages_last_user?: string
diff --git a/packages/web/src/content/docs/keybinds.mdx b/packages/web/src/content/docs/keybinds.mdx
index 35610af51..b7e370825 100644
--- a/packages/web/src/content/docs/keybinds.mdx
+++ b/packages/web/src/content/docs/keybinds.mdx
@@ -32,6 +32,8 @@ OpenCode has a list of keybinds that you can customize through the OpenCode conf
"messages_half_page_down": "ctrl+alt+d",
"messages_first": "ctrl+g,home",
"messages_last": "ctrl+alt+g,end",
+ "messages_next": "none",
+ "messages_previous": "none",
"messages_copy": "<leader>y",
"messages_undo": "<leader>u",
"messages_redo": "<leader>r",