From 29e8943ea909df784df9941242f2dd590724dc6a Mon Sep 17 00:00:00 2001 From: konard Date: Mon, 15 Jun 2026 06:54:17 +0000 Subject: [PATCH 1/5] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/ProverCoderAI/docker-git/issues/404 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 00000000..c6ae0736 --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-06-15T06:54:17.322Z for PR creation at branch issue-404-5a7f728e1091 for issue https://github.com/ProverCoderAI/docker-git/issues/404 \ No newline at end of file From d98ada765f91fae4a10e29595487b20b866f169a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 07:02:31 +0000 Subject: [PATCH 2/5] chore(release): version packages --- packages/app/CHANGELOG.md | 9 +++++++++ packages/app/package.json | 2 +- packages/docker-git-session-sync/CHANGELOG.md | 6 ++++++ packages/docker-git-session-sync/package.json | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index ee8953b4..5007dc54 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,14 @@ # @prover-coder-ai/docker-git +## 1.3.2 + +### Patch Changes + +- chore: automated version bump + +- Updated dependencies []: + - @prover-coder-ai/docker-git-session-sync@1.0.61 + ## 1.3.1 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 038b8a5e..2444966d 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@prover-coder-ai/docker-git", - "version": "1.3.1", + "version": "1.3.2", "description": "docker-git Bun and Gridland CLI plus browser frontend", "main": "dist/src/docker-git/main.js", "bin": { diff --git a/packages/docker-git-session-sync/CHANGELOG.md b/packages/docker-git-session-sync/CHANGELOG.md index 4b5617a6..4d228119 100644 --- a/packages/docker-git-session-sync/CHANGELOG.md +++ b/packages/docker-git-session-sync/CHANGELOG.md @@ -1,5 +1,11 @@ # @prover-coder-ai/docker-git-session-sync +## 1.0.61 + +### Patch Changes + +- chore: automated version bump + ## 1.0.60 ### Patch Changes diff --git a/packages/docker-git-session-sync/package.json b/packages/docker-git-session-sync/package.json index 3e287cb6..8ecf929d 100644 --- a/packages/docker-git-session-sync/package.json +++ b/packages/docker-git-session-sync/package.json @@ -1,6 +1,6 @@ { "name": "@prover-coder-ai/docker-git-session-sync", - "version": "1.0.60", + "version": "1.0.61", "description": "Standalone docker-git AI agent session synchronization tool", "main": "dist/docker-git-session-sync.js", "bin": { From 77b8694d73fdf8383d5089f42dc33e131d69d2b2 Mon Sep 17 00:00:00 2001 From: konard Date: Mon, 15 Jun 2026 07:09:26 +0000 Subject: [PATCH 3/5] fix(terminal): suppress alternate screen for project terminals to enable scrollback Project terminals run inside tmux, which switches xterm into the alternate screen buffer (DEC 47/1047/1049). The alternate screen keeps no scrollback, so output was cleared on every repaint and wheel scroll had nothing to reveal ("constantly clears all text, only shows one page"). Wire the already-implemented suppressAlternateScreen query-suppression option on for project terminals via a new pure gating module (terminal-screen-policy), keeping tmux/TUI output in xterm's normal 50k-line scrollback buffer. Refs #404 --- .../terminal-scrollback-alternate-screen.md | 11 ++++ packages/terminal/src/web/index.ts | 1 + .../src/web/terminal-panel-runtime.ts | 7 ++- .../src/web/terminal-screen-policy.ts | 34 +++++++++++++ .../web/terminal-alternate-screen.test.ts | 51 +++++++++++++++++++ 5 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 .changeset/terminal-scrollback-alternate-screen.md create mode 100644 packages/terminal/src/web/terminal-screen-policy.ts create mode 100644 packages/terminal/tests/web/terminal-alternate-screen.test.ts diff --git a/.changeset/terminal-scrollback-alternate-screen.md b/.changeset/terminal-scrollback-alternate-screen.md new file mode 100644 index 00000000..05c00b66 --- /dev/null +++ b/.changeset/terminal-scrollback-alternate-screen.md @@ -0,0 +1,11 @@ +--- +"@prover-coder-ai/docker-git-terminal": patch +--- + +Fix project terminals clearing all output and showing only one page (no scroll). + +Project terminals run inside tmux, which switches xterm into the alternate +screen buffer (DEC private modes 47/1047/1049). The alternate screen keeps no +scrollback, so output was wiped on every repaint and wheel scrolling had nothing +to reveal. Project terminals now suppress the alternate screen so tmux/TUI output +stays in xterm's normal buffer and accumulates in the 50k-line scrollback. diff --git a/packages/terminal/src/web/index.ts b/packages/terminal/src/web/index.ts index 1097c693..5825bd7f 100644 --- a/packages/terminal/src/web/index.ts +++ b/packages/terminal/src/web/index.ts @@ -57,6 +57,7 @@ export * from "./terminal-panel-runtime-types.js" export * from "./terminal-panel-runtime.js" export * from "./terminal-query-suppression.js" export * from "./terminal-reconnect.js" +export * from "./terminal-screen-policy.js" export * from "./terminal-state.js" export * from "./terminal-wheel-scroll.js" export * from "./terminal.js" diff --git a/packages/terminal/src/web/terminal-panel-runtime.ts b/packages/terminal/src/web/terminal-panel-runtime.ts index ac0bc0d9..d3cbda1c 100644 --- a/packages/terminal/src/web/terminal-panel-runtime.ts +++ b/packages/terminal/src/web/terminal-panel-runtime.ts @@ -21,6 +21,7 @@ import type { TerminalSocketConnectArgs, TerminalSocketRef } from "./terminal-panel-runtime-types.js" +import { shouldAllowTerminalMouseTracking, shouldSuppressTerminalAlternateScreen } from "./terminal-screen-policy.js" import { attachTerminalWheelScroll } from "./terminal-wheel-scroll.js" import { isPendingActiveTerminalSession } from "./terminal.js" @@ -190,9 +191,6 @@ const resolveMountHost = ( return hostRef.current } -const shouldAllowTerminalMouseTracking = (session: TerminalLifecycleArgs["session"]): boolean => - session.browserProjectId !== undefined - const mountTerminalSession = (args: TerminalLifecycleArgs): (() => void) | undefined => { const host = resolveMountHost(args) if (host === null) { @@ -204,7 +202,8 @@ const mountTerminalSession = (args: TerminalLifecycleArgs): (() => void) | undef const socketRef: TerminalSocketRef = { current: null } const { fitAddon, terminal } = createTerminalRuntime(host, { querySuppression: { - allowMouseTracking: shouldAllowTerminalMouseTracking(args.session) + allowMouseTracking: shouldAllowTerminalMouseTracking(args.session), + suppressAlternateScreen: shouldSuppressTerminalAlternateScreen(args.session) } }) const terminalInputController = createTerminalInputController(terminal, socketRef) diff --git a/packages/terminal/src/web/terminal-screen-policy.ts b/packages/terminal/src/web/terminal-screen-policy.ts new file mode 100644 index 00000000..dcd5f046 --- /dev/null +++ b/packages/terminal/src/web/terminal-screen-policy.ts @@ -0,0 +1,34 @@ +import type { ActiveTerminalSession } from "./terminal.js" + +// CHANGE: Treat sessions carrying a browser project id as tmux-backed project terminals. +// WHY: Project terminals run inside tmux, which both drives mouse tracking and switches +// xterm into the alternate screen; auth/login terminals stay conservative. +// REF: issue-404 terminal cannot scroll. +// PURITY: CORE +// COMPLEXITY: O(1)/O(1) +export const isProjectTerminalSession = (session: ActiveTerminalSession): boolean => + session.browserProjectId !== undefined + +/** + * Whether xterm should allow DEC private mouse tracking modes for a session. + * + * @pure true + * @complexity O(1) + */ +export const shouldAllowTerminalMouseTracking = (session: ActiveTerminalSession): boolean => + isProjectTerminalSession(session) + +/** + * Whether xterm should suppress the alternate screen (DEC 47/1047/1049) for a session. + * + * Project terminals run inside tmux: with the alternate screen active xterm keeps no + * scrollback, so the terminal "constantly clears all text and only shows one page" and + * wheel scrolling has nothing to scroll. Suppressing it keeps tmux/TUI output in xterm's + * normal buffer where it accumulates in the 50k-line scrollback. + * + * @pure true + * @complexity O(1) + */ +// REF: issue-404 terminal cannot scroll. +export const shouldSuppressTerminalAlternateScreen = (session: ActiveTerminalSession): boolean => + isProjectTerminalSession(session) diff --git a/packages/terminal/tests/web/terminal-alternate-screen.test.ts b/packages/terminal/tests/web/terminal-alternate-screen.test.ts new file mode 100644 index 00000000..8dc8356f --- /dev/null +++ b/packages/terminal/tests/web/terminal-alternate-screen.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest" + +import { + isProjectTerminalSession, + shouldAllowTerminalMouseTracking, + shouldSuppressTerminalAlternateScreen +} from "../../src/web/terminal-screen-policy.js" +import type { ActiveTerminalSession } from "../../src/web/terminal.js" + +const baseSession: ActiveTerminalSession = { + closePath: "/projects/by-key/demo/terminal-sessions/abc", + exitMessage: "ended", + header: "SSH terminal: demo", + pendingDeleteMessage: "closed", + readyMessage: "ready", + session: { + createdAt: "2026-04-21T00:00:00.000Z", + id: "abc", + projectId: "project-demo", + sshCommand: "ssh dev@demo", + status: "ready" + }, + subtitle: "ssh dev@demo", + websocketPath: "/projects/by-key/demo/terminal-sessions/abc/ws" +} + +const projectSession: ActiveTerminalSession = { + ...baseSession, + browserProjectId: "project-demo", + browserProjectKey: "project-key-demo", + browserProjectName: "demo" +} + +describe("terminal alternate screen suppression gating", () => { + it("suppresses the alternate screen for tmux-backed project terminals", () => { + // Project terminals run inside tmux: keeping the alternate screen off lets output + // accumulate in xterm's scrollback so wheel scrolling can reveal earlier history. + expect(shouldSuppressTerminalAlternateScreen(projectSession)).toBe(true) + expect(shouldAllowTerminalMouseTracking(projectSession)).toBe(true) + }) + + it("keeps the alternate screen for non-project (auth) terminals", () => { + expect(shouldSuppressTerminalAlternateScreen(baseSession)).toBe(false) + expect(shouldAllowTerminalMouseTracking(baseSession)).toBe(false) + }) + + it("classifies project sessions by their browser project id", () => { + expect(isProjectTerminalSession(projectSession)).toBe(true) + expect(isProjectTerminalSession(baseSession)).toBe(false) + }) +}) From fc0dc8b10015630786a64aca17f85ff1f0693aed Mon Sep 17 00:00:00 2001 From: konard Date: Mon, 15 Jun 2026 07:19:57 +0000 Subject: [PATCH 4/5] Revert "Initial commit with task details" This reverts commit 29e8943ea909df784df9941242f2dd590724dc6a. --- .gitkeep | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index c6ae0736..00000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-06-15T06:54:17.322Z for PR creation at branch issue-404-5a7f728e1091 for issue https://github.com/ProverCoderAI/docker-git/issues/404 \ No newline at end of file From 1097a6a8740818156594b70d46a800eabc359039 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:50:42 +0000 Subject: [PATCH 5/5] fix(terminal): preload tmux history for native scrollback --- .../api/src/services/terminal-sessions.ts | 19 +++++++++++++++++++ packages/api/tests/terminal-sessions.test.ts | 10 +++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/api/src/services/terminal-sessions.ts b/packages/api/src/services/terminal-sessions.ts index e3e26beb..e637731e 100644 --- a/packages/api/src/services/terminal-sessions.ts +++ b/packages/api/src/services/terminal-sessions.ts @@ -111,6 +111,8 @@ const terminalSessionStateRelativePath: ReadonlyArray = [".orch", "state const tmuxMissingMessage = "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." 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 +const tmuxInitialScrollbackLines = 10_000 +const tmuxInitialScrollbackMaxBytes = 1024 * 1024 const DurableTerminalSessionSchema = Schema.Struct({ id: Schema.String, @@ -901,6 +903,22 @@ const renderTmuxRightClickBindingCommands = (): ReadonlyArray => [ ...tmuxRightClickSuppressBindings.map(renderTmuxRightClickSuppressBinding) ] +// CHANGE: Build a bounded tmux pane-history preload before browser attach. +// WHY: Browser scrolling must use xterm's local scrollback instead of sending wheel input to tmux. +// QUOTE(ТЗ): "при открытии страницы он загружает сразу например историю из последних 10к символов" +// REF: user-message-2026-06-15-native-terminal-scrollback +// SOURCE: n/a +// FORMAT THEOREM: attach(tmux) -> preload(suffix(history(tmux)), <= tmuxInitialScrollbackMaxBytes) +// PURITY: CORE +// INVARIANT: emitted command is bounded and shell-quotes the tmux target. +// COMPLEXITY: O(1)/O(1) +const renderTmuxInitialScrollbackCommand = (tmuxName: string): string => + [ + `tmux capture-pane -p -J -S -${tmuxInitialScrollbackLines} -t ${shellQuote(tmuxName)} 2>/dev/null`, + `tail -c ${tmuxInitialScrollbackMaxBytes}`, + "sed 's/$/\\r/'" + ].join(" | ") + " || true" + const writeBufferToProjectContainer = ( containerName: string, containerPath: string, @@ -1119,6 +1137,7 @@ export const renderTmuxAttachCommand = ( `tmux set-option -t ${shellQuote(args.tmuxName)} history-limit 50000 >/dev/null 2>&1 || true`, `tmux set-option -t ${shellQuote(args.tmuxName)} mouse on >/dev/null 2>&1 || true`, ...renderTmuxRightClickBindingCommands(), + renderTmuxInitialScrollbackCommand(args.tmuxName), `exec tmux attach-session -t ${shellQuote(args.tmuxName)}` ].join("; ") return `bash --noprofile --norc -lc ${shellQuote(script)}` diff --git a/packages/api/tests/terminal-sessions.test.ts b/packages/api/tests/terminal-sessions.test.ts index 76a6beae..017a92f1 100644 --- a/packages/api/tests/terminal-sessions.test.ts +++ b/packages/api/tests/terminal-sessions.test.ts @@ -276,6 +276,11 @@ describe("terminal sessions service", () => { expect(command).toContain("unbind-key -T root M-MouseDown3StatusRight") expect(command).toContain("unbind-key -T root M-MouseDown3Border") expect(command).not.toContain("display-menu") + expect(command).toContain("tmux capture-pane") + expect(command).toContain("-S -10000") + expect(command).toContain("tail -c 1048576") + expect(command).toContain("sed") + expect(command).toContain("s/$/\\r/") expect(command).toContain("tmux attach-session -t") expect(command).toContain("docker-git-session-1") expect(command).toContain("/home/dev/project with spaces") @@ -287,6 +292,7 @@ describe("terminal sessions service", () => { const sessionHistoryLimitIndex = command.lastIndexOf("history-limit 50000") const mouseOnIndex = command.indexOf("mouse on") const rightClickBindingIndex = command.indexOf("MouseDown3Pane") + const initialScrollbackIndex = command.indexOf("tmux capture-pane") const attachSessionIndex = command.indexOf("tmux attach-session -t") expect(startServerIndex).toBeGreaterThanOrEqual(0) @@ -296,6 +302,7 @@ describe("terminal sessions service", () => { expect(sessionHistoryLimitIndex).toBeGreaterThanOrEqual(0) expect(mouseOnIndex).toBeGreaterThanOrEqual(0) expect(rightClickBindingIndex).toBeGreaterThan(mouseOnIndex) + expect(initialScrollbackIndex).toBeGreaterThan(rightClickBindingIndex) expect(attachSessionIndex).toBeGreaterThanOrEqual(0) expect(startServerIndex).toBeLessThan(globalHistoryLimitIndex) expect(globalHistoryLimitIndex).toBeLessThan(newSessionIndex) @@ -303,7 +310,8 @@ describe("terminal sessions service", () => { expect(statusOffIndex).toBeLessThan(sessionHistoryLimitIndex) expect(sessionHistoryLimitIndex).toBeLessThan(mouseOnIndex) expect(mouseOnIndex).toBeLessThan(rightClickBindingIndex) - expect(rightClickBindingIndex).toBeLessThan(attachSessionIndex) + expect(rightClickBindingIndex).toBeLessThan(initialScrollbackIndex) + expect(initialScrollbackIndex).toBeLessThan(attachSessionIndex) }) it("fails before creating a durable session when tmux is unavailable", async () => {