aiden-runtime 4.1.2 → 4.1.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.
@@ -33,6 +33,39 @@
33
33
  * don't have to drive a prompt API. The prompt-loop function wires
34
34
  * the parser to the existing `ChatPromptApi.readLine`.
35
35
  */
36
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
37
+ if (k2 === undefined) k2 = k;
38
+ var desc = Object.getOwnPropertyDescriptor(m, k);
39
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
40
+ desc = { enumerable: true, get: function() { return m[k]; } };
41
+ }
42
+ Object.defineProperty(o, k2, desc);
43
+ }) : (function(o, m, k, k2) {
44
+ if (k2 === undefined) k2 = k;
45
+ o[k2] = m[k];
46
+ }));
47
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
48
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
49
+ }) : function(o, v) {
50
+ o["default"] = v;
51
+ });
52
+ var __importStar = (this && this.__importStar) || (function () {
53
+ var ownKeys = function(o) {
54
+ ownKeys = Object.getOwnPropertyNames || function (o) {
55
+ var ar = [];
56
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
57
+ return ar;
58
+ };
59
+ return ownKeys(o);
60
+ };
61
+ return function (mod) {
62
+ if (mod && mod.__esModule) return mod;
63
+ var result = {};
64
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
65
+ __setModuleDefault(result, mod);
66
+ return result;
67
+ };
68
+ })();
36
69
  Object.defineProperty(exports, "__esModule", { value: true });
37
70
  exports.parsePromotionInput = parsePromotionInput;
38
71
  exports.formatCandidateList = formatCandidateList;
@@ -116,16 +149,113 @@ function formatCandidateList(candidates) {
116
149
  return lines.join('\n');
117
150
  }
118
151
  /**
119
- * Drive the approval prompt. Renders the candidate list, reads ONE
120
- * line, parses, returns approved Candidate[]. On unparseable input
121
- * re-prompts ONCE; second failure defaults to skip with a dim line
122
- * explaining why nothing was promoted.
152
+ * Drive the approval prompt. Two paths:
153
+ *
154
+ * 1. Interactive checkbox (TTY): @inquirer/prompts.checkbox, space
155
+ * to toggle, enter to confirm, esc/ctrl+c to skip. Default
156
+ * selection is NONE — the user explicitly opts in. v4.1.3-essentials
157
+ * promotion-ux replaces what used to be a text-input chore.
158
+ *
159
+ * 2. Text-input fallback (non-TTY / piped / CI): renders the
160
+ * numbered list and reads a single line. Parser handles "1,3"
161
+ * / "1-3" / "all" / "skip" / "". Re-prompts ONCE on garbage,
162
+ * then defaults to skip. The original Phase v4.1.2-memory-D
163
+ * behavior, preserved verbatim.
164
+ *
165
+ * Auto-routes via `process.stdout.isTTY`; tests override via opts.
123
166
  *
124
167
  * No mid-session state leakage — purely a session-end interaction.
125
168
  */
126
- async function promptForApproval(api, display, candidates) {
169
+ async function promptForApproval(api, display, candidates, opts = {}) {
127
170
  if (candidates.length === 0)
128
171
  return [];
172
+ const useInteractive = opts.forceInteractive === true
173
+ ? true
174
+ : opts.forceTextInput === true
175
+ ? false
176
+ : !!process.stdout.isTTY;
177
+ if (useInteractive) {
178
+ return promptForApprovalInteractive(display, candidates);
179
+ }
180
+ return promptForApprovalText(api, display, candidates);
181
+ }
182
+ /**
183
+ * v4.1.3-essentials promotion-ux: interactive multi-select checkbox.
184
+ * Uses @inquirer/prompts.checkbox (already a runtime dep — same
185
+ * library as the model picker, setup wizard, approval prompts).
186
+ *
187
+ * Choices render with the source-type tag inline so the user sees
188
+ * "[decision] X" / "[open item] Y" / "[user said] Z" — matches the
189
+ * tag set the text-input renderer uses.
190
+ *
191
+ * Exit paths:
192
+ * - User confirms with at least one box checked → return selected
193
+ * - User confirms with zero boxes checked → dim note, return []
194
+ * - User hits Esc / Ctrl+C (inquirer throws) → dim note, return []
195
+ *
196
+ * All three "nothing selected" paths produce the same outcome — empty
197
+ * array — matching the user's Q5 default ("empty/skip/esc all
198
+ * equivalent").
199
+ *
200
+ * Lazy-require inquirer so test harnesses without a TTY don't crash
201
+ * importing the module. Same pattern setupWizard / callbacks /
202
+ * modelPicker already use.
203
+ */
204
+ async function promptForApprovalInteractive(display, candidates) {
205
+ // Dynamic ES import (not CommonJS require) so vitest's vi.mock can
206
+ // intercept the call in tests. The runtime behavior is identical
207
+ // for our purpose — single one-shot lazy load on first call.
208
+ const inq = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
209
+ const heading = `${candidates.length} thing${candidates.length === 1 ? '' : 's'} ` +
210
+ `worth remembering this session.`;
211
+ display.write('\n' + heading + '\n');
212
+ try {
213
+ const selected = await inq.checkbox({
214
+ message: 'Promote which to durable memory? (space toggles · enter confirms)',
215
+ choices: candidates.map((c, i) => ({
216
+ name: `${typeTag(c)} ${c.text}`,
217
+ value: i,
218
+ checked: false,
219
+ })),
220
+ loop: false,
221
+ pageSize: Math.min(10, candidates.length),
222
+ });
223
+ if (selected.length === 0) {
224
+ display.dim('Nothing promoted to durable facts.');
225
+ return [];
226
+ }
227
+ return selected.map((i) => candidates[i]);
228
+ }
229
+ catch {
230
+ // Inquirer throws on Ctrl+C / Esc — treat as skip.
231
+ display.dim('Skipped: nothing promoted to durable facts.');
232
+ return [];
233
+ }
234
+ }
235
+ /**
236
+ * Source-type tag matching the text-input renderer's format. Kept as
237
+ * a helper so the interactive choice labels stay in sync with the
238
+ * text path if a new `Candidate.source` value lands.
239
+ */
240
+ function typeTag(c) {
241
+ if (c.source === 'explicit')
242
+ return '[user said]';
243
+ if (c.source === 'decision')
244
+ return '[decision]';
245
+ return '[open item]';
246
+ }
247
+ /**
248
+ * Phase v4.1.2-memory-D text-input loop. Renders the candidate list,
249
+ * reads ONE line, parses, returns approved Candidate[]. On unparseable
250
+ * input re-prompts ONCE; second failure defaults to skip with a dim
251
+ * line explaining why nothing was promoted.
252
+ *
253
+ * Kept as the non-TTY fallback (pipes, CI, test harness) so the
254
+ * promotion-flow contract continues to work without an interactive
255
+ * shell. v4.1.3-essentials promotion-ux renamed this from
256
+ * `promptForApproval` so the public entry point can dispatch.
257
+ */
258
+ async function promptForApprovalText(api, display, candidates) {
129
259
  display.write('\n' + formatCandidateList(candidates) + '\n');
130
260
  for (let attempt = 0; attempt < 2; attempt += 1) {
131
261
  const raw = await api.readLine('Promote > ');
@@ -36,6 +36,48 @@ const TerminalRenderer = require('marked-terminal').default ?? require('marked-t
36
36
  function paint(kind) {
37
37
  return (text) => (0, skinEngine_1.getSkinEngine)().applyColors(text, kind);
38
38
  }
39
+ /**
40
+ * v4.1.3-essentials: bold (`**foo**`) markdown emphasis renders as
41
+ * ANSI bold + underline. Previously painted 'brand' (orange) which
42
+ * collided with the heading hierarchy. Briefly tried bold + bright-
43
+ * white; landed on bold + underline because underline carries
44
+ * emphasis without consuming a color slot — the palette stays
45
+ * available for state semantics (yellow=degraded, red=error, etc.).
46
+ *
47
+ * ANSI sequence: `\x1b[1m\x1b[4m{text}\x1b[24m\x1b[22m` — bold ON +
48
+ * underline ON, then underline OFF + bold OFF. Reset order matters
49
+ * (underline first, bold second) so the closing codes don't reorder
50
+ * styles surprisingly on terminals that batch SGR updates.
51
+ *
52
+ * Bypasses the skin system intentionally — bold-as-underline is an
53
+ * opinionated default for this slice. Same caveat as the prior bold-
54
+ * as-color iteration: nested markdown loses the outer style after
55
+ * close (pre-existing limitation of the painter-stack architecture).
56
+ *
57
+ * Honors `NO_COLOR=1` per the standard (skips the wrap entirely).
58
+ * Strictly speaking `NO_COLOR` is about color and underline isn't
59
+ * a color, but the wrap still emits ANSI escapes; honoring the env
60
+ * var keeps output paste-safe in scripted contexts.
61
+ */
62
+ function paintBoldUnderline(text) {
63
+ if (process.env.NO_COLOR && process.env.NO_COLOR !== '')
64
+ return text;
65
+ return `\x1b[1m\x1b[4m${text}\x1b[24m\x1b[22m`;
66
+ }
67
+ /**
68
+ * v4.1.3-essentials reply-polish: bold-on + skin paint + bold-off.
69
+ * Used by the 4-tier heading hierarchy so each level can pick its own
70
+ * color while sharing the bold weight. Emit order matches the rest of
71
+ * the painter stack: outer wrap is bold, inner wrap is fg color.
72
+ *
73
+ * Honors `NO_COLOR=1` via the skin engine's own gate; the bold ANSI
74
+ * still emits because bold is a weight, not a color (matches the
75
+ * paintBoldUnderline convention for `**bold**`).
76
+ */
77
+ function paintBold(kind) {
78
+ const colorize = paint(kind);
79
+ return (text) => `\x1b[1m${colorize(text)}\x1b[22m`;
80
+ }
39
81
  /**
40
82
  * Render a fenced code block: top divider with language label, body
41
83
  * with optional syntax highlighting, bottom divider.
@@ -51,10 +93,37 @@ function paint(kind) {
51
93
  * the renderer with; the older positional path is kept for
52
94
  * compatibility.
53
95
  */
96
+ /**
97
+ * v4.1.3-essentials reply-polish: 24-bit dark background applied per
98
+ * line so code stands out from prose.
99
+ *
100
+ * Color choice: `\x1b[48;2;50;50;60m` (#32323c, slightly bluish dark
101
+ * grey). The original `30,30,30` (#1e1e1e) was invisible against VS
102
+ * Code's integrated terminal default (also #1e1e1e) and barely
103
+ * distinct from Windows Terminal's One Half Dark. #32323c is visibly
104
+ * different from every common dark-terminal default (Campbell, One
105
+ * Half Dark, Solarized Dark, Monokai, VS Code) while staying subtle
106
+ * enough to read as "code chrome" rather than a jarring highlight.
107
+ *
108
+ * Used by BOTH the block path (fenced code blocks) and the inline
109
+ * path (`` `code` `` spans) so the two affordances visually agree —
110
+ * inline code reads as "this is code" via the same chrome as block
111
+ * code, just shorter.
112
+ *
113
+ * NOTE: \x1b[49m is "default background", terminating the per-line
114
+ * background scope cleanly. Each body line is wrapped individually
115
+ * rather than wrapping the whole block, so the background doesn't
116
+ * bleed across the closing horizontal rule (which already paints fg
117
+ * muted with its own reset).
118
+ */
119
+ const CODE_BG_ON = '\x1b[48;2;50;50;60m';
120
+ const CODE_BG_OFF = '\x1b[49m';
54
121
  function renderCodeBlock(code, lang) {
55
122
  const sk = (0, skinEngine_1.getSkinEngine)();
56
123
  const width = Math.min(process.stdout.columns ?? 80, 100) - 4;
57
124
  const langLabel = (lang ?? '').trim();
125
+ // v4.1.3-essentials reply-polish: language tag on the top rule
126
+ // already shipped; keep it. Bottom rule unlabeled (closing fence).
58
127
  const top = langLabel
59
128
  ? `── ${langLabel} ${'─'.repeat(Math.max(0, width - langLabel.length - 4))}`
60
129
  : '─'.repeat(width);
@@ -62,7 +131,22 @@ function renderCodeBlock(code, lang) {
62
131
  const body = (0, syntaxHighlight_1.isSupportedLang)(langLabel)
63
132
  ? (0, syntaxHighlight_1.highlightCode)(code, langLabel)
64
133
  : code;
65
- const indented = body.split('\n').map((ln) => ` ${ln}`).join('\n');
134
+ // v4.1.3-essentials reply-polish: each body line gets:
135
+ // - 2-space outer indent (existing reply container indent)
136
+ // - left rail `│ ` painted muted (mirrors blockquote's `┃ ` rail
137
+ // with a different glyph so they're visually distinct)
138
+ // - 24-bit dark background wrapping the rail + content (subtle
139
+ // "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
+ 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');
66
150
  return [
67
151
  sk.applyColors(top, 'muted'),
68
152
  indented,
@@ -82,26 +166,61 @@ function renderBlockquote(quote) {
82
166
  .join('\n') + '\n';
83
167
  }
84
168
  /**
85
- * Marked-terminal heading callback gets the rendered heading text +
86
- * level. We paint h1 in brand-bold, h2 in brand, h3+ in heading.
169
+ * v4.1.3-essentials reply-polish: 4-tier heading hierarchy using the
170
+ * existing palette colors so visual weight differs per level even
171
+ * though we don't introduce a new ColorKind.
172
+ *
173
+ * H1 — brand + bold + UPPERCASE (major section heading)
174
+ * H2 — brand + bold (subsection — same hue as H1
175
+ * but sentence-case + no caps)
176
+ * H3 — agent + bold (off-white, lighter weight
177
+ * than brand)
178
+ * H4+ — muted + bold (quietest — same grey as the
179
+ * reply container's chrome)
180
+ *
181
+ * v4.1.3-essentials reply-polish: spacing tightened from `\n\n` to
182
+ * `\n` per level. marked-terminal contributes its own block
183
+ * separator (one more newline) → total `\n\n` between heading and
184
+ * next block = single blank line, matching paragraph rhythm.
185
+ * Previously this emitted `\n\n\n\n` (three blank lines) which made
186
+ * structured replies feel cramped at top and over-aired between
187
+ * sections.
87
188
  */
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';
189
+ // 4-tier hierarchy. Called by the prototype-level `heading` override
190
+ // in getReplyRenderer() which extracts depth from the token first.
191
+ // Plain `(text, depth)` signature; the marked v15 / v14 / positional
192
+ // translation happens in the override.
193
+ //
194
+ // Each tier ends with `\n\n` to fence the heading from the next block
195
+ // with a blank line. Earlier we tried `\n` (single trailing newline)
196
+ // assuming marked-terminal's `section()` wrapper added its own
197
+ // padding — but the prototype-level override bypasses section(), so
198
+ // we own the spacing end-to-end. Result with `\n\n`: heading visible
199
+ // on its own line, blank line separates it from the next paragraph /
200
+ // heading / list. Matches the paragraph rhythm (`text\n\n`).
201
+ function renderHeading(text, depth) {
202
+ if (depth <= 1)
203
+ return paintBold('brand')(text.toUpperCase()) + '\n\n';
204
+ if (depth === 2)
205
+ return paintBold('brand')(text) + '\n\n';
206
+ if (depth === 3)
207
+ return paintBold('agent')(text) + '\n\n';
208
+ return paintBold('muted')(text) + '\n\n';
94
209
  }
95
210
  /**
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).
211
+ * v4.1.3-essentials reply-polish: the `opts.listitem` callback used to
212
+ * own bullet rendering but marked-terminal's outer `list` method
213
+ * ALSO emits a `* ` prefix, producing visible double bullets
214
+ * (` * ▸ item`). The fix is a prototype-level override on BOTH
215
+ * `list` and `listitem` (mirrors the existing pattern for `code` and
216
+ * `link`). See the override block in getReplyRenderer().
217
+ *
218
+ * This callback now just returns the inner text unchanged so the
219
+ * prototype-level `list` override can do the bullet + indent work
220
+ * with full nesting-depth context.
99
221
  */
100
222
  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`;
223
+ return text;
105
224
  }
106
225
  /**
107
226
  * Singleton — caching is fine since options bind to the active skin
@@ -121,14 +240,33 @@ function getReplyRenderer() {
121
240
  // method directly below.
122
241
  const opts = {
123
242
  blockquote: renderBlockquote,
124
- heading: renderHeading,
125
- firstHeading: (text, _level, _raw) => paint('brand')(text.toUpperCase()) + '\n\n',
243
+ // v4.1.3-essentials reply-polish: `opts.heading` and `opts.firstHeading`
244
+ // both removed. marked-terminal calls `opts.heading(text)` with ONLY
245
+ // text (audit-confirmed via toString), dropping the depth info we
246
+ // need for the 4-tier hierarchy. The prototype-level `renderer.heading`
247
+ // override below owns the depth extraction + tier selection end-to-end.
248
+ // marked-terminal's stripped-args call path never reaches our callback.
126
249
  hr: () => paint('muted')('─'.repeat(Math.min(process.stdout.columns ?? 80, 100) - 4)) + '\n',
127
250
  listitem: renderListItem,
128
251
  paragraph: (text) => `${text}\n\n`,
129
- strong: paint('brand'),
252
+ // v4.1.3-essentials: bold renders as ANSI bold + underline
253
+ // (was 'brand' / orange, then bright-white; landed on underline
254
+ // so the color palette stays available for state semantics).
255
+ strong: paintBoldUnderline,
130
256
  em: paint('muted'),
131
- codespan: (text) => paint('accent')(`\`${text}\``),
257
+ // v4.1.3-essentials reply-polish: inline `` `code` `` — strip
258
+ // the literal backticks (used to leak into the visible output)
259
+ // and wrap with the same dark background as fenced code blocks.
260
+ // Visual consistency: inline code reads as "this is code" via the
261
+ // same chrome as block code, just shorter. One leading + trailing
262
+ // space inside the bg span gives the chrome a bit of padding so
263
+ // letters don't sit flush against the bg edge.
264
+ //
265
+ // Trade-off (accepted): if an inline-code span breaks across a
266
+ // line wrap, the bg painting may show a visual seam at the wrap
267
+ // point. Acceptable for v4.1.3 — revertable to Path A (no bg) if
268
+ // visual smoke surfaces a real problem.
269
+ codespan: (text) => `${CODE_BG_ON} ${paint('accent')(text)} ${CODE_BG_OFF}`,
132
270
  del: paint('muted'),
133
271
  // marked-terminal calls opts.link with the ASSEMBLED visual
134
272
  // (already OSC8-wrapped when the host terminal supports it),
@@ -185,6 +323,159 @@ function getReplyRenderer() {
185
323
  const painted = paint('accent')(label);
186
324
  return `\x1b]8;;${url}\x1b\\${painted}\x1b]8;;\x1b\\`;
187
325
  };
326
+ // v4.1.3-essentials reply-polish: prototype-level `heading` override.
327
+ //
328
+ // Why: marked-terminal's internal `heading` method extracts the
329
+ // token's depth, then calls `opts.heading(text)` with ONLY the
330
+ // text — dropping the level info on the floor. Our 4-tier hierarchy
331
+ // (H1 brand+caps, H2 brand, H3 agent, H4+ muted) needs level
332
+ // context, so we must own the whole method.
333
+ //
334
+ // The override accepts marked v15's token-object shape and falls
335
+ // through to v14 positional for unit tests that pass plain strings.
336
+ renderer.heading = function (textOrToken, levelArg, _raw) {
337
+ let text;
338
+ let depth;
339
+ if (typeof textOrToken === 'object' && textOrToken !== null) {
340
+ const tok = textOrToken;
341
+ depth = typeof tok.depth === 'number' ? tok.depth : 1;
342
+ // Prefer parseInline for rich heading content (e.g. `## H2 with **bold**`).
343
+ // Falls through to tok.text for the common plain-text case.
344
+ const parser = this.parser;
345
+ if (tok.tokens && parser?.parseInline) {
346
+ text = parser.parseInline(tok.tokens);
347
+ }
348
+ else {
349
+ text = String(tok.text ?? '');
350
+ }
351
+ }
352
+ else {
353
+ text = String(textOrToken ?? '');
354
+ depth = typeof levelArg === 'number' ? levelArg : 1;
355
+ }
356
+ return renderHeading(text, depth);
357
+ };
358
+ // v4.1.3-essentials reply-polish: prototype-level list overrides.
359
+ //
360
+ // Why two functions and a depth counter:
361
+ // - marked-terminal's default `list` injects a `* ` (or `N. `)
362
+ // prefix BEFORE calling our `opts.listitem` callback, producing
363
+ // visible double bullets — see audit. Owning `list` at the
364
+ // prototype level lets us suppress that and emit our own.
365
+ // - Nesting depth determines the bullet glyph: top-level gets `•`
366
+ // and any deeper level gets `▸`. marked doesn't pass depth to
367
+ // the renderer, so we track it on the renderer instance via a
368
+ // counter that increments on `list`-enter and decrements on
369
+ // exit. This works because marked walks the token tree
370
+ // synchronously: a nested list's `list` call always completes
371
+ // between its parent's `list`-enter and `list`-exit.
372
+ // - Items already had their child markdown rendered via the
373
+ // prototype's `listitem` (which we leave as a passthrough above
374
+ // in the opts block — it just returns the inner text). The
375
+ // body string we receive in `list` is the concatenated children;
376
+ // each child can itself be a nested list rendering, whose own
377
+ // `list` call already handled its bullets + indent.
378
+ //
379
+ // Numbered lists: `start` and `ordered` come from the token; we
380
+ // emit `N.` prefix in muted to keep the visual rhythm consistent
381
+ // with bulleted lists but preserve numeric semantics.
382
+ //
383
+ // Indent: 2 spaces per nesting level. Top-level items therefore
384
+ // sit at column 2 (matching the rest of the reply container's
385
+ // chrome); nested at column 4, 6, etc.
386
+ const proto = renderer;
387
+ proto._listDepth = 0;
388
+ renderer.listitem = function (text, _task, _checked) {
389
+ // marked v15 may pass a token object; the assembled-text fallback
390
+ // covers older signatures. Either way we want the inner text
391
+ // unchanged here — bullet + indent is owned by `list` below.
392
+ if (typeof text === 'object' && text !== null) {
393
+ const tok = text;
394
+ if (typeof tok.text === 'string')
395
+ return tok.text;
396
+ const parser = this.parser;
397
+ return parser?.parseInline?.(tok.tokens ?? []) ?? '';
398
+ }
399
+ return String(text ?? '');
400
+ };
401
+ renderer.list = function (body, ordered, start) {
402
+ // marked v15 token shape: { ordered, start, items: [token, ...] }
403
+ // Older positional shape: (body, ordered, start)
404
+ let isOrdered = false;
405
+ let startNum = 1;
406
+ let items;
407
+ // CRITICAL: increment depth BEFORE walking items. Item walking via
408
+ // `parser.parse(it.tokens)` recurses into our own override for any
409
+ // nested list tokens — those nested calls need to see the parent's
410
+ // incremented depth so they pick the deeper bullet glyph (▸) and
411
+ // indent. If we increment AFTER `parser.parse`, the nested call
412
+ // sees depth=0, renders at top-level styling, and the visible
413
+ // nesting collapses. Confirmed via runtime trace.
414
+ proto._listDepth = (proto._listDepth ?? 0) + 1;
415
+ const depth = proto._listDepth;
416
+ if (typeof body === 'object' && body !== null) {
417
+ const tok = body;
418
+ isOrdered = tok.ordered === true;
419
+ 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).
425
+ const parser = this.parser;
426
+ items = (tok.items ?? []).map((it) => {
427
+ if (it.tokens && parser?.parse) {
428
+ return parser.parse(it.tokens);
429
+ }
430
+ return it.text ?? '';
431
+ });
432
+ }
433
+ else {
434
+ isOrdered = ordered === true;
435
+ startNum = typeof start === 'number' ? start : 1;
436
+ // Positional `body` is the already-concatenated rendered items.
437
+ // Split on newlines that introduce a fresh item; marked emits
438
+ // each item as its own logical line. Best-effort — marked v15
439
+ // path above is the production case.
440
+ const raw = String(body ?? '');
441
+ items = raw.split('\n').filter((ln) => ln.trim().length > 0);
442
+ }
443
+ const indent = ' '.repeat(depth);
444
+ // Top-level bullet `•` (filled); nested `▸` (arrow-like) for
445
+ // visual depth differentiation. Numbered lists override with
446
+ // `N.` regardless of depth.
447
+ const bulletGlyph = depth === 1 ? '•' : '▸';
448
+ const lines = [];
449
+ for (let i = 0; i < items.length; i += 1) {
450
+ const item = items[i];
451
+ // Each item may itself contain newlines (nested list output,
452
+ // multi-line paragraph). Indent every line of the rendered
453
+ // item AFTER the first — the first line takes the bullet, the
454
+ // continuation lines align under the bullet's content column.
455
+ const marker = isOrdered
456
+ ? paint('muted')(`${startNum + i}.`)
457
+ : paint('muted')(bulletGlyph);
458
+ const itemLines = item.split('\n');
459
+ const head = itemLines[0] ?? '';
460
+ const tail = itemLines.slice(1);
461
+ lines.push(`${indent}${marker} ${head}`);
462
+ // Continuation lines: if they already have content, align them
463
+ // under the bullet's text column (indent + marker-width + 1
464
+ // space). marked-terminal's nested lists arrive pre-indented so
465
+ // we pass them through.
466
+ for (const tailLine of tail) {
467
+ if (tailLine.length === 0)
468
+ continue;
469
+ lines.push(tailLine);
470
+ }
471
+ }
472
+ proto._listDepth -= 1;
473
+ // Top-level list closes with a trailing newline to separate from
474
+ // the next block; nested lists return without extra padding so
475
+ // they nest cleanly inside their parent item.
476
+ const out = lines.join('\n');
477
+ return proto._listDepth === 0 ? out + '\n' : out + '\n';
478
+ };
188
479
  cachedRenderer = {
189
480
  render(text) {
190
481
  try {
@@ -49,10 +49,17 @@ 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.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],
55
55
  heading: BRAND_ORANGE,
56
+ // v4.1.3-repl-polish: session = soft cyan (ex-muted); used for IDs
57
+ // and the session-end card header labels.
58
+ session: [0x6f, 0xb3, 0xd2],
59
+ // v4.1.3-repl-polish: degraded = amber yellow; distinct from warn
60
+ // (which shares the colour) so callers can differentiate in code
61
+ // even though they render identically.
62
+ degraded: [0xff, 0xc1, 0x07],
56
63
  },
57
64
  glyphs: {
58
65
  bullet: '•',
@@ -79,6 +86,8 @@ const LIGHT_SKIN = {
79
86
  success: [0x1b, 0x5e, 0x20],
80
87
  muted: [0x60, 0x60, 0x60],
81
88
  heading: [0xc4, 0x42, 0x10],
89
+ session: [0x00, 0x55, 0x88],
90
+ degraded: [0x80, 0x60, 0x00],
82
91
  },
83
92
  glyphs: { ...DEFAULT_SKIN.glyphs },
84
93
  };
@@ -96,6 +105,8 @@ const MONOCHROME_SKIN = {
96
105
  success: null,
97
106
  muted: null,
98
107
  heading: null,
108
+ session: null,
109
+ degraded: null,
99
110
  },
100
111
  glyphs: {
101
112
  bullet: '*',
@@ -86,6 +86,20 @@ exports.TOOL_PRIMARY_ARG = {
86
86
  system_info: '',
87
87
  now_playing: '',
88
88
  get_natural_events: '',
89
+ // ── v4.1.4-media — three-layer media-control bundle ──────────────────
90
+ // `media_sessions` has no args by schema; the empty-arg preview is
91
+ // suppressed by buildToolPreview returning ''.
92
+ // `media_transport` → preview by target ("spotify"), the actionable
93
+ // identifier the user typed. `action` is intentionally NOT chosen —
94
+ // GSMTC actions (play/pause/toggle) are short, the target is the
95
+ // discriminator.
96
+ // `media_key` is layer-3 fallback; show `action` since there's no
97
+ // target to surface (it's a blind keystroke).
98
+ // `app_input` shows `app` so the user sees which window got the keys.
99
+ media_sessions: '',
100
+ media_transport: 'target',
101
+ media_key: 'action',
102
+ app_input: 'app',
89
103
  };
90
104
  /**
91
105
  * Maximum visible characters for the preview value. Long commands /
@@ -2,27 +2,19 @@
2
2
  // core/tools/nowPlaying.ts — Live media session query via Windows WinRT
3
3
  // Uses GlobalSystemMediaTransportControlsSessionManager (works for Spotify,
4
4
  // YouTube in browser, Windows Media Player, and any SMTC-registered app).
5
+ //
6
+ // v4.1.4-media: the WinRT `Await` PS5.1 reflection bridge moved into the
7
+ // shared helper `tools/v4/system/_psHelpers.ts::winRtAwaitPreamble()` so
8
+ // the three GSMTC callers (this file + mediaSessions + mediaTransport)
9
+ // share one canonical implementation.
5
10
  Object.defineProperty(exports, "__esModule", { value: true });
6
11
  exports.getNowPlaying = getNowPlaying;
7
12
  const child_process_1 = require("child_process");
8
13
  const util_1 = require("util");
14
+ const _psHelpers_1 = require("../../tools/v4/system/_psHelpers");
9
15
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
10
- // PowerShell uses System.WindowsRuntimeSystemExtensions.AsTask to bridge
11
- // WinRT IAsyncOperation<T> into a .NET Task — required because PS5.1 cannot
12
- // await WinRT async operations natively via .GetAwaiter().
13
16
  const PS_SCRIPT = `
14
- Add-Type -AssemblyName System.Runtime.WindowsRuntime
15
- function Await($WinRtTask, $ResultType) {
16
- $m = ([System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object {
17
- $_.Name -eq 'AsTask' -and
18
- $_.GetParameters().Count -eq 1 -and
19
- $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation\`1'
20
- })[0]
21
- $m = $m.MakeGenericMethod($ResultType)
22
- $t = $m.Invoke($null, @($WinRtTask))
23
- $t.Wait(-1) | Out-Null
24
- $t.Result
25
- }
17
+ ${(0, _psHelpers_1.winRtAwaitPreamble)()}
26
18
  $mgType = [Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control,ContentType=WindowsRuntime]
27
19
  $mgr = Await ($mgType::RequestAsync()) $mgType
28
20
  $s = $mgr.GetCurrentSession()