@pencil-agent/nano-pencil 1.11.42 → 1.11.44
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/modes/interactive/components/assistant-message.js +10 -20
- package/dist/modes/interactive/interactive-mode.js +18 -1
- package/dist/modes/interactive/theme/dark.json +7 -5
- package/dist/modes/interactive/theme/light.json +7 -5
- package/dist/modes/interactive/theme/theme-schema.json +1 -24
- package/dist/modes/interactive/theme/theme.d.ts +2 -2
- package/dist/modes/interactive/theme/theme.js +0 -6
- package/dist/modes/interactive/theme/warm.json +1 -5
- package/dist/node_modules/@pencil-agent/ai/models.generated.js +7 -7
- package/dist/node_modules/@pencil-agent/tui/terminal.d.ts +0 -2
- package/dist/node_modules/@pencil-agent/tui/terminal.js +1 -21
- package/dist/node_modules/@pencil-agent/tui/tui.d.ts +2 -0
- package/dist/node_modules/@pencil-agent/tui/tui.js +29 -19
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* [TO]: Consumed by modes/interactive/components/index.ts
|
|
5
5
|
* [HERE]: modes/interactive/components/assistant-message.ts - assistant message display
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { Container, Markdown, Spacer, Text } from "@pencil-agent/tui";
|
|
8
8
|
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
|
9
9
|
/**
|
|
10
10
|
* Component that renders a complete assistant message
|
|
@@ -39,42 +39,32 @@ export class AssistantMessageComponent extends Container {
|
|
|
39
39
|
// Clear content container
|
|
40
40
|
this.contentContainer.clear();
|
|
41
41
|
const hasVisibleContent = message.content.some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
if (hasVisibleContent) {
|
|
43
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
44
|
+
}
|
|
44
45
|
// Render content in order
|
|
45
46
|
for (let i = 0; i < message.content.length; i++) {
|
|
46
47
|
const content = message.content[i];
|
|
47
48
|
if (content.type === "text" && content.text.trim()) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.contentContainer.addChild(new Spacer(1));
|
|
52
|
-
}
|
|
53
|
-
addedAssistantLabelForText = true;
|
|
54
|
-
}
|
|
55
|
-
const textBox = new Box(1, 1, (text) => theme.bg("assistantMessageBg", text));
|
|
56
|
-
textBox.addChild(new Markdown(content.text.trim(), 0, 0, this.markdownTheme, {
|
|
57
|
-
color: (text) => theme.fg("assistantMessageText", text),
|
|
58
|
-
}));
|
|
59
|
-
this.contentContainer.addChild(textBox);
|
|
49
|
+
// Assistant text messages with no background - trim the text
|
|
50
|
+
// Set paddingY=0 to avoid extra spacing before tool executions
|
|
51
|
+
this.contentContainer.addChild(new Markdown(content.text.trim(), 1, 0, this.markdownTheme));
|
|
60
52
|
}
|
|
61
53
|
else if (content.type === "thinking" && content.thinking.trim()) {
|
|
62
|
-
seenThinking = true;
|
|
63
54
|
// Add spacing only when another visible assistant content block follows.
|
|
64
55
|
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
|
65
56
|
const hasVisibleContentAfter = message.content
|
|
66
57
|
.slice(i + 1)
|
|
67
58
|
.some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
|
|
68
|
-
this.contentContainer.addChild(new Spacer(1));
|
|
69
|
-
const thinkingLabel = new Text(theme.italic(theme.fg("thinkingText", "I'm thinking...")), 1, 0);
|
|
70
59
|
if (this.hideThinkingBlock) {
|
|
71
|
-
|
|
60
|
+
// Show static "Thinking..." label when hidden
|
|
61
|
+
this.contentContainer.addChild(new Text(theme.italic(theme.fg("thinkingText", "Thinking...")), 1, 0));
|
|
72
62
|
if (hasVisibleContentAfter) {
|
|
73
63
|
this.contentContainer.addChild(new Spacer(1));
|
|
74
64
|
}
|
|
75
65
|
}
|
|
76
66
|
else {
|
|
77
|
-
|
|
67
|
+
// Thinking traces in thinkingText color, italic
|
|
78
68
|
this.contentContainer.addChild(new Markdown(content.thinking.trim(), 1, 0, this.markdownTheme, {
|
|
79
69
|
color: (text) => theme.fg("thinkingText", text),
|
|
80
70
|
italic: true,
|
|
@@ -829,7 +829,6 @@ export class InteractiveMode {
|
|
|
829
829
|
this.pendingTools.clear();
|
|
830
830
|
// Render any messages added via setup, or show empty session
|
|
831
831
|
this.renderInitialMessages();
|
|
832
|
-
this.ui.requestRender();
|
|
833
832
|
return { cancelled: false };
|
|
834
833
|
},
|
|
835
834
|
fork: async (entryId) => {
|
|
@@ -2882,6 +2881,10 @@ export class InteractiveMode {
|
|
|
2882
2881
|
const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`;
|
|
2883
2882
|
this.showStatus(`Session compacted ${times}`);
|
|
2884
2883
|
}
|
|
2884
|
+
// Force full re-render to reset viewport state after rebuilding chat.
|
|
2885
|
+
// Without this, maxLinesRendered retains the old value and the viewport
|
|
2886
|
+
// may point past the actual content end after compaction or session switch.
|
|
2887
|
+
this.ui.requestRender(true);
|
|
2885
2888
|
}
|
|
2886
2889
|
async getUserInput() {
|
|
2887
2890
|
return new Promise((resolve) => {
|
|
@@ -2895,6 +2898,20 @@ export class InteractiveMode {
|
|
|
2895
2898
|
this.chatContainer.clear();
|
|
2896
2899
|
const context = this.sessionManager.buildSessionContext();
|
|
2897
2900
|
this.renderSessionContext(context);
|
|
2901
|
+
// Re-add optimistic user messages not yet persisted to session.
|
|
2902
|
+
// Cleared by chatContainer.clear() above but absent from buildSessionContext().
|
|
2903
|
+
for (const msg of this.optimisticUserMessages) {
|
|
2904
|
+
this.addMessageToChat({
|
|
2905
|
+
role: "user",
|
|
2906
|
+
content: [{ type: "text", text: msg.text }],
|
|
2907
|
+
timestamp: Date.now(),
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
// Force full re-render to reset maxLinesRendered, which tracks the
|
|
2911
|
+
// terminal working area. After a clear+rebuild, content may be shorter
|
|
2912
|
+
// than the previous working area, causing the viewport to point past
|
|
2913
|
+
// the actual content end.
|
|
2914
|
+
this.ui.requestRender(true);
|
|
2898
2915
|
}
|
|
2899
2916
|
// =========================================================================
|
|
2900
2917
|
// Key handlers
|
|
@@ -30,11 +30,10 @@
|
|
|
30
30
|
"dim": "dimGray",
|
|
31
31
|
"text": "",
|
|
32
32
|
"thinkingText": "gray",
|
|
33
|
+
|
|
33
34
|
"selectedBg": "selectedBg",
|
|
34
35
|
"userMessageBg": "userMsgBg",
|
|
35
36
|
"userMessageText": "",
|
|
36
|
-
"assistantMessageBg": "#1e1e24",
|
|
37
|
-
"assistantMessageText": "",
|
|
38
37
|
"customMessageBg": "customMsgBg",
|
|
39
38
|
"customMessageText": "",
|
|
40
39
|
"customMessageLabel": "#9575cd",
|
|
@@ -43,6 +42,7 @@
|
|
|
43
42
|
"toolErrorBg": "toolErrorBg",
|
|
44
43
|
"toolTitle": "",
|
|
45
44
|
"toolOutput": "gray",
|
|
45
|
+
|
|
46
46
|
"mdHeading": "#f0c674",
|
|
47
47
|
"mdLink": "#81a2be",
|
|
48
48
|
"mdLinkUrl": "dimGray",
|
|
@@ -53,9 +53,11 @@
|
|
|
53
53
|
"mdQuoteBorder": "gray",
|
|
54
54
|
"mdHr": "gray",
|
|
55
55
|
"mdListBullet": "accent",
|
|
56
|
+
|
|
56
57
|
"toolDiffAdded": "green",
|
|
57
58
|
"toolDiffRemoved": "red",
|
|
58
59
|
"toolDiffContext": "gray",
|
|
60
|
+
|
|
59
61
|
"syntaxComment": "#6A9955",
|
|
60
62
|
"syntaxKeyword": "#569CD6",
|
|
61
63
|
"syntaxFunction": "#DCDCAA",
|
|
@@ -65,15 +67,15 @@
|
|
|
65
67
|
"syntaxType": "#4EC9B0",
|
|
66
68
|
"syntaxOperator": "#D4D4D4",
|
|
67
69
|
"syntaxPunctuation": "#D4D4D4",
|
|
70
|
+
|
|
68
71
|
"thinkingOff": "darkGray",
|
|
69
72
|
"thinkingMinimal": "#6e6e6e",
|
|
70
73
|
"thinkingLow": "#5f87af",
|
|
71
74
|
"thinkingMedium": "#81a2be",
|
|
72
75
|
"thinkingHigh": "#b294bb",
|
|
73
76
|
"thinkingXhigh": "#d183e8",
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
"assistantLabel": "gray"
|
|
77
|
+
|
|
78
|
+
"bashMode": "green"
|
|
77
79
|
},
|
|
78
80
|
"export": {
|
|
79
81
|
"pageBg": "#18181e",
|
|
@@ -29,11 +29,10 @@
|
|
|
29
29
|
"dim": "dimGray",
|
|
30
30
|
"text": "",
|
|
31
31
|
"thinkingText": "mediumGray",
|
|
32
|
+
|
|
32
33
|
"selectedBg": "selectedBg",
|
|
33
34
|
"userMessageBg": "userMsgBg",
|
|
34
35
|
"userMessageText": "",
|
|
35
|
-
"assistantMessageBg": "#f0f0f0",
|
|
36
|
-
"assistantMessageText": "",
|
|
37
36
|
"customMessageBg": "customMsgBg",
|
|
38
37
|
"customMessageText": "",
|
|
39
38
|
"customMessageLabel": "#7e57c2",
|
|
@@ -42,6 +41,7 @@
|
|
|
42
41
|
"toolErrorBg": "toolErrorBg",
|
|
43
42
|
"toolTitle": "",
|
|
44
43
|
"toolOutput": "mediumGray",
|
|
44
|
+
|
|
45
45
|
"mdHeading": "yellow",
|
|
46
46
|
"mdLink": "blue",
|
|
47
47
|
"mdLinkUrl": "dimGray",
|
|
@@ -52,9 +52,11 @@
|
|
|
52
52
|
"mdQuoteBorder": "mediumGray",
|
|
53
53
|
"mdHr": "mediumGray",
|
|
54
54
|
"mdListBullet": "green",
|
|
55
|
+
|
|
55
56
|
"toolDiffAdded": "green",
|
|
56
57
|
"toolDiffRemoved": "red",
|
|
57
58
|
"toolDiffContext": "mediumGray",
|
|
59
|
+
|
|
58
60
|
"syntaxComment": "#008000",
|
|
59
61
|
"syntaxKeyword": "#0000FF",
|
|
60
62
|
"syntaxFunction": "#795E26",
|
|
@@ -64,15 +66,15 @@
|
|
|
64
66
|
"syntaxType": "#267F99",
|
|
65
67
|
"syntaxOperator": "#000000",
|
|
66
68
|
"syntaxPunctuation": "#000000",
|
|
69
|
+
|
|
67
70
|
"thinkingOff": "lightGray",
|
|
68
71
|
"thinkingMinimal": "#767676",
|
|
69
72
|
"thinkingLow": "blue",
|
|
70
73
|
"thinkingMedium": "teal",
|
|
71
74
|
"thinkingHigh": "#875f87",
|
|
72
75
|
"thinkingXhigh": "#8b008b",
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
"assistantLabel": "mediumGray"
|
|
76
|
+
|
|
77
|
+
"bashMode": "green"
|
|
76
78
|
},
|
|
77
79
|
"export": {
|
|
78
80
|
"pageBg": "#f8f8f8",
|
|
@@ -3,10 +3,7 @@
|
|
|
3
3
|
"title": "NanoPencil Theme",
|
|
4
4
|
"description": "Theme schema for NanoPencil",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"required": [
|
|
7
|
-
"name",
|
|
8
|
-
"colors"
|
|
9
|
-
],
|
|
6
|
+
"required": ["name", "colors"],
|
|
10
7
|
"properties": {
|
|
11
8
|
"$schema": {
|
|
12
9
|
"type": "string",
|
|
@@ -49,13 +46,9 @@
|
|
|
49
46
|
"dim",
|
|
50
47
|
"text",
|
|
51
48
|
"thinkingText",
|
|
52
|
-
"userLabel",
|
|
53
|
-
"assistantLabel",
|
|
54
49
|
"selectedBg",
|
|
55
50
|
"userMessageBg",
|
|
56
51
|
"userMessageText",
|
|
57
|
-
"assistantMessageBg",
|
|
58
|
-
"assistantMessageText",
|
|
59
52
|
"customMessageBg",
|
|
60
53
|
"customMessageText",
|
|
61
54
|
"customMessageLabel",
|
|
@@ -139,14 +132,6 @@
|
|
|
139
132
|
"$ref": "#/$defs/colorValue",
|
|
140
133
|
"description": "Thinking block text color"
|
|
141
134
|
},
|
|
142
|
-
"userLabel": {
|
|
143
|
-
"$ref": "#/$defs/colorValue",
|
|
144
|
-
"description": "User role label color"
|
|
145
|
-
},
|
|
146
|
-
"assistantLabel": {
|
|
147
|
-
"$ref": "#/$defs/colorValue",
|
|
148
|
-
"description": "Assistant role label color"
|
|
149
|
-
},
|
|
150
135
|
"selectedBg": {
|
|
151
136
|
"$ref": "#/$defs/colorValue",
|
|
152
137
|
"description": "Selected item background"
|
|
@@ -306,14 +291,6 @@
|
|
|
306
291
|
"bashMode": {
|
|
307
292
|
"$ref": "#/$defs/colorValue",
|
|
308
293
|
"description": "Editor border color in bash mode"
|
|
309
|
-
},
|
|
310
|
-
"assistantMessageBg": {
|
|
311
|
-
"$ref": "#/$defs/colorValue",
|
|
312
|
-
"description": "Assistant message background"
|
|
313
|
-
},
|
|
314
|
-
"assistantMessageText": {
|
|
315
|
-
"$ref": "#/$defs/colorValue",
|
|
316
|
-
"description": "Assistant message text color"
|
|
317
294
|
}
|
|
318
295
|
},
|
|
319
296
|
"additionalProperties": false
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* [HERE]: modes/interactive/theme/theme.ts - theme loader and definitions
|
|
6
6
|
*/
|
|
7
7
|
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@pencil-agent/tui";
|
|
8
|
-
export type ThemeColor = "accent" | "border" | "borderAccent" | "borderMuted" | "success" | "error" | "warning" | "muted" | "dim" | "text" | "thinkingText" | "
|
|
9
|
-
export type ThemeBg = "selectedBg" | "userMessageBg" | "customMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg"
|
|
8
|
+
export type ThemeColor = "accent" | "border" | "borderAccent" | "borderMuted" | "success" | "error" | "warning" | "muted" | "dim" | "text" | "thinkingText" | "userMessageText" | "customMessageText" | "customMessageLabel" | "toolTitle" | "toolOutput" | "mdHeading" | "mdLink" | "mdLinkUrl" | "mdCode" | "mdCodeBlock" | "mdCodeBlockBorder" | "mdQuote" | "mdQuoteBorder" | "mdHr" | "mdListBullet" | "toolDiffAdded" | "toolDiffRemoved" | "toolDiffContext" | "syntaxComment" | "syntaxKeyword" | "syntaxFunction" | "syntaxVariable" | "syntaxString" | "syntaxNumber" | "syntaxType" | "syntaxOperator" | "syntaxPunctuation" | "thinkingOff" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh" | "bashMode";
|
|
9
|
+
export type ThemeBg = "selectedBg" | "userMessageBg" | "customMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
10
10
|
type ColorMode = "truecolor" | "256color";
|
|
11
11
|
export declare class Theme {
|
|
12
12
|
readonly name?: string;
|
|
@@ -35,15 +35,10 @@ const ThemeJsonSchema = Type.Object({
|
|
|
35
35
|
dim: ColorValueSchema,
|
|
36
36
|
text: ColorValueSchema,
|
|
37
37
|
thinkingText: ColorValueSchema,
|
|
38
|
-
// Role labels (2 colors)
|
|
39
|
-
userLabel: ColorValueSchema,
|
|
40
|
-
assistantLabel: ColorValueSchema,
|
|
41
38
|
// Backgrounds & Content Text (11 colors)
|
|
42
39
|
selectedBg: ColorValueSchema,
|
|
43
40
|
userMessageBg: ColorValueSchema,
|
|
44
41
|
userMessageText: ColorValueSchema,
|
|
45
|
-
assistantMessageBg: ColorValueSchema,
|
|
46
|
-
assistantMessageText: ColorValueSchema,
|
|
47
42
|
customMessageBg: ColorValueSchema,
|
|
48
43
|
customMessageText: ColorValueSchema,
|
|
49
44
|
customMessageLabel: ColorValueSchema,
|
|
@@ -469,7 +464,6 @@ function createTheme(themeJson, mode, sourcePath) {
|
|
|
469
464
|
const bgColorKeys = new Set([
|
|
470
465
|
"selectedBg",
|
|
471
466
|
"userMessageBg",
|
|
472
|
-
"assistantMessageBg",
|
|
473
467
|
"customMessageBg",
|
|
474
468
|
"toolPendingBg",
|
|
475
469
|
"toolSuccessBg",
|
|
@@ -35,8 +35,6 @@
|
|
|
35
35
|
"selectedBg": "selectedBg",
|
|
36
36
|
"userMessageBg": "userMsgBg",
|
|
37
37
|
"userMessageText": "warmLight",
|
|
38
|
-
"assistantMessageBg": "#242018",
|
|
39
|
-
"assistantMessageText": "",
|
|
40
38
|
"customMessageBg": "customMsgBg",
|
|
41
39
|
"customMessageText": "",
|
|
42
40
|
"customMessageLabel": "#b8956b",
|
|
@@ -73,9 +71,7 @@
|
|
|
73
71
|
"thinkingMedium": "warmBrown",
|
|
74
72
|
"thinkingHigh": "#b294bb",
|
|
75
73
|
"thinkingXhigh": "#d183e8",
|
|
76
|
-
"bashMode": "green"
|
|
77
|
-
"userLabel": "accent",
|
|
78
|
-
"assistantLabel": "gray"
|
|
74
|
+
"bashMode": "green"
|
|
79
75
|
},
|
|
80
76
|
"export": {
|
|
81
77
|
"pageBg": "#1a1814",
|
|
@@ -7460,13 +7460,13 @@ export const MODELS = {
|
|
|
7460
7460
|
reasoning: true,
|
|
7461
7461
|
input: ["text"],
|
|
7462
7462
|
cost: {
|
|
7463
|
-
input: 0.
|
|
7463
|
+
input: 0.5,
|
|
7464
7464
|
output: 2.1500000000000004,
|
|
7465
|
-
cacheRead: 0.
|
|
7465
|
+
cacheRead: 0.35,
|
|
7466
7466
|
cacheWrite: 0,
|
|
7467
7467
|
},
|
|
7468
7468
|
contextWindow: 163840,
|
|
7469
|
-
maxTokens:
|
|
7469
|
+
maxTokens: 4096,
|
|
7470
7470
|
},
|
|
7471
7471
|
"deepseek/deepseek-v3.1-terminus": {
|
|
7472
7472
|
id: "deepseek/deepseek-v3.1-terminus",
|
|
@@ -10095,13 +10095,13 @@ export const MODELS = {
|
|
|
10095
10095
|
reasoning: false,
|
|
10096
10096
|
input: ["text"],
|
|
10097
10097
|
cost: {
|
|
10098
|
-
input: 0.
|
|
10099
|
-
output: 0.
|
|
10100
|
-
cacheRead: 0.
|
|
10098
|
+
input: 0.15,
|
|
10099
|
+
output: 0.7999999999999999,
|
|
10100
|
+
cacheRead: 0.12,
|
|
10101
10101
|
cacheWrite: 0,
|
|
10102
10102
|
},
|
|
10103
10103
|
contextWindow: 262144,
|
|
10104
|
-
maxTokens:
|
|
10104
|
+
maxTokens: 262144,
|
|
10105
10105
|
},
|
|
10106
10106
|
"qwen/qwen3-coder-plus": {
|
|
10107
10107
|
id: "qwen/qwen3-coder-plus",
|
|
@@ -37,8 +37,6 @@ export declare class ProcessTerminal implements Terminal {
|
|
|
37
37
|
private inputHandler?;
|
|
38
38
|
private resizeHandler?;
|
|
39
39
|
private _kittyProtocolActive;
|
|
40
|
-
private cursorVisible;
|
|
41
|
-
private cursorStyleConfigured;
|
|
42
40
|
private stdinBuffer?;
|
|
43
41
|
private stdinDataHandler?;
|
|
44
42
|
private writeLogPath;
|
|
@@ -17,8 +17,6 @@ export class ProcessTerminal {
|
|
|
17
17
|
inputHandler;
|
|
18
18
|
resizeHandler;
|
|
19
19
|
_kittyProtocolActive = false;
|
|
20
|
-
cursorVisible = true;
|
|
21
|
-
cursorStyleConfigured = false;
|
|
22
20
|
stdinBuffer;
|
|
23
21
|
stdinDataHandler;
|
|
24
22
|
writeLogPath = process.env.NANOPENCIL_TUI_WRITE_LOG || "";
|
|
@@ -28,8 +26,6 @@ export class ProcessTerminal {
|
|
|
28
26
|
start(onInput, onResize) {
|
|
29
27
|
this.inputHandler = onInput;
|
|
30
28
|
this.resizeHandler = onResize;
|
|
31
|
-
this.cursorVisible = true;
|
|
32
|
-
this.cursorStyleConfigured = false;
|
|
33
29
|
// Save previous state and enable raw mode
|
|
34
30
|
this.wasRaw = process.stdin.isRaw || false;
|
|
35
31
|
if (process.stdin.setRawMode) {
|
|
@@ -237,26 +233,10 @@ export class ProcessTerminal {
|
|
|
237
233
|
// lines === 0: no movement
|
|
238
234
|
}
|
|
239
235
|
hideCursor() {
|
|
240
|
-
if (!this.cursorVisible) {
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
236
|
process.stdout.write("\x1b[?25l");
|
|
244
|
-
this.cursorVisible = false;
|
|
245
237
|
}
|
|
246
238
|
showCursor() {
|
|
247
|
-
|
|
248
|
-
if (!this.cursorStyleConfigured) {
|
|
249
|
-
// Use a steady bar cursor to avoid terminal-side blinking in the TUI editor.
|
|
250
|
-
buffer += "\x1b[?12l\x1b[6 q";
|
|
251
|
-
this.cursorStyleConfigured = true;
|
|
252
|
-
}
|
|
253
|
-
if (!this.cursorVisible) {
|
|
254
|
-
buffer += "\x1b[?25h";
|
|
255
|
-
this.cursorVisible = true;
|
|
256
|
-
}
|
|
257
|
-
if (buffer) {
|
|
258
|
-
process.stdout.write(buffer);
|
|
259
|
-
}
|
|
239
|
+
process.stdout.write("\x1b[?25h");
|
|
260
240
|
}
|
|
261
241
|
clearLine() {
|
|
262
242
|
process.stdout.write("\x1b[K");
|
|
@@ -148,6 +148,7 @@ export declare class TUI extends Container {
|
|
|
148
148
|
private previousViewportTop;
|
|
149
149
|
private fullRedrawCount;
|
|
150
150
|
private stopped;
|
|
151
|
+
private synchronizedOutputEnabled;
|
|
151
152
|
private overlayStack;
|
|
152
153
|
constructor(terminal: Terminal, showHardwareCursor?: boolean);
|
|
153
154
|
get fullRedraws(): number;
|
|
@@ -160,6 +161,7 @@ export declare class TUI extends Container {
|
|
|
160
161
|
* When false, empty rows remain (reduces redraws on slower terminals).
|
|
161
162
|
*/
|
|
162
163
|
setClearOnShrink(enabled: boolean): void;
|
|
164
|
+
private wrapRenderBuffer;
|
|
163
165
|
setFocus(component: Component | null): void;
|
|
164
166
|
/**
|
|
165
167
|
* Show an overlay component with configurable positioning and sizing.
|
|
@@ -17,6 +17,15 @@ import { extractSegments, sliceByColumn, sliceWithWidth, visibleWidth } from "./
|
|
|
17
17
|
export function isFocusable(component) {
|
|
18
18
|
return component !== null && "focused" in component;
|
|
19
19
|
}
|
|
20
|
+
function shouldUseSynchronizedOutput() {
|
|
21
|
+
if (process.env.NANOPENCIL_SYNC_OUTPUT === "0") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || "";
|
|
25
|
+
// Warp has shown intermittent delayed/hidden frame presentation with our
|
|
26
|
+
// diff renderer, so prefer plain writes there until its TUI path is stable.
|
|
27
|
+
return termProgram !== "warpterminal";
|
|
28
|
+
}
|
|
20
29
|
/**
|
|
21
30
|
* Cursor position marker - APC (Application Program Command) sequence.
|
|
22
31
|
* This is a zero-width escape sequence that terminals ignore.
|
|
@@ -90,6 +99,7 @@ export class TUI extends Container {
|
|
|
90
99
|
previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
|
|
91
100
|
fullRedrawCount = 0;
|
|
92
101
|
stopped = false;
|
|
102
|
+
synchronizedOutputEnabled = shouldUseSynchronizedOutput();
|
|
93
103
|
// Overlay stack for modal components rendered on top of base content
|
|
94
104
|
overlayStack = [];
|
|
95
105
|
constructor(terminal, showHardwareCursor) {
|
|
@@ -125,6 +135,12 @@ export class TUI extends Container {
|
|
|
125
135
|
setClearOnShrink(enabled) {
|
|
126
136
|
this.clearOnShrink = enabled;
|
|
127
137
|
}
|
|
138
|
+
wrapRenderBuffer(buffer) {
|
|
139
|
+
if (!this.synchronizedOutputEnabled) {
|
|
140
|
+
return buffer;
|
|
141
|
+
}
|
|
142
|
+
return `\x1b[?2026h${buffer}\x1b[?2026l`;
|
|
143
|
+
}
|
|
128
144
|
setFocus(component) {
|
|
129
145
|
// Clear focused flag on old component
|
|
130
146
|
if (isFocusable(this.focusedComponent)) {
|
|
@@ -646,6 +662,7 @@ export class TUI extends Container {
|
|
|
646
662
|
return;
|
|
647
663
|
const width = this.terminal.columns;
|
|
648
664
|
const height = this.terminal.rows;
|
|
665
|
+
let viewportTop = Math.max(0, this.maxLinesRendered - height);
|
|
649
666
|
let prevViewportTop = this.previousViewportTop;
|
|
650
667
|
let hardwareCursorRow = this.hardwareCursorRow;
|
|
651
668
|
const computeLineDiff = (targetRow) => {
|
|
@@ -659,8 +676,6 @@ export class TUI extends Container {
|
|
|
659
676
|
if (this.overlayStack.length > 0) {
|
|
660
677
|
newLines = this.compositeOverlays(newLines, width, height);
|
|
661
678
|
}
|
|
662
|
-
const nextWorkingHeight = Math.max(this.maxLinesRendered, newLines.length);
|
|
663
|
-
let viewportTop = Math.max(0, nextWorkingHeight - height);
|
|
664
679
|
// Extract cursor position before applying line resets (marker must be found first)
|
|
665
680
|
const cursorPos = this.extractCursorPosition(newLines, height);
|
|
666
681
|
newLines = this.applyLineResets(newLines);
|
|
@@ -669,7 +684,7 @@ export class TUI extends Container {
|
|
|
669
684
|
// Helper to clear scrollback and viewport and render all new lines
|
|
670
685
|
const fullRender = (clear) => {
|
|
671
686
|
this.fullRedrawCount += 1;
|
|
672
|
-
let buffer = "
|
|
687
|
+
let buffer = "";
|
|
673
688
|
if (clear)
|
|
674
689
|
buffer += "\x1b[3J\x1b[2J\x1b[H"; // Clear scrollback, screen, and home
|
|
675
690
|
for (let i = 0; i < newLines.length; i++) {
|
|
@@ -677,8 +692,7 @@ export class TUI extends Container {
|
|
|
677
692
|
buffer += "\r\n";
|
|
678
693
|
buffer += newLines[i];
|
|
679
694
|
}
|
|
680
|
-
buffer
|
|
681
|
-
this.terminal.write(buffer);
|
|
695
|
+
this.terminal.write(this.wrapRenderBuffer(buffer));
|
|
682
696
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
683
697
|
this.hardwareCursorRow = this.cursorRow;
|
|
684
698
|
// Reset max lines when clearing, otherwise track growth
|
|
@@ -689,7 +703,7 @@ export class TUI extends Container {
|
|
|
689
703
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
690
704
|
}
|
|
691
705
|
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
|
|
692
|
-
this.positionHardwareCursor(cursorPos, newLines.length
|
|
706
|
+
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
693
707
|
this.previousLines = newLines;
|
|
694
708
|
this.previousWidth = width;
|
|
695
709
|
};
|
|
@@ -745,14 +759,14 @@ export class TUI extends Container {
|
|
|
745
759
|
const appendStart = appendedLines && firstChanged === this.previousLines.length && firstChanged > 0;
|
|
746
760
|
// No changes - but still need to update hardware cursor position if it moved
|
|
747
761
|
if (firstChanged === -1) {
|
|
748
|
-
this.positionHardwareCursor(cursorPos, newLines.length
|
|
762
|
+
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
749
763
|
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
|
|
750
764
|
return;
|
|
751
765
|
}
|
|
752
766
|
// All changes are in deleted lines (nothing to render, just clear)
|
|
753
767
|
if (firstChanged >= newLines.length) {
|
|
754
768
|
if (this.previousLines.length > newLines.length) {
|
|
755
|
-
let buffer = "
|
|
769
|
+
let buffer = "";
|
|
756
770
|
// Move to end of new content (clamp to 0 for empty content)
|
|
757
771
|
const targetRow = Math.max(0, newLines.length - 1);
|
|
758
772
|
const lineDiff = computeLineDiff(targetRow);
|
|
@@ -779,12 +793,11 @@ export class TUI extends Container {
|
|
|
779
793
|
if (extraLines > 0) {
|
|
780
794
|
buffer += `\x1b[${extraLines}A`;
|
|
781
795
|
}
|
|
782
|
-
buffer
|
|
783
|
-
this.terminal.write(buffer);
|
|
796
|
+
this.terminal.write(this.wrapRenderBuffer(buffer));
|
|
784
797
|
this.cursorRow = targetRow;
|
|
785
798
|
this.hardwareCursorRow = targetRow;
|
|
786
799
|
}
|
|
787
|
-
this.positionHardwareCursor(cursorPos, newLines.length
|
|
800
|
+
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
788
801
|
this.previousLines = newLines;
|
|
789
802
|
this.previousWidth = width;
|
|
790
803
|
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
|
|
@@ -801,7 +814,7 @@ export class TUI extends Container {
|
|
|
801
814
|
}
|
|
802
815
|
// Render from first changed line to end
|
|
803
816
|
// Build buffer with all updates wrapped in synchronized output
|
|
804
|
-
let buffer = "
|
|
817
|
+
let buffer = "";
|
|
805
818
|
const prevViewportBottom = prevViewportTop + height - 1;
|
|
806
819
|
const moveTargetRow = appendStart ? firstChanged - 1 : firstChanged;
|
|
807
820
|
if (moveTargetRow > prevViewportBottom) {
|
|
@@ -879,7 +892,6 @@ export class TUI extends Container {
|
|
|
879
892
|
// Move cursor back to end of new content
|
|
880
893
|
buffer += `\x1b[${extraLines}A`;
|
|
881
894
|
}
|
|
882
|
-
buffer += "\x1b[?2026l"; // End synchronized output
|
|
883
895
|
if (process.env.NANOPENCIL_TUI_DEBUG === "1") {
|
|
884
896
|
const debugDir = "/tmp/tui";
|
|
885
897
|
fs.mkdirSync(debugDir, { recursive: true });
|
|
@@ -909,7 +921,7 @@ export class TUI extends Container {
|
|
|
909
921
|
fs.writeFileSync(debugPath, debugData);
|
|
910
922
|
}
|
|
911
923
|
// Write entire buffer at once
|
|
912
|
-
this.terminal.write(buffer);
|
|
924
|
+
this.terminal.write(this.wrapRenderBuffer(buffer));
|
|
913
925
|
// Track cursor position for next render
|
|
914
926
|
// cursorRow tracks end of content (for viewport calculation)
|
|
915
927
|
// hardwareCursorRow tracks actual terminal cursor position (for movement)
|
|
@@ -919,7 +931,7 @@ export class TUI extends Container {
|
|
|
919
931
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
920
932
|
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
|
|
921
933
|
// Position hardware cursor for IME
|
|
922
|
-
this.positionHardwareCursor(cursorPos, newLines.length
|
|
934
|
+
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
923
935
|
this.previousLines = newLines;
|
|
924
936
|
this.previousWidth = width;
|
|
925
937
|
}
|
|
@@ -928,7 +940,7 @@ export class TUI extends Container {
|
|
|
928
940
|
* @param cursorPos The cursor position extracted from rendered output, or null
|
|
929
941
|
* @param totalLines Total number of rendered lines
|
|
930
942
|
*/
|
|
931
|
-
positionHardwareCursor(cursorPos, totalLines
|
|
943
|
+
positionHardwareCursor(cursorPos, totalLines) {
|
|
932
944
|
if (!cursorPos || totalLines <= 0) {
|
|
933
945
|
this.terminal.hideCursor();
|
|
934
946
|
return;
|
|
@@ -936,10 +948,8 @@ export class TUI extends Container {
|
|
|
936
948
|
// Clamp cursor position to valid range
|
|
937
949
|
const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
|
|
938
950
|
const targetCol = Math.max(0, cursorPos.col);
|
|
939
|
-
const targetScreenRow = Math.max(0, Math.min(targetRow - currentViewportTop, terminalHeight - 1));
|
|
940
|
-
const currentScreenRow = Math.max(0, Math.min(this.hardwareCursorRow - previousViewportTop, terminalHeight - 1));
|
|
941
951
|
// Move cursor from current position to target
|
|
942
|
-
const rowDelta =
|
|
952
|
+
const rowDelta = targetRow - this.hardwareCursorRow;
|
|
943
953
|
let buffer = "";
|
|
944
954
|
if (rowDelta > 0) {
|
|
945
955
|
buffer += `\x1b[${rowDelta}B`; // Move down
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pencil-agent/nano-pencil",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.44",
|
|
4
4
|
"description": "CLI writing agent with read, bash, edit, write tools and session management. Supports DashScope Coding Plan. Soul enabled by default for AI personality evolution.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|