@code-yeongyu/senpi 2026.7.2 → 2026.7.3
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.
- package/CHANGELOG.md +28 -0
- package/dist/core/dynamic-prompt/build.d.ts +13 -0
- package/dist/core/dynamic-prompt/build.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/build.js +24 -21
- package/dist/core/dynamic-prompt/build.js.map +1 -1
- package/dist/core/dynamic-prompt/exploration.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/exploration.js +1 -3
- package/dist/core/dynamic-prompt/exploration.js.map +1 -1
- package/dist/core/dynamic-prompt/index.d.ts +1 -1
- package/dist/core/dynamic-prompt/index.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/index.js.map +1 -1
- package/dist/core/dynamic-prompt/intent-gate.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/intent-gate.js +20 -35
- package/dist/core/dynamic-prompt/intent-gate.js.map +1 -1
- package/dist/core/dynamic-prompt/parallel-tools.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/parallel-tools.js +2 -4
- package/dist/core/dynamic-prompt/parallel-tools.js.map +1 -1
- package/dist/core/dynamic-prompt/style.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/style.js +7 -26
- package/dist/core/dynamic-prompt/style.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.d.ts +3 -0
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.d.ts.map +1 -0
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +16 -0
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js.map +1 -0
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.js +3 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.js +3 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.js +6 -2
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.js +5 -3
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +68 -8
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/presets.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/presets.js +9 -0
- package/dist/core/extensions/builtin/prompt-preset/presets.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
- package/dist/core/extensions/builtin/prompt-preset/settings.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +24 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/model-resolver.js +5 -5
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/settings.md +1 -1
- package/examples/extensions/question.ts +1 -0
- package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +6 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js +4 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +98 -0
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.d.ts +56 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js +38 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts +42 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +46 -10
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/together.models.js +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/together.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +4 -4
- package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +3 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/mux.d.ts +4 -0
- package/node_modules/@earendil-works/pi-tui/dist/mux.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-tui/dist/mux.js +10 -0
- package/node_modules/@earendil-works/pi-tui/dist/mux.js.map +1 -0
- package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts +29 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui.js +397 -89
- package/node_modules/@earendil-works/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts +10 -0
- package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/utils.js +53 -13
- package/node_modules/@earendil-works/pi-tui/dist/utils.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +2 -1
- package/npm-shrinkwrap.json +9 -9
- package/package.json +4 -4
|
@@ -6,6 +6,7 @@ import * as os from "node:os";
|
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { performance } from "node:perf_hooks";
|
|
8
8
|
import { isKeyRelease, matchesKey } from "./keys.js";
|
|
9
|
+
import { isMultiplexerSession, useLegacyMuxRender, viewportRenderEnabled } from "./mux.js";
|
|
9
10
|
import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, parseTerminalColorSchemeReport, } from "./terminal-colors.js";
|
|
10
11
|
import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
|
|
11
12
|
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
|
|
@@ -55,6 +56,103 @@ export function isFocusable(component) {
|
|
|
55
56
|
*/
|
|
56
57
|
export const CURSOR_MARKER = "\x1b_pi:c\x07";
|
|
57
58
|
export { visibleWidth };
|
|
59
|
+
const renderErrorLoggedClasses = new Set();
|
|
60
|
+
let renderErrorLogWrites = 0;
|
|
61
|
+
const DIAGNOSTIC_LOG_MODE = 0o600;
|
|
62
|
+
const VIEWPORT_RENDER_OVERSCAN = 16;
|
|
63
|
+
// Keep scroll-region wins cheap when a few visible rows mutate during append streaming.
|
|
64
|
+
const MAX_SCROLL_DIFF_ROWS = 4;
|
|
65
|
+
const viewportRenderStats = {
|
|
66
|
+
lastNormalizedLines: 0,
|
|
67
|
+
lastKittyImageScannedLines: 0,
|
|
68
|
+
totalNormalizedLines: 0,
|
|
69
|
+
totalKittyImageScannedLines: 0,
|
|
70
|
+
boundedFrames: 0,
|
|
71
|
+
escapedFrames: 0,
|
|
72
|
+
fullFrames: 0,
|
|
73
|
+
};
|
|
74
|
+
export function __renderErrorLogStats() {
|
|
75
|
+
if (process.env.PI_TUI_TEST_SEAMS !== "1") {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return { writes: renderErrorLogWrites };
|
|
79
|
+
}
|
|
80
|
+
export function __viewportRenderStats() {
|
|
81
|
+
if (process.env.PI_TUI_TEST_SEAMS !== "1") {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return { ...viewportRenderStats };
|
|
85
|
+
}
|
|
86
|
+
function recordViewportRenderStats(normalizedLines, mode) {
|
|
87
|
+
viewportRenderStats.lastNormalizedLines = normalizedLines;
|
|
88
|
+
viewportRenderStats.totalNormalizedLines += normalizedLines;
|
|
89
|
+
if (mode === "bounded") {
|
|
90
|
+
viewportRenderStats.boundedFrames += 1;
|
|
91
|
+
}
|
|
92
|
+
else if (mode === "escaped") {
|
|
93
|
+
viewportRenderStats.escapedFrames += 1;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
viewportRenderStats.fullFrames += 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function recordKittyImageScanStats(scannedLines) {
|
|
100
|
+
viewportRenderStats.lastKittyImageScannedLines = scannedLines;
|
|
101
|
+
viewportRenderStats.totalKittyImageScannedLines += scannedLines;
|
|
102
|
+
}
|
|
103
|
+
function componentRenderErrorName(component) {
|
|
104
|
+
return component.constructor.name || "AnonymousComponent";
|
|
105
|
+
}
|
|
106
|
+
function logRenderErrorOnce(component, error) {
|
|
107
|
+
const componentName = componentRenderErrorName(component);
|
|
108
|
+
if (renderErrorLoggedClasses.has(componentName)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
renderErrorLoggedClasses.add(componentName);
|
|
112
|
+
renderErrorLogWrites += 1;
|
|
113
|
+
const errorText = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
114
|
+
const logPath = path.join(os.homedir(), ".senpi", "agent", "senpi-debug.log");
|
|
115
|
+
const msg = `[${new Date().toISOString()}] render error: ${componentName}: ${errorText}\n`;
|
|
116
|
+
appendRenderErrorLogBestEffort(logPath, msg);
|
|
117
|
+
}
|
|
118
|
+
function appendRenderErrorLogBestEffort(logPath, msg) {
|
|
119
|
+
try {
|
|
120
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
121
|
+
fs.appendFileSync(logPath, msg, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
122
|
+
chmodDiagnosticLogBestEffort(logPath);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error instanceof Error) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function writeRenderDiagnosticBestEffort(logPath, data) {
|
|
132
|
+
try {
|
|
133
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
134
|
+
fs.writeFileSync(logPath, data, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
135
|
+
chmodDiagnosticLogBestEffort(logPath);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error instanceof Error) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function chmodDiagnosticLogBestEffort(logPath) {
|
|
146
|
+
try {
|
|
147
|
+
fs.chmodSync(logPath, DIAGNOSTIC_LOG_MODE);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error instanceof Error) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
58
156
|
/** Parse a SizeValue into absolute value given a reference size */
|
|
59
157
|
function parseSizeValue(value, referenceSize) {
|
|
60
158
|
if (value === undefined)
|
|
@@ -120,7 +218,16 @@ export class Container {
|
|
|
120
218
|
render(width) {
|
|
121
219
|
const lines = [];
|
|
122
220
|
for (const child of this.children) {
|
|
123
|
-
|
|
221
|
+
let childLines;
|
|
222
|
+
try {
|
|
223
|
+
childLines = child.render(width);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
logRenderErrorOnce(child, error);
|
|
227
|
+
const componentName = componentRenderErrorName(child);
|
|
228
|
+
// Focus ownership stays unchanged; render containment must not steal or clear focus implicitly.
|
|
229
|
+
childLines = [`[render error: ${componentName}]`];
|
|
230
|
+
}
|
|
124
231
|
for (const line of childLines) {
|
|
125
232
|
lines.push(line);
|
|
126
233
|
}
|
|
@@ -133,9 +240,12 @@ export class Container {
|
|
|
133
240
|
*/
|
|
134
241
|
export class TUI extends Container {
|
|
135
242
|
static { this.MIN_RENDER_INTERVAL_MS = 16; }
|
|
136
|
-
|
|
243
|
+
#lastCursorVisibility;
|
|
244
|
+
#muxDetector;
|
|
245
|
+
constructor(terminal, options) {
|
|
137
246
|
super();
|
|
138
247
|
this.previousLines = [];
|
|
248
|
+
this.previousRawLines = [];
|
|
139
249
|
this.previousKittyImageIds = new Set();
|
|
140
250
|
this.previousWidth = 0;
|
|
141
251
|
this.previousHeight = 0;
|
|
@@ -151,6 +261,8 @@ export class TUI extends Container {
|
|
|
151
261
|
this.maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered)
|
|
152
262
|
this.previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
|
|
153
263
|
this.fullRedrawCount = 0;
|
|
264
|
+
this.muxViewportRepaintCount = 0;
|
|
265
|
+
this.overWideCrashDumpWritten = false;
|
|
154
266
|
this.stopped = false;
|
|
155
267
|
this.pendingOsc11BackgroundReplies = 0;
|
|
156
268
|
this.pendingOsc11BackgroundQueries = [];
|
|
@@ -161,13 +273,19 @@ export class TUI extends Container {
|
|
|
161
273
|
this.overlayStack = [];
|
|
162
274
|
this.overlayFocusRestore = { status: "inactive" };
|
|
163
275
|
this.terminal = terminal;
|
|
164
|
-
|
|
165
|
-
|
|
276
|
+
// Preserve existing positional boolean callers while allowing explicit render-policy overrides.
|
|
277
|
+
const normalizedOptions = typeof options === "boolean" ? { showHardwareCursor: options } : (options ?? {});
|
|
278
|
+
this.#muxDetector = normalizedOptions.muxDetector ?? isMultiplexerSession;
|
|
279
|
+
if (normalizedOptions.showHardwareCursor !== undefined) {
|
|
280
|
+
this.showHardwareCursor = normalizedOptions.showHardwareCursor;
|
|
166
281
|
}
|
|
167
282
|
}
|
|
168
283
|
get fullRedraws() {
|
|
169
284
|
return this.fullRedrawCount;
|
|
170
285
|
}
|
|
286
|
+
get muxViewportRepaints() {
|
|
287
|
+
return this.muxViewportRepaintCount;
|
|
288
|
+
}
|
|
171
289
|
getShowHardwareCursor() {
|
|
172
290
|
return this.showHardwareCursor;
|
|
173
291
|
}
|
|
@@ -176,7 +294,7 @@ export class TUI extends Container {
|
|
|
176
294
|
return;
|
|
177
295
|
this.showHardwareCursor = enabled;
|
|
178
296
|
if (!enabled) {
|
|
179
|
-
this
|
|
297
|
+
this.#setCursorVisibility(false);
|
|
180
298
|
}
|
|
181
299
|
this.requestRender();
|
|
182
300
|
}
|
|
@@ -318,7 +436,7 @@ export class TUI extends Container {
|
|
|
318
436
|
if (!options?.nonCapturing && this.isOverlayVisible(entry)) {
|
|
319
437
|
this.setFocus(component);
|
|
320
438
|
}
|
|
321
|
-
this
|
|
439
|
+
this.#setCursorVisibility(false);
|
|
322
440
|
this.requestRender();
|
|
323
441
|
// Return handle for controlling this overlay
|
|
324
442
|
return {
|
|
@@ -334,7 +452,7 @@ export class TUI extends Container {
|
|
|
334
452
|
this.setFocus(topVisible?.component ?? entry.preFocus);
|
|
335
453
|
}
|
|
336
454
|
if (this.overlayStack.length === 0)
|
|
337
|
-
this
|
|
455
|
+
this.#setCursorVisibility(false);
|
|
338
456
|
this.requestRender();
|
|
339
457
|
}
|
|
340
458
|
},
|
|
@@ -416,7 +534,7 @@ export class TUI extends Container {
|
|
|
416
534
|
this.setFocus(topVisible?.component ?? overlay.preFocus);
|
|
417
535
|
}
|
|
418
536
|
if (this.overlayStack.length === 0)
|
|
419
|
-
this
|
|
537
|
+
this.#setCursorVisibility(false);
|
|
420
538
|
this.requestRender();
|
|
421
539
|
}
|
|
422
540
|
/** Check if there are any visible overlays */
|
|
@@ -451,8 +569,9 @@ export class TUI extends Container {
|
|
|
451
569
|
}
|
|
452
570
|
start() {
|
|
453
571
|
this.stopped = false;
|
|
572
|
+
this.#lastCursorVisibility = undefined;
|
|
454
573
|
this.terminal.start((data) => this.handleInput(data), () => this.requestRender());
|
|
455
|
-
this
|
|
574
|
+
this.#setCursorVisibility(false);
|
|
456
575
|
if (this.terminalColorSchemeNotificationsEnabled) {
|
|
457
576
|
this.terminal.write("\x1b[?2031h");
|
|
458
577
|
}
|
|
@@ -513,9 +632,12 @@ export class TUI extends Container {
|
|
|
513
632
|
}
|
|
514
633
|
this.terminal.write("\r\n");
|
|
515
634
|
}
|
|
516
|
-
this
|
|
635
|
+
this.#lastCursorVisibility = undefined;
|
|
636
|
+
this.#setCursorVisibility(true);
|
|
517
637
|
this.terminal.stop();
|
|
638
|
+
this.#lastCursorVisibility = undefined;
|
|
518
639
|
this.previousLines = [];
|
|
640
|
+
this.previousRawLines = [];
|
|
519
641
|
this.previousKittyImageIds.clear();
|
|
520
642
|
this.previousWidth = 0;
|
|
521
643
|
this.previousHeight = 0;
|
|
@@ -524,10 +646,22 @@ export class TUI extends Container {
|
|
|
524
646
|
this.maxLinesRendered = 0;
|
|
525
647
|
this.previousViewportTop = 0;
|
|
526
648
|
}
|
|
649
|
+
#setCursorVisibility(visible) {
|
|
650
|
+
if (this.#lastCursorVisibility === visible)
|
|
651
|
+
return;
|
|
652
|
+
if (visible) {
|
|
653
|
+
this.terminal.showCursor();
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
this.terminal.hideCursor();
|
|
657
|
+
}
|
|
658
|
+
this.#lastCursorVisibility = visible;
|
|
659
|
+
}
|
|
527
660
|
requestRender(force = false, source = "unknown") {
|
|
528
661
|
if (force) {
|
|
529
662
|
this.inputRenderPending = false;
|
|
530
663
|
this.previousLines = [];
|
|
664
|
+
this.previousRawLines = [];
|
|
531
665
|
this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
|
|
532
666
|
this.previousHeight = -1; // -1 triggers heightChanged, forcing a full clear
|
|
533
667
|
this.cursorRow = 0;
|
|
@@ -905,17 +1039,83 @@ export class TUI extends Container {
|
|
|
905
1039
|
*/
|
|
906
1040
|
static { this.FRAME_BEGIN = "\x1b[?2026h\x1b[?7l"; }
|
|
907
1041
|
static { this.FRAME_END = "\x1b[?7h\x1b[?2026l"; }
|
|
908
|
-
|
|
909
|
-
|
|
1042
|
+
setPreviousLines(lines, rawLines) {
|
|
1043
|
+
this.previousLines = lines;
|
|
1044
|
+
this.previousRawLines = rawLines;
|
|
1045
|
+
}
|
|
1046
|
+
normalizeLine(line) {
|
|
1047
|
+
if (isImageLine(line)) {
|
|
1048
|
+
return { line, normalized: false };
|
|
1049
|
+
}
|
|
1050
|
+
return { line: normalizeTerminalOutput(line) + TUI.SEGMENT_RESET, normalized: true };
|
|
1051
|
+
}
|
|
1052
|
+
applyLineResets(lines, mode = "full") {
|
|
1053
|
+
const normalizedLines = [];
|
|
1054
|
+
let normalizedCount = 0;
|
|
910
1055
|
for (let i = 0; i < lines.length; i++) {
|
|
911
1056
|
const line = lines[i];
|
|
912
|
-
if (
|
|
913
|
-
|
|
1057
|
+
if (isImageLine(line)) {
|
|
1058
|
+
normalizedLines.push(line);
|
|
1059
|
+
}
|
|
1060
|
+
else {
|
|
1061
|
+
normalizedLines.push(normalizeTerminalOutput(line) + TUI.SEGMENT_RESET);
|
|
1062
|
+
normalizedCount += 1;
|
|
914
1063
|
}
|
|
915
1064
|
}
|
|
916
|
-
|
|
1065
|
+
recordViewportRenderStats(normalizedCount, mode);
|
|
1066
|
+
return {
|
|
1067
|
+
lines: normalizedLines,
|
|
1068
|
+
firstRawChanged: 0,
|
|
1069
|
+
compareEndExclusive: normalizedLines.length,
|
|
1070
|
+
bounded: false,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
applyViewportLineResets(rawLines, viewportTop, height, stableDimensions) {
|
|
1074
|
+
if (!viewportRenderEnabled() ||
|
|
1075
|
+
!stableDimensions ||
|
|
1076
|
+
this.previousLines.length === 0 ||
|
|
1077
|
+
this.previousLines.length !== rawLines.length ||
|
|
1078
|
+
this.previousRawLines.length !== rawLines.length) {
|
|
1079
|
+
return this.applyLineResets(rawLines);
|
|
1080
|
+
}
|
|
1081
|
+
const windowStart = Math.max(0, viewportTop - VIEWPORT_RENDER_OVERSCAN);
|
|
1082
|
+
const windowEnd = Math.min(rawLines.length, viewportTop + height + VIEWPORT_RENDER_OVERSCAN);
|
|
1083
|
+
let firstRawChanged = -1;
|
|
1084
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
1085
|
+
if (rawLines[i] === this.previousRawLines[i]) {
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
if (firstRawChanged === -1) {
|
|
1089
|
+
firstRawChanged = i;
|
|
1090
|
+
}
|
|
1091
|
+
if (i < windowStart || i >= windowEnd) {
|
|
1092
|
+
return this.applyLineResets(rawLines, "escaped");
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const lines = this.previousLines.slice();
|
|
1096
|
+
let normalizedCount = 0;
|
|
1097
|
+
if (firstRawChanged !== -1) {
|
|
1098
|
+
for (let i = windowStart; i < windowEnd; i++) {
|
|
1099
|
+
if (rawLines[i] === this.previousRawLines[i]) {
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
const normalized = this.normalizeLine(rawLines[i] ?? "");
|
|
1103
|
+
lines[i] = normalized.line;
|
|
1104
|
+
if (normalized.normalized) {
|
|
1105
|
+
normalizedCount += 1;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
recordViewportRenderStats(normalizedCount, "bounded");
|
|
1110
|
+
return {
|
|
1111
|
+
lines,
|
|
1112
|
+
firstRawChanged,
|
|
1113
|
+
compareEndExclusive: windowEnd,
|
|
1114
|
+
bounded: true,
|
|
1115
|
+
};
|
|
917
1116
|
}
|
|
918
1117
|
collectKittyImageIds(lines) {
|
|
1118
|
+
recordKittyImageScanStats(lines.length);
|
|
919
1119
|
const ids = new Set();
|
|
920
1120
|
for (const line of lines) {
|
|
921
1121
|
for (const id of extractKittyImageIds(line)) {
|
|
@@ -963,6 +1163,24 @@ export class TUI extends Container {
|
|
|
963
1163
|
expandForLines(newLines);
|
|
964
1164
|
return { firstChanged: expandedFirstChanged, lastChanged: expandedLastChanged };
|
|
965
1165
|
}
|
|
1166
|
+
changedRangeNeedsKittyImageExpansion(lines, firstChanged, lastChanged) {
|
|
1167
|
+
if (this.previousKittyImageIds.size > 0) {
|
|
1168
|
+
return true;
|
|
1169
|
+
}
|
|
1170
|
+
const start = Math.max(0, firstChanged);
|
|
1171
|
+
const end = Math.min(lines.length - 1, lastChanged);
|
|
1172
|
+
let scannedLines = 0;
|
|
1173
|
+
for (let i = start; i <= end; i++) {
|
|
1174
|
+
scannedLines += 1;
|
|
1175
|
+
const line = lines[i] ?? "";
|
|
1176
|
+
if (isImageLine(line) || extractKittyImageIds(line).length > 0) {
|
|
1177
|
+
recordKittyImageScanStats(scannedLines);
|
|
1178
|
+
return true;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
recordKittyImageScanStats(scannedLines);
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
966
1184
|
deleteChangedKittyImages(firstChanged, lastChanged) {
|
|
967
1185
|
if (firstChanged < 0 || lastChanged < firstChanged)
|
|
968
1186
|
return "";
|
|
@@ -978,6 +1196,9 @@ export class TUI extends Container {
|
|
|
978
1196
|
getViewportRows(lines, viewportTop, height) {
|
|
979
1197
|
return Array.from({ length: height }, (_, row) => lines[viewportTop + row] ?? "");
|
|
980
1198
|
}
|
|
1199
|
+
shouldPreserveMuxScrollback() {
|
|
1200
|
+
return this.#muxDetector() && !useLegacyMuxRender();
|
|
1201
|
+
}
|
|
981
1202
|
createViewportInsertScrollPlan(newLines, prevViewportTop, height, lineCountDelta) {
|
|
982
1203
|
if (lineCountDelta <= 0 || lineCountDelta >= height || this.overlayStack.length > 0) {
|
|
983
1204
|
return undefined;
|
|
@@ -1001,18 +1222,23 @@ export class TUI extends Container {
|
|
|
1001
1222
|
if (regionHeight <= 0 || regionHeight < lineCountDelta) {
|
|
1002
1223
|
return undefined;
|
|
1003
1224
|
}
|
|
1225
|
+
const mutatedRows = [];
|
|
1004
1226
|
for (let row = 0; row < regionHeight - lineCountDelta; row++) {
|
|
1005
1227
|
if (previousVisible[row + lineCountDelta] !== nextVisible[row]) {
|
|
1006
|
-
|
|
1228
|
+
mutatedRows.push({ screenRow: row, content: nextVisible[row] });
|
|
1229
|
+
if (mutatedRows.length > MAX_SCROLL_DIFF_ROWS) {
|
|
1230
|
+
return undefined;
|
|
1231
|
+
}
|
|
1007
1232
|
}
|
|
1008
1233
|
}
|
|
1009
1234
|
return {
|
|
1010
1235
|
viewportTop,
|
|
1011
1236
|
regionBottom: regionHeight - 1,
|
|
1012
1237
|
insertedRows: nextVisible.slice(regionHeight - lineCountDelta, regionHeight),
|
|
1238
|
+
mutatedRows,
|
|
1013
1239
|
};
|
|
1014
1240
|
}
|
|
1015
|
-
renderViewportInsertScroll(plan, newLines, cursorPos, width, height) {
|
|
1241
|
+
renderViewportInsertScroll(plan, newLines, rawLines, cursorPos, width, height) {
|
|
1016
1242
|
let buffer = TUI.FRAME_BEGIN;
|
|
1017
1243
|
const regionTop = 0;
|
|
1018
1244
|
const regionBottom = plan.regionBottom;
|
|
@@ -1021,27 +1247,35 @@ export class TUI extends Container {
|
|
|
1021
1247
|
buffer += "\n".repeat(plan.insertedRows.length);
|
|
1022
1248
|
buffer += "\x1b[r";
|
|
1023
1249
|
const firstInsertedScreenRow = regionBottom - plan.insertedRows.length + 1;
|
|
1250
|
+
let finalPaintedScreenRow = firstInsertedScreenRow + plan.insertedRows.length - 1;
|
|
1024
1251
|
for (let index = 0; index < plan.insertedRows.length; index++) {
|
|
1025
1252
|
const screenRow = firstInsertedScreenRow + index;
|
|
1026
|
-
buffer += `\x1b[${screenRow + 1};1H\x1b[2K`;
|
|
1253
|
+
buffer += `\x1b[${screenRow + 1};1H\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1027
1254
|
buffer += plan.insertedRows[index] ?? "";
|
|
1028
1255
|
}
|
|
1256
|
+
for (const row of plan.mutatedRows) {
|
|
1257
|
+
buffer += `\x1b[${row.screenRow + 1};1H\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1258
|
+
buffer += row.content;
|
|
1259
|
+
finalPaintedScreenRow = row.screenRow;
|
|
1260
|
+
}
|
|
1029
1261
|
buffer += TUI.FRAME_END;
|
|
1030
1262
|
this.terminal.write(buffer);
|
|
1031
1263
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1032
|
-
this.hardwareCursorRow = plan.viewportTop +
|
|
1264
|
+
this.hardwareCursorRow = plan.viewportTop + finalPaintedScreenRow;
|
|
1033
1265
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1034
1266
|
this.previousViewportTop = plan.viewportTop;
|
|
1035
1267
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1036
|
-
this.
|
|
1268
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1037
1269
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1038
1270
|
this.previousWidth = width;
|
|
1039
1271
|
this.previousHeight = height;
|
|
1040
1272
|
}
|
|
1041
|
-
renderScrollbackReplay(newLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow) {
|
|
1273
|
+
renderScrollbackReplay(newLines, rawLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow) {
|
|
1042
1274
|
let buffer = TUI.FRAME_BEGIN;
|
|
1043
1275
|
buffer += this.deleteKittyImages(this.previousKittyImageIds);
|
|
1044
|
-
|
|
1276
|
+
if (!this.shouldPreserveMuxScrollback()) {
|
|
1277
|
+
buffer += "\x1b[3J";
|
|
1278
|
+
}
|
|
1045
1279
|
const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
|
|
1046
1280
|
if (currentScreenRow > 0) {
|
|
1047
1281
|
buffer += `\x1b[${currentScreenRow}A`;
|
|
@@ -1050,7 +1284,7 @@ export class TUI extends Container {
|
|
|
1050
1284
|
for (let row = 0; row < bufferLength; row++) {
|
|
1051
1285
|
if (row > 0)
|
|
1052
1286
|
buffer += "\r\n";
|
|
1053
|
-
buffer +=
|
|
1287
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1054
1288
|
buffer += newLines[row] ?? "";
|
|
1055
1289
|
}
|
|
1056
1290
|
buffer += TUI.FRAME_END;
|
|
@@ -1060,11 +1294,42 @@ export class TUI extends Container {
|
|
|
1060
1294
|
this.maxLinesRendered = newLines.length;
|
|
1061
1295
|
this.previousViewportTop = Math.max(0, bufferLength - height);
|
|
1062
1296
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1063
|
-
this.
|
|
1297
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1064
1298
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1065
1299
|
this.previousWidth = width;
|
|
1066
1300
|
this.previousHeight = height;
|
|
1067
1301
|
}
|
|
1302
|
+
renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, viewportTop = Math.max(0, newLines.length - height)) {
|
|
1303
|
+
const previousVisible = this.getViewportRows(this.previousLines, this.previousViewportTop, height);
|
|
1304
|
+
const nextVisible = this.getViewportRows(newLines, viewportTop, height);
|
|
1305
|
+
if (previousVisible.some(isImageLine) || nextVisible.some(isImageLine)) {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
let buffer = TUI.FRAME_BEGIN;
|
|
1309
|
+
const currentScreenRow = Math.max(0, Math.min(height - 1, this.hardwareCursorRow - this.previousViewportTop));
|
|
1310
|
+
if (currentScreenRow > 0) {
|
|
1311
|
+
buffer += `\x1b[${currentScreenRow}A`;
|
|
1312
|
+
}
|
|
1313
|
+
for (let row = 0; row < height; row++) {
|
|
1314
|
+
if (row > 0)
|
|
1315
|
+
buffer += "\r\n";
|
|
1316
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1317
|
+
buffer += newLines[viewportTop + row] ?? "";
|
|
1318
|
+
}
|
|
1319
|
+
buffer += TUI.FRAME_END;
|
|
1320
|
+
this.terminal.write(buffer);
|
|
1321
|
+
this.muxViewportRepaintCount += 1;
|
|
1322
|
+
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1323
|
+
this.hardwareCursorRow = viewportTop + Math.max(0, height - 1);
|
|
1324
|
+
this.maxLinesRendered = newLines.length;
|
|
1325
|
+
this.previousViewportTop = viewportTop;
|
|
1326
|
+
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1327
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1328
|
+
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1329
|
+
this.previousWidth = width;
|
|
1330
|
+
this.previousHeight = height;
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1068
1333
|
/** Splice overlay content into a base line at a specific column. Single-pass optimized. */
|
|
1069
1334
|
compositeLineAt(baseLine, overlayLine, startCol, overlayWidth, totalWidth) {
|
|
1070
1335
|
if (isImageLine(baseLine))
|
|
@@ -1153,17 +1418,23 @@ export class TUI extends Container {
|
|
|
1153
1418
|
}
|
|
1154
1419
|
// Extract cursor position before applying line resets (marker must be found first)
|
|
1155
1420
|
const cursorPos = this.extractCursorPosition(newLines, height);
|
|
1156
|
-
|
|
1421
|
+
const rawLines = newLines;
|
|
1422
|
+
const normalizedLines = this.applyViewportLineResets(rawLines, prevViewportTop, height, !widthChanged && !heightChanged);
|
|
1423
|
+
newLines = normalizedLines.lines;
|
|
1424
|
+
const preserveMuxScrollback = this.shouldPreserveMuxScrollback();
|
|
1157
1425
|
// Helper to clear scrollback and viewport and render all new lines
|
|
1158
|
-
const fullRender = (clear) => {
|
|
1426
|
+
const fullRender = (clear, clearScrollback = clear) => {
|
|
1159
1427
|
this.fullRedrawCount += 1;
|
|
1160
1428
|
let buffer = TUI.FRAME_BEGIN;
|
|
1161
1429
|
if (clear) {
|
|
1162
1430
|
buffer += this.deleteKittyImages(this.previousKittyImageIds);
|
|
1163
|
-
buffer += "\x1b[2J\x1b[H
|
|
1431
|
+
buffer += "\x1b[2J\x1b[H";
|
|
1432
|
+
if (clearScrollback && !preserveMuxScrollback) {
|
|
1433
|
+
buffer += "\x1b[3J";
|
|
1434
|
+
}
|
|
1164
1435
|
}
|
|
1165
1436
|
else {
|
|
1166
|
-
buffer +=
|
|
1437
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1167
1438
|
}
|
|
1168
1439
|
for (let i = 0; i < newLines.length; i++) {
|
|
1169
1440
|
if (i > 0)
|
|
@@ -1197,7 +1468,7 @@ export class TUI extends Container {
|
|
|
1197
1468
|
const bufferLength = Math.max(height, newLines.length);
|
|
1198
1469
|
this.previousViewportTop = Math.max(0, bufferLength - height);
|
|
1199
1470
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1200
|
-
this.
|
|
1471
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1201
1472
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1202
1473
|
this.previousWidth = width;
|
|
1203
1474
|
this.previousHeight = height;
|
|
@@ -1208,7 +1479,8 @@ export class TUI extends Container {
|
|
|
1208
1479
|
return;
|
|
1209
1480
|
const logPath = path.join(os.homedir(), ".senpi", "agent", "senpi-debug.log");
|
|
1210
1481
|
const msg = `[${new Date().toISOString()}] fullRender: ${reason} (prev=${this.previousLines.length}, new=${newLines.length}, height=${height})\n`;
|
|
1211
|
-
fs.appendFileSync(logPath, msg);
|
|
1482
|
+
fs.appendFileSync(logPath, msg, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
1483
|
+
chmodDiagnosticLogBestEffort(logPath);
|
|
1212
1484
|
};
|
|
1213
1485
|
// First render - just output everything without clearing (assumes clean screen)
|
|
1214
1486
|
if (this.previousLines.length === 0 && !widthChanged && !heightChanged) {
|
|
@@ -1219,7 +1491,8 @@ export class TUI extends Container {
|
|
|
1219
1491
|
// Width changes always need a full re-render because wrapping changes.
|
|
1220
1492
|
if (widthChanged) {
|
|
1221
1493
|
logRedraw(`terminal width changed (${this.previousWidth} -> ${width})`);
|
|
1222
|
-
|
|
1494
|
+
// In multiplexers, re-emit the viewport without 3J so pane history survives; an older copy may remain above.
|
|
1495
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1223
1496
|
return;
|
|
1224
1497
|
}
|
|
1225
1498
|
// Height changes normally need a full re-render to keep the visible viewport aligned,
|
|
@@ -1227,7 +1500,14 @@ export class TUI extends Container {
|
|
|
1227
1500
|
// In that environment, a full redraw causes the entire history to replay on every toggle.
|
|
1228
1501
|
if (heightChanged && !isTermuxSession()) {
|
|
1229
1502
|
logRedraw(`terminal height changed (${this.previousHeight} -> ${height})`);
|
|
1230
|
-
|
|
1503
|
+
if (preserveMuxScrollback) {
|
|
1504
|
+
if (!this.renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height)) {
|
|
1505
|
+
fullRender(true, false);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
else {
|
|
1509
|
+
fullRender(true);
|
|
1510
|
+
}
|
|
1231
1511
|
return;
|
|
1232
1512
|
}
|
|
1233
1513
|
// Content shrunk below the working area and no overlays - re-render to clear empty rows
|
|
@@ -1235,14 +1515,16 @@ export class TUI extends Container {
|
|
|
1235
1515
|
// Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var
|
|
1236
1516
|
if (this.clearOnShrink && newLines.length < this.maxLinesRendered && this.overlayStack.length === 0) {
|
|
1237
1517
|
logRedraw(`clearOnShrink (maxLinesRendered=${this.maxLinesRendered})`);
|
|
1238
|
-
fullRender(true);
|
|
1518
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1239
1519
|
return;
|
|
1240
1520
|
}
|
|
1241
1521
|
// Find first and last changed lines
|
|
1242
1522
|
let firstChanged = -1;
|
|
1243
1523
|
let lastChanged = -1;
|
|
1244
1524
|
const maxLines = Math.max(newLines.length, this.previousLines.length);
|
|
1245
|
-
|
|
1525
|
+
const diffScanStart = normalizedLines.bounded && normalizedLines.firstRawChanged !== -1 ? normalizedLines.firstRawChanged : 0;
|
|
1526
|
+
const diffScanEndExclusive = normalizedLines.bounded ? normalizedLines.compareEndExclusive : maxLines;
|
|
1527
|
+
for (let i = diffScanStart; i < diffScanEndExclusive; i++) {
|
|
1246
1528
|
const oldLine = i < this.previousLines.length ? this.previousLines[i] : "";
|
|
1247
1529
|
const newLine = i < newLines.length ? newLines[i] : "";
|
|
1248
1530
|
if (oldLine !== newLine) {
|
|
@@ -1260,7 +1542,8 @@ export class TUI extends Container {
|
|
|
1260
1542
|
}
|
|
1261
1543
|
lastChanged = newLines.length - 1;
|
|
1262
1544
|
}
|
|
1263
|
-
|
|
1545
|
+
const needsKittyImageExpansion = firstChanged !== -1 && this.changedRangeNeedsKittyImageExpansion(newLines, firstChanged, lastChanged);
|
|
1546
|
+
if (needsKittyImageExpansion) {
|
|
1264
1547
|
const expandedRange = this.expandChangedRangeForKittyImages(firstChanged, lastChanged, newLines);
|
|
1265
1548
|
firstChanged = expandedRange.firstChanged;
|
|
1266
1549
|
lastChanged = expandedRange.lastChanged;
|
|
@@ -1275,7 +1558,7 @@ export class TUI extends Container {
|
|
|
1275
1558
|
return;
|
|
1276
1559
|
}
|
|
1277
1560
|
if (insertScrollPlan) {
|
|
1278
|
-
this.renderViewportInsertScroll(insertScrollPlan, newLines, cursorPos, width, height);
|
|
1561
|
+
this.renderViewportInsertScroll(insertScrollPlan, newLines, rawLines, cursorPos, width, height);
|
|
1279
1562
|
return;
|
|
1280
1563
|
}
|
|
1281
1564
|
// All changes are in deleted lines (nothing to render, just clear)
|
|
@@ -1287,7 +1570,7 @@ export class TUI extends Container {
|
|
|
1287
1570
|
const targetRow = Math.max(0, newLines.length - 1);
|
|
1288
1571
|
if (targetRow < prevViewportTop) {
|
|
1289
1572
|
logRedraw(`deleted lines moved viewport up (${targetRow} < ${prevViewportTop})`);
|
|
1290
|
-
fullRender(true);
|
|
1573
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1291
1574
|
return;
|
|
1292
1575
|
}
|
|
1293
1576
|
const lineDiff = computeLineDiff(targetRow);
|
|
@@ -1300,7 +1583,7 @@ export class TUI extends Container {
|
|
|
1300
1583
|
const extraLines = this.previousLines.length - newLines.length;
|
|
1301
1584
|
if (extraLines > height) {
|
|
1302
1585
|
logRedraw(`extraLines > height (${extraLines} > ${height})`);
|
|
1303
|
-
fullRender(true);
|
|
1586
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1304
1587
|
return;
|
|
1305
1588
|
}
|
|
1306
1589
|
const clearStartOffset = newLines.length === 0 ? 0 : 1;
|
|
@@ -1308,7 +1591,7 @@ export class TUI extends Container {
|
|
|
1308
1591
|
buffer += `\x1b[${clearStartOffset}B`;
|
|
1309
1592
|
}
|
|
1310
1593
|
for (let i = 0; i < extraLines; i++) {
|
|
1311
|
-
buffer +=
|
|
1594
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1312
1595
|
if (i < extraLines - 1)
|
|
1313
1596
|
buffer += "\x1b[1B";
|
|
1314
1597
|
}
|
|
@@ -1322,7 +1605,7 @@ export class TUI extends Container {
|
|
|
1322
1605
|
this.hardwareCursorRow = targetRow;
|
|
1323
1606
|
}
|
|
1324
1607
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1325
|
-
this.
|
|
1608
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1326
1609
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1327
1610
|
this.previousWidth = width;
|
|
1328
1611
|
this.previousHeight = height;
|
|
@@ -1352,12 +1635,20 @@ export class TUI extends Container {
|
|
|
1352
1635
|
}
|
|
1353
1636
|
if (firstVisibleChanged === -1) {
|
|
1354
1637
|
if (lineCountDelta !== 0) {
|
|
1355
|
-
|
|
1638
|
+
if (preserveMuxScrollback) {
|
|
1639
|
+
// Above-viewport scrollback may stay stale in mux panes; only the visible viewport is repainted.
|
|
1640
|
+
if (!this.renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, viewportTop)) {
|
|
1641
|
+
fullRender(true, false);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
else {
|
|
1645
|
+
this.renderScrollbackReplay(newLines, rawLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow);
|
|
1646
|
+
}
|
|
1356
1647
|
return;
|
|
1357
1648
|
}
|
|
1358
1649
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1359
1650
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1360
|
-
this.
|
|
1651
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1361
1652
|
this.previousWidth = width;
|
|
1362
1653
|
this.previousHeight = height;
|
|
1363
1654
|
this.previousViewportTop = viewportTop;
|
|
@@ -1374,7 +1665,7 @@ export class TUI extends Container {
|
|
|
1374
1665
|
for (let row = 0; row < height; row++) {
|
|
1375
1666
|
if (row > 0)
|
|
1376
1667
|
buffer += "\r\n";
|
|
1377
|
-
buffer +=
|
|
1668
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1378
1669
|
buffer += newLines[viewportTop + row] ?? "";
|
|
1379
1670
|
}
|
|
1380
1671
|
buffer += TUI.FRAME_END;
|
|
@@ -1384,7 +1675,7 @@ export class TUI extends Container {
|
|
|
1384
1675
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1385
1676
|
this.previousViewportTop = viewportTop;
|
|
1386
1677
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1387
|
-
this.
|
|
1678
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1388
1679
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1389
1680
|
this.previousWidth = width;
|
|
1390
1681
|
this.previousHeight = height;
|
|
@@ -1433,12 +1724,12 @@ export class TUI extends Container {
|
|
|
1433
1724
|
const imageStartScreenRow = i - viewportTop;
|
|
1434
1725
|
if (imageStartScreenRow < 0 || imageStartScreenRow + imageReservedRows > height) {
|
|
1435
1726
|
logRedraw(`kitty image pre-clear would scroll (${imageStartScreenRow} + ${imageReservedRows} > ${height})`);
|
|
1436
|
-
fullRender(true);
|
|
1727
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1437
1728
|
return;
|
|
1438
1729
|
}
|
|
1439
|
-
buffer +=
|
|
1730
|
+
buffer += `\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1440
1731
|
for (let row = 1; row < imageReservedRows; row++) {
|
|
1441
|
-
buffer +=
|
|
1732
|
+
buffer += `\r\n\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1442
1733
|
}
|
|
1443
1734
|
buffer += `\x1b[${imageReservedRows - 1}A`;
|
|
1444
1735
|
buffer += line;
|
|
@@ -1446,32 +1737,44 @@ export class TUI extends Container {
|
|
|
1446
1737
|
i += imageReservedRows - 1;
|
|
1447
1738
|
continue;
|
|
1448
1739
|
}
|
|
1449
|
-
buffer +=
|
|
1450
|
-
|
|
1740
|
+
buffer += `\x1b[2K${TUI.SEGMENT_RESET}`; // Clear current line
|
|
1741
|
+
const lineWidth = visibleWidth(line);
|
|
1742
|
+
if (!isImage && lineWidth > width) {
|
|
1451
1743
|
// Log all lines to crash file for debugging
|
|
1452
1744
|
const crashLogPath = path.join(os.homedir(), ".senpi", "agent", "senpi-crash.log");
|
|
1453
1745
|
const crashData = [
|
|
1454
1746
|
`Crash at ${new Date().toISOString()}`,
|
|
1455
1747
|
`Terminal width: ${width}`,
|
|
1456
|
-
`Line ${i} visible width: ${
|
|
1748
|
+
`Line ${i} visible width: ${lineWidth}`,
|
|
1457
1749
|
"",
|
|
1458
1750
|
"=== All rendered lines ===",
|
|
1459
1751
|
...newLines.map((l, idx) => `[${idx}] (w=${visibleWidth(l)}) ${l}`),
|
|
1460
1752
|
"",
|
|
1461
1753
|
].join("\n");
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1754
|
+
if (process.env.PI_TUI_STRICT_RENDER === "1") {
|
|
1755
|
+
const crashDumpWritten = writeRenderDiagnosticBestEffort(crashLogPath, crashData);
|
|
1756
|
+
// Clean up terminal state before throwing
|
|
1757
|
+
this.stop();
|
|
1758
|
+
const errorMsg = [
|
|
1759
|
+
`Rendered line ${i} exceeds terminal width (${lineWidth} > ${width}).`,
|
|
1760
|
+
"",
|
|
1761
|
+
"This is likely caused by a custom TUI component not truncating its output.",
|
|
1762
|
+
"Use visibleWidth() to measure and truncateToWidth() to truncate lines.",
|
|
1763
|
+
"",
|
|
1764
|
+
crashDumpWritten
|
|
1765
|
+
? `Debug log written to: ${crashLogPath}`
|
|
1766
|
+
: `Debug log could not be written to: ${crashLogPath}`,
|
|
1767
|
+
].join("\n");
|
|
1768
|
+
throw new Error(errorMsg);
|
|
1769
|
+
}
|
|
1770
|
+
if (!this.overWideCrashDumpWritten) {
|
|
1771
|
+
writeRenderDiagnosticBestEffort(crashLogPath, crashData);
|
|
1772
|
+
this.overWideCrashDumpWritten = true;
|
|
1773
|
+
}
|
|
1774
|
+
const truncatedLine = sliceByColumn(line, 0, width, true) + TUI.SEGMENT_RESET;
|
|
1775
|
+
newLines[i] = truncatedLine;
|
|
1776
|
+
buffer += truncatedLine;
|
|
1777
|
+
continue;
|
|
1475
1778
|
}
|
|
1476
1779
|
buffer += line;
|
|
1477
1780
|
}
|
|
@@ -1487,7 +1790,7 @@ export class TUI extends Container {
|
|
|
1487
1790
|
}
|
|
1488
1791
|
const extraLines = this.previousLines.length - newLines.length;
|
|
1489
1792
|
for (let i = newLines.length; i < this.previousLines.length; i++) {
|
|
1490
|
-
buffer +=
|
|
1793
|
+
buffer += `\r\n\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1491
1794
|
}
|
|
1492
1795
|
// Move cursor back to end of new content
|
|
1493
1796
|
buffer += `\x1b[${extraLines}A`;
|
|
@@ -1519,7 +1822,8 @@ export class TUI extends Container {
|
|
|
1519
1822
|
"=== buffer ===",
|
|
1520
1823
|
JSON.stringify(buffer),
|
|
1521
1824
|
].join("\n");
|
|
1522
|
-
fs.writeFileSync(debugPath, debugData);
|
|
1825
|
+
fs.writeFileSync(debugPath, debugData, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
1826
|
+
chmodDiagnosticLogBestEffort(debugPath);
|
|
1523
1827
|
}
|
|
1524
1828
|
// Write entire buffer at once
|
|
1525
1829
|
this.terminal.write(buffer);
|
|
@@ -1533,8 +1837,10 @@ export class TUI extends Container {
|
|
|
1533
1837
|
this.previousViewportTop = Math.max(prevViewportTop, finalCursorRow - height + 1);
|
|
1534
1838
|
// Position hardware cursor for IME
|
|
1535
1839
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1536
|
-
this.
|
|
1537
|
-
|
|
1840
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1841
|
+
if (needsKittyImageExpansion) {
|
|
1842
|
+
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1843
|
+
}
|
|
1538
1844
|
this.previousWidth = width;
|
|
1539
1845
|
this.previousHeight = height;
|
|
1540
1846
|
}
|
|
@@ -1544,34 +1850,36 @@ export class TUI extends Container {
|
|
|
1544
1850
|
* @param totalLines Total number of rendered lines
|
|
1545
1851
|
*/
|
|
1546
1852
|
positionHardwareCursor(cursorPos, totalLines) {
|
|
1547
|
-
if (!cursorPos || totalLines <= 0) {
|
|
1548
|
-
this.terminal.hideCursor();
|
|
1549
|
-
return;
|
|
1550
|
-
}
|
|
1551
|
-
// Clamp cursor position to valid range
|
|
1552
|
-
const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
|
|
1553
|
-
const targetCol = Math.max(0, cursorPos.col);
|
|
1554
|
-
// Move cursor from current position to target
|
|
1555
|
-
const rowDelta = targetRow - this.hardwareCursorRow;
|
|
1556
1853
|
let buffer = "";
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
1854
|
+
if (!cursorPos || totalLines <= 0) {
|
|
1855
|
+
if (this.#lastCursorVisibility !== false) {
|
|
1856
|
+
buffer += "\x1b[?25l";
|
|
1857
|
+
this.#lastCursorVisibility = false;
|
|
1858
|
+
}
|
|
1559
1859
|
}
|
|
1560
|
-
else
|
|
1561
|
-
|
|
1860
|
+
else {
|
|
1861
|
+
// Clamp cursor position to valid range
|
|
1862
|
+
const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
|
|
1863
|
+
const targetCol = Math.max(0, cursorPos.col);
|
|
1864
|
+
// Move cursor from current position to target
|
|
1865
|
+
const rowDelta = targetRow - this.hardwareCursorRow;
|
|
1866
|
+
if (rowDelta > 0) {
|
|
1867
|
+
buffer += `\x1b[${rowDelta}B`; // Move down
|
|
1868
|
+
}
|
|
1869
|
+
else if (rowDelta < 0) {
|
|
1870
|
+
buffer += `\x1b[${-rowDelta}A`; // Move up
|
|
1871
|
+
}
|
|
1872
|
+
// Move to absolute column (1-indexed)
|
|
1873
|
+
buffer += `\x1b[${targetCol + 1}G`;
|
|
1874
|
+
this.hardwareCursorRow = targetRow;
|
|
1875
|
+
if (this.#lastCursorVisibility !== this.showHardwareCursor) {
|
|
1876
|
+
buffer += this.showHardwareCursor ? "\x1b[?25h" : "\x1b[?25l";
|
|
1877
|
+
this.#lastCursorVisibility = this.showHardwareCursor;
|
|
1878
|
+
}
|
|
1562
1879
|
}
|
|
1563
|
-
// Move to absolute column (1-indexed)
|
|
1564
|
-
buffer += `\x1b[${targetCol + 1}G`;
|
|
1565
1880
|
if (buffer) {
|
|
1566
1881
|
this.terminal.write(buffer);
|
|
1567
1882
|
}
|
|
1568
|
-
this.hardwareCursorRow = targetRow;
|
|
1569
|
-
if (this.showHardwareCursor) {
|
|
1570
|
-
this.terminal.showCursor();
|
|
1571
|
-
}
|
|
1572
|
-
else {
|
|
1573
|
-
this.terminal.hideCursor();
|
|
1574
|
-
}
|
|
1575
1883
|
}
|
|
1576
1884
|
/**
|
|
1577
1885
|
* Query the terminal's default background color with OSC 11 (`ESC ] 11 ; ? BEL`).
|