Skip to content

Commit 27dc01e

Browse files
committed
fix(terminal): preload tmux history for native scrollback
1 parent fc0dc8b commit 27dc01e

2 files changed

Lines changed: 28 additions & 1 deletion

File tree

packages/api/src/services/terminal-sessions.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ const terminalSessionStateRelativePath: ReadonlyArray<string> = [".orch", "state
111111
const tmuxMissingMessage =
112112
"tmux is not available in this project container. Apply docker-git config or rebuild the project image so tmux is installed, then reopen this SSH terminal session."
113113
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu
114+
const tmuxInitialScrollbackLines = 10_000
115+
const tmuxInitialScrollbackMaxBytes = 1024 * 1024
114116

115117
const DurableTerminalSessionSchema = Schema.Struct({
116118
id: Schema.String,
@@ -901,6 +903,22 @@ const renderTmuxRightClickBindingCommands = (): ReadonlyArray<string> => [
901903
...tmuxRightClickSuppressBindings.map(renderTmuxRightClickSuppressBinding)
902904
]
903905

906+
// CHANGE: Build a bounded tmux pane-history preload before browser attach.
907+
// WHY: Browser scrolling must use xterm's local scrollback instead of sending wheel input to tmux.
908+
// QUOTE(ТЗ): "при открытии страницы он загружает сразу например историю из последних 10к символов"
909+
// REF: user-message-2026-06-15-native-terminal-scrollback
910+
// SOURCE: n/a
911+
// FORMAT THEOREM: attach(tmux) -> preload(suffix(history(tmux)), <= tmuxInitialScrollbackMaxBytes)
912+
// PURITY: CORE
913+
// INVARIANT: emitted command is bounded and shell-quotes the tmux target.
914+
// COMPLEXITY: O(1)/O(1)
915+
const renderTmuxInitialScrollbackCommand = (tmuxName: string): string =>
916+
[
917+
`tmux capture-pane -p -J -S -${tmuxInitialScrollbackLines} -t ${shellQuote(tmuxName)} 2>/dev/null`,
918+
`tail -c ${tmuxInitialScrollbackMaxBytes}`,
919+
"sed 's/$/\\r/'"
920+
].join(" | ") + " || true"
921+
904922
const writeBufferToProjectContainer = (
905923
containerName: string,
906924
containerPath: string,
@@ -1119,6 +1137,7 @@ export const renderTmuxAttachCommand = (
11191137
`tmux set-option -t ${shellQuote(args.tmuxName)} history-limit 50000 >/dev/null 2>&1 || true`,
11201138
`tmux set-option -t ${shellQuote(args.tmuxName)} mouse on >/dev/null 2>&1 || true`,
11211139
...renderTmuxRightClickBindingCommands(),
1140+
renderTmuxInitialScrollbackCommand(args.tmuxName),
11221141
`exec tmux attach-session -t ${shellQuote(args.tmuxName)}`
11231142
].join("; ")
11241143
return `bash --noprofile --norc -lc ${shellQuote(script)}`

packages/api/tests/terminal-sessions.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,11 @@ describe("terminal sessions service", () => {
276276
expect(command).toContain("unbind-key -T root M-MouseDown3StatusRight")
277277
expect(command).toContain("unbind-key -T root M-MouseDown3Border")
278278
expect(command).not.toContain("display-menu")
279+
expect(command).toContain("tmux capture-pane")
280+
expect(command).toContain("-S -10000")
281+
expect(command).toContain("tail -c 1048576")
282+
expect(command).toContain("sed")
283+
expect(command).toContain("s/$/\\r/")
279284
expect(command).toContain("tmux attach-session -t")
280285
expect(command).toContain("docker-git-session-1")
281286
expect(command).toContain("/home/dev/project with spaces")
@@ -287,6 +292,7 @@ describe("terminal sessions service", () => {
287292
const sessionHistoryLimitIndex = command.lastIndexOf("history-limit 50000")
288293
const mouseOnIndex = command.indexOf("mouse on")
289294
const rightClickBindingIndex = command.indexOf("MouseDown3Pane")
295+
const initialScrollbackIndex = command.indexOf("tmux capture-pane")
290296
const attachSessionIndex = command.indexOf("tmux attach-session -t")
291297

292298
expect(startServerIndex).toBeGreaterThanOrEqual(0)
@@ -296,14 +302,16 @@ describe("terminal sessions service", () => {
296302
expect(sessionHistoryLimitIndex).toBeGreaterThanOrEqual(0)
297303
expect(mouseOnIndex).toBeGreaterThanOrEqual(0)
298304
expect(rightClickBindingIndex).toBeGreaterThan(mouseOnIndex)
305+
expect(initialScrollbackIndex).toBeGreaterThan(rightClickBindingIndex)
299306
expect(attachSessionIndex).toBeGreaterThanOrEqual(0)
300307
expect(startServerIndex).toBeLessThan(globalHistoryLimitIndex)
301308
expect(globalHistoryLimitIndex).toBeLessThan(newSessionIndex)
302309
expect(newSessionIndex).toBeLessThan(statusOffIndex)
303310
expect(statusOffIndex).toBeLessThan(sessionHistoryLimitIndex)
304311
expect(sessionHistoryLimitIndex).toBeLessThan(mouseOnIndex)
305312
expect(mouseOnIndex).toBeLessThan(rightClickBindingIndex)
306-
expect(rightClickBindingIndex).toBeLessThan(attachSessionIndex)
313+
expect(rightClickBindingIndex).toBeLessThan(initialScrollbackIndex)
314+
expect(initialScrollbackIndex).toBeLessThan(attachSessionIndex)
307315
})
308316

309317
it("fails before creating a durable session when tmux is unavailable", async () => {

0 commit comments

Comments
 (0)