aiden-runtime 4.1.2 → 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.
Files changed (36) hide show
  1. package/dist/cli/v4/aidenCLI.js +10 -0
  2. package/dist/cli/v4/callbacks.js +85 -13
  3. package/dist/cli/v4/chatSession.js +250 -24
  4. package/dist/cli/v4/commands/doctor.js +23 -27
  5. package/dist/cli/v4/commands/model.js +30 -1
  6. package/dist/cli/v4/defaultSoul.js +69 -2
  7. package/dist/cli/v4/display/capabilityCard.js +135 -0
  8. package/dist/cli/v4/display/frame.js +234 -0
  9. package/dist/cli/v4/display/progressBar.js +137 -0
  10. package/dist/cli/v4/display/sessionEndCard.js +127 -0
  11. package/dist/cli/v4/display/toolTrail.js +172 -0
  12. package/dist/cli/v4/display.js +891 -153
  13. package/dist/cli/v4/doctor.js +377 -75
  14. package/dist/cli/v4/promotionPrompt.js +135 -5
  15. package/dist/cli/v4/replyRenderer.js +487 -26
  16. package/dist/cli/v4/skinEngine.js +26 -4
  17. package/dist/cli/v4/toolPreview.js +82 -19
  18. package/dist/core/tools/nowPlaying.js +7 -15
  19. package/dist/core/v4/aidenAgent.js +9 -0
  20. package/dist/core/v4/promptBuilder.js +2 -1
  21. package/dist/core/v4/sessionDistiller.js +48 -1
  22. package/dist/core/v4/toolRegistry.js +16 -1
  23. package/dist/core/version.js +1 -1
  24. package/dist/moat/plannerGuard.js +19 -0
  25. package/dist/providers/v4/anthropicAdapter.js +25 -2
  26. package/dist/providers/v4/errors.js +92 -0
  27. package/dist/tools/v4/index.js +24 -1
  28. package/dist/tools/v4/sessions/recallSession.js +14 -0
  29. package/dist/tools/v4/system/_psHelpers.js +70 -2
  30. package/dist/tools/v4/system/appInput.js +154 -0
  31. package/dist/tools/v4/system/appLaunch.js +136 -10
  32. package/dist/tools/v4/system/mediaKey.js +35 -4
  33. package/dist/tools/v4/system/mediaSessions.js +163 -0
  34. package/dist/tools/v4/system/mediaTransport.js +211 -0
  35. package/package.json +2 -1
  36. package/skills/system_control.md +56 -6
@@ -27,15 +27,63 @@
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) {
37
43
  return (text) => (0, skinEngine_1.getSkinEngine)().applyColors(text, kind);
38
44
  }
45
+ /**
46
+ * v4.1.3-essentials: bold (`**foo**`) markdown emphasis renders as
47
+ * ANSI bold + underline. Previously painted 'brand' (orange) which
48
+ * collided with the heading hierarchy. Briefly tried bold + bright-
49
+ * white; landed on bold + underline because underline carries
50
+ * emphasis without consuming a color slot — the palette stays
51
+ * available for state semantics (yellow=degraded, red=error, etc.).
52
+ *
53
+ * ANSI sequence: `\x1b[1m\x1b[4m{text}\x1b[24m\x1b[22m` — bold ON +
54
+ * underline ON, then underline OFF + bold OFF. Reset order matters
55
+ * (underline first, bold second) so the closing codes don't reorder
56
+ * styles surprisingly on terminals that batch SGR updates.
57
+ *
58
+ * Bypasses the skin system intentionally — bold-as-underline is an
59
+ * opinionated default for this slice. Same caveat as the prior bold-
60
+ * as-color iteration: nested markdown loses the outer style after
61
+ * close (pre-existing limitation of the painter-stack architecture).
62
+ *
63
+ * Honors `NO_COLOR=1` per the standard (skips the wrap entirely).
64
+ * Strictly speaking `NO_COLOR` is about color and underline isn't
65
+ * a color, but the wrap still emits ANSI escapes; honoring the env
66
+ * var keeps output paste-safe in scripted contexts.
67
+ */
68
+ function paintBoldUnderline(text) {
69
+ if (process.env.NO_COLOR && process.env.NO_COLOR !== '')
70
+ return text;
71
+ return `\x1b[1m\x1b[4m${text}\x1b[24m\x1b[22m`;
72
+ }
73
+ /**
74
+ * v4.1.3-essentials reply-polish: bold-on + skin paint + bold-off.
75
+ * Used by the 4-tier heading hierarchy so each level can pick its own
76
+ * color while sharing the bold weight. Emit order matches the rest of
77
+ * the painter stack: outer wrap is bold, inner wrap is fg color.
78
+ *
79
+ * Honors `NO_COLOR=1` via the skin engine's own gate; the bold ANSI
80
+ * still emits because bold is a weight, not a color (matches the
81
+ * paintBoldUnderline convention for `**bold**`).
82
+ */
83
+ function paintBold(kind) {
84
+ const colorize = paint(kind);
85
+ return (text) => `\x1b[1m${colorize(text)}\x1b[22m`;
86
+ }
39
87
  /**
40
88
  * Render a fenced code block: top divider with language label, body
41
89
  * with optional syntax highlighting, bottom divider.
@@ -51,10 +99,41 @@ function paint(kind) {
51
99
  * the renderer with; the older positional path is kept for
52
100
  * compatibility.
53
101
  */
102
+ /**
103
+ * v4.1.3-essentials reply-polish: 24-bit dark background applied per
104
+ * line so code stands out from prose.
105
+ *
106
+ * Color choice: `\x1b[48;2;50;50;60m` (#32323c, slightly bluish dark
107
+ * grey). The original `30,30,30` (#1e1e1e) was invisible against VS
108
+ * Code's integrated terminal default (also #1e1e1e) and barely
109
+ * distinct from Windows Terminal's One Half Dark. #32323c is visibly
110
+ * different from every common dark-terminal default (Campbell, One
111
+ * Half Dark, Solarized Dark, Monokai, VS Code) while staying subtle
112
+ * enough to read as "code chrome" rather than a jarring highlight.
113
+ *
114
+ * Used by BOTH the block path (fenced code blocks) and the inline
115
+ * path (`` `code` `` spans) so the two affordances visually agree —
116
+ * inline code reads as "this is code" via the same chrome as block
117
+ * code, just shorter.
118
+ *
119
+ * NOTE: \x1b[49m is "default background", terminating the per-line
120
+ * background scope cleanly. Each body line is wrapped individually
121
+ * rather than wrapping the whole block, so the background doesn't
122
+ * bleed across the closing horizontal rule (which already paints fg
123
+ * muted with its own reset).
124
+ */
125
+ const CODE_BG_ON = '\x1b[48;2;50;50;60m';
126
+ const CODE_BG_OFF = '\x1b[49m';
54
127
  function renderCodeBlock(code, lang) {
55
128
  const sk = (0, skinEngine_1.getSkinEngine)();
56
- 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)();
57
134
  const langLabel = (lang ?? '').trim();
135
+ // v4.1.3-essentials reply-polish: language tag on the top rule
136
+ // already shipped; keep it. Bottom rule unlabeled (closing fence).
58
137
  const top = langLabel
59
138
  ? `── ${langLabel} ${'─'.repeat(Math.max(0, width - langLabel.length - 4))}`
60
139
  : '─'.repeat(width);
@@ -62,11 +141,43 @@ function renderCodeBlock(code, lang) {
62
141
  const body = (0, syntaxHighlight_1.isSupportedLang)(langLabel)
63
142
  ? (0, syntaxHighlight_1.highlightCode)(code, langLabel)
64
143
  : code;
65
- const indented = body.split('\n').map((ln) => ` ${ln}`).join('\n');
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)
157
+ // - left rail `│ ` painted muted (mirrors blockquote's `┃ ` rail
158
+ // with a different glyph so they're visually distinct)
159
+ // - 24-bit dark background wrapping the rail + content (subtle
160
+ // "this is code" affordance without going full TUI box-frame)
161
+ const rail = sk.applyColors('│', 'muted');
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.
66
177
  return [
67
- sk.applyColors(top, 'muted'),
178
+ `${gutter}${sk.applyColors(top, 'muted')}`,
68
179
  indented,
69
- sk.applyColors(bot, 'muted'),
180
+ `${gutter}${sk.applyColors(bot, 'muted')}`,
70
181
  '',
71
182
  ].join('\n') + '\n';
72
183
  }
@@ -82,26 +193,152 @@ function renderBlockquote(quote) {
82
193
  .join('\n') + '\n';
83
194
  }
84
195
  /**
85
- * Marked-terminal heading callback gets the rendered heading text +
86
- * level. We paint h1 in brand-bold, h2 in brand, h3+ in heading.
196
+ * v4.1.3-essentials reply-polish: 4-tier heading hierarchy using the
197
+ * existing palette colors so visual weight differs per level even
198
+ * though we don't introduce a new ColorKind.
199
+ *
200
+ * H1 — brand + bold + UPPERCASE (major section heading)
201
+ * H2 — brand + bold (subsection — same hue as H1
202
+ * but sentence-case + no caps)
203
+ * H3 — agent + bold (off-white, lighter weight
204
+ * than brand)
205
+ * H4+ — muted + bold (quietest — same grey as the
206
+ * reply container's chrome)
207
+ *
208
+ * v4.1.3-essentials reply-polish: spacing tightened from `\n\n` to
209
+ * `\n` per level. marked-terminal contributes its own block
210
+ * separator (one more newline) → total `\n\n` between heading and
211
+ * next block = single blank line, matching paragraph rhythm.
212
+ * Previously this emitted `\n\n\n\n` (three blank lines) which made
213
+ * structured replies feel cramped at top and over-aired between
214
+ * sections.
87
215
  */
88
- function renderHeading(text, level, _raw) {
89
- if (level <= 1)
90
- return paint('brand')(text.toUpperCase()) + '\n\n';
91
- if (level === 2)
92
- return paint('brand')(text) + '\n\n';
93
- return paint('heading')(text) + '\n\n';
216
+ // 4-tier hierarchy. Called by the prototype-level `heading` override
217
+ // in getReplyRenderer() which extracts depth from the token first.
218
+ // Plain `(text, depth)` signature; the marked v15 / v14 / positional
219
+ // translation happens in the override.
220
+ //
221
+ // Each tier ends with `\n\n` to fence the heading from the next block
222
+ // with a blank line. Earlier we tried `\n` (single trailing newline)
223
+ // assuming marked-terminal's `section()` wrapper added its own
224
+ // padding — but the prototype-level override bypasses section(), so
225
+ // we own the spacing end-to-end. Result with `\n\n`: heading visible
226
+ // on its own line, blank line separates it from the next paragraph /
227
+ // heading / list. Matches the paragraph rhythm (`text\n\n`).
228
+ function renderHeading(text, depth) {
229
+ if (depth <= 1)
230
+ return paintBold('brand')(text.toUpperCase()) + '\n\n';
231
+ if (depth === 2)
232
+ return paintBold('brand')(text) + '\n\n';
233
+ if (depth === 3)
234
+ return paintBold('agent')(text) + '\n\n';
235
+ return paintBold('muted')(text) + '\n\n';
94
236
  }
95
237
  /**
96
- * List items get a `▸ ` glyph in muted; numbered lists keep their
97
- * numeric prefix (marked-terminal already prepends `N.` for ordered
98
- * lists, so we just paint the body).
238
+ * v4.1.3-essentials reply-polish: the `opts.listitem` callback used to
239
+ * own bullet rendering but marked-terminal's outer `list` method
240
+ * ALSO emits a `* ` prefix, producing visible double bullets
241
+ * (` * ▸ item`). The fix is a prototype-level override on BOTH
242
+ * `list` and `listitem` (mirrors the existing pattern for `code` and
243
+ * `link`). See the override block in getReplyRenderer().
244
+ *
245
+ * This callback now just returns the inner text unchanged so the
246
+ * prototype-level `list` override can do the bullet + indent work
247
+ * with full nesting-depth context.
99
248
  */
100
249
  function renderListItem(text) {
101
- // marked-terminal feeds us the rendered child text. Strip its
102
- // default tab prefix so our two-space indent stays consistent.
103
- const body = text.replace(/^\s+/, '');
104
- return ` ${paint('muted')('▸')} ${body}\n`;
250
+ return text;
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('');
105
342
  }
106
343
  /**
107
344
  * Singleton — caching is fine since options bind to the active skin
@@ -121,14 +358,33 @@ function getReplyRenderer() {
121
358
  // method directly below.
122
359
  const opts = {
123
360
  blockquote: renderBlockquote,
124
- heading: renderHeading,
125
- firstHeading: (text, _level, _raw) => paint('brand')(text.toUpperCase()) + '\n\n',
126
- hr: () => paint('muted')('─'.repeat(Math.min(process.stdout.columns ?? 80, 100) - 4)) + '\n',
361
+ // v4.1.3-essentials reply-polish: `opts.heading` and `opts.firstHeading`
362
+ // both removed. marked-terminal calls `opts.heading(text)` with ONLY
363
+ // text (audit-confirmed via toString), dropping the depth info we
364
+ // need for the 4-tier hierarchy. The prototype-level `renderer.heading`
365
+ // override below owns the depth extraction + tier selection end-to-end.
366
+ // marked-terminal's stripped-args call path never reaches our callback.
367
+ hr: () => paint('muted')('─'.repeat((0, frame_1.getBodyWidth)())) + '\n',
127
368
  listitem: renderListItem,
128
369
  paragraph: (text) => `${text}\n\n`,
129
- strong: paint('brand'),
370
+ // v4.1.3-essentials: bold renders as ANSI bold + underline
371
+ // (was 'brand' / orange, then bright-white; landed on underline
372
+ // so the color palette stays available for state semantics).
373
+ strong: paintBoldUnderline,
130
374
  em: paint('muted'),
131
- codespan: (text) => paint('accent')(`\`${text}\``),
375
+ // v4.1.3-essentials reply-polish: inline `` `code` `` — strip
376
+ // the literal backticks (used to leak into the visible output)
377
+ // and wrap with the same dark background as fenced code blocks.
378
+ // Visual consistency: inline code reads as "this is code" via the
379
+ // same chrome as block code, just shorter. One leading + trailing
380
+ // space inside the bg span gives the chrome a bit of padding so
381
+ // letters don't sit flush against the bg edge.
382
+ //
383
+ // Trade-off (accepted): if an inline-code span breaks across a
384
+ // line wrap, the bg painting may show a visual seam at the wrap
385
+ // point. Acceptable for v4.1.3 — revertable to Path A (no bg) if
386
+ // visual smoke surfaces a real problem.
387
+ codespan: (text) => `${CODE_BG_ON} ${paint('accent')(text)} ${CODE_BG_OFF}`,
132
388
  del: paint('muted'),
133
389
  // marked-terminal calls opts.link with the ASSEMBLED visual
134
390
  // (already OSC8-wrapped when the host terminal supports it),
@@ -136,7 +392,12 @@ function getReplyRenderer() {
136
392
  link: (assembled) => paint('accent')(assembled),
137
393
  href: paint('accent'),
138
394
  text: (text) => text,
139
- 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)(),
140
401
  showSectionPrefix: false,
141
402
  reflowText: false,
142
403
  tab: 2,
@@ -185,6 +446,174 @@ function getReplyRenderer() {
185
446
  const painted = paint('accent')(label);
186
447
  return `\x1b]8;;${url}\x1b\\${painted}\x1b]8;;\x1b\\`;
187
448
  };
449
+ // v4.1.3-essentials reply-polish: prototype-level `heading` override.
450
+ //
451
+ // Why: marked-terminal's internal `heading` method extracts the
452
+ // token's depth, then calls `opts.heading(text)` with ONLY the
453
+ // text — dropping the level info on the floor. Our 4-tier hierarchy
454
+ // (H1 brand+caps, H2 brand, H3 agent, H4+ muted) needs level
455
+ // context, so we must own the whole method.
456
+ //
457
+ // The override accepts marked v15's token-object shape and falls
458
+ // through to v14 positional for unit tests that pass plain strings.
459
+ renderer.heading = function (textOrToken, levelArg, _raw) {
460
+ let text;
461
+ let depth;
462
+ if (typeof textOrToken === 'object' && textOrToken !== null) {
463
+ const tok = textOrToken;
464
+ depth = typeof tok.depth === 'number' ? tok.depth : 1;
465
+ // Prefer parseInline for rich heading content (e.g. `## H2 with **bold**`).
466
+ // Falls through to tok.text for the common plain-text case.
467
+ const parser = this.parser;
468
+ if (tok.tokens && parser?.parseInline) {
469
+ text = parser.parseInline(tok.tokens);
470
+ }
471
+ else {
472
+ text = String(tok.text ?? '');
473
+ }
474
+ }
475
+ else {
476
+ text = String(textOrToken ?? '');
477
+ depth = typeof levelArg === 'number' ? levelArg : 1;
478
+ }
479
+ return renderHeading(text, depth);
480
+ };
481
+ // v4.1.3-essentials reply-polish: prototype-level list overrides.
482
+ //
483
+ // Why two functions and a depth counter:
484
+ // - marked-terminal's default `list` injects a `* ` (or `N. `)
485
+ // prefix BEFORE calling our `opts.listitem` callback, producing
486
+ // visible double bullets — see audit. Owning `list` at the
487
+ // prototype level lets us suppress that and emit our own.
488
+ // - Nesting depth determines the bullet glyph: top-level gets `•`
489
+ // and any deeper level gets `▸`. marked doesn't pass depth to
490
+ // the renderer, so we track it on the renderer instance via a
491
+ // counter that increments on `list`-enter and decrements on
492
+ // exit. This works because marked walks the token tree
493
+ // synchronously: a nested list's `list` call always completes
494
+ // between its parent's `list`-enter and `list`-exit.
495
+ // - Items already had their child markdown rendered via the
496
+ // prototype's `listitem` (which we leave as a passthrough above
497
+ // in the opts block — it just returns the inner text). The
498
+ // body string we receive in `list` is the concatenated children;
499
+ // each child can itself be a nested list rendering, whose own
500
+ // `list` call already handled its bullets + indent.
501
+ //
502
+ // Numbered lists: `start` and `ordered` come from the token; we
503
+ // emit `N.` prefix in muted to keep the visual rhythm consistent
504
+ // with bulleted lists but preserve numeric semantics.
505
+ //
506
+ // Indent: 2 spaces per nesting level. Top-level items therefore
507
+ // sit at column 2 (matching the rest of the reply container's
508
+ // chrome); nested at column 4, 6, etc.
509
+ const proto = renderer;
510
+ proto._listDepth = 0;
511
+ renderer.listitem = function (text, _task, _checked) {
512
+ // marked v15 may pass a token object; the assembled-text fallback
513
+ // covers older signatures. Either way we want the inner text
514
+ // unchanged here — bullet + indent is owned by `list` below.
515
+ if (typeof text === 'object' && text !== null) {
516
+ const tok = text;
517
+ if (typeof tok.text === 'string')
518
+ return tok.text;
519
+ const parser = this.parser;
520
+ return parser?.parseInline?.(tok.tokens ?? []) ?? '';
521
+ }
522
+ return String(text ?? '');
523
+ };
524
+ renderer.list = function (body, ordered, start) {
525
+ // marked v15 token shape: { ordered, start, items: [token, ...] }
526
+ // Older positional shape: (body, ordered, start)
527
+ let isOrdered = false;
528
+ let startNum = 1;
529
+ let items;
530
+ // CRITICAL: increment depth BEFORE walking items. Item walking via
531
+ // `parser.parse(it.tokens)` recurses into our own override for any
532
+ // nested list tokens — those nested calls need to see the parent's
533
+ // incremented depth so they pick the deeper bullet glyph (▸) and
534
+ // indent. If we increment AFTER `parser.parse`, the nested call
535
+ // sees depth=0, renders at top-level styling, and the visible
536
+ // nesting collapses. Confirmed via runtime trace.
537
+ proto._listDepth = (proto._listDepth ?? 0) + 1;
538
+ const depth = proto._listDepth;
539
+ if (typeof body === 'object' && body !== null) {
540
+ const tok = body;
541
+ isOrdered = tok.ordered === true;
542
+ startNum = typeof tok.start === 'number' ? tok.start : 1;
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).
564
+ const parser = this.parser;
565
+ items = (tok.items ?? []).map((it) => {
566
+ if (!parser)
567
+ return it.text ?? '';
568
+ return renderListItemTokens(it, parser);
569
+ });
570
+ }
571
+ else {
572
+ isOrdered = ordered === true;
573
+ startNum = typeof start === 'number' ? start : 1;
574
+ // Positional `body` is the already-concatenated rendered items.
575
+ // Split on newlines that introduce a fresh item; marked emits
576
+ // each item as its own logical line. Best-effort — marked v15
577
+ // path above is the production case.
578
+ const raw = String(body ?? '');
579
+ items = raw.split('\n').filter((ln) => ln.trim().length > 0);
580
+ }
581
+ const indent = ' '.repeat(depth);
582
+ // Top-level bullet `•` (filled); nested `▸` (arrow-like) for
583
+ // visual depth differentiation. Numbered lists override with
584
+ // `N.` regardless of depth.
585
+ const bulletGlyph = depth === 1 ? '•' : '▸';
586
+ const lines = [];
587
+ for (let i = 0; i < items.length; i += 1) {
588
+ const item = items[i];
589
+ // Each item may itself contain newlines (nested list output,
590
+ // multi-line paragraph). Indent every line of the rendered
591
+ // item AFTER the first — the first line takes the bullet, the
592
+ // continuation lines align under the bullet's content column.
593
+ const marker = isOrdered
594
+ ? paint('muted')(`${startNum + i}.`)
595
+ : paint('muted')(bulletGlyph);
596
+ const itemLines = item.split('\n');
597
+ const head = itemLines[0] ?? '';
598
+ const tail = itemLines.slice(1);
599
+ lines.push(`${indent}${marker} ${head}`);
600
+ // Continuation lines: if they already have content, align them
601
+ // under the bullet's text column (indent + marker-width + 1
602
+ // space). marked-terminal's nested lists arrive pre-indented so
603
+ // we pass them through.
604
+ for (const tailLine of tail) {
605
+ if (tailLine.length === 0)
606
+ continue;
607
+ lines.push(tailLine);
608
+ }
609
+ }
610
+ proto._listDepth -= 1;
611
+ // Top-level list closes with a trailing newline to separate from
612
+ // the next block; nested lists return without extra padding so
613
+ // they nest cleanly inside their parent item.
614
+ const out = lines.join('\n');
615
+ return proto._listDepth === 0 ? out + '\n' : out + '\n';
616
+ };
188
617
  cachedRenderer = {
189
618
  render(text) {
190
619
  try {
@@ -194,7 +623,21 @@ function getReplyRenderer() {
194
623
  // if other code transiently swaps the renderer.
195
624
  marked_1.marked.setOptions({ renderer: renderer });
196
625
  const out = marked_1.marked.parse(text);
197
- 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);
198
641
  }
199
642
  catch {
200
643
  return text;
@@ -203,6 +646,24 @@ function getReplyRenderer() {
203
646
  };
204
647
  return cachedRenderer;
205
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
+ }
206
667
  /** Test reset — drops the cached renderer so a skin change picks up. */
207
668
  function _resetForTests() {
208
669
  cachedRenderer = null;
@@ -49,10 +49,23 @@ const DEFAULT_SKIN = {
49
49
  error: [0xf4, 0x47, 0x47],
50
50
  warn: [0xff, 0xc1, 0x07],
51
51
  success: [0x4c, 0xaf, 0x50],
52
- // Phase 22 colour discipline: soft cyan replaces grey for secondary
53
- // text (timestamps, hints, tips, dim diagnostics).
54
- muted: [0x6f, 0xb3, 0xd2],
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,
62
+ // v4.1.3-repl-polish: session = soft cyan (ex-muted); used for IDs
63
+ // and the session-end card header labels.
64
+ session: [0x6f, 0xb3, 0xd2],
65
+ // v4.1.3-repl-polish: degraded = amber yellow; distinct from warn
66
+ // (which shares the colour) so callers can differentiate in code
67
+ // even though they render identically.
68
+ degraded: [0xff, 0xc1, 0x07],
56
69
  },
57
70
  glyphs: {
58
71
  bullet: '•',
@@ -77,8 +90,15 @@ const LIGHT_SKIN = {
77
90
  error: [0xb0, 0x10, 0x10],
78
91
  warn: [0x80, 0x60, 0x00],
79
92
  success: [0x1b, 0x5e, 0x20],
80
- 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],
81
99
  heading: [0xc4, 0x42, 0x10],
100
+ session: [0x00, 0x55, 0x88],
101
+ degraded: [0x80, 0x60, 0x00],
82
102
  },
83
103
  glyphs: { ...DEFAULT_SKIN.glyphs },
84
104
  };
@@ -96,6 +116,8 @@ const MONOCHROME_SKIN = {
96
116
  success: null,
97
117
  muted: null,
98
118
  heading: null,
119
+ session: null,
120
+ degraded: null,
99
121
  },
100
122
  glyphs: {
101
123
  bullet: '*',