@code-yeongyu/senpi 2026.7.2 → 2026.7.4
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 +44 -0
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +2 -1
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +2 -1
- package/dist/cli/startup-ui.js.map +1 -1
- 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/hooks/dispatcher.d.ts +2 -0
- package/dist/core/extensions/builtin/hooks/dispatcher.d.ts.map +1 -1
- package/dist/core/extensions/builtin/hooks/dispatcher.js +50 -17
- package/dist/core/extensions/builtin/hooks/dispatcher.js.map +1 -1
- package/dist/core/extensions/builtin/hooks/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/hooks/index.js +11 -1
- package/dist/core/extensions/builtin/hooks/index.js.map +1 -1
- package/dist/core/extensions/builtin/hooks/output-bounds.d.ts.map +1 -1
- package/dist/core/extensions/builtin/hooks/output-bounds.js +3 -7
- package/dist/core/extensions/builtin/hooks/output-bounds.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 +3 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +89 -58
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +7 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/hidden-stdout-log.d.ts +2 -0
- package/dist/core/hidden-stdout-log.d.ts.map +1 -0
- package/dist/core/hidden-stdout-log.js +17 -0
- package/dist/core/hidden-stdout-log.js.map +1 -0
- package/dist/core/model-resolver.js +5 -5
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/output-guard.d.ts +2 -0
- package/dist/core/output-guard.d.ts.map +1 -1
- package/dist/core/output-guard.js +63 -0
- package/dist/core/output-guard.js.map +1 -1
- package/dist/core/provider-attribution.d.ts.map +1 -1
- package/dist/core/provider-attribution.js +0 -10
- package/dist/core/provider-attribution.js.map +1 -1
- package/dist/core/sensitive-output.d.ts +3 -0
- package/dist/core/sensitive-output.d.ts.map +1 -0
- package/dist/core/sensitive-output.js +12 -0
- package/dist/core/sensitive-output.js.map +1 -0
- package/dist/core/tools/edit.js +2 -2
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +41 -4
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/interactive-stderr-guard.d.ts +3 -0
- package/dist/modes/interactive/interactive-stderr-guard.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-stderr-guard.js +24 -0
- package/dist/modes/interactive/interactive-stderr-guard.js.map +1 -0
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +7 -0
- package/dist/package-manager-cli.js.map +1 -1
- package/docs/extensions.md +14 -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/api/openai-codex-responses.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +10 -2
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +126 -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/nvidia.models.d.ts +28 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.js +19 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.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 +56 -20
- 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/oauth/device-code.d.ts +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.js +9 -2
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.js +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.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/dist/utils/retry.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/index.d.ts +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/index.js.map +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/terminal.d.ts +17 -0
- package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/terminal.js +75 -22
- package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
- 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 +401 -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,11 @@ export class TUI extends Container {
|
|
|
451
569
|
}
|
|
452
570
|
start() {
|
|
453
571
|
this.stopped = false;
|
|
572
|
+
this.renderRequested = false;
|
|
573
|
+
this.inputRenderPending = false;
|
|
574
|
+
this.#lastCursorVisibility = undefined;
|
|
454
575
|
this.terminal.start((data) => this.handleInput(data), () => this.requestRender());
|
|
455
|
-
this
|
|
576
|
+
this.#setCursorVisibility(false);
|
|
456
577
|
if (this.terminalColorSchemeNotificationsEnabled) {
|
|
457
578
|
this.terminal.write("\x1b[?2031h");
|
|
458
579
|
}
|
|
@@ -494,6 +615,8 @@ export class TUI extends Container {
|
|
|
494
615
|
}
|
|
495
616
|
stop() {
|
|
496
617
|
this.stopped = true;
|
|
618
|
+
this.renderRequested = false;
|
|
619
|
+
this.inputRenderPending = false;
|
|
497
620
|
if (this.renderTimer) {
|
|
498
621
|
clearTimeout(this.renderTimer);
|
|
499
622
|
this.renderTimer = undefined;
|
|
@@ -513,9 +636,12 @@ export class TUI extends Container {
|
|
|
513
636
|
}
|
|
514
637
|
this.terminal.write("\r\n");
|
|
515
638
|
}
|
|
516
|
-
this
|
|
639
|
+
this.#lastCursorVisibility = undefined;
|
|
640
|
+
this.#setCursorVisibility(true);
|
|
517
641
|
this.terminal.stop();
|
|
642
|
+
this.#lastCursorVisibility = undefined;
|
|
518
643
|
this.previousLines = [];
|
|
644
|
+
this.previousRawLines = [];
|
|
519
645
|
this.previousKittyImageIds.clear();
|
|
520
646
|
this.previousWidth = 0;
|
|
521
647
|
this.previousHeight = 0;
|
|
@@ -524,10 +650,22 @@ export class TUI extends Container {
|
|
|
524
650
|
this.maxLinesRendered = 0;
|
|
525
651
|
this.previousViewportTop = 0;
|
|
526
652
|
}
|
|
653
|
+
#setCursorVisibility(visible) {
|
|
654
|
+
if (this.#lastCursorVisibility === visible)
|
|
655
|
+
return;
|
|
656
|
+
if (visible) {
|
|
657
|
+
this.terminal.showCursor();
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
this.terminal.hideCursor();
|
|
661
|
+
}
|
|
662
|
+
this.#lastCursorVisibility = visible;
|
|
663
|
+
}
|
|
527
664
|
requestRender(force = false, source = "unknown") {
|
|
528
665
|
if (force) {
|
|
529
666
|
this.inputRenderPending = false;
|
|
530
667
|
this.previousLines = [];
|
|
668
|
+
this.previousRawLines = [];
|
|
531
669
|
this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
|
|
532
670
|
this.previousHeight = -1; // -1 triggers heightChanged, forcing a full clear
|
|
533
671
|
this.cursorRow = 0;
|
|
@@ -905,17 +1043,83 @@ export class TUI extends Container {
|
|
|
905
1043
|
*/
|
|
906
1044
|
static { this.FRAME_BEGIN = "\x1b[?2026h\x1b[?7l"; }
|
|
907
1045
|
static { this.FRAME_END = "\x1b[?7h\x1b[?2026l"; }
|
|
908
|
-
|
|
909
|
-
|
|
1046
|
+
setPreviousLines(lines, rawLines) {
|
|
1047
|
+
this.previousLines = lines;
|
|
1048
|
+
this.previousRawLines = rawLines;
|
|
1049
|
+
}
|
|
1050
|
+
normalizeLine(line) {
|
|
1051
|
+
if (isImageLine(line)) {
|
|
1052
|
+
return { line, normalized: false };
|
|
1053
|
+
}
|
|
1054
|
+
return { line: normalizeTerminalOutput(line) + TUI.SEGMENT_RESET, normalized: true };
|
|
1055
|
+
}
|
|
1056
|
+
applyLineResets(lines, mode = "full") {
|
|
1057
|
+
const normalizedLines = [];
|
|
1058
|
+
let normalizedCount = 0;
|
|
910
1059
|
for (let i = 0; i < lines.length; i++) {
|
|
911
1060
|
const line = lines[i];
|
|
912
|
-
if (
|
|
913
|
-
|
|
1061
|
+
if (isImageLine(line)) {
|
|
1062
|
+
normalizedLines.push(line);
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
normalizedLines.push(normalizeTerminalOutput(line) + TUI.SEGMENT_RESET);
|
|
1066
|
+
normalizedCount += 1;
|
|
914
1067
|
}
|
|
915
1068
|
}
|
|
916
|
-
|
|
1069
|
+
recordViewportRenderStats(normalizedCount, mode);
|
|
1070
|
+
return {
|
|
1071
|
+
lines: normalizedLines,
|
|
1072
|
+
firstRawChanged: 0,
|
|
1073
|
+
compareEndExclusive: normalizedLines.length,
|
|
1074
|
+
bounded: false,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
applyViewportLineResets(rawLines, viewportTop, height, stableDimensions) {
|
|
1078
|
+
if (!viewportRenderEnabled() ||
|
|
1079
|
+
!stableDimensions ||
|
|
1080
|
+
this.previousLines.length === 0 ||
|
|
1081
|
+
this.previousLines.length !== rawLines.length ||
|
|
1082
|
+
this.previousRawLines.length !== rawLines.length) {
|
|
1083
|
+
return this.applyLineResets(rawLines);
|
|
1084
|
+
}
|
|
1085
|
+
const windowStart = Math.max(0, viewportTop - VIEWPORT_RENDER_OVERSCAN);
|
|
1086
|
+
const windowEnd = Math.min(rawLines.length, viewportTop + height + VIEWPORT_RENDER_OVERSCAN);
|
|
1087
|
+
let firstRawChanged = -1;
|
|
1088
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
1089
|
+
if (rawLines[i] === this.previousRawLines[i]) {
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
if (firstRawChanged === -1) {
|
|
1093
|
+
firstRawChanged = i;
|
|
1094
|
+
}
|
|
1095
|
+
if (i < windowStart || i >= windowEnd) {
|
|
1096
|
+
return this.applyLineResets(rawLines, "escaped");
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const lines = this.previousLines.slice();
|
|
1100
|
+
let normalizedCount = 0;
|
|
1101
|
+
if (firstRawChanged !== -1) {
|
|
1102
|
+
for (let i = windowStart; i < windowEnd; i++) {
|
|
1103
|
+
if (rawLines[i] === this.previousRawLines[i]) {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
const normalized = this.normalizeLine(rawLines[i] ?? "");
|
|
1107
|
+
lines[i] = normalized.line;
|
|
1108
|
+
if (normalized.normalized) {
|
|
1109
|
+
normalizedCount += 1;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
recordViewportRenderStats(normalizedCount, "bounded");
|
|
1114
|
+
return {
|
|
1115
|
+
lines,
|
|
1116
|
+
firstRawChanged,
|
|
1117
|
+
compareEndExclusive: windowEnd,
|
|
1118
|
+
bounded: true,
|
|
1119
|
+
};
|
|
917
1120
|
}
|
|
918
1121
|
collectKittyImageIds(lines) {
|
|
1122
|
+
recordKittyImageScanStats(lines.length);
|
|
919
1123
|
const ids = new Set();
|
|
920
1124
|
for (const line of lines) {
|
|
921
1125
|
for (const id of extractKittyImageIds(line)) {
|
|
@@ -963,6 +1167,24 @@ export class TUI extends Container {
|
|
|
963
1167
|
expandForLines(newLines);
|
|
964
1168
|
return { firstChanged: expandedFirstChanged, lastChanged: expandedLastChanged };
|
|
965
1169
|
}
|
|
1170
|
+
changedRangeNeedsKittyImageExpansion(lines, firstChanged, lastChanged) {
|
|
1171
|
+
if (this.previousKittyImageIds.size > 0) {
|
|
1172
|
+
return true;
|
|
1173
|
+
}
|
|
1174
|
+
const start = Math.max(0, firstChanged);
|
|
1175
|
+
const end = Math.min(lines.length - 1, lastChanged);
|
|
1176
|
+
let scannedLines = 0;
|
|
1177
|
+
for (let i = start; i <= end; i++) {
|
|
1178
|
+
scannedLines += 1;
|
|
1179
|
+
const line = lines[i] ?? "";
|
|
1180
|
+
if (isImageLine(line) || extractKittyImageIds(line).length > 0) {
|
|
1181
|
+
recordKittyImageScanStats(scannedLines);
|
|
1182
|
+
return true;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
recordKittyImageScanStats(scannedLines);
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
966
1188
|
deleteChangedKittyImages(firstChanged, lastChanged) {
|
|
967
1189
|
if (firstChanged < 0 || lastChanged < firstChanged)
|
|
968
1190
|
return "";
|
|
@@ -978,6 +1200,9 @@ export class TUI extends Container {
|
|
|
978
1200
|
getViewportRows(lines, viewportTop, height) {
|
|
979
1201
|
return Array.from({ length: height }, (_, row) => lines[viewportTop + row] ?? "");
|
|
980
1202
|
}
|
|
1203
|
+
shouldPreserveMuxScrollback() {
|
|
1204
|
+
return this.#muxDetector() && !useLegacyMuxRender();
|
|
1205
|
+
}
|
|
981
1206
|
createViewportInsertScrollPlan(newLines, prevViewportTop, height, lineCountDelta) {
|
|
982
1207
|
if (lineCountDelta <= 0 || lineCountDelta >= height || this.overlayStack.length > 0) {
|
|
983
1208
|
return undefined;
|
|
@@ -1001,18 +1226,23 @@ export class TUI extends Container {
|
|
|
1001
1226
|
if (regionHeight <= 0 || regionHeight < lineCountDelta) {
|
|
1002
1227
|
return undefined;
|
|
1003
1228
|
}
|
|
1229
|
+
const mutatedRows = [];
|
|
1004
1230
|
for (let row = 0; row < regionHeight - lineCountDelta; row++) {
|
|
1005
1231
|
if (previousVisible[row + lineCountDelta] !== nextVisible[row]) {
|
|
1006
|
-
|
|
1232
|
+
mutatedRows.push({ screenRow: row, content: nextVisible[row] });
|
|
1233
|
+
if (mutatedRows.length > MAX_SCROLL_DIFF_ROWS) {
|
|
1234
|
+
return undefined;
|
|
1235
|
+
}
|
|
1007
1236
|
}
|
|
1008
1237
|
}
|
|
1009
1238
|
return {
|
|
1010
1239
|
viewportTop,
|
|
1011
1240
|
regionBottom: regionHeight - 1,
|
|
1012
1241
|
insertedRows: nextVisible.slice(regionHeight - lineCountDelta, regionHeight),
|
|
1242
|
+
mutatedRows,
|
|
1013
1243
|
};
|
|
1014
1244
|
}
|
|
1015
|
-
renderViewportInsertScroll(plan, newLines, cursorPos, width, height) {
|
|
1245
|
+
renderViewportInsertScroll(plan, newLines, rawLines, cursorPos, width, height) {
|
|
1016
1246
|
let buffer = TUI.FRAME_BEGIN;
|
|
1017
1247
|
const regionTop = 0;
|
|
1018
1248
|
const regionBottom = plan.regionBottom;
|
|
@@ -1021,27 +1251,35 @@ export class TUI extends Container {
|
|
|
1021
1251
|
buffer += "\n".repeat(plan.insertedRows.length);
|
|
1022
1252
|
buffer += "\x1b[r";
|
|
1023
1253
|
const firstInsertedScreenRow = regionBottom - plan.insertedRows.length + 1;
|
|
1254
|
+
let finalPaintedScreenRow = firstInsertedScreenRow + plan.insertedRows.length - 1;
|
|
1024
1255
|
for (let index = 0; index < plan.insertedRows.length; index++) {
|
|
1025
1256
|
const screenRow = firstInsertedScreenRow + index;
|
|
1026
|
-
buffer += `\x1b[${screenRow + 1};1H\x1b[2K`;
|
|
1257
|
+
buffer += `\x1b[${screenRow + 1};1H\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1027
1258
|
buffer += plan.insertedRows[index] ?? "";
|
|
1028
1259
|
}
|
|
1260
|
+
for (const row of plan.mutatedRows) {
|
|
1261
|
+
buffer += `\x1b[${row.screenRow + 1};1H\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1262
|
+
buffer += row.content;
|
|
1263
|
+
finalPaintedScreenRow = row.screenRow;
|
|
1264
|
+
}
|
|
1029
1265
|
buffer += TUI.FRAME_END;
|
|
1030
1266
|
this.terminal.write(buffer);
|
|
1031
1267
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1032
|
-
this.hardwareCursorRow = plan.viewportTop +
|
|
1268
|
+
this.hardwareCursorRow = plan.viewportTop + finalPaintedScreenRow;
|
|
1033
1269
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1034
1270
|
this.previousViewportTop = plan.viewportTop;
|
|
1035
1271
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1036
|
-
this.
|
|
1272
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1037
1273
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1038
1274
|
this.previousWidth = width;
|
|
1039
1275
|
this.previousHeight = height;
|
|
1040
1276
|
}
|
|
1041
|
-
renderScrollbackReplay(newLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow) {
|
|
1277
|
+
renderScrollbackReplay(newLines, rawLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow) {
|
|
1042
1278
|
let buffer = TUI.FRAME_BEGIN;
|
|
1043
1279
|
buffer += this.deleteKittyImages(this.previousKittyImageIds);
|
|
1044
|
-
|
|
1280
|
+
if (!this.shouldPreserveMuxScrollback()) {
|
|
1281
|
+
buffer += "\x1b[3J";
|
|
1282
|
+
}
|
|
1045
1283
|
const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
|
|
1046
1284
|
if (currentScreenRow > 0) {
|
|
1047
1285
|
buffer += `\x1b[${currentScreenRow}A`;
|
|
@@ -1050,7 +1288,7 @@ export class TUI extends Container {
|
|
|
1050
1288
|
for (let row = 0; row < bufferLength; row++) {
|
|
1051
1289
|
if (row > 0)
|
|
1052
1290
|
buffer += "\r\n";
|
|
1053
|
-
buffer +=
|
|
1291
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1054
1292
|
buffer += newLines[row] ?? "";
|
|
1055
1293
|
}
|
|
1056
1294
|
buffer += TUI.FRAME_END;
|
|
@@ -1060,10 +1298,41 @@ export class TUI extends Container {
|
|
|
1060
1298
|
this.maxLinesRendered = newLines.length;
|
|
1061
1299
|
this.previousViewportTop = Math.max(0, bufferLength - height);
|
|
1062
1300
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1063
|
-
this.
|
|
1301
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1302
|
+
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1303
|
+
this.previousWidth = width;
|
|
1304
|
+
this.previousHeight = height;
|
|
1305
|
+
}
|
|
1306
|
+
renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, viewportTop = Math.max(0, newLines.length - height)) {
|
|
1307
|
+
const previousVisible = this.getViewportRows(this.previousLines, this.previousViewportTop, height);
|
|
1308
|
+
const nextVisible = this.getViewportRows(newLines, viewportTop, height);
|
|
1309
|
+
if (previousVisible.some(isImageLine) || nextVisible.some(isImageLine)) {
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
let buffer = TUI.FRAME_BEGIN;
|
|
1313
|
+
const currentScreenRow = Math.max(0, Math.min(height - 1, this.hardwareCursorRow - this.previousViewportTop));
|
|
1314
|
+
if (currentScreenRow > 0) {
|
|
1315
|
+
buffer += `\x1b[${currentScreenRow}A`;
|
|
1316
|
+
}
|
|
1317
|
+
for (let row = 0; row < height; row++) {
|
|
1318
|
+
if (row > 0)
|
|
1319
|
+
buffer += "\r\n";
|
|
1320
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1321
|
+
buffer += newLines[viewportTop + row] ?? "";
|
|
1322
|
+
}
|
|
1323
|
+
buffer += TUI.FRAME_END;
|
|
1324
|
+
this.terminal.write(buffer);
|
|
1325
|
+
this.muxViewportRepaintCount += 1;
|
|
1326
|
+
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1327
|
+
this.hardwareCursorRow = viewportTop + Math.max(0, height - 1);
|
|
1328
|
+
this.maxLinesRendered = newLines.length;
|
|
1329
|
+
this.previousViewportTop = viewportTop;
|
|
1330
|
+
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1331
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1064
1332
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1065
1333
|
this.previousWidth = width;
|
|
1066
1334
|
this.previousHeight = height;
|
|
1335
|
+
return true;
|
|
1067
1336
|
}
|
|
1068
1337
|
/** Splice overlay content into a base line at a specific column. Single-pass optimized. */
|
|
1069
1338
|
compositeLineAt(baseLine, overlayLine, startCol, overlayWidth, totalWidth) {
|
|
@@ -1153,17 +1422,23 @@ export class TUI extends Container {
|
|
|
1153
1422
|
}
|
|
1154
1423
|
// Extract cursor position before applying line resets (marker must be found first)
|
|
1155
1424
|
const cursorPos = this.extractCursorPosition(newLines, height);
|
|
1156
|
-
|
|
1425
|
+
const rawLines = newLines;
|
|
1426
|
+
const normalizedLines = this.applyViewportLineResets(rawLines, prevViewportTop, height, !widthChanged && !heightChanged);
|
|
1427
|
+
newLines = normalizedLines.lines;
|
|
1428
|
+
const preserveMuxScrollback = this.shouldPreserveMuxScrollback();
|
|
1157
1429
|
// Helper to clear scrollback and viewport and render all new lines
|
|
1158
|
-
const fullRender = (clear) => {
|
|
1430
|
+
const fullRender = (clear, clearScrollback = clear) => {
|
|
1159
1431
|
this.fullRedrawCount += 1;
|
|
1160
1432
|
let buffer = TUI.FRAME_BEGIN;
|
|
1161
1433
|
if (clear) {
|
|
1162
1434
|
buffer += this.deleteKittyImages(this.previousKittyImageIds);
|
|
1163
|
-
buffer += "\x1b[2J\x1b[H
|
|
1435
|
+
buffer += "\x1b[2J\x1b[H";
|
|
1436
|
+
if (clearScrollback && !preserveMuxScrollback) {
|
|
1437
|
+
buffer += "\x1b[3J";
|
|
1438
|
+
}
|
|
1164
1439
|
}
|
|
1165
1440
|
else {
|
|
1166
|
-
buffer +=
|
|
1441
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1167
1442
|
}
|
|
1168
1443
|
for (let i = 0; i < newLines.length; i++) {
|
|
1169
1444
|
if (i > 0)
|
|
@@ -1197,7 +1472,7 @@ export class TUI extends Container {
|
|
|
1197
1472
|
const bufferLength = Math.max(height, newLines.length);
|
|
1198
1473
|
this.previousViewportTop = Math.max(0, bufferLength - height);
|
|
1199
1474
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1200
|
-
this.
|
|
1475
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1201
1476
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1202
1477
|
this.previousWidth = width;
|
|
1203
1478
|
this.previousHeight = height;
|
|
@@ -1208,7 +1483,8 @@ export class TUI extends Container {
|
|
|
1208
1483
|
return;
|
|
1209
1484
|
const logPath = path.join(os.homedir(), ".senpi", "agent", "senpi-debug.log");
|
|
1210
1485
|
const msg = `[${new Date().toISOString()}] fullRender: ${reason} (prev=${this.previousLines.length}, new=${newLines.length}, height=${height})\n`;
|
|
1211
|
-
fs.appendFileSync(logPath, msg);
|
|
1486
|
+
fs.appendFileSync(logPath, msg, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
1487
|
+
chmodDiagnosticLogBestEffort(logPath);
|
|
1212
1488
|
};
|
|
1213
1489
|
// First render - just output everything without clearing (assumes clean screen)
|
|
1214
1490
|
if (this.previousLines.length === 0 && !widthChanged && !heightChanged) {
|
|
@@ -1219,7 +1495,8 @@ export class TUI extends Container {
|
|
|
1219
1495
|
// Width changes always need a full re-render because wrapping changes.
|
|
1220
1496
|
if (widthChanged) {
|
|
1221
1497
|
logRedraw(`terminal width changed (${this.previousWidth} -> ${width})`);
|
|
1222
|
-
|
|
1498
|
+
// In multiplexers, re-emit the viewport without 3J so pane history survives; an older copy may remain above.
|
|
1499
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1223
1500
|
return;
|
|
1224
1501
|
}
|
|
1225
1502
|
// Height changes normally need a full re-render to keep the visible viewport aligned,
|
|
@@ -1227,7 +1504,14 @@ export class TUI extends Container {
|
|
|
1227
1504
|
// In that environment, a full redraw causes the entire history to replay on every toggle.
|
|
1228
1505
|
if (heightChanged && !isTermuxSession()) {
|
|
1229
1506
|
logRedraw(`terminal height changed (${this.previousHeight} -> ${height})`);
|
|
1230
|
-
|
|
1507
|
+
if (preserveMuxScrollback) {
|
|
1508
|
+
if (!this.renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height)) {
|
|
1509
|
+
fullRender(true, false);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
else {
|
|
1513
|
+
fullRender(true);
|
|
1514
|
+
}
|
|
1231
1515
|
return;
|
|
1232
1516
|
}
|
|
1233
1517
|
// Content shrunk below the working area and no overlays - re-render to clear empty rows
|
|
@@ -1235,14 +1519,16 @@ export class TUI extends Container {
|
|
|
1235
1519
|
// Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var
|
|
1236
1520
|
if (this.clearOnShrink && newLines.length < this.maxLinesRendered && this.overlayStack.length === 0) {
|
|
1237
1521
|
logRedraw(`clearOnShrink (maxLinesRendered=${this.maxLinesRendered})`);
|
|
1238
|
-
fullRender(true);
|
|
1522
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1239
1523
|
return;
|
|
1240
1524
|
}
|
|
1241
1525
|
// Find first and last changed lines
|
|
1242
1526
|
let firstChanged = -1;
|
|
1243
1527
|
let lastChanged = -1;
|
|
1244
1528
|
const maxLines = Math.max(newLines.length, this.previousLines.length);
|
|
1245
|
-
|
|
1529
|
+
const diffScanStart = normalizedLines.bounded && normalizedLines.firstRawChanged !== -1 ? normalizedLines.firstRawChanged : 0;
|
|
1530
|
+
const diffScanEndExclusive = normalizedLines.bounded ? normalizedLines.compareEndExclusive : maxLines;
|
|
1531
|
+
for (let i = diffScanStart; i < diffScanEndExclusive; i++) {
|
|
1246
1532
|
const oldLine = i < this.previousLines.length ? this.previousLines[i] : "";
|
|
1247
1533
|
const newLine = i < newLines.length ? newLines[i] : "";
|
|
1248
1534
|
if (oldLine !== newLine) {
|
|
@@ -1260,7 +1546,8 @@ export class TUI extends Container {
|
|
|
1260
1546
|
}
|
|
1261
1547
|
lastChanged = newLines.length - 1;
|
|
1262
1548
|
}
|
|
1263
|
-
|
|
1549
|
+
const needsKittyImageExpansion = firstChanged !== -1 && this.changedRangeNeedsKittyImageExpansion(newLines, firstChanged, lastChanged);
|
|
1550
|
+
if (needsKittyImageExpansion) {
|
|
1264
1551
|
const expandedRange = this.expandChangedRangeForKittyImages(firstChanged, lastChanged, newLines);
|
|
1265
1552
|
firstChanged = expandedRange.firstChanged;
|
|
1266
1553
|
lastChanged = expandedRange.lastChanged;
|
|
@@ -1275,7 +1562,7 @@ export class TUI extends Container {
|
|
|
1275
1562
|
return;
|
|
1276
1563
|
}
|
|
1277
1564
|
if (insertScrollPlan) {
|
|
1278
|
-
this.renderViewportInsertScroll(insertScrollPlan, newLines, cursorPos, width, height);
|
|
1565
|
+
this.renderViewportInsertScroll(insertScrollPlan, newLines, rawLines, cursorPos, width, height);
|
|
1279
1566
|
return;
|
|
1280
1567
|
}
|
|
1281
1568
|
// All changes are in deleted lines (nothing to render, just clear)
|
|
@@ -1287,7 +1574,7 @@ export class TUI extends Container {
|
|
|
1287
1574
|
const targetRow = Math.max(0, newLines.length - 1);
|
|
1288
1575
|
if (targetRow < prevViewportTop) {
|
|
1289
1576
|
logRedraw(`deleted lines moved viewport up (${targetRow} < ${prevViewportTop})`);
|
|
1290
|
-
fullRender(true);
|
|
1577
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1291
1578
|
return;
|
|
1292
1579
|
}
|
|
1293
1580
|
const lineDiff = computeLineDiff(targetRow);
|
|
@@ -1300,7 +1587,7 @@ export class TUI extends Container {
|
|
|
1300
1587
|
const extraLines = this.previousLines.length - newLines.length;
|
|
1301
1588
|
if (extraLines > height) {
|
|
1302
1589
|
logRedraw(`extraLines > height (${extraLines} > ${height})`);
|
|
1303
|
-
fullRender(true);
|
|
1590
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1304
1591
|
return;
|
|
1305
1592
|
}
|
|
1306
1593
|
const clearStartOffset = newLines.length === 0 ? 0 : 1;
|
|
@@ -1308,7 +1595,7 @@ export class TUI extends Container {
|
|
|
1308
1595
|
buffer += `\x1b[${clearStartOffset}B`;
|
|
1309
1596
|
}
|
|
1310
1597
|
for (let i = 0; i < extraLines; i++) {
|
|
1311
|
-
buffer +=
|
|
1598
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1312
1599
|
if (i < extraLines - 1)
|
|
1313
1600
|
buffer += "\x1b[1B";
|
|
1314
1601
|
}
|
|
@@ -1322,7 +1609,7 @@ export class TUI extends Container {
|
|
|
1322
1609
|
this.hardwareCursorRow = targetRow;
|
|
1323
1610
|
}
|
|
1324
1611
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1325
|
-
this.
|
|
1612
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1326
1613
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1327
1614
|
this.previousWidth = width;
|
|
1328
1615
|
this.previousHeight = height;
|
|
@@ -1352,12 +1639,20 @@ export class TUI extends Container {
|
|
|
1352
1639
|
}
|
|
1353
1640
|
if (firstVisibleChanged === -1) {
|
|
1354
1641
|
if (lineCountDelta !== 0) {
|
|
1355
|
-
|
|
1642
|
+
if (preserveMuxScrollback) {
|
|
1643
|
+
// Above-viewport scrollback may stay stale in mux panes; only the visible viewport is repainted.
|
|
1644
|
+
if (!this.renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, viewportTop)) {
|
|
1645
|
+
fullRender(true, false);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
else {
|
|
1649
|
+
this.renderScrollbackReplay(newLines, rawLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow);
|
|
1650
|
+
}
|
|
1356
1651
|
return;
|
|
1357
1652
|
}
|
|
1358
1653
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1359
1654
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1360
|
-
this.
|
|
1655
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1361
1656
|
this.previousWidth = width;
|
|
1362
1657
|
this.previousHeight = height;
|
|
1363
1658
|
this.previousViewportTop = viewportTop;
|
|
@@ -1374,7 +1669,7 @@ export class TUI extends Container {
|
|
|
1374
1669
|
for (let row = 0; row < height; row++) {
|
|
1375
1670
|
if (row > 0)
|
|
1376
1671
|
buffer += "\r\n";
|
|
1377
|
-
buffer +=
|
|
1672
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1378
1673
|
buffer += newLines[viewportTop + row] ?? "";
|
|
1379
1674
|
}
|
|
1380
1675
|
buffer += TUI.FRAME_END;
|
|
@@ -1384,7 +1679,7 @@ export class TUI extends Container {
|
|
|
1384
1679
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1385
1680
|
this.previousViewportTop = viewportTop;
|
|
1386
1681
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1387
|
-
this.
|
|
1682
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1388
1683
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1389
1684
|
this.previousWidth = width;
|
|
1390
1685
|
this.previousHeight = height;
|
|
@@ -1433,12 +1728,12 @@ export class TUI extends Container {
|
|
|
1433
1728
|
const imageStartScreenRow = i - viewportTop;
|
|
1434
1729
|
if (imageStartScreenRow < 0 || imageStartScreenRow + imageReservedRows > height) {
|
|
1435
1730
|
logRedraw(`kitty image pre-clear would scroll (${imageStartScreenRow} + ${imageReservedRows} > ${height})`);
|
|
1436
|
-
fullRender(true);
|
|
1731
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1437
1732
|
return;
|
|
1438
1733
|
}
|
|
1439
|
-
buffer +=
|
|
1734
|
+
buffer += `\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1440
1735
|
for (let row = 1; row < imageReservedRows; row++) {
|
|
1441
|
-
buffer +=
|
|
1736
|
+
buffer += `\r\n\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1442
1737
|
}
|
|
1443
1738
|
buffer += `\x1b[${imageReservedRows - 1}A`;
|
|
1444
1739
|
buffer += line;
|
|
@@ -1446,32 +1741,44 @@ export class TUI extends Container {
|
|
|
1446
1741
|
i += imageReservedRows - 1;
|
|
1447
1742
|
continue;
|
|
1448
1743
|
}
|
|
1449
|
-
buffer +=
|
|
1450
|
-
|
|
1744
|
+
buffer += `\x1b[2K${TUI.SEGMENT_RESET}`; // Clear current line
|
|
1745
|
+
const lineWidth = visibleWidth(line);
|
|
1746
|
+
if (!isImage && lineWidth > width) {
|
|
1451
1747
|
// Log all lines to crash file for debugging
|
|
1452
1748
|
const crashLogPath = path.join(os.homedir(), ".senpi", "agent", "senpi-crash.log");
|
|
1453
1749
|
const crashData = [
|
|
1454
1750
|
`Crash at ${new Date().toISOString()}`,
|
|
1455
1751
|
`Terminal width: ${width}`,
|
|
1456
|
-
`Line ${i} visible width: ${
|
|
1752
|
+
`Line ${i} visible width: ${lineWidth}`,
|
|
1457
1753
|
"",
|
|
1458
1754
|
"=== All rendered lines ===",
|
|
1459
1755
|
...newLines.map((l, idx) => `[${idx}] (w=${visibleWidth(l)}) ${l}`),
|
|
1460
1756
|
"",
|
|
1461
1757
|
].join("\n");
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1758
|
+
if (process.env.PI_TUI_STRICT_RENDER === "1") {
|
|
1759
|
+
const crashDumpWritten = writeRenderDiagnosticBestEffort(crashLogPath, crashData);
|
|
1760
|
+
// Clean up terminal state before throwing
|
|
1761
|
+
this.stop();
|
|
1762
|
+
const errorMsg = [
|
|
1763
|
+
`Rendered line ${i} exceeds terminal width (${lineWidth} > ${width}).`,
|
|
1764
|
+
"",
|
|
1765
|
+
"This is likely caused by a custom TUI component not truncating its output.",
|
|
1766
|
+
"Use visibleWidth() to measure and truncateToWidth() to truncate lines.",
|
|
1767
|
+
"",
|
|
1768
|
+
crashDumpWritten
|
|
1769
|
+
? `Debug log written to: ${crashLogPath}`
|
|
1770
|
+
: `Debug log could not be written to: ${crashLogPath}`,
|
|
1771
|
+
].join("\n");
|
|
1772
|
+
throw new Error(errorMsg);
|
|
1773
|
+
}
|
|
1774
|
+
if (!this.overWideCrashDumpWritten) {
|
|
1775
|
+
writeRenderDiagnosticBestEffort(crashLogPath, crashData);
|
|
1776
|
+
this.overWideCrashDumpWritten = true;
|
|
1777
|
+
}
|
|
1778
|
+
const truncatedLine = sliceByColumn(line, 0, width, true) + TUI.SEGMENT_RESET;
|
|
1779
|
+
newLines[i] = truncatedLine;
|
|
1780
|
+
buffer += truncatedLine;
|
|
1781
|
+
continue;
|
|
1475
1782
|
}
|
|
1476
1783
|
buffer += line;
|
|
1477
1784
|
}
|
|
@@ -1487,7 +1794,7 @@ export class TUI extends Container {
|
|
|
1487
1794
|
}
|
|
1488
1795
|
const extraLines = this.previousLines.length - newLines.length;
|
|
1489
1796
|
for (let i = newLines.length; i < this.previousLines.length; i++) {
|
|
1490
|
-
buffer +=
|
|
1797
|
+
buffer += `\r\n\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1491
1798
|
}
|
|
1492
1799
|
// Move cursor back to end of new content
|
|
1493
1800
|
buffer += `\x1b[${extraLines}A`;
|
|
@@ -1519,7 +1826,8 @@ export class TUI extends Container {
|
|
|
1519
1826
|
"=== buffer ===",
|
|
1520
1827
|
JSON.stringify(buffer),
|
|
1521
1828
|
].join("\n");
|
|
1522
|
-
fs.writeFileSync(debugPath, debugData);
|
|
1829
|
+
fs.writeFileSync(debugPath, debugData, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
1830
|
+
chmodDiagnosticLogBestEffort(debugPath);
|
|
1523
1831
|
}
|
|
1524
1832
|
// Write entire buffer at once
|
|
1525
1833
|
this.terminal.write(buffer);
|
|
@@ -1533,8 +1841,10 @@ export class TUI extends Container {
|
|
|
1533
1841
|
this.previousViewportTop = Math.max(prevViewportTop, finalCursorRow - height + 1);
|
|
1534
1842
|
// Position hardware cursor for IME
|
|
1535
1843
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1536
|
-
this.
|
|
1537
|
-
|
|
1844
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1845
|
+
if (needsKittyImageExpansion) {
|
|
1846
|
+
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1847
|
+
}
|
|
1538
1848
|
this.previousWidth = width;
|
|
1539
1849
|
this.previousHeight = height;
|
|
1540
1850
|
}
|
|
@@ -1544,34 +1854,36 @@ export class TUI extends Container {
|
|
|
1544
1854
|
* @param totalLines Total number of rendered lines
|
|
1545
1855
|
*/
|
|
1546
1856
|
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
1857
|
let buffer = "";
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
1858
|
+
if (!cursorPos || totalLines <= 0) {
|
|
1859
|
+
if (this.#lastCursorVisibility !== false) {
|
|
1860
|
+
buffer += "\x1b[?25l";
|
|
1861
|
+
this.#lastCursorVisibility = false;
|
|
1862
|
+
}
|
|
1559
1863
|
}
|
|
1560
|
-
else
|
|
1561
|
-
|
|
1864
|
+
else {
|
|
1865
|
+
// Clamp cursor position to valid range
|
|
1866
|
+
const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
|
|
1867
|
+
const targetCol = Math.max(0, cursorPos.col);
|
|
1868
|
+
// Move cursor from current position to target
|
|
1869
|
+
const rowDelta = targetRow - this.hardwareCursorRow;
|
|
1870
|
+
if (rowDelta > 0) {
|
|
1871
|
+
buffer += `\x1b[${rowDelta}B`; // Move down
|
|
1872
|
+
}
|
|
1873
|
+
else if (rowDelta < 0) {
|
|
1874
|
+
buffer += `\x1b[${-rowDelta}A`; // Move up
|
|
1875
|
+
}
|
|
1876
|
+
// Move to absolute column (1-indexed)
|
|
1877
|
+
buffer += `\x1b[${targetCol + 1}G`;
|
|
1878
|
+
this.hardwareCursorRow = targetRow;
|
|
1879
|
+
if (this.#lastCursorVisibility !== this.showHardwareCursor) {
|
|
1880
|
+
buffer += this.showHardwareCursor ? "\x1b[?25h" : "\x1b[?25l";
|
|
1881
|
+
this.#lastCursorVisibility = this.showHardwareCursor;
|
|
1882
|
+
}
|
|
1562
1883
|
}
|
|
1563
|
-
// Move to absolute column (1-indexed)
|
|
1564
|
-
buffer += `\x1b[${targetCol + 1}G`;
|
|
1565
1884
|
if (buffer) {
|
|
1566
1885
|
this.terminal.write(buffer);
|
|
1567
1886
|
}
|
|
1568
|
-
this.hardwareCursorRow = targetRow;
|
|
1569
|
-
if (this.showHardwareCursor) {
|
|
1570
|
-
this.terminal.showCursor();
|
|
1571
|
-
}
|
|
1572
|
-
else {
|
|
1573
|
-
this.terminal.hideCursor();
|
|
1574
|
-
}
|
|
1575
1887
|
}
|
|
1576
1888
|
/**
|
|
1577
1889
|
* Query the terminal's default background color with OSC 11 (`ESC ] 11 ; ? BEL`).
|