@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.
@@ -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
- if (dashboardAutoHideTimer) {
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
- clearTeamDashboardTimer();
133
- ctx.ui.setStatus("team", renderTeamFooterStatus(teammates));
134
- ctx.ui.setWidget("team-dashboard", dashboardVisible || hasRunning || teammates.length > 0 ? renderTeamDashboard(teammates, 80, { expanded: dashboardVisible }) : undefined);
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 TokenSave token savings for this session (/tokensave [summary|history|plan <cmd>])",
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: 2.5,
10837
- output: 7.5,
10838
- cacheRead: 0.5,
10839
- cacheWrite: 3.125,
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.13,
11177
- output: 0.85,
11178
- cacheRead: 0.024999999999999998,
11176
+ input: 0.125,
11177
+ output: 0.84,
11178
+ cacheRead: 0,
11179
11179
  cacheWrite: 0,
11180
11180
  },
11181
11181
  contextWindow: 131072,
11182
- maxTokens: 98304,
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.39999999999999997,
13933
- output: 2,
13934
- cacheRead: 0.08,
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: 1,
13950
- output: 3,
13951
- cacheRead: 0.19999999999999998,
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(argumentText);
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: argumentText,
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, isFocusable, CURSOR_MARKER, visibleWidth
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, isFocusable, CURSOR_MARKER, visibleWidth
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
- const line = newLines[i];
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;