@aragon-agent/cli 0.6.26 → 0.6.28

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.
@@ -13,10 +13,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
13
13
  import React from 'react';
14
14
  import { Text } from 'ink';
15
15
  import { pickGlyphs } from '../glyphs.js';
16
+ import { expandTabs } from '../display-tab.js';
16
17
  function QueuedEntryImpl({ text, theme, caps, }) {
17
18
  const glyphs = pickGlyphs(caps);
18
19
  const lines = text.split('\n');
19
- const first = lines[0] ?? '';
20
+ // Same reason as `UserEntry`: a queued draft is a draft, so its TABs must
21
+ // occupy the columns the composer showed them at.
22
+ const first = expandTabs(lines[0] ?? '');
20
23
  const extra = lines.length - 1;
21
24
  const suffix = extra > 0 ? ` ${glyphs.ellipsis}+${extra} line${extra === 1 ? '' : 's'}` : '';
22
25
  return (_jsx(Text, { wrap: "truncate", color: theme.muted, children: `Queue: ${first}${suffix}` }));
@@ -14,9 +14,16 @@ import React from 'react';
14
14
  import { Box, Text } from 'ink';
15
15
  import { pickGlyphs } from '../glyphs.js';
16
16
  import { USER_ENTRY_MAX_ROWS } from '../composer-limits.js';
17
+ import { expandTabs } from '../display-tab.js';
17
18
  function UserEntryImpl({ text, theme, caps, }) {
18
19
  const glyphs = pickGlyphs(caps);
19
- const all = text.split('\n');
20
+ // TAB IS EXPANDED HERE, AT THE RENDER BOUNDARY, AND NOWHERE ABOVE IT.
21
+ // The payload keeps its `\t` (the model must receive the original
22
+ // indentation), so `<Text>` used to hand the raw byte to the terminal and
23
+ // the terminal painted an EIGHT-column stop while the composer painted a
24
+ // FOUR-column one. Splitting first and expanding per line keeps this row
25
+ // identical to the row the user saw in the draft.
26
+ const all = text.split('\n').map((line) => expandTabs(line));
20
27
  // A submitted 5 000-line paste otherwise asks Yoga to lay out 5 000 nodes
21
28
  // inside a measured entry -- the hazard `render-cache.ts:37` already records
22
29
  // in the same words ("a multi-megabyte 'code block' is almost always a pasted
@@ -32,9 +32,11 @@
32
32
  * `selectWindow` takes `offset` as an INPUT, would close a loop from the offset
33
33
  * through the mounted set and the measured heights back to the offset.
34
34
  */
35
+ import stringWidth from 'string-width';
35
36
  import { FAST_LIMITS } from '../../fast/limits.js';
36
37
  import { COMPACTION_LIMITS } from '../../compaction/limits.js';
37
38
  import { GUTTER_WIDTH } from './Gutter.js';
39
+ import { expandTabs } from '../display-tab.js';
38
40
  import { USER_ENTRY_MAX_ROWS } from '../composer-limits.js';
39
41
  export const VIRTUAL_LIMITS = {
40
42
  /**
@@ -159,21 +161,52 @@ export function entryRevision(entry) {
159
161
  export function heightKey(entry, cols, expanded, density, flags = '') {
160
162
  return `${entry.id}|${entryRevision(entry)}|${cols}|${expanded ? 1 : 0}|${density}|${flags}`;
161
163
  }
162
- /** Rows a string occupies at `usable` columns, wrapping included. At least 1. */
164
+ /**
165
+ * Rows a string occupies at `usable` columns, wrapping included. At least 1.
166
+ *
167
+ * COUNTED IN DISPLAY COLUMNS, NOT UTF-16 CODE UNITS, and the difference is a
168
+ * correctness bug rather than a nicety: a CJK glyph is one code unit and two
169
+ * cells, and a TAB is one code unit and up to `TAB_WIDTH` cells. Counting code
170
+ * units therefore UNDER-estimates, which this file's own note names as the
171
+ * unsafe direction ("an over-estimate makes the scroll position slightly
172
+ * conservative and self-corrects ... an under-estimate can put the newest
173
+ * output off the bottom of a viewport the user believes is pinned"). It also
174
+ * had to change together with the renderers: a stored `\t` is now expanded at
175
+ * paint time, so measuring the unexpanded string would keep the estimate short
176
+ * while the painted row got taller.
177
+ */
163
178
  function wrappedRows(text, usable) {
164
179
  const width = Math.max(1, usable);
165
180
  let rows = 0;
166
- let lineLen = 0;
167
- for (let i = 0; i < text.length; i += 1) {
168
- if (text.charCodeAt(i) === 10) {
169
- rows += Math.max(1, Math.ceil(lineLen / width));
170
- lineLen = 0;
171
- }
172
- else {
173
- lineLen += 1;
174
- }
181
+ let start = 0;
182
+ for (;;) {
183
+ const end = text.indexOf('\n', start);
184
+ const line = end === -1 ? text.slice(start) : text.slice(start, end);
185
+ rows += Math.max(1, Math.ceil(displayColumns(line) / width));
186
+ if (end === -1)
187
+ return rows;
188
+ start = end + 1;
175
189
  }
176
- return rows + Math.max(1, Math.ceil(lineLen / width));
190
+ }
191
+ /**
192
+ * Printable ASCII with no TAB: one code unit is one cell, so the width table is
193
+ * not consulted at all.
194
+ *
195
+ * THE FAST PATH IS LOAD-BEARING, not a micro-optimisation. `stringWidth`
196
+ * strips ANSI and walks the East-Asian width table for every code point, and
197
+ * this function runs over EVERY unmeasured entry of a 10 000-entry document
198
+ * (`render-budget.test.tsx` mounts exactly that); measuring them all with the
199
+ * table turned that test into a timeout. Transcript text is overwhelmingly
200
+ * ASCII, so the branch is taken almost always.
201
+ */
202
+ const PLAIN_ASCII = /^[\x20-\x7e]*$/;
203
+ /** Display columns of one logical line: TABs expanded, CJK counted twice. */
204
+ function displayColumns(line) {
205
+ if (line.indexOf('\t') === -1 && PLAIN_ASCII.test(line))
206
+ return line.length;
207
+ // `expandTabs` mirrors what `entries/*` paints; `stringWidth` is the width
208
+ // table the composer already wraps with, so the two agree on CJK and emoji.
209
+ return stringWidth(expandTabs(line));
177
210
  }
178
211
  /** Collapsed preview rows in `ToolCard`; duplicated as a NUMBER, not imported. */
179
212
  const TOOL_COLLAPSED_LINES = 8;
@@ -82,6 +82,10 @@ export declare function hasPasteFrame(input: string): boolean;
82
82
  * nothing on screen to explain it.
83
83
  *
84
84
  * This is strictly better than v0.6.3 for both consumers: a pasted key carrying a
85
- * stray CR or an ANSI sequence is now cleaned rather than stored raw.
85
+ * stray CR or an ANSI sequence is now cleaned rather than stored raw. The
86
+ * unframed branch strips escape SEQUENCES before the control sweep, because
87
+ * deleting only the introducer left `[0m` in the stored value -- and in a
88
+ * settings field that residue is invisible behind `maskDot` while the value
89
+ * handed to `registerSecret()` is silently wrong.
86
90
  */
87
91
  export declare function stripPasteFrames(input: string): string;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { PASTE_CLOSE, PASTE_OPEN } from '../input/limits.js';
9
9
  import { sanitisePaste } from '../input/paste-parse.js';
10
+ import { stripControlSequences } from '../input/control-sequences.js';
10
11
  /**
11
12
  * Remove every C0 byte, DEL and C1 byte from text that arrived OUTSIDE a frame
12
13
  * (I-14). NO CR -> LF conversion, and that omission is the point.
@@ -101,11 +102,15 @@ export function hasPasteFrame(input) {
101
102
  * nothing on screen to explain it.
102
103
  *
103
104
  * This is strictly better than v0.6.3 for both consumers: a pasted key carrying a
104
- * stray CR or an ANSI sequence is now cleaned rather than stored raw.
105
+ * stray CR or an ANSI sequence is now cleaned rather than stored raw. The
106
+ * unframed branch strips escape SEQUENCES before the control sweep, because
107
+ * deleting only the introducer left `[0m` in the stored value -- and in a
108
+ * settings field that residue is invisible behind `maskDot` while the value
109
+ * handed to `registerSecret()` is silently wrong.
105
110
  */
106
111
  export function stripPasteFrames(input) {
107
112
  if (!hasPasteFrame(input))
108
- return sanitiseTyped(input);
113
+ return sanitiseTyped(stripControlSequences(input));
109
114
  return splitPasteFrames(input)
110
115
  .map((segment) => (segment.kind === 'paste' ? sanitiseTyped(segment.text) : segment.text))
111
116
  .join('');
@@ -1,12 +1,21 @@
1
1
  import { formatCost, formatDuration, formatTokens, promptTokensOf } from '../agent/usage.js';
2
2
  import { formatRetryLine } from '../agent/retry-view.js';
3
+ import { expandTabs } from './display-tab.js';
3
4
  const DEFAULT_MAX_ENTRIES = 200;
4
5
  const CONTINUATION_INDENT = ' ';
5
6
  const TOOL_INDENT = ' ';
6
7
  const MAX_ARG_CHARS = 60;
7
- /** Prefix the first line with `marker`, indent the rest to line up under it. */
8
+ /**
9
+ * Prefix the first line with `marker`, indent the rest to line up under it.
10
+ *
11
+ * `expandTabs` RUNS BEFORE THE PREFIX IS ADDED, so a stored line lands on the
12
+ * same columns the TUI drew it at. Without it this replay -- the only record
13
+ * left once the alternate screen is torn down -- disagreed with the screen
14
+ * about every TAB, because the terminal painted an eight-column stop under text
15
+ * the composer had laid out at four.
16
+ */
8
17
  function block(marker, text) {
9
- const lines = text.replace(/\r\n/g, '\n').split('\n');
18
+ const lines = expandTabs(text.replace(/\r\n/g, '\n')).split('\n');
10
19
  const head = `${marker} ${lines[0] ?? ''}`.trimEnd();
11
20
  const rest = lines.slice(1).map((l) => `${CONTINUATION_INDENT}${l}`.trimEnd());
12
21
  return [head, ...rest];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.6.26",
3
+ "version": "0.6.28",
4
4
  "description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
5
5
  "license": "MIT",
6
6
  "author": "AragonAgent contributors",
@@ -56,7 +56,7 @@
56
56
  "prepublishOnly": "npm run build"
57
57
  },
58
58
  "dependencies": {
59
- "@aragon-agent/core": "^0.2.38",
59
+ "@aragon-agent/core": "^0.2.40",
60
60
  "chalk": "^5.6.2",
61
61
  "cli-highlight": "^2.0.0",
62
62
  "commander": "^12.1.0",