@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.
@@ -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 { Box, Container, Markdown, Spacer, Text } from "@pencil-agent/tui";
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
- let addedAssistantLabelForText = false;
43
- let seenThinking = false;
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
- if (!addedAssistantLabelForText) {
49
- // Top spacing before first text when this message did not start with thinking.
50
- if (!seenThinking) {
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
- this.contentContainer.addChild(thinkingLabel);
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
- this.contentContainer.addChild(thinkingLabel);
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
- "bashMode": "green",
75
- "userLabel": "accent",
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
- "bashMode": "green",
74
- "userLabel": "teal",
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" | "userLabel" | "assistantLabel" | "assistantMessageText" | "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" | "assistantMessageBg";
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.44999999999999996,
7463
+ input: 0.5,
7464
7464
  output: 2.1500000000000004,
7465
- cacheRead: 0.22499999999999998,
7465
+ cacheRead: 0.35,
7466
7466
  cacheWrite: 0,
7467
7467
  },
7468
7468
  contextWindow: 163840,
7469
- maxTokens: 65536,
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.12,
10099
- output: 0.75,
10100
- cacheRead: 0.06,
10098
+ input: 0.15,
10099
+ output: 0.7999999999999999,
10100
+ cacheRead: 0.12,
10101
10101
  cacheWrite: 0,
10102
10102
  },
10103
10103
  contextWindow: 262144,
10104
- maxTokens: 65536,
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
- let buffer = "";
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 = "\x1b[?2026h"; // Begin synchronized output
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 += "\x1b[?2026l"; // End synchronized output
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, prevViewportTop, this.previousViewportTop, height);
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, prevViewportTop, this.previousViewportTop, height);
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 = "\x1b[?2026h";
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 += "\x1b[?2026l";
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, prevViewportTop, this.previousViewportTop, height);
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 = "\x1b[?2026h"; // Begin synchronized output
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, prevViewportTop, this.previousViewportTop, height);
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, previousViewportTop, currentViewportTop, terminalHeight) {
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 = targetScreenRow - currentScreenRow;
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.42",
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": {