aicodeman 1.26.1 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +1 -0
  2. package/dist/config/buffer-limits.d.ts +20 -0
  3. package/dist/config/buffer-limits.d.ts.map +1 -1
  4. package/dist/config/buffer-limits.js +42 -0
  5. package/dist/config/buffer-limits.js.map +1 -1
  6. package/dist/mux-interface.d.ts +6 -1
  7. package/dist/mux-interface.d.ts.map +1 -1
  8. package/dist/session.d.ts.map +1 -1
  9. package/dist/session.js +19 -0
  10. package/dist/session.js.map +1 -1
  11. package/dist/tmux-manager.d.ts +41 -5
  12. package/dist/tmux-manager.d.ts.map +1 -1
  13. package/dist/tmux-manager.js +156 -40
  14. package/dist/tmux-manager.js.map +1 -1
  15. package/dist/utils/omp-session-resolver.d.ts +14 -0
  16. package/dist/utils/omp-session-resolver.d.ts.map +1 -1
  17. package/dist/utils/omp-session-resolver.js +21 -2
  18. package/dist/utils/omp-session-resolver.js.map +1 -1
  19. package/dist/web/public/admin-ui.js.gz +0 -0
  20. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  21. package/dist/web/public/app.78bf0bf8.js +38 -0
  22. package/dist/web/public/app.78bf0bf8.js.br +0 -0
  23. package/dist/web/public/app.78bf0bf8.js.gz +0 -0
  24. package/dist/web/public/approvals-ui.js.gz +0 -0
  25. package/dist/web/public/{constants.7a635e34.js → constants.9cd8ed69.js} +23 -1
  26. package/dist/web/public/constants.9cd8ed69.js.br +0 -0
  27. package/dist/web/public/constants.9cd8ed69.js.gz +0 -0
  28. package/dist/web/public/cron-ui.js.gz +0 -0
  29. package/dist/web/public/entrance-animations.js +9 -1
  30. package/dist/web/public/entrance-animations.js.br +0 -0
  31. package/dist/web/public/entrance-animations.js.gz +0 -0
  32. package/dist/web/public/home-sessions.js.gz +0 -0
  33. package/dist/web/public/i18n.9db8c4e0.js.gz +0 -0
  34. package/dist/web/public/{image-input.ee16ad88.js → image-input.cd4b97c4.js} +13 -2
  35. package/dist/web/public/image-input.cd4b97c4.js.br +0 -0
  36. package/dist/web/public/image-input.cd4b97c4.js.gz +0 -0
  37. package/dist/web/public/index.html +24 -11
  38. package/dist/web/public/index.html.br +0 -0
  39. package/dist/web/public/index.html.gz +0 -0
  40. package/dist/web/public/input-cjk.87f14251.js.gz +0 -0
  41. package/dist/web/public/keyboard-accessory.daf18302.js.gz +0 -0
  42. package/dist/web/public/{mobile-handlers.0f83a608.js → mobile-handlers.2e9c214a.js} +7 -0
  43. package/dist/web/public/mobile-handlers.2e9c214a.js.br +0 -0
  44. package/dist/web/public/mobile-handlers.2e9c214a.js.gz +0 -0
  45. package/dist/web/public/mobile-overview.js.gz +0 -0
  46. package/dist/web/public/mobile.65176832.css +1 -0
  47. package/dist/web/public/mobile.65176832.css.br +0 -0
  48. package/dist/web/public/mobile.65176832.css.gz +0 -0
  49. package/dist/web/public/notification-manager.36ea4624.js.gz +0 -0
  50. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  51. package/dist/web/public/panels-ui.9eb5b235.js.gz +0 -0
  52. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  53. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  54. package/dist/web/public/readmymind-ui.js.gz +0 -0
  55. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  56. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  57. package/dist/web/public/session-lineage.js.gz +0 -0
  58. package/dist/web/public/session-ui.cd222710.js.gz +0 -0
  59. package/dist/web/public/settings-ui.c175db9a.js +67 -0
  60. package/dist/web/public/settings-ui.c175db9a.js.br +0 -0
  61. package/dist/web/public/settings-ui.c175db9a.js.gz +0 -0
  62. package/dist/web/public/styles.6fed0e1b.css +1 -0
  63. package/dist/web/public/styles.6fed0e1b.css.br +0 -0
  64. package/dist/web/public/styles.6fed0e1b.css.gz +0 -0
  65. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  66. package/dist/web/public/sw.js.gz +0 -0
  67. package/dist/web/public/tab-rail-resize.42c24949.js.gz +0 -0
  68. package/dist/web/public/terminal-keycode229-recovery.6ea8fe37.js +1 -0
  69. package/dist/web/public/terminal-keycode229-recovery.6ea8fe37.js.br +0 -0
  70. package/dist/web/public/terminal-keycode229-recovery.6ea8fe37.js.gz +0 -0
  71. package/dist/web/public/terminal-ui.9480b2a1.js +2 -0
  72. package/dist/web/public/terminal-ui.9480b2a1.js.br +0 -0
  73. package/dist/web/public/terminal-ui.9480b2a1.js.gz +0 -0
  74. package/dist/web/public/ultracode-panel.js.gz +0 -0
  75. package/dist/web/public/ultracode-windows.js.gz +0 -0
  76. package/dist/web/public/upload.html.gz +0 -0
  77. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  78. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  79. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  80. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  81. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  82. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  83. package/dist/web/public/vendor/xterm-predictive-echo.bd6882b8.js.gz +0 -0
  84. package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
  85. package/dist/web/public/vendor/xterm.css.gz +0 -0
  86. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  87. package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
  88. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  89. package/dist/web/public/webview-tabs.js +199 -9
  90. package/dist/web/public/webview-tabs.js.br +0 -0
  91. package/dist/web/public/webview-tabs.js.gz +0 -0
  92. package/dist/web/response-viewer-transcript.d.ts +20 -0
  93. package/dist/web/response-viewer-transcript.d.ts.map +1 -1
  94. package/dist/web/response-viewer-transcript.js +27 -0
  95. package/dist/web/response-viewer-transcript.js.map +1 -1
  96. package/dist/web/routes/file-routes.d.ts.map +1 -1
  97. package/dist/web/routes/file-routes.js +16 -26
  98. package/dist/web/routes/file-routes.js.map +1 -1
  99. package/dist/web/routes/session-routes.d.ts.map +1 -1
  100. package/dist/web/routes/session-routes.js +41 -7
  101. package/dist/web/routes/session-routes.js.map +1 -1
  102. package/dist/web/schemas.d.ts +4 -0
  103. package/dist/web/schemas.d.ts.map +1 -1
  104. package/dist/web/schemas.js +9 -0
  105. package/dist/web/schemas.js.map +1 -1
  106. package/package.json +1 -1
  107. package/skills/codeman/reference/endpoints.md +1 -0
  108. package/dist/web/public/app.3a89ac57.js +0 -38
  109. package/dist/web/public/app.3a89ac57.js.br +0 -0
  110. package/dist/web/public/app.3a89ac57.js.gz +0 -0
  111. package/dist/web/public/constants.7a635e34.js.br +0 -0
  112. package/dist/web/public/constants.7a635e34.js.gz +0 -0
  113. package/dist/web/public/image-input.ee16ad88.js.br +0 -0
  114. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  115. package/dist/web/public/mobile-handlers.0f83a608.js.br +0 -0
  116. package/dist/web/public/mobile-handlers.0f83a608.js.gz +0 -0
  117. package/dist/web/public/mobile.7112f27d.css +0 -1
  118. package/dist/web/public/mobile.7112f27d.css.br +0 -0
  119. package/dist/web/public/mobile.7112f27d.css.gz +0 -0
  120. package/dist/web/public/settings-ui.554f9cdd.js +0 -67
  121. package/dist/web/public/settings-ui.554f9cdd.js.br +0 -0
  122. package/dist/web/public/settings-ui.554f9cdd.js.gz +0 -0
  123. package/dist/web/public/styles.298f81f2.css +0 -1
  124. package/dist/web/public/styles.298f81f2.css.br +0 -0
  125. package/dist/web/public/styles.298f81f2.css.gz +0 -0
  126. package/dist/web/public/terminal-ui.a39b5aa7.js +0 -2
  127. package/dist/web/public/terminal-ui.a39b5aa7.js.br +0 -0
  128. package/dist/web/public/terminal-ui.a39b5aa7.js.gz +0 -0
@@ -422,6 +422,60 @@ function truncatePaneLineByVisibleColumns(line, maxColumns) {
422
422
  export function normalizeScrollbackEol(buffer) {
423
423
  return buffer.replace(/\r?\n/g, '\r\n');
424
424
  }
425
+ /**
426
+ * Read the pane's cursor and size, or null when tmux cannot say.
427
+ *
428
+ * Every field is validated together: a caller that gets a value back can place
429
+ * a caret with it, and one that gets null must not try.
430
+ */
431
+ export function queryPaneCursor(run) {
432
+ let raw;
433
+ try {
434
+ raw = run().trim();
435
+ }
436
+ catch (cursorErr) {
437
+ console.error('[TmuxManager] Failed to query pane cursor after capture:', cursorErr);
438
+ return null;
439
+ }
440
+ const [cursorX, cursorY, cols, rows] = raw.split(/\s+/).map((value) => parseInt(value, 10));
441
+ if (!Number.isFinite(cursorX) ||
442
+ !Number.isFinite(cursorY) ||
443
+ !Number.isFinite(cols) ||
444
+ !Number.isFinite(rows) ||
445
+ cursorX < 0 ||
446
+ cursorY < 0 ||
447
+ cols <= 0 ||
448
+ rows <= 0) {
449
+ return null;
450
+ }
451
+ return { cols, rows, cursorX, cursorY };
452
+ }
453
+ /** SGR attributes, which is all `capture-pane -e` emits. */
454
+ // eslint-disable-next-line no-control-regex
455
+ const CAPTURE_STYLE_SEQUENCE = /\x1b\[[0-9;:]*m/g;
456
+ /** Whether a capture holds anything a reader would see, styles discounted. */
457
+ export function hasVisibleContent(capture) {
458
+ return /\S/.test(capture.replace(CAPTURE_STYLE_SEQUENCE, ''));
459
+ }
460
+ /**
461
+ * Put the caret back where the pane has it, counting UP from the bottom of what
462
+ * was just replayed.
463
+ *
464
+ * Relative rather than absolute (`CUP`) on purpose. `\x1b[<row>;<col>H` numbers
465
+ * rows from the top of the browser's screen, so it only lands correctly while
466
+ * the browser's row count equals the pane's — and it need not, because
467
+ * `resizeWindow` fires its tmux resize without waiting, so a capture can be
468
+ * taken before a requested resize has been applied. Counting up from the last
469
+ * replayed row is anchored to the content instead, which is the thing both ends
470
+ * genuinely share.
471
+ */
472
+ export function formatCursorRestore(geometry) {
473
+ const up = Math.max(0, geometry.rows - 1 - geometry.cursorY);
474
+ const right = Math.max(0, geometry.cursorX);
475
+ // `\r` first so the column is known: the replay leaves the caret wherever the
476
+ // last row's text ended.
477
+ return `${up > 0 ? `\x1b[${up}A` : ''}\r${right > 0 ? `\x1b[${right}C` : ''}`;
478
+ }
425
479
  export function formatPaneSnapshot(lines, geometry) {
426
480
  const cols = Math.max(1, geometry.cols);
427
481
  // Paint the full pane width. Earlier this dropped the rightmost column
@@ -610,7 +664,7 @@ export function remoteTmuxSessionName(sessionId) {
610
664
  * even when the remote path contains spaces.
611
665
  */
612
666
  export function buildRemoteLaunchCommand(options) {
613
- const { mode, remote, sessionId, claudeMode, allowedTools } = options;
667
+ const { mode, remote, sessionId, claudeMode, allowedTools, ompConfig, resumeSessionId } = options;
614
668
  // §6.3: honor the session's EFFECTIVE claude permission mode on remote instead of
615
669
  // hardcoding --dangerously-skip-permissions, so a non-granted multi-user user's
616
670
  // downgraded 'auto' actually reaches the remote agent (the default command otherwise
@@ -619,11 +673,68 @@ export function buildRemoteLaunchCommand(options) {
619
673
  // `defaultRemoteCommandForMode`: `claude` lives under a per-user PATH entry that
620
674
  // only an interactive login shell resolves (see that function's comment).
621
675
  const override = remote.commands?.[mode];
622
- const modeCommand = override
623
- ? override
624
- : mode === 'claude'
625
- ? remoteLoginShellCommand(`claude${buildClaudePermissionFlags(claudeMode, allowedTools)}`)
626
- : defaultRemoteCommandForMode(mode);
676
+ let modeCommand;
677
+ if (override) {
678
+ modeCommand = override;
679
+ }
680
+ else if (mode === 'claude') {
681
+ // Deterministic conversation pinning for SSH-remote claude (mirrors the
682
+ // docker-claude shape in claudeDockerPaneCommand, INCLUDING the distinct
683
+ // resumeId branch it declares — this used to only mirror the same-id
684
+ // fallback shape, silently dropping an explicit resumeSessionId that
685
+ // differs from sessionId, e.g. a resume-from-history launch): the FIRST
686
+ // run creates the conversation under --session-id <sessionId>; a respawn
687
+ // / reattach re-runs the same idempotent command, --session-id exits
688
+ // non-zero ("already in use"), and the `||` fallback RESUMES that same
689
+ // conversation. Without a pinned id, every reattach relaunched a bare
690
+ // `claude` and started a NEW conversation (found live 2026-08-29: remote
691
+ // claude ctrl-d / ctrl-c relaunched a fresh session). A per-host
692
+ // `commands.claude` override stays authoritative (admin's explicit
693
+ // choice) and skips this entirely.
694
+ const permFlags = buildClaudePermissionFlags(claudeMode, allowedTools);
695
+ const cmd = `claude${permFlags}`;
696
+ // Defense in depth, mirroring claudeDockerPaneCommand's own belt-and-braces check:
697
+ // sessionId is server-minted and always safe in practice, but this command is built
698
+ // as a single shellescaped string and then executed as shell code on the remote
699
+ // host, so an unsafe value here is validated rather than trusted.
700
+ if (!RESUME_ID_SAFE.test(sessionId)) {
701
+ modeCommand = remoteLoginShellCommand(cmd);
702
+ }
703
+ else {
704
+ const rid = resumeSessionId && RESUME_ID_SAFE.test(resumeSessionId) ? resumeSessionId : undefined;
705
+ modeCommand = remoteLoginShellCommand(rid && rid !== sessionId
706
+ ? `${cmd} --resume ${rid} || ${cmd} --session-id ${sessionId}`
707
+ : `${cmd} --session-id ${sessionId} || ${cmd} --resume ${sessionId}`);
708
+ }
709
+ }
710
+ else if (mode === 'omp') {
711
+ // Remote OMP respawn must RESUME the same conversation instead of
712
+ // relaunching fresh (found live 2026-08-29: remote ctrl-c/ctrl-d relaunched
713
+ // a brand-new omp session). The pinned id, when known, is passed as an
714
+ // explicit --resume; otherwise fall back to omp's own "most recent"
715
+ // --continue so a dead-pane respawn still lands back in the conversation.
716
+ // Rendered through the CLI registry (buildSpawnCommandFromRegistry), the
717
+ // SAME mode-agnostic path local/docker spawns use — not appendResumeFlag(),
718
+ // which would hand the id to the login shell as $0 after the quoted `-c
719
+ // 'omp'`, and not a raw buildOmpCommand() call, which the registry refactor
720
+ // (#347) deleted. Gives every registry CLI with a resume form this
721
+ // behaviour for free, and the flags can't drift from the local builder.
722
+ const ompEntry = getCli('omp');
723
+ const ompCmd = ompEntry
724
+ ? (buildSpawnCommandFromRegistry(ompEntry, {
725
+ mode: 'omp',
726
+ sessionId,
727
+ ompConfig: {
728
+ ...ompConfig,
729
+ resumeSessionId: resumeSessionId || ompConfig?.resumeSessionId,
730
+ },
731
+ }) ?? 'omp')
732
+ : 'omp';
733
+ modeCommand = remoteLoginShellCommand(ompCmd);
734
+ }
735
+ else {
736
+ modeCommand = defaultRemoteCommandForMode(mode);
737
+ }
627
738
  const remoteName = remoteTmuxSessionName(sessionId);
628
739
  // Innermost: the command tmux runs in the new pane. Run via `/bin/sh -c` by
629
740
  // tmux, so the path needs shell-quoting here. `exec` replaces the shell with
@@ -1554,7 +1665,7 @@ export class TmuxManager extends EventEmitter {
1554
1665
  const fullCmd = docker
1555
1666
  ? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
1556
1667
  : remote
1557
- ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools })
1668
+ ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools, ompConfig, resumeSessionId })
1558
1669
  : localFullCmd;
1559
1670
  // Create tmux session in three steps to handle cold-start (no server running)
1560
1671
  // and avoid the race where the command exits before remain-on-exit is set:
@@ -1758,7 +1869,7 @@ export class TmuxManager extends EventEmitter {
1758
1869
  const fullCmd = docker
1759
1870
  ? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
1760
1871
  : remote
1761
- ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools })
1872
+ ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools, ompConfig, resumeSessionId })
1762
1873
  : localFullCmd;
1763
1874
  try {
1764
1875
  // Same per-CLI env setup as createSession, re-applied so the respawned pane inherits it.
@@ -2809,11 +2920,14 @@ export class TmuxManager extends EventEmitter {
2809
2920
  * the browser xterm reproduces the live frame. Used for fast tab switches.
2810
2921
  * - Full history (`opts.fullHistory`): `capture-pane -p -e -J -S -<N>` grabs
2811
2922
  * the tmux scrollback (COD-47, bounded to the configured history limit),
2812
- * returned as linear scrollback text with SGR codes preserved (NOT
2813
- * repositioned — a multi-screen history can't be painted into a single
2814
- * visible frame, so the snapshot repaint is skipped). `-J` re-joins lines
2815
- * hard-wrapped at the pane width so they reflow in the browser xterm.
2816
- * Used for full page reloads so the user gets back their scroll history.
2923
+ * returned as linear scrollback text with SGR codes preserved. Rows are not
2924
+ * repainted at absolute positions — a multi-screen history can't be painted
2925
+ * into a single visible frame — but the capture DOES end with a cursor move
2926
+ * putting the caret back where the pane has it, counted up from the last
2927
+ * replayed row. `-J` re-joins lines hard-wrapped at the pane width so they
2928
+ * reflow in the browser xterm. Used for full page reloads so the user gets
2929
+ * back their scroll history. Returns '' for a pane holding nothing visible,
2930
+ * so the caller keeps whatever history it already had.
2817
2931
  * Caveat: lines tmux has already evicted past its history-limit are gone.
2818
2932
  */
2819
2933
  /**
@@ -2872,35 +2986,37 @@ export class TmuxManager extends EventEmitter {
2872
2986
  execOpts.maxBuffer =
2873
2987
  (opts?.maxCaptureBytes ?? DEFAULT_TERMINAL_BUFFER_MAX_BYTES) + FULL_HISTORY_CAPTURE_SLACK_BYTES;
2874
2988
  }
2875
- const buffer = execSync(`${this.tmux()} ${captureFlags} -t ${shellescape(target)}`, execOpts).replace(/\n+$/g, '');
2876
- // Full-history spans many screens — return it as raw linear scrollback
2877
- // rather than repainting rows at single-screen absolute positions. tmux
2878
- // joins scrollback rows with a bare `\n`; normalize to `\r\n` so a fresh
2879
- // xterm (convertEol:false) starts each replayed line at column 0 instead
2880
- // of staircasing diagonally (COD-138).
2989
+ const rawCapture = execSync(`${this.tmux()} ${captureFlags} -t ${shellescape(target)}`, execOpts);
2990
+ // Query the cursor BEFORE deciding anything else. On the full-history path
2991
+ // it settles both how the capture is trimmed and whether a cursor move is
2992
+ // appended, and those two have to agree: trailing blank rows are only safe
2993
+ // to keep when a move follows to put the caret back above them.
2994
+ const geometry = queryPaneCursor(() => execSync(`${this.tmux()} display-message -p -t ${shellescape(target)} '#{cursor_x} #{cursor_y} #{pane_width} #{pane_height}'`, { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }));
2881
2995
  if (fullHistory) {
2882
- return normalizeScrollbackEol(buffer);
2883
- }
2884
- try {
2885
- const cursor = execSync(`${this.tmux()} display-message -p -t ${shellescape(target)} '#{cursor_x} #{cursor_y} #{pane_width} #{pane_height}'`, {
2886
- encoding: 'utf-8',
2887
- timeout: EXEC_TIMEOUT_MS,
2888
- }).trim();
2889
- const [cursorX, cursorY, cols, rows] = cursor.split(/\s+/).map((value) => parseInt(value, 10));
2890
- if (Number.isFinite(cursorX) &&
2891
- Number.isFinite(cursorY) &&
2892
- Number.isFinite(cols) &&
2893
- Number.isFinite(rows) &&
2894
- cursorX >= 0 &&
2895
- cursorY >= 0 &&
2896
- cols > 0 &&
2897
- rows > 0) {
2898
- return formatPaneSnapshot(buffer.split('\n'), { cols, rows, cursorX, cursorY });
2899
- }
2900
- }
2901
- catch (cursorErr) {
2902
- console.error('[TmuxManager] Failed to query pane cursor after capture:', cursorErr);
2996
+ // Without geometry there is no cursor move, so fall back to the old trim.
2997
+ // Keeping the blank rows here would park the caret at the bottom of the
2998
+ // pane with nothing to correct it — worse than not trying at all.
2999
+ if (!geometry)
3000
+ return normalizeScrollbackEol(rawCapture.replace(/\n+$/g, ''));
3001
+ // Take the line terminator off and nothing else. The trailing blank rows
3002
+ // that remain are the real bottom of the screen, and the cursor move
3003
+ // below counts up from it. tmux joins rows with a bare `\n`; normalize to
3004
+ // `\r\n` so a fresh xterm (convertEol:false) starts each replayed line at
3005
+ // column 0 instead of staircasing diagonally (COD-138).
3006
+ const trimmed = rawCapture.replace(/\n$/, '');
3007
+ // An all-blank pane has to keep reading as "nothing to replay". The caller
3008
+ // treats an empty string as "capture unavailable" and keeps the byte
3009
+ // history; blank rows plus a cursor move are not empty, so without this a
3010
+ // blank pane REPLACES that history with a blank screen — the downgrade
3011
+ // `_replayWouldShrinkBuffer` exists to refuse, arriving from the server
3012
+ // side where that guard cannot see it.
3013
+ if (!hasVisibleContent(trimmed))
3014
+ return '';
3015
+ return `${normalizeScrollbackEol(trimmed)}${formatCursorRestore(geometry)}`;
2903
3016
  }
3017
+ const buffer = rawCapture.replace(/\n+$/g, '');
3018
+ if (geometry)
3019
+ return formatPaneSnapshot(buffer.split('\n'), geometry);
2904
3020
  // Cursor query failed or geometry was invalid, so we skip the absolute-
2905
3021
  // positioned snapshot repaint and fall back to the raw capture. Normalize
2906
3022
  // its bare `\n` line endings to `\r\n` so the replay doesn't staircase