diff --git a/backend/internal/adapters/runtime/zellij/commands.go b/backend/internal/adapters/runtime/zellij/commands.go index e64d1921..5d201e04 100644 --- a/backend/internal/adapters/runtime/zellij/commands.go +++ b/backend/internal/adapters/runtime/zellij/commands.go @@ -86,7 +86,15 @@ func attachArgs(id string) []string { func embeddedClientOptions() []string { return []string{ "--pane-frames", "false", - "--mouse-mode", "false", + // Mouse mode MUST be on. The terminal runs `zellij attach` in the + // alternate screen buffer, and xterm.js is configured with + // scrollback: 0 (XtermTerminal.tsx), so there is no local scrollback + // to fall back on. With mouse mode off, zellij never enables mouse + // tracking — wheel events reach neither xterm (no buffer) nor zellij + // (no tracking) and scroll is completely dead. Enabling mouse mode + // makes zellij intercept wheel events and scroll its own scrollback. + // Text selection still works via shift-drag (xterm.js built-in bypass). + "--mouse-mode", "true", "--advanced-mouse-actions", "false", "--mouse-hover-effects", "false", "--focus-follows-mouse", "false", diff --git a/backend/internal/adapters/runtime/zellij/zellij_test.go b/backend/internal/adapters/runtime/zellij/zellij_test.go index 610a62ec..657b575f 100644 --- a/backend/internal/adapters/runtime/zellij/zellij_test.go +++ b/backend/internal/adapters/runtime/zellij/zellij_test.go @@ -89,7 +89,7 @@ func containsKey(values []string, key string) bool { func TestCommandBuilders(t *testing.T) { embeddedOptions := []string{ "--pane-frames", "false", - "--mouse-mode", "false", + "--mouse-mode", "true", "--advanced-mouse-actions", "false", "--mouse-hover-effects", "false", "--focus-follows-mouse", "false", @@ -441,7 +441,7 @@ func TestAttachCommandUsesEmbeddedClientOptions(t *testing.T) { } embeddedOptions := []string{ "--pane-frames", "false", - "--mouse-mode", "false", + "--mouse-mode", "true", "--advanced-mouse-actions", "false", "--mouse-hover-effects", "false", "--focus-follows-mouse", "false", diff --git a/backend/internal/terminal/attachment_integration_test.go b/backend/internal/terminal/attachment_integration_test.go index c2eb899d..5b20b81a 100644 --- a/backend/internal/terminal/attachment_integration_test.go +++ b/backend/internal/terminal/attachment_integration_test.go @@ -53,8 +53,9 @@ func TestAttachmentStreamsRealZellijPane(t *testing.T) { eventually(t, 5*time.Second, func() bool { return strings.Contains(got.string(), "AO_MARKER_42") }) // A fresh attach must carry zellij's alt-screen init handshake. Mouse - // reporting is deliberately disabled for AO's embedded client, so this test - // should not require SGR mouse mode. + // mode is enabled (--mouse-mode true) so wheel events reach zellij and + // scroll its internal scrollback; the attach enables SGR mouse tracking + // as part of that handshake. eventually(t, 5*time.Second, func() bool { out := got.string() return strings.Contains(out, "\x1b[?1049h") diff --git a/frontend/src/renderer/components/XtermTerminal.tsx b/frontend/src/renderer/components/XtermTerminal.tsx index 3851bc5f..c0e5f007 100644 --- a/frontend/src/renderer/components/XtermTerminal.tsx +++ b/frontend/src/renderer/components/XtermTerminal.tsx @@ -130,9 +130,12 @@ export function XtermTerminal(props: XtermTerminalProps) { // The mux PTY runs `zellij attach` (backend AttachCommand), a // full-screen alt-buffer app that owns scrollback itself — same as // yyork. xterm's own buffer never accumulates history (the alt screen - // doesn't feed scrollback), and wheel events reach zellij as mouse - // reports instead of scrolling locally. 0 also stops FitAddon - // reserving ~14px on the right for a scrollbar that can never appear. + // doesn't feed scrollback). The backend enables zellij mouse mode + // (--mouse-mode true in embeddedClientOptions), so wheel events reach + // zellij as SGR mouse reports and scroll zellij's internal scrollback. + // 0 also stops FitAddon reserving ~14px on the right for a scrollbar + // that can never appear. Text selection still works via shift-drag + // (xterm.js built-in mouse-tracking bypass). scrollback: 0, theme: props.theme === "dark" ? terminalThemes.dark : terminalThemes.light, });