@pencil-agent/nano-pencil 1.14.2 → 1.14.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/dist/build-meta.json +3 -3
- package/dist/builtin-extensions.d.ts +4 -0
- package/dist/builtin-extensions.js +16 -16
- package/dist/core/i18n/slash-commands.d.ts +11 -0
- package/dist/core/i18n/slash-commands.js +15 -4
- package/dist/core/i18n/slash-commands.zh.d.ts +11 -0
- package/dist/core/i18n/slash-commands.zh.js +15 -4
- package/dist/core/runtime/slash-command-catalog.d.ts +2 -1
- package/dist/core/runtime/slash-command-catalog.js +9 -1
- package/dist/core/slash-commands.d.ts +9 -1
- package/dist/core/slash-commands.js +77 -32
- package/dist/extensions/defaults/browser/index.js +1 -1
- package/dist/extensions/defaults/debug/index.js +28 -3
- package/dist/extensions/defaults/link-world/index.js +1 -1
- package/dist/extensions/defaults/loop/cron/cron-scheduler.js +19 -0
- package/dist/extensions/defaults/security-audit/index.js +4 -4
- package/dist/extensions/defaults/team/team-ui.js +4 -14
- package/dist/extensions/defaults/token-save/index.js +8 -1
- package/dist/modes/interactive/interactive-mode.js +5 -15
- package/dist/node_modules/@pencil-agent/ai/models.generated.js +14 -14
- package/dist/node_modules/@pencil-agent/tui/autocomplete.js +7 -2
- package/dist/node_modules/@pencil-agent/tui/tui.d.ts +8 -3
- package/dist/node_modules/@pencil-agent/tui/tui.js +63 -31
- package/dist/packages/mem-core/extension.js +150 -83
- package/package.json +2 -2
|
@@ -11,12 +11,8 @@ import { renderTeamDashboard, renderTeamFooterStatus } from "./team-dashboard.js
|
|
|
11
11
|
import { formatPsycheWeights } from "./team-psyche.js";
|
|
12
12
|
export const TEAM_MESSAGE_TYPE = "team";
|
|
13
13
|
let dashboardVisible = false;
|
|
14
|
-
let dashboardAutoHideTimer;
|
|
15
14
|
export function clearTeamDashboardTimer() {
|
|
16
|
-
|
|
17
|
-
clearTimeout(dashboardAutoHideTimer);
|
|
18
|
-
dashboardAutoHideTimer = undefined;
|
|
19
|
-
}
|
|
15
|
+
// Kept for the extension shutdown contract; dashboard visibility is now explicit.
|
|
20
16
|
}
|
|
21
17
|
export function createTeamMessageRenderer() {
|
|
22
18
|
return (message, _options, theme) => {
|
|
@@ -129,15 +125,9 @@ export function toggleTeamDashboard(ctx, teamRuntime) {
|
|
|
129
125
|
export function updateTeamUi(ctx, teamRuntime) {
|
|
130
126
|
const teammates = teamRuntime.getAllTeammates();
|
|
131
127
|
const hasRunning = teammates.some((teammate) => teammate.status === "running");
|
|
132
|
-
|
|
133
|
-
ctx.ui.setStatus("team", renderTeamFooterStatus(teammates));
|
|
134
|
-
ctx.ui.setWidget("team-dashboard",
|
|
135
|
-
if (!dashboardVisible && !hasRunning && teammates.length > 0) {
|
|
136
|
-
dashboardAutoHideTimer = setTimeout(() => {
|
|
137
|
-
ctx.ui.setWidget("team-dashboard", undefined);
|
|
138
|
-
dashboardAutoHideTimer = undefined;
|
|
139
|
-
}, 30_000);
|
|
140
|
-
}
|
|
128
|
+
const shouldShowTeamUi = dashboardVisible || hasRunning;
|
|
129
|
+
ctx.ui.setStatus("team", shouldShowTeamUi ? renderTeamFooterStatus(teammates) : undefined);
|
|
130
|
+
ctx.ui.setWidget("team-dashboard", shouldShowTeamUi ? renderTeamDashboard(teammates, 80, { expanded: dashboardVisible }) : undefined);
|
|
141
131
|
}
|
|
142
132
|
export function setTeamActivity(ctx, lines) {
|
|
143
133
|
const [firstLine] = lines;
|
|
@@ -29,7 +29,14 @@ export default function tokenSaveExtension(api) {
|
|
|
29
29
|
configuredFilters = filters;
|
|
30
30
|
});
|
|
31
31
|
api.registerCommand("tokensave", {
|
|
32
|
-
description: "Show
|
|
32
|
+
description: "Show how much shell output was shortened. Usage: /tokensave [summary|history|reload|plan <cmd>]",
|
|
33
|
+
getArgumentCompletions: (argumentPrefix) => {
|
|
34
|
+
const prefix = argumentPrefix.trim().toLowerCase();
|
|
35
|
+
const values = ["summary", "history", "reload", "plan"]
|
|
36
|
+
.filter((value) => value.startsWith(prefix))
|
|
37
|
+
.map((value) => ({ value, label: value }));
|
|
38
|
+
return values.length > 0 ? values : null;
|
|
39
|
+
},
|
|
33
40
|
async handler(args, ctx) {
|
|
34
41
|
const trimmed = args.trim();
|
|
35
42
|
if (trimmed === "reload") {
|
|
@@ -13,7 +13,7 @@ import { createCompactionSummaryMessage } from "../../core/messages.js";
|
|
|
13
13
|
import { listMCPServers, setMCPServerEnabled } from "../../core/mcp/mcp-config.js";
|
|
14
14
|
import { resolveModelScope } from "../../core/model-resolver.js";
|
|
15
15
|
import { SessionManager, } from "../../core/session/session-manager.js";
|
|
16
|
-
import { getLocalizedCommands } from "../../core/slash-commands.js";
|
|
16
|
+
import { formatSlashCommandDescription, getLocalizedCommands, inferSlashCommandCategory, } from "../../core/slash-commands.js";
|
|
17
17
|
import { t } from "../../core/i18n/index.js";
|
|
18
18
|
import { getActivePersonaId, getPersonaDir, getPersonaMcpConfigPath, getPersonaMemoryDir, getPersonaSoulDir, listPersonas, setActivePersonaId, toAbsolutePath, } from "../../core/persona/persona-manager.js";
|
|
19
19
|
import { NANOPENCIL_ALI_TOKEN_PLAN_ANTHROPIC_PROVIDER, NANOPENCIL_ALI_TOKEN_PLAN_OPENAI_PROVIDER, NANOPENCIL_WHATS_NEW, } from "../../nanopencil-defaults.js";
|
|
@@ -230,7 +230,7 @@ export class InteractiveMode {
|
|
|
230
230
|
const localizedCommands = getLocalizedCommands(t);
|
|
231
231
|
const slashCommands = localizedCommands.map((command) => ({
|
|
232
232
|
name: command.name,
|
|
233
|
-
description: command.description,
|
|
233
|
+
description: formatSlashCommandDescription(command.description, command.category, t),
|
|
234
234
|
}));
|
|
235
235
|
const modelCommand = slashCommands.find((command) => command.name === "model");
|
|
236
236
|
if (modelCommand) {
|
|
@@ -261,13 +261,13 @@ export class InteractiveMode {
|
|
|
261
261
|
// Convert prompt templates to SlashCommand format for autocomplete
|
|
262
262
|
const templateCommands = this.session.promptTemplates.map((cmd) => ({
|
|
263
263
|
name: cmd.name,
|
|
264
|
-
description: cmd.description,
|
|
264
|
+
description: formatSlashCommandDescription(cmd.description, inferSlashCommandCategory(cmd.name, "prompt"), t),
|
|
265
265
|
}));
|
|
266
266
|
// Convert extension commands to SlashCommand format
|
|
267
267
|
const builtinCommandNames = new Set(slashCommands.map((c) => c.name));
|
|
268
268
|
const extensionCommands = (this.session.extensionRunner?.getRegisteredCommands(builtinCommandNames) ?? []).map((cmd) => ({
|
|
269
269
|
name: cmd.name,
|
|
270
|
-
description: cmd.description ?? "(extension command)",
|
|
270
|
+
description: formatSlashCommandDescription(cmd.description ?? "(extension command)", inferSlashCommandCategory(cmd.name, "extension"), t),
|
|
271
271
|
getArgumentCompletions: cmd.getArgumentCompletions,
|
|
272
272
|
}));
|
|
273
273
|
// Build skill commands from session.skills (if enabled)
|
|
@@ -279,7 +279,7 @@ export class InteractiveMode {
|
|
|
279
279
|
this.skillCommands.set(commandName, skill.filePath);
|
|
280
280
|
skillCommandList.push({
|
|
281
281
|
name: commandName,
|
|
282
|
-
description: skill.description,
|
|
282
|
+
description: formatSlashCommandDescription(skill.description, inferSlashCommandCategory(skill.name, "skill"), t),
|
|
283
283
|
});
|
|
284
284
|
}
|
|
285
285
|
}
|
|
@@ -1984,11 +1984,6 @@ export class InteractiveMode {
|
|
|
1984
1984
|
this.editor.setText("");
|
|
1985
1985
|
return;
|
|
1986
1986
|
}
|
|
1987
|
-
if (text === "/debug") {
|
|
1988
|
-
this.handleDebugCommand();
|
|
1989
|
-
this.editor.setText("");
|
|
1990
|
-
return;
|
|
1991
|
-
}
|
|
1992
1987
|
if (text === "/arminsayshi") {
|
|
1993
1988
|
this.handleArminSaysHi();
|
|
1994
1989
|
this.editor.setText("");
|
|
@@ -2337,11 +2332,6 @@ export class InteractiveMode {
|
|
|
2337
2332
|
clear();
|
|
2338
2333
|
return true;
|
|
2339
2334
|
}
|
|
2340
|
-
if (text === "/debug") {
|
|
2341
|
-
this.handleDebugCommand();
|
|
2342
|
-
clear();
|
|
2343
|
-
return true;
|
|
2344
|
-
}
|
|
2345
2335
|
if (text === "/arminsayshi") {
|
|
2346
2336
|
this.handleArminSaysHi();
|
|
2347
2337
|
clear();
|
|
@@ -10833,10 +10833,10 @@ export const MODELS = {
|
|
|
10833
10833
|
reasoning: true,
|
|
10834
10834
|
input: ["text"],
|
|
10835
10835
|
cost: {
|
|
10836
|
-
input:
|
|
10837
|
-
output:
|
|
10838
|
-
cacheRead: 0.
|
|
10839
|
-
cacheWrite:
|
|
10836
|
+
input: 1.25,
|
|
10837
|
+
output: 3.75,
|
|
10838
|
+
cacheRead: 0.25,
|
|
10839
|
+
cacheWrite: 1.5625,
|
|
10840
10840
|
},
|
|
10841
10841
|
contextWindow: 1000000,
|
|
10842
10842
|
maxTokens: 65536,
|
|
@@ -11173,13 +11173,13 @@ export const MODELS = {
|
|
|
11173
11173
|
reasoning: true,
|
|
11174
11174
|
input: ["text"],
|
|
11175
11175
|
cost: {
|
|
11176
|
-
input: 0.
|
|
11177
|
-
output: 0.
|
|
11178
|
-
cacheRead: 0
|
|
11176
|
+
input: 0.125,
|
|
11177
|
+
output: 0.84,
|
|
11178
|
+
cacheRead: 0,
|
|
11179
11179
|
cacheWrite: 0,
|
|
11180
11180
|
},
|
|
11181
11181
|
contextWindow: 131072,
|
|
11182
|
-
maxTokens:
|
|
11182
|
+
maxTokens: 131070,
|
|
11183
11183
|
},
|
|
11184
11184
|
"z-ai/glm-4.5-air:free": {
|
|
11185
11185
|
id: "z-ai/glm-4.5-air:free",
|
|
@@ -13929,9 +13929,9 @@ export const MODELS = {
|
|
|
13929
13929
|
reasoning: true,
|
|
13930
13930
|
input: ["text", "image"],
|
|
13931
13931
|
cost: {
|
|
13932
|
-
input: 0.
|
|
13933
|
-
output:
|
|
13934
|
-
cacheRead: 0.
|
|
13932
|
+
input: 0.14,
|
|
13933
|
+
output: 0.28,
|
|
13934
|
+
cacheRead: 0.0028,
|
|
13935
13935
|
cacheWrite: 0,
|
|
13936
13936
|
},
|
|
13937
13937
|
contextWindow: 1050000,
|
|
@@ -13946,9 +13946,9 @@ export const MODELS = {
|
|
|
13946
13946
|
reasoning: true,
|
|
13947
13947
|
input: ["text", "image"],
|
|
13948
13948
|
cost: {
|
|
13949
|
-
input:
|
|
13950
|
-
output:
|
|
13951
|
-
cacheRead: 0.
|
|
13949
|
+
input: 0.435,
|
|
13950
|
+
output: 0.87,
|
|
13951
|
+
cacheRead: 0.0036,
|
|
13952
13952
|
cacheWrite: 0,
|
|
13953
13953
|
},
|
|
13954
13954
|
contextWindow: 1050000,
|
|
@@ -62,6 +62,10 @@ function parsePathPrefix(prefix) {
|
|
|
62
62
|
}
|
|
63
63
|
return { rawPrefix: prefix, isAtPrefix: false, isQuotedPrefix: false };
|
|
64
64
|
}
|
|
65
|
+
function getCurrentArgumentPrefix(argumentText) {
|
|
66
|
+
const match = argumentText.match(/(?:^|\s)(\S*)$/);
|
|
67
|
+
return match?.[1] ?? argumentText;
|
|
68
|
+
}
|
|
65
69
|
function buildCompletionValue(path, options) {
|
|
66
70
|
const needsQuotes = options.isQuotedPrefix || path.includes(" ");
|
|
67
71
|
const prefix = options.isAtPrefix ? "@" : "";
|
|
@@ -179,6 +183,7 @@ export class CombinedAutocompleteProvider {
|
|
|
179
183
|
// Space found - complete command arguments
|
|
180
184
|
const commandName = textBeforeCursor.slice(1, spaceIndex); // Command without "/"
|
|
181
185
|
const argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space
|
|
186
|
+
const argumentPrefix = getCurrentArgumentPrefix(argumentText);
|
|
182
187
|
const command = this.commands.find((cmd) => {
|
|
183
188
|
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
184
189
|
return name === commandName;
|
|
@@ -186,13 +191,13 @@ export class CombinedAutocompleteProvider {
|
|
|
186
191
|
if (!command || !("getArgumentCompletions" in command) || !command.getArgumentCompletions) {
|
|
187
192
|
return null; // No argument completion for this command
|
|
188
193
|
}
|
|
189
|
-
const argumentSuggestions = command.getArgumentCompletions(
|
|
194
|
+
const argumentSuggestions = command.getArgumentCompletions(argumentPrefix);
|
|
190
195
|
if (!argumentSuggestions || argumentSuggestions.length === 0) {
|
|
191
196
|
return null;
|
|
192
197
|
}
|
|
193
198
|
return {
|
|
194
199
|
items: argumentSuggestions,
|
|
195
|
-
prefix:
|
|
200
|
+
prefix: argumentPrefix,
|
|
196
201
|
};
|
|
197
202
|
}
|
|
198
203
|
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Minimal TUI implementation with differential rendering
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
|
-
* [WHO]: Component, Focusable,
|
|
5
|
+
* [WHO]: Provides Component, Focusable, Container, TUI, CURSOR_MARKER, isFocusable
|
|
6
6
|
* [FROM]: Depends on node:fs, node:os, node:path, ./keys.js, ./terminal-image.js
|
|
7
7
|
* [TO]: Consumed by packages/tui/src/index.ts
|
|
8
|
-
* [HERE]: packages/tui/src/tui.ts -
|
|
8
|
+
* [HERE]: packages/tui/src/tui.ts - core terminal render loop and component tree
|
|
9
9
|
*/
|
|
10
10
|
import type { Terminal } from "./terminal.js";
|
|
11
11
|
import { visibleWidth } from "./utils.js";
|
|
@@ -32,7 +32,7 @@ export interface Component {
|
|
|
32
32
|
* Invalidate any cached rendering state.
|
|
33
33
|
* Called when theme changes or when component needs to re-render from scratch.
|
|
34
34
|
*/
|
|
35
|
-
invalidate(): void;
|
|
35
|
+
invalidate?(): void;
|
|
36
36
|
}
|
|
37
37
|
type InputListenerResult = {
|
|
38
38
|
consume?: boolean;
|
|
@@ -150,6 +150,8 @@ export declare class TUI extends Container {
|
|
|
150
150
|
private fullRedrawCount;
|
|
151
151
|
private stopped;
|
|
152
152
|
private synchronizedOutputEnabled;
|
|
153
|
+
private strictWidthErrors;
|
|
154
|
+
private widthViolationLogged;
|
|
153
155
|
private overlayStack;
|
|
154
156
|
constructor(terminal: Terminal, showHardwareCursor?: boolean);
|
|
155
157
|
get fullRedraws(): number;
|
|
@@ -209,6 +211,9 @@ export declare class TUI extends Container {
|
|
|
209
211
|
* @returns Cursor position { row, col } or null if no marker found
|
|
210
212
|
*/
|
|
211
213
|
private extractCursorPosition;
|
|
214
|
+
private getWidthViolationError;
|
|
215
|
+
private logWidthViolation;
|
|
216
|
+
private constrainRenderedLines;
|
|
212
217
|
private doRender;
|
|
213
218
|
/**
|
|
214
219
|
* Position the hardware cursor for IME candidate window.
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Minimal TUI implementation with differential rendering
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
|
-
* [WHO]: Component, Focusable,
|
|
5
|
+
* [WHO]: Provides Component, Focusable, Container, TUI, CURSOR_MARKER, isFocusable
|
|
6
6
|
* [FROM]: Depends on node:fs, node:os, node:path, ./keys.js, ./terminal-image.js
|
|
7
7
|
* [TO]: Consumed by packages/tui/src/index.ts
|
|
8
|
-
* [HERE]: packages/tui/src/tui.ts -
|
|
8
|
+
* [HERE]: packages/tui/src/tui.ts - core terminal render loop and component tree
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
@@ -112,6 +112,8 @@ export class TUI extends Container {
|
|
|
112
112
|
fullRedrawCount = 0;
|
|
113
113
|
stopped = false;
|
|
114
114
|
synchronizedOutputEnabled = shouldUseSynchronizedOutput();
|
|
115
|
+
strictWidthErrors = process.env.NANOPENCIL_TUI_STRICT_WIDTH === "1";
|
|
116
|
+
widthViolationLogged = false;
|
|
115
117
|
// Overlay stack for modal components rendered on top of base content
|
|
116
118
|
overlayStack = [];
|
|
117
119
|
constructor(terminal, showHardwareCursor) {
|
|
@@ -685,6 +687,63 @@ export class TUI extends Container {
|
|
|
685
687
|
}
|
|
686
688
|
return null;
|
|
687
689
|
}
|
|
690
|
+
getWidthViolationError(lineIndex, lineWidth, width, crashLogPath) {
|
|
691
|
+
const errorMsg = [
|
|
692
|
+
`Rendered line ${lineIndex} exceeds terminal width (${lineWidth} > ${width}).`,
|
|
693
|
+
"",
|
|
694
|
+
"This is likely caused by a custom TUI component not truncating its output.",
|
|
695
|
+
"Use visibleWidth() to measure and truncateToWidth() to truncate lines.",
|
|
696
|
+
"",
|
|
697
|
+
`Debug log written to: ${crashLogPath}`,
|
|
698
|
+
].join("\n");
|
|
699
|
+
return new Error(errorMsg);
|
|
700
|
+
}
|
|
701
|
+
logWidthViolation(lineIndex, lineWidth, width, lines) {
|
|
702
|
+
const crashLogPath = path.join(os.homedir(), ".nanopencil", "agent", "nanopencil-crash.log");
|
|
703
|
+
if (!this.widthViolationLogged || this.strictWidthErrors) {
|
|
704
|
+
const crashData = [
|
|
705
|
+
`Render width violation at ${new Date().toISOString()}`,
|
|
706
|
+
`Terminal width: ${width}`,
|
|
707
|
+
`Line ${lineIndex} visible width: ${lineWidth}`,
|
|
708
|
+
`Mode: ${this.strictWidthErrors ? "strict" : "truncated"}`,
|
|
709
|
+
"",
|
|
710
|
+
"=== All rendered lines ===",
|
|
711
|
+
...lines.map((line, idx) => `[${idx}] (w=${visibleWidth(line)}) ${line}`),
|
|
712
|
+
"",
|
|
713
|
+
].join("\n");
|
|
714
|
+
try {
|
|
715
|
+
fs.mkdirSync(path.dirname(crashLogPath), { recursive: true });
|
|
716
|
+
fs.writeFileSync(crashLogPath, crashData);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
// Diagnostics must never take down the TUI render loop.
|
|
720
|
+
}
|
|
721
|
+
this.widthViolationLogged = true;
|
|
722
|
+
}
|
|
723
|
+
return crashLogPath;
|
|
724
|
+
}
|
|
725
|
+
constrainRenderedLines(lines, width) {
|
|
726
|
+
if (width <= 0)
|
|
727
|
+
return lines;
|
|
728
|
+
let constrained;
|
|
729
|
+
for (let i = 0; i < lines.length; i++) {
|
|
730
|
+
const line = lines[i];
|
|
731
|
+
if (isImageLine(line))
|
|
732
|
+
continue;
|
|
733
|
+
const lineWidth = visibleWidth(line);
|
|
734
|
+
if (lineWidth <= width)
|
|
735
|
+
continue;
|
|
736
|
+
const crashLogPath = this.logWidthViolation(i, lineWidth, width, lines);
|
|
737
|
+
if (this.strictWidthErrors) {
|
|
738
|
+
this.stop();
|
|
739
|
+
throw this.getWidthViolationError(i, lineWidth, width, crashLogPath);
|
|
740
|
+
}
|
|
741
|
+
if (!constrained)
|
|
742
|
+
constrained = lines.slice();
|
|
743
|
+
constrained[i] = sliceByColumn(line, 0, width, true);
|
|
744
|
+
}
|
|
745
|
+
return constrained ?? lines;
|
|
746
|
+
}
|
|
688
747
|
doRender() {
|
|
689
748
|
if (this.stopped)
|
|
690
749
|
return;
|
|
@@ -706,6 +765,7 @@ export class TUI extends Container {
|
|
|
706
765
|
}
|
|
707
766
|
// Extract cursor position before applying line resets (marker must be found first)
|
|
708
767
|
const cursorPos = this.extractCursorPosition(newLines, height);
|
|
768
|
+
newLines = this.constrainRenderedLines(newLines, width);
|
|
709
769
|
newLines = this.applyLineResets(newLines);
|
|
710
770
|
// Width changed - need full re-render (line wrapping changes)
|
|
711
771
|
const widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;
|
|
@@ -873,35 +933,7 @@ export class TUI extends Container {
|
|
|
873
933
|
if (i > firstChanged)
|
|
874
934
|
buffer += "\r\n";
|
|
875
935
|
buffer += "\x1b[2K"; // Clear current line
|
|
876
|
-
|
|
877
|
-
const isImage = isImageLine(line);
|
|
878
|
-
if (!isImage && visibleWidth(line) > width) {
|
|
879
|
-
// Log all lines to crash file for debugging
|
|
880
|
-
const crashLogPath = path.join(os.homedir(), ".nanopencil", "agent", "nanopencil-crash.log");
|
|
881
|
-
const crashData = [
|
|
882
|
-
`Crash at ${new Date().toISOString()}`,
|
|
883
|
-
`Terminal width: ${width}`,
|
|
884
|
-
`Line ${i} visible width: ${visibleWidth(line)}`,
|
|
885
|
-
"",
|
|
886
|
-
"=== All rendered lines ===",
|
|
887
|
-
...newLines.map((l, idx) => `[${idx}] (w=${visibleWidth(l)}) ${l}`),
|
|
888
|
-
"",
|
|
889
|
-
].join("\n");
|
|
890
|
-
fs.mkdirSync(path.dirname(crashLogPath), { recursive: true });
|
|
891
|
-
fs.writeFileSync(crashLogPath, crashData);
|
|
892
|
-
// Clean up terminal state before throwing
|
|
893
|
-
this.stop();
|
|
894
|
-
const errorMsg = [
|
|
895
|
-
`Rendered line ${i} exceeds terminal width (${visibleWidth(line)} > ${width}).`,
|
|
896
|
-
"",
|
|
897
|
-
"This is likely caused by a custom TUI component not truncating its output.",
|
|
898
|
-
"Use visibleWidth() to measure and truncateToWidth() to truncate lines.",
|
|
899
|
-
"",
|
|
900
|
-
`Debug log written to: ${crashLogPath}`,
|
|
901
|
-
].join("\n");
|
|
902
|
-
throw new Error(errorMsg);
|
|
903
|
-
}
|
|
904
|
-
buffer += line;
|
|
936
|
+
buffer += newLines[i];
|
|
905
937
|
}
|
|
906
938
|
// Track where cursor ended up after rendering
|
|
907
939
|
let finalCursorRow = renderEnd;
|