diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index ad12e1e0de5..91c51d77b76 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -59,6 +59,10 @@ export const dict = { "command.message.previous.description": "Go to the previous user message", "command.message.next": "Next message", "command.message.next.description": "Go to the next user message", + "command.message.top": "Jump to top", + "command.message.top.description": "Jump to the top of the chat", + "command.message.bottom": "Jump to bottom", + "command.message.bottom.description": "Jump to the bottom of the chat", "command.model.choose": "Choose model", "command.model.choose.description": "Select a different model", "command.mcp.toggle": "Toggle MCPs", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 6d29170081a..af1bbdbcc98 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -900,9 +900,17 @@ export default function Page() { } const focusInput = () => inputRef?.focus() + const jumpToTop = () => { + const el = scroller + if (!el) return + autoScroll.pause() + el.scrollTo({ top: 0, behavior: "auto" }) + } useSessionCommands({ navigateMessageByOffset, + jumpToTop, + jumpToBottom: () => resumeScroll(), setActiveMessage, focusInput, review: reviewTab, diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 1a2e777f522..60b6ce4a61d 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -24,6 +24,8 @@ import { useSessionLayout } from "@/pages/session/session-layout" export type SessionCommandContext = { navigateMessageByOffset: (offset: number) => void + jumpToTop: () => void + jumpToBottom: () => void setActiveMessage: (message: UserMessage | undefined) => void focusInput: () => void review?: () => boolean @@ -114,6 +116,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => { } const navigateMessageByOffset = actions.navigateMessageByOffset + const jumpToTop = actions.jumpToTop + const jumpToBottom = actions.jumpToBottom const setActiveMessage = actions.setActiveMessage const focusInput = actions.focusInput @@ -345,6 +349,22 @@ export const useSessionCommands = (actions: SessionCommandContext) => { disabled: !params.id, onSelect: () => navigateMessageByOffset(1), }), + sessionCommand({ + id: "message.top", + title: language.t("command.message.top"), + description: language.t("command.message.top.description"), + keybind: "mod+shift+arrowup", + disabled: !params.id, + onSelect: jumpToTop, + }), + sessionCommand({ + id: "message.bottom", + title: language.t("command.message.bottom"), + description: language.t("command.message.bottom.description"), + keybind: "mod+shift+arrowdown", + disabled: !params.id, + onSelect: jumpToBottom, + }), modelCommand({ id: "model.choose", title: language.t("command.model.choose"), diff --git a/packages/desktop/src/i18n/en.ts b/packages/desktop/src/i18n/en.ts index f93fe58f77a..14035f22e15 100644 --- a/packages/desktop/src/i18n/en.ts +++ b/packages/desktop/src/i18n/en.ts @@ -17,6 +17,8 @@ export const dict = { "desktop.menu.view.forward": "Forward", "desktop.menu.view.previousSession": "Previous Session", "desktop.menu.view.nextSession": "Next Session", + "desktop.menu.view.jumpToTop": "Jump to Top", + "desktop.menu.view.jumpToBottom": "Jump to Bottom", "desktop.menu.help.documentation": "OpenCode Documentation", "desktop.menu.help.supportForum": "Support Forum", "desktop.menu.help.shareFeedback": "Share Feedback", diff --git a/packages/desktop/src/menu.ts b/packages/desktop/src/menu.ts index 9005dd702f7..5d4d5778b69 100644 --- a/packages/desktop/src/menu.ts +++ b/packages/desktop/src/menu.ts @@ -148,6 +148,16 @@ export async function createMenu(trigger: (id: string) => void) { text: t("desktop.menu.view.nextSession"), accelerator: "Option+ArrowDown", }), + await MenuItem.new({ + action: () => trigger("message.top"), + text: t("desktop.menu.view.jumpToTop"), + accelerator: "Shift+Cmd+ArrowUp", + }), + await MenuItem.new({ + action: () => trigger("message.bottom"), + text: t("desktop.menu.view.jumpToBottom"), + accelerator: "Shift+Cmd+ArrowDown", + }), await PredefinedMenuItem.new({ item: "Separator", }), diff --git a/packages/web/src/content/docs/keybinds.mdx b/packages/web/src/content/docs/keybinds.mdx index 74ef30577e1..6a47bec6b7b 100644 --- a/packages/web/src/content/docs/keybinds.mdx +++ b/packages/web/src/content/docs/keybinds.mdx @@ -155,6 +155,17 @@ The OpenCode desktop app prompt input supports common Readline/Emacs-style short --- +## Desktop chat navigation shortcuts + +These shortcuts are available in the desktop app command system. + +| Shortcut | Action | +| --------------------- | -------------------------- | +| `mod+shift+arrowup` | Jump to top of the chat | +| `mod+shift+arrowdown` | Jump to bottom of the chat | + +--- + ## Shift+Enter Some terminals don't send modifier keys with Enter by default. You may need to configure your terminal to send `Shift+Enter` as an escape sequence.