aiden-runtime 4.1.3 → 4.1.4

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.
@@ -27,10 +27,16 @@
27
27
  */
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.getReplyRenderer = getReplyRenderer;
30
+ exports.normalizeBlankLines = normalizeBlankLines;
30
31
  exports._resetForTests = _resetForTests;
31
32
  const marked_1 = require("marked");
32
33
  const skinEngine_1 = require("./skinEngine");
33
34
  const syntaxHighlight_1 = require("./syntaxHighlight");
35
+ // v4.1.4 reply-quality polish: single source of truth for frame math.
36
+ // Replaces 3 inline `Math.min(process.stdout.columns ?? 80, 100) - 4`
37
+ // callsites in this file with `getBodyWidth()` and adds soft-wrap for
38
+ // code-block lines that previously overflowed the viewport.
39
+ const frame_1 = require("./display/frame");
34
40
  // eslint-disable-next-line @typescript-eslint/no-var-requires
35
41
  const TerminalRenderer = require('marked-terminal').default ?? require('marked-terminal');
36
42
  function paint(kind) {
@@ -120,7 +126,11 @@ const CODE_BG_ON = '\x1b[48;2;50;50;60m';
120
126
  const CODE_BG_OFF = '\x1b[49m';
121
127
  function renderCodeBlock(code, lang) {
122
128
  const sk = (0, skinEngine_1.getSkinEngine)();
123
- const width = Math.min(process.stdout.columns ?? 80, 100) - 4;
129
+ // v4.1.4 reply-quality polish: width sourced from frame.ts. Same
130
+ // visual budget as the v4.1.3 formula (cols capped at 100, minus
131
+ // gutter+2) — but expressed via the shared helper so it tracks any
132
+ // future width-policy change in one place.
133
+ const width = (0, frame_1.getBodyWidth)();
124
134
  const langLabel = (lang ?? '').trim();
125
135
  // v4.1.3-essentials reply-polish: language tag on the top rule
126
136
  // already shipped; keep it. Bottom rule unlabeled (closing fence).
@@ -131,26 +141,43 @@ function renderCodeBlock(code, lang) {
131
141
  const body = (0, syntaxHighlight_1.isSupportedLang)(langLabel)
132
142
  ? (0, syntaxHighlight_1.highlightCode)(code, langLabel)
133
143
  : code;
134
- // v4.1.3-essentials reply-polish: each body line gets:
135
- // - 2-space outer indent (existing reply container indent)
144
+ // v4.1.4 reply-quality polish: per-line soft wrap. The rail + bg
145
+ // chrome adds 4 visible columns (` `, padding spaces around the
146
+ // line). Subtract those so wrap math targets the actual content
147
+ // budget. `hard: true` ensures even pathological long tokens
148
+ // (minified JS, hashes) break instead of escaping the frame.
149
+ //
150
+ // Width inside the body of a code line:
151
+ // gutter (3) + `│ ` (2) + leading-space (1) + CONTENT + trailing-space (1)
152
+ // → content budget = width - gutter - 4. We further cap at width to
153
+ // keep the fence rule aligned with the body's right margin.
154
+ const contentBudget = Math.max(8, width - frame_1.GUTTER - 4);
155
+ // v4.1.3-essentials reply-polish (preserved): each body line gets:
156
+ // - frame gutter (was 2-space outer indent; now uses shared GUTTER)
136
157
  // - left rail `│ ` painted muted (mirrors blockquote's `┃ ` rail
137
158
  // with a different glyph so they're visually distinct)
138
159
  // - 24-bit dark background wrapping the rail + content (subtle
139
160
  // "this is code" affordance without going full TUI box-frame)
140
- //
141
- // Strip the optional ANSI-only NO_COLOR gate by emitting bg codes
142
- // unconditionally — the skin engine already short-circuits inner
143
- // paint calls when NO_COLOR is set, and bare bg codes degrade
144
- // gracefully on terminals that don't render them.
145
161
  const rail = sk.applyColors('│', 'muted');
146
- const indented = body
147
- .split('\n')
148
- .map((ln) => ` ${rail} ${CODE_BG_ON} ${ln} ${CODE_BG_OFF}`)
149
- .join('\n');
162
+ const gutter = (0, frame_1.getIndent)(0);
163
+ // Wrap each source line independently — code-block semantics demand
164
+ // that a "logical line" remains visible as one continued unit even
165
+ // when soft-wrapped. The CODE_BG painting closes per VISUAL line so
166
+ // a wrap break doesn't bleed bg across the rail of the next row.
167
+ const wrappedLines = [];
168
+ for (const srcLine of body.split('\n')) {
169
+ const wrapped = (0, frame_1.wrap)(srcLine, contentBudget, { trim: false, hard: true });
170
+ for (const visualLine of wrapped.split('\n')) {
171
+ wrappedLines.push(`${gutter}${rail} ${CODE_BG_ON} ${visualLine} ${CODE_BG_OFF}`);
172
+ }
173
+ }
174
+ const indented = wrappedLines.join('\n');
175
+ // Top + bottom fence rules sit at the gutter too — visually anchors
176
+ // the block as a unit inside the assistant frame.
150
177
  return [
151
- sk.applyColors(top, 'muted'),
178
+ `${gutter}${sk.applyColors(top, 'muted')}`,
152
179
  indented,
153
- sk.applyColors(bot, 'muted'),
180
+ `${gutter}${sk.applyColors(bot, 'muted')}`,
154
181
  '',
155
182
  ].join('\n') + '\n';
156
183
  }
@@ -222,6 +249,97 @@ function renderHeading(text, depth) {
222
249
  function renderListItem(text) {
223
250
  return text;
224
251
  }
252
+ /**
253
+ * v4.1.4 reply-quality polish — Fix C helper.
254
+ *
255
+ * Render a single list item's tokens correctly, expanding inline
256
+ * emphasis (strong, em, codespan) that the prior `parser.parse` path
257
+ * silently stranded as raw text.
258
+ *
259
+ * Marked v15 token shapes for list items:
260
+ *
261
+ * Tight list (default — no blank lines between items):
262
+ * list_item.tokens = [
263
+ * { type: 'text', text: '**bold**',
264
+ * tokens: [ { type: 'strong', ... } ] ← inline children
265
+ * }
266
+ * ]
267
+ *
268
+ * Loose list (blank line between items, OR an item with multiple
269
+ * paragraphs):
270
+ * list_item.tokens = [
271
+ * { type: 'paragraph', tokens: [ inline children… ] },
272
+ * { type: 'paragraph', tokens: [ … ] },
273
+ * ]
274
+ *
275
+ * Nested list (a list-token inside an item):
276
+ * list_item.tokens = [
277
+ * { type: 'text', tokens: [...] }, ← the item's own text first
278
+ * { type: 'list', items: [...] }, ← then the nested list
279
+ * ]
280
+ *
281
+ * Item with fenced code block:
282
+ * list_item.tokens = [
283
+ * { type: 'text', ... },
284
+ * { type: 'code', text: '…', lang: '…' },
285
+ * ]
286
+ *
287
+ * Dispatch rules:
288
+ * - `text` with nested `.tokens` → parseInline(tokens)
289
+ * - `text` with only `.text` → fall through to raw text
290
+ * - `paragraph` → parseInline(paragraph.tokens) + '\n'
291
+ * - `list` / `code` / other block → parser.parse([token]) (block path)
292
+ *
293
+ * Returns the joined rendered string. Pure-ish: depends on marked's
294
+ * parser instance (closure-captured) but never mutates it.
295
+ */
296
+ function renderListItemTokens(it, parser) {
297
+ const toks = Array.isArray(it.tokens) ? it.tokens : [];
298
+ if (toks.length === 0)
299
+ return it.text ?? '';
300
+ const out = [];
301
+ for (const raw of toks) {
302
+ if (typeof raw !== 'object' || raw === null)
303
+ continue;
304
+ const tk = raw;
305
+ const type = tk.type;
306
+ // Inline-only wrapper (tight-list common case). The `text` outer
307
+ // token holds inline children we want to expand into ANSI.
308
+ if (type === 'text') {
309
+ if (Array.isArray(tk.tokens) && tk.tokens.length > 0 && parser.parseInline) {
310
+ out.push(parser.parseInline(tk.tokens));
311
+ }
312
+ else {
313
+ out.push(tk.text ?? '');
314
+ }
315
+ continue;
316
+ }
317
+ // Paragraph block (loose-list case). Marked wraps each paragraph's
318
+ // inline content in `.tokens`; render those inline + append a
319
+ // newline so multi-paragraph items stack visually.
320
+ if (type === 'paragraph') {
321
+ if (Array.isArray(tk.tokens) && tk.tokens.length > 0 && parser.parseInline) {
322
+ out.push(parser.parseInline(tk.tokens));
323
+ out.push('\n');
324
+ }
325
+ else {
326
+ out.push(tk.text ?? '');
327
+ }
328
+ continue;
329
+ }
330
+ // Nested list, fenced code, or any other block-level token. The
331
+ // block parser handles these via the normal dispatch (which calls
332
+ // back into our own `renderer.list` override for nested lists —
333
+ // depth counter is already incremented before we got here).
334
+ if (parser.parse) {
335
+ out.push(parser.parse([tk]));
336
+ continue;
337
+ }
338
+ // Last-resort fallback: drop the token's text in raw.
339
+ out.push(tk.text ?? '');
340
+ }
341
+ return out.join('');
342
+ }
225
343
  /**
226
344
  * Singleton — caching is fine since options bind to the active skin
227
345
  * via paint callbacks (which read getSkinEngine() each call).
@@ -246,7 +364,7 @@ function getReplyRenderer() {
246
364
  // need for the 4-tier hierarchy. The prototype-level `renderer.heading`
247
365
  // override below owns the depth extraction + tier selection end-to-end.
248
366
  // marked-terminal's stripped-args call path never reaches our callback.
249
- hr: () => paint('muted')('─'.repeat(Math.min(process.stdout.columns ?? 80, 100) - 4)) + '\n',
367
+ hr: () => paint('muted')('─'.repeat((0, frame_1.getBodyWidth)())) + '\n',
250
368
  listitem: renderListItem,
251
369
  paragraph: (text) => `${text}\n\n`,
252
370
  // v4.1.3-essentials: bold renders as ANSI bold + underline
@@ -274,7 +392,12 @@ function getReplyRenderer() {
274
392
  link: (assembled) => paint('accent')(assembled),
275
393
  href: paint('accent'),
276
394
  text: (text) => text,
277
- width: Math.min(process.stdout.columns ?? 80, 100),
395
+ // v4.1.4 reply-quality polish: marked-terminal's `width` is the
396
+ // *outer* canvas it formats into. Frame-aware body width keeps the
397
+ // tables / hr / hard-wrap targets inside our gutter envelope.
398
+ // `reflowText: false` (below) stays off — we own prose wrap via
399
+ // frame.wrap() in the display layer, not here.
400
+ width: (0, frame_1.getBodyWidth)(),
278
401
  showSectionPrefix: false,
279
402
  reflowText: false,
280
403
  tab: 2,
@@ -417,17 +540,32 @@ function getReplyRenderer() {
417
540
  const tok = body;
418
541
  isOrdered = tok.ordered === true;
419
542
  startNum = typeof tok.start === 'number' ? tok.start : 1;
420
- // marked v15: renderer instance has a `parser` field pointing
421
- // back to the Parser; `Parser.parse(tokens)` walks the token
422
- // tree dispatching back to renderer methods (including this
423
- // very `list` override for nested lists, which is what makes
424
- // the depth counter increment properly).
543
+ // v4.1.4 reply-quality polish Fix C (token-type dispatch).
544
+ //
545
+ // Prior implementation called `parser.parse(it.tokens)` and let
546
+ // marked's block-parser dispatch each token. For tight-list items
547
+ // marked v15 wraps the item's content in a `text`-type outer
548
+ // token whose `.tokens` array holds the actual inline tokens
549
+ // (strong, em, codespan…). `parser.parse` dispatched the outer
550
+ // wrapper to `renderer.text` (our `opts.text` = identity), which
551
+ // returned the RAW raw `**bold**` source string — never recursing
552
+ // into the inline children. Result: literal asterisks in every
553
+ // bullet that contained inline emphasis.
554
+ //
555
+ // Fix: walk each top-level token by type. Tight-list items have
556
+ // a `text` wrapper → use `parseInline` on its nested tokens to
557
+ // expand strong/em/codespan. Loose-list items have block-level
558
+ // `paragraph`/`list`/`code` tokens → those need block-level
559
+ // recursion (delegates back to our list override for nested
560
+ // lists, preserving the depth counter).
561
+ //
562
+ // Confirmed against marked v15 token shapes from `marked.lexer`
563
+ // (see scripts/smoke-issue-c-tokens.ts).
425
564
  const parser = this.parser;
426
565
  items = (tok.items ?? []).map((it) => {
427
- if (it.tokens && parser?.parse) {
428
- return parser.parse(it.tokens);
429
- }
430
- return it.text ?? '';
566
+ if (!parser)
567
+ return it.text ?? '';
568
+ return renderListItemTokens(it, parser);
431
569
  });
432
570
  }
433
571
  else {
@@ -485,7 +623,21 @@ function getReplyRenderer() {
485
623
  // if other code transiently swaps the renderer.
486
624
  marked_1.marked.setOptions({ renderer: renderer });
487
625
  const out = marked_1.marked.parse(text);
488
- return typeof out === 'string' ? out : String(out);
626
+ const raw = typeof out === 'string' ? out : String(out);
627
+ // v4.1.4 Part 1.6 Issue I — collapse excess vertical spacing.
628
+ //
629
+ // Our `opts.paragraph` callback emits `text\n\n`, our
630
+ // `renderCodeBlock` ends with `\n\n`, and marked-terminal's
631
+ // outer block dispatch ALSO emits `\n\n` between adjacent
632
+ // blocks. Result: 4 newlines (3 visible blank lines) between
633
+ // paragraphs, after code blocks, between paragraphs and lists.
634
+ // Root-cause fix would require auditing marked-terminal's
635
+ // between-block separator across every override (risk-prone).
636
+ // Band-aid: collapse any run of 3+ newlines down to exactly 2
637
+ // (= one blank line). Mechanically safe — can only REMOVE
638
+ // excess whitespace, never add bad spacing. Existing single-
639
+ // blank-line gaps pass through unchanged.
640
+ return normalizeBlankLines(raw);
489
641
  }
490
642
  catch {
491
643
  return text;
@@ -494,6 +646,24 @@ function getReplyRenderer() {
494
646
  };
495
647
  return cachedRenderer;
496
648
  }
649
+ /**
650
+ * v4.1.4 Part 1.6 Issue I — collapse runs of 3+ consecutive newlines
651
+ * down to exactly 2 (a single blank line). Exported for unit-test
652
+ * access; pure with no side effects.
653
+ *
654
+ * Confirmed via `scripts/smoke-issue-i-spacing.ts`:
655
+ * - "A\n\n\n\nB" → "A\n\nB" (2 paras → 1 blank line)
656
+ * - "A\n\n\n\n\nB" → "A\n\nB" (3+ blanks all collapse)
657
+ * - "A\n\nB" → "A\n\nB" (already correct, unchanged)
658
+ * - "A\nB" → "A\nB" (single newline preserved)
659
+ * - "A\n" → "A\n" (trailing pass-through)
660
+ *
661
+ * Does NOT touch the list-under-padding case (lists ending with a
662
+ * single `\n` before a paragraph) — that's a v4.1.5 follow-up.
663
+ */
664
+ function normalizeBlankLines(text) {
665
+ return text.replace(/\n{3,}/g, '\n\n');
666
+ }
497
667
  /** Test reset — drops the cached renderer so a skin change picks up. */
498
668
  function _resetForTests() {
499
669
  cachedRenderer = null;
@@ -49,9 +49,15 @@ const DEFAULT_SKIN = {
49
49
  error: [0xf4, 0x47, 0x47],
50
50
  warn: [0xff, 0xc1, 0x07],
51
51
  success: [0x4c, 0xaf, 0x50],
52
- // v4.1.3-repl-polish: muted is now true grey (#888) so it reads as
53
- // genuinely secondary. The soft-cyan that was here moved to 'session'.
54
- muted: [0x88, 0x88, 0x88],
52
+ // v4.1.4 reply-quality polish: muted shifts from neutral grey
53
+ // (#888888) to warm Aiden-tinted dim (#b8a89a). Mid-grey at +56
54
+ // brightness on red/green channels with a slight cool-down on
55
+ // blue, putting muted in the same warm family as brand orange
56
+ // (#FF6B35) without competing with it. Reads as "intentional
57
+ // dim" rather than washed-out terminal grey. Used by tool-trail
58
+ // gutter, status footer, code-block rail, blockquote rail, and
59
+ // display.dim() — surfaces the user reads constantly.
60
+ muted: [0xb8, 0xa8, 0x9a],
55
61
  heading: BRAND_ORANGE,
56
62
  // v4.1.3-repl-polish: session = soft cyan (ex-muted); used for IDs
57
63
  // and the session-end card header labels.
@@ -84,7 +90,12 @@ const LIGHT_SKIN = {
84
90
  error: [0xb0, 0x10, 0x10],
85
91
  warn: [0x80, 0x60, 0x00],
86
92
  success: [0x1b, 0x5e, 0x20],
87
- muted: [0x60, 0x60, 0x60],
93
+ // v4.1.4 reply-quality polish: proportional warm-shift for the
94
+ // light skin too. Was neutral #606060; new value #7a6e5e keeps the
95
+ // dark-on-light contrast budget but adds the same warm tint as the
96
+ // default skin's muted so themed surfaces feel coherent across
97
+ // skin switches.
98
+ muted: [0x7a, 0x6e, 0x5e],
88
99
  heading: [0xc4, 0x42, 0x10],
89
100
  session: [0x00, 0x55, 0x88],
90
101
  degraded: [0x80, 0x60, 0x00],
@@ -29,9 +29,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.TOOL_PRIMARY_ARG = void 0;
30
30
  exports.buildToolPreview = buildToolPreview;
31
31
  /**
32
- * Map of tool-name → name of the property in `args` that should render
33
- * as the at-a-glance preview. Stable contract; tests assert specific
34
- * entries.
32
+ * Map of tool-name → preview extractor (string key OR function).
33
+ * Stable contract; tests assert specific entries.
35
34
  */
36
35
  exports.TOOL_PRIMARY_ARG = {
37
36
  // ── terminal / execution ─────────────────────────────────────────────
@@ -100,6 +99,37 @@ exports.TOOL_PRIMARY_ARG = {
100
99
  media_transport: 'target',
101
100
  media_key: 'action',
102
101
  app_input: 'app',
102
+ // ── v4.1.4 Phase 3b' (Issue H) ───────────────────────────────────────
103
+ // app_launch needs custom logic: when `app === 'explorer.exe'` the
104
+ // binary is just the URI dispatcher and the meaningful target is in
105
+ // `args[0]` (e.g. 'spotify:track/...'). Surface the protocol scheme
106
+ // ('spotify') rather than the dispatch binary. Falls through to the
107
+ // app name for normal exe launches.
108
+ app_launch: (args) => {
109
+ if (!args || typeof args !== 'object')
110
+ return '';
111
+ const a = args;
112
+ const appRaw = typeof a.app === 'string' ? a.app.trim() : '';
113
+ // URI-protocol case: explorer.exe + 'scheme:...' in args[0].
114
+ if (appRaw.toLowerCase() === 'explorer.exe' && Array.isArray(a.args)) {
115
+ const first = a.args[0];
116
+ if (typeof first === 'string' && first.length > 0) {
117
+ // Scheme requires ≥2 chars so Windows drive letters
118
+ // (`C:/path`) don't mis-detect as the scheme `C`. Real URI
119
+ // schemes (spotify, vscode, http, file, etc.) are all
120
+ // multi-char by RFC.
121
+ const m = first.match(/^([A-Za-z][A-Za-z0-9+.-]+):/);
122
+ if (m)
123
+ return m[1]; // 'spotify:track/...' → 'spotify'
124
+ return first; // No protocol — surface the raw arg
125
+ }
126
+ }
127
+ return appRaw;
128
+ },
129
+ // Clipboard write — the actual text being copied is the meaningful
130
+ // target. Reads have no args worth showing (empty schema).
131
+ clipboard_write: 'text',
132
+ clipboard_read: '',
103
133
  };
104
134
  /**
105
135
  * Maximum visible characters for the preview value. Long commands /
@@ -121,28 +151,47 @@ function buildToolPreview(toolName, args) {
121
151
  if (!Object.prototype.hasOwnProperty.call(exports.TOOL_PRIMARY_ARG, toolName)) {
122
152
  return null;
123
153
  }
124
- const argKey = exports.TOOL_PRIMARY_ARG[toolName];
125
- if (argKey === '')
126
- return '';
127
- if (!args || typeof args !== 'object')
128
- return '';
129
- const raw = args[argKey];
130
- if (raw === undefined || raw === null)
131
- return '';
154
+ const extractor = exports.TOOL_PRIMARY_ARG[toolName];
155
+ // v4.1.4 Phase 3b' (Issue H1): function extractor path. Used by
156
+ // tools whose preview can't be expressed as a single key lookup
157
+ // (e.g. app_launch with URI-protocol routing through explorer.exe).
132
158
  let str;
133
- if (typeof raw === 'string') {
134
- str = raw;
135
- }
136
- else if (typeof raw === 'number' || typeof raw === 'boolean') {
137
- str = String(raw);
138
- }
139
- else {
159
+ if (typeof extractor === 'function') {
140
160
  try {
141
- str = JSON.stringify(raw);
161
+ const out = extractor(args);
162
+ str = typeof out === 'string' ? out : '';
142
163
  }
143
164
  catch {
165
+ // Extractor threw — degrade to empty preview rather than crash
166
+ // the tool-row render. The tool name + state cluster still
167
+ // carries enough info.
168
+ str = '';
169
+ }
170
+ }
171
+ else {
172
+ // String-key path (legacy, unchanged behaviour).
173
+ const argKey = extractor;
174
+ if (argKey === '')
175
+ return '';
176
+ if (!args || typeof args !== 'object')
177
+ return '';
178
+ const raw = args[argKey];
179
+ if (raw === undefined || raw === null)
180
+ return '';
181
+ if (typeof raw === 'string') {
182
+ str = raw;
183
+ }
184
+ else if (typeof raw === 'number' || typeof raw === 'boolean') {
144
185
  str = String(raw);
145
186
  }
187
+ else {
188
+ try {
189
+ str = JSON.stringify(raw);
190
+ }
191
+ catch {
192
+ str = String(raw);
193
+ }
194
+ }
146
195
  }
147
196
  // Collapse whitespace so multi-line commands stay on one preview row.
148
197
  str = str.replace(/\s+/g, ' ').trim();
@@ -650,6 +650,15 @@ class AidenAgent {
650
650
  else if (evt.type === 'tool_call') {
651
651
  runOptions.onToolCallStart?.(evt.toolCall);
652
652
  }
653
+ else if (evt.type === 'progress') {
654
+ // v4.1.4 Part 1.6 — drive the per-turn token progress bar.
655
+ // Defensive try/catch — a misbehaving display sink must not
656
+ // tear down the stream consumer.
657
+ try {
658
+ runOptions.onProgress?.(evt.outputTokens, evt.maxTokens);
659
+ }
660
+ catch { /* progress sink errors don't block streaming */ }
661
+ }
653
662
  else if (evt.type === 'done') {
654
663
  finalOutput = evt.output;
655
664
  }
@@ -128,7 +128,8 @@ const EXECUTION_DISCIPLINE_PROSE = [
128
128
  'file"), you MUST immediately make the corresponding tool call in the same response.',
129
129
  'Never end your turn with a promise of future action — execute it now. Every',
130
130
  'response should either contain tool calls that make progress, or deliver a final',
131
- 'result. Responses that only describe intentions without acting are not acceptable.',
131
+ 'result. When the user requests an action, take it. When the user requests',
132
+ 'discussion, discuss.',
132
133
  ].join('\n');
133
134
  /**
134
135
  * Llama-3.3-specific tool-call format guard. Adapter-side recovery picks
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
4
  // AUTO-GENERATED by scripts/inject-version.js — do not edit by hand
5
- exports.VERSION = '4.1.3';
5
+ exports.VERSION = '4.1.4';
@@ -114,7 +114,7 @@ class AnthropicAdapter {
114
114
  };
115
115
  return;
116
116
  }
117
- yield* decodeStream(reply.body);
117
+ yield* decodeStream(reply.body, input.maxTokens ?? DEFAULT_MAX_TOKENS);
118
118
  }
119
119
  // ── Request body assembly ────────────────────────────────────────────────
120
120
  buildBody(input, streaming) {
@@ -404,13 +404,20 @@ function decodeUsage(u) {
404
404
  }
405
405
  return out;
406
406
  }
407
- async function* decodeStream(body) {
407
+ async function* decodeStream(body, maxTokens) {
408
408
  const blocks = new Map();
409
409
  const toolCalls = [];
410
410
  let stopReason;
411
411
  let usage = undefined;
412
412
  // Stable text emission order: walk content blocks by index at end-of-stream.
413
413
  const textOrder = [];
414
+ // v4.1.4 Part 1.6: track the last-emitted output-token count so we
415
+ // only yield a `progress` event when the counter actually advances.
416
+ // Anthropic emits `message_delta.usage.output_tokens` as a running
417
+ // total — multiple deltas may carry the same value if no new tokens
418
+ // were produced between them. Deduping keeps the event stream
419
+ // proportional to real progress.
420
+ let lastProgressEmitted = -1;
414
421
  for await (const payload of (0, chatCompletionsAdapter_1.parseSseStream)(body)) {
415
422
  if (!payload || payload === '[DONE]')
416
423
  continue;
@@ -490,6 +497,22 @@ async function* decodeStream(body) {
490
497
  stopReason = evt.delta.stop_reason;
491
498
  if (evt.usage) {
492
499
  usage = { ...(usage ?? {}), ...evt.usage };
500
+ // v4.1.4 Part 1.6 — emit a `progress` event when the running
501
+ // output-token counter advances. The display layer uses these
502
+ // for the ▰▱ progress bar. Deduped via `lastProgressEmitted`
503
+ // so a stream of message_delta events with no real progress
504
+ // doesn't flood the consumer.
505
+ const outputTokens = typeof evt.usage.output_tokens === 'number'
506
+ ? evt.usage.output_tokens
507
+ : -1;
508
+ if (outputTokens > lastProgressEmitted) {
509
+ lastProgressEmitted = outputTokens;
510
+ yield {
511
+ type: 'progress',
512
+ outputTokens,
513
+ maxTokens,
514
+ };
515
+ }
493
516
  }
494
517
  break;
495
518
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-runtime",
3
- "version": "4.1.3",
3
+ "version": "4.1.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -277,6 +277,7 @@
277
277
  "twilio": "^5.13.1",
278
278
  "uuid": "^9.0.0",
279
279
  "whatsapp-web.js": "^1.26.0",
280
+ "wrap-ansi": "^9.0.2",
280
281
  "ws": "^8.20.0"
281
282
  },
282
283
  "optionalDependencies": {