mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix: handle macOS terminal command arrow scrolling (#5846)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -302,6 +302,21 @@ export function useTerminalKeyboardShortcuts({
|
||||
return
|
||||
}
|
||||
|
||||
if (action.type === 'scrollViewport') {
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
const pane = manager.getActivePane() ?? manager.getPanes()[0]
|
||||
if (!pane) {
|
||||
return
|
||||
}
|
||||
if (action.position === 'top') {
|
||||
pane.terminal.scrollToLine(0)
|
||||
} else {
|
||||
pane.terminal.scrollToBottom()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Cmd+[ / Cmd+] cycles active split pane focus.
|
||||
if (action.type === 'focusPane') {
|
||||
const panes = manager.getPanes()
|
||||
|
||||
@@ -130,6 +130,26 @@ describe('resolveTerminalShortcutAction', () => {
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('maps Cmd+↑/↓ on macOS to terminal scrollback top/bottom navigation', () => {
|
||||
expect(
|
||||
resolveTerminalShortcutAction(event({ key: 'ArrowUp', code: 'ArrowUp', metaKey: true }), true)
|
||||
).toEqual({ type: 'scrollViewport', position: 'top' })
|
||||
expect(
|
||||
resolveTerminalShortcutAction(
|
||||
event({ key: 'ArrowDown', code: 'ArrowDown', metaKey: true }),
|
||||
true
|
||||
)
|
||||
).toEqual({ type: 'scrollViewport', position: 'bottom' })
|
||||
|
||||
// Cmd+Shift+Arrow is selection territory; leave it to focused apps/shells.
|
||||
expect(
|
||||
resolveTerminalShortcutAction(
|
||||
event({ key: 'ArrowUp', code: 'ArrowUp', metaKey: true, shiftKey: true }),
|
||||
true
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('preserves existing non-Mac terminal pane shortcuts', () => {
|
||||
expect(
|
||||
resolveTerminalShortcutAction(event({ key: 'f', code: 'KeyF', ctrlKey: true }), false)
|
||||
|
||||
@@ -37,6 +37,7 @@ export type TerminalShortcutAction =
|
||||
| { type: 'toggleExpandActivePane' }
|
||||
| { type: 'closeActivePane' }
|
||||
| { type: 'splitActivePane'; direction: 'vertical' | 'horizontal' }
|
||||
| { type: 'scrollViewport'; position: 'top' | 'bottom' }
|
||||
| { type: 'sendInput'; data: string }
|
||||
|
||||
export function resolveTerminalShortcutAction(
|
||||
@@ -129,6 +130,14 @@ export function resolveTerminalShortcutAction(
|
||||
if (event.key === 'ArrowRight') {
|
||||
return { type: 'sendInput', data: '\x05' }
|
||||
}
|
||||
// Why: macOS terminal users expect Cmd+↑/↓ to jump through scrollback
|
||||
// without writing escape bytes into the shell.
|
||||
if (event.key === 'ArrowUp') {
|
||||
return { type: 'scrollViewport', position: 'top' }
|
||||
}
|
||||
if (event.key === 'ArrowDown') {
|
||||
return { type: 'scrollViewport', position: 'bottom' }
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -230,6 +230,31 @@ async function getActiveBackgroundTerminalTabId(page: Page): Promise<string | nu
|
||||
})
|
||||
}
|
||||
|
||||
async function getActiveTerminalViewport(
|
||||
page: Page
|
||||
): Promise<{ viewportY: number; baseY: number }> {
|
||||
return page.evaluate(() => {
|
||||
const state = window.__store?.getState()
|
||||
const worktreeId = state?.activeWorktreeId
|
||||
const tabId =
|
||||
state?.activeTabType === 'terminal'
|
||||
? state.activeTabId
|
||||
: worktreeId
|
||||
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
|
||||
: null
|
||||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||||
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
|
||||
const buffer = pane?.terminal.buffer.active
|
||||
if (!buffer) {
|
||||
throw new Error('No active terminal buffer')
|
||||
}
|
||||
return {
|
||||
viewportY: buffer.viewportY,
|
||||
baseY: buffer.baseY
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function enableKittyKeyboardReporting(page: Page, flags: number): Promise<void> {
|
||||
await page.evaluate(async (flags) => {
|
||||
const state = window.__store?.getState()
|
||||
@@ -782,6 +807,60 @@ test.describe('Terminal Shortcuts', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('Cmd+Up/Down scrolls terminal viewport without writing to the PTY on macOS', async ({
|
||||
orcaPage,
|
||||
electronApp
|
||||
}) => {
|
||||
test.skip(!isMac, 'Cmd+Up/Down terminal scroll navigation is macOS-only')
|
||||
|
||||
await installMainProcessPtyWriteSpy(electronApp)
|
||||
|
||||
const ptyId = await waitForActivePanePtyId(orcaPage)
|
||||
const marker = `CMD_ARROW_SCROLL_${Date.now()}`
|
||||
await execInTerminal(orcaPage, ptyId, `for i in {1..120}; do echo ${marker}_$i; done`)
|
||||
await waitForTerminalOutput(orcaPage, `${marker}_120`)
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const viewport = await getActiveTerminalViewport(orcaPage)
|
||||
return viewport.baseY > 0 && viewport.viewportY === viewport.baseY
|
||||
},
|
||||
{
|
||||
timeout: 5_000,
|
||||
message: 'terminal did not settle at the bottom with scrollback for Cmd+Up/Down repro'
|
||||
}
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
await clearPtyWriteLog(electronApp)
|
||||
await focusActiveTerminal(orcaPage)
|
||||
await orcaPage.keyboard.press('Meta+ArrowUp')
|
||||
await expect
|
||||
.poll(async () => getActiveTerminalViewport(orcaPage), {
|
||||
timeout: 5_000,
|
||||
message: 'Cmd+Up did not scroll the terminal viewport to the top'
|
||||
})
|
||||
.toMatchObject({ viewportY: 0 })
|
||||
expect(await getPtyWrites(electronApp)).toEqual([])
|
||||
|
||||
await focusActiveTerminal(orcaPage)
|
||||
await orcaPage.keyboard.press('Meta+ArrowDown')
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const viewport = await getActiveTerminalViewport(orcaPage)
|
||||
return viewport.viewportY === viewport.baseY
|
||||
},
|
||||
{
|
||||
timeout: 5_000,
|
||||
message: 'Cmd+Down did not scroll the terminal viewport to the bottom'
|
||||
}
|
||||
)
|
||||
.toBe(true)
|
||||
expect(await getPtyWrites(electronApp)).toEqual([])
|
||||
})
|
||||
|
||||
test('Shift with Russian layout text reaches the PTY as Cyrillic under kitty keyboard reporting', async ({
|
||||
orcaPage,
|
||||
electronApp
|
||||
|
||||
Reference in New Issue
Block a user