Skip to content

Commit af214d3

Browse files
will-marellaWill@CambridgeWill@Cambridge
authored
Add keybindable commands to navigate between user messages (#5078)
Co-authored-by: Will@Cambridge <[email protected]> Co-authored-by: Will@Cambridge <[email protected]>
1 parent 3f0afd7 commit af214d3

File tree

4 files changed

+74
-0
lines changed

4 files changed

+74
-0
lines changed

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,52 @@ export function Session() {
201201
let prompt: PromptRef
202202
const keybind = useKeybind()
203203

204+
// Helper: Find next visible message boundary in direction
205+
const findNextVisibleMessage = (direction: "next" | "prev"): string | null => {
206+
const children = scroll.getChildren()
207+
const messagesList = messages()
208+
const scrollTop = scroll.y
209+
210+
// Get visible messages sorted by position, filtering for valid non-synthetic, non-ignored content
211+
const visibleMessages = children
212+
.filter((c) => {
213+
if (!c.id) return false
214+
const message = messagesList.find((m) => m.id === c.id)
215+
if (!message) return false
216+
217+
// Check if message has valid non-synthetic, non-ignored text parts
218+
const parts = sync.data.part[message.id]
219+
if (!parts || !Array.isArray(parts)) return false
220+
221+
return parts.some((part) => part && part.type === "text" && !part.synthetic && !part.ignored)
222+
})
223+
.sort((a, b) => a.y - b.y)
224+
225+
if (visibleMessages.length === 0) return null
226+
227+
if (direction === "next") {
228+
// Find first message below current position
229+
return visibleMessages.find((c) => c.y > scrollTop + 10)?.id ?? null
230+
}
231+
// Find last message above current position
232+
return [...visibleMessages].reverse().find((c) => c.y < scrollTop - 10)?.id ?? null
233+
}
234+
235+
// Helper: Scroll to message in direction or fallback to page scroll
236+
const scrollToMessage = (direction: "next" | "prev", dialog: ReturnType<typeof useDialog>) => {
237+
const targetID = findNextVisibleMessage(direction)
238+
239+
if (!targetID) {
240+
scroll.scrollBy(direction === "next" ? scroll.height : -scroll.height)
241+
dialog.clear()
242+
return
243+
}
244+
245+
const child = scroll.getChildren().find((c) => c.id === targetID)
246+
if (child) scroll.scrollBy(child.y - scroll.y - 1)
247+
dialog.clear()
248+
}
249+
204250
useKeyboard((evt) => {
205251
if (dialog.stack.length > 0) return
206252

@@ -634,6 +680,22 @@ export function Session() {
634680
}
635681
},
636682
},
683+
{
684+
title: "Next message",
685+
value: "session.message.next",
686+
keybind: "messages_next",
687+
category: "Session",
688+
disabled: true,
689+
onSelect: (dialog) => scrollToMessage("next", dialog),
690+
},
691+
{
692+
title: "Previous message",
693+
value: "session.message.previous",
694+
keybind: "messages_previous",
695+
category: "Session",
696+
disabled: true,
697+
onSelect: (dialog) => scrollToMessage("prev", dialog),
698+
},
637699
{
638700
title: "Copy last assistant message",
639701
value: "messages.copy",

packages/opencode/src/config/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,8 @@ export namespace Config {
456456
.describe("Scroll messages down by half page"),
457457
messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
458458
messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
459+
messages_next: z.string().optional().default("none").describe("Navigate to next message"),
460+
messages_previous: z.string().optional().default("none").describe("Navigate to previous message"),
459461
messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"),
460462
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
461463
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),

packages/sdk/js/src/gen/types.gen.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,14 @@ export type KeybindsConfig = {
858858
* Navigate to last message
859859
*/
860860
messages_last?: string
861+
/**
862+
* Navigate to next message
863+
*/
864+
messages_next?: string
865+
/**
866+
* Navigate to previous message
867+
*/
868+
messages_previous?: string
861869
/**
862870
* Navigate to last user message
863871
*/

packages/web/src/content/docs/keybinds.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ OpenCode has a list of keybinds that you can customize through the OpenCode conf
3232
"messages_half_page_down": "ctrl+alt+d",
3333
"messages_first": "ctrl+g,home",
3434
"messages_last": "ctrl+alt+g,end",
35+
"messages_next": "none",
36+
"messages_previous": "none",
3537
"messages_copy": "<leader>y",
3638
"messages_undo": "<leader>u",
3739
"messages_redo": "<leader>r",

0 commit comments

Comments
 (0)