aiden-runtime 4.7.0 → 4.8.1

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/README.md +12 -1
  2. package/dist/cli/v4/aidenCLI.js +40 -5
  3. package/dist/cli/v4/callbacks.js +52 -31
  4. package/dist/cli/v4/chatSession.js +55 -8
  5. package/dist/cli/v4/commands/help.js +22 -11
  6. package/dist/cli/v4/commands/runs.js +42 -24
  7. package/dist/cli/v4/commands/skills.js +15 -17
  8. package/dist/cli/v4/commands/update.js +14 -2
  9. package/dist/cli/v4/commands/usage.js +17 -5
  10. package/dist/cli/v4/daemonAgentBuilder.js +1 -0
  11. package/dist/cli/v4/design/tokens.js +265 -0
  12. package/dist/cli/v4/display/framedPanel.js +116 -0
  13. package/dist/cli/v4/display/toolTrail.js +2 -2
  14. package/dist/cli/v4/display.js +489 -164
  15. package/dist/cli/v4/onboarding/disclaimer.js +42 -10
  16. package/dist/cli/v4/onboarding/loading.js +24 -1
  17. package/dist/cli/v4/onboarding/successScreen.js +17 -8
  18. package/dist/cli/v4/pasteIntercept.js +214 -70
  19. package/dist/cli/v4/replyRenderer.js +213 -58
  20. package/dist/cli/v4/setupWizard.js +19 -2
  21. package/dist/cli/v4/skinEngine.js +13 -0
  22. package/dist/cli/v4/table.js +65 -8
  23. package/dist/core/v4/aidenAgent.js +23 -0
  24. package/dist/core/v4/auxiliaryClient.js +46 -13
  25. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +13 -8
  26. package/dist/core/v4/promptBuilder.js +51 -0
  27. package/dist/core/v4/subagent/childBuilder.js +1 -0
  28. package/dist/core/v4/subagent/spawnSubAgent.js +7 -1
  29. package/dist/core/v4/ui/banner.js +16 -16
  30. package/dist/core/v4/update/executeInstall.js +10 -6
  31. package/dist/core/v4/update/installMethodDetect.js +7 -0
  32. package/dist/core/version.js +67 -2
  33. package/dist/moat/approvalEngine.js +14 -0
  34. package/dist/tools/v4/index.js +54 -0
  35. package/dist/tools/v4/subagent/spawnSubAgentTool.js +23 -0
  36. package/package.json +1 -3
@@ -32,11 +32,14 @@ exports._resetForTests = _resetForTests;
32
32
  const marked_1 = require("marked");
33
33
  const skinEngine_1 = require("./skinEngine");
34
34
  const syntaxHighlight_1 = require("./syntaxHighlight");
35
+ // v4.8.0 Slice 8 — token-sourced bullet glyphs + task-list markers.
36
+ const tokens_1 = require("./design/tokens");
35
37
  // v4.1.4 reply-quality polish: single source of truth for frame math.
36
38
  // Replaces 3 inline `Math.min(process.stdout.columns ?? 80, 100) - 4`
37
39
  // callsites in this file with `getBodyWidth()` and adds soft-wrap for
38
40
  // code-block lines that previously overflowed the viewport.
39
41
  const frame_1 = require("./display/frame");
42
+ const box_1 = require("./box");
40
43
  // eslint-disable-next-line @typescript-eslint/no-var-requires
41
44
  const TerminalRenderer = require('marked-terminal').default ?? require('marked-terminal');
42
45
  function paint(kind) {
@@ -131,61 +134,45 @@ function paintBold(kind) {
131
134
  const CODE_BG_ON = '\x1b[48;2;50;50;60m';
132
135
  const CODE_BG_OFF = '\x1b[49m';
133
136
  function renderCodeBlock(code, lang) {
137
+ // v4.8.0 Slice 9 hotfix — top-divider asymmetric chrome.
138
+ //
139
+ // ── python ─────────────────────────────────────────
140
+ // print("Hello, world!")
141
+ // greet("Aiden")
142
+ //
143
+ // The earlier Slice 9 version used `▎` left-rail on every line and
144
+ // visually competed with the dark-bg syntax highlighting. This
145
+ // revision drops the rail, keeps a single muted `──` top divider
146
+ // with the language label in brand orange, indents body content,
147
+ // and omits the bottom border (Slice 4 asymmetric signature).
148
+ // CODE_BG_ON/OFF envelope preserved.
134
149
  const sk = (0, skinEngine_1.getSkinEngine)();
135
- // v4.1.4 reply-quality polish: width sourced from frame.ts. Same
136
- // visual budget as the v4.1.3 formula (cols capped at 100, minus
137
- // gutter+2) — but expressed via the shared helper so it tracks any
138
- // future width-policy change in one place.
139
150
  const width = (0, frame_1.getBodyWidth)();
140
151
  const langLabel = (lang ?? '').trim();
141
- // v4.1.3-essentials reply-polish: language tag on the top rule
142
- // already shipped; keep it. Bottom rule unlabeled (closing fence).
143
- const top = langLabel
144
- ? `── ${langLabel} ${'─'.repeat(Math.max(0, width - langLabel.length - 4))}`
145
- : '─'.repeat(width);
146
- const bot = '─'.repeat(width);
147
152
  const body = (0, syntaxHighlight_1.isSupportedLang)(langLabel)
148
153
  ? (0, syntaxHighlight_1.highlightCode)(code, langLabel)
149
154
  : code;
150
- // v4.1.4 reply-quality polish: per-line soft wrap. The rail + bg
151
- // chrome adds 4 visible columns (` │ `, padding spaces around the
152
- // line). Subtract those so wrap math targets the actual content
153
- // budget. `hard: true` ensures even pathological long tokens
154
- // (minified JS, hashes) break instead of escaping the frame.
155
- //
156
- // Width inside the body of a code line:
157
- // gutter (3) + `│ ` (2) + leading-space (1) + CONTENT + trailing-space (1)
158
- // → content budget = width - gutter - 4. We further cap at width to
159
- // keep the fence rule aligned with the body's right margin.
160
- const contentBudget = Math.max(8, width - frame_1.GUTTER - 4);
161
- // v4.1.3-essentials reply-polish (preserved): each body line gets:
162
- // - frame gutter (was 2-space outer indent; now uses shared GUTTER)
163
- // - left rail `│ ` painted muted (mirrors blockquote's `┃ ` rail
164
- // with a different glyph so they're visually distinct)
165
- // - 24-bit dark background wrapping the rail + content (subtle
166
- // "this is code" affordance without going full TUI box-frame)
167
- const rail = sk.applyColors('│', 'muted');
168
- const gutter = (0, frame_1.getIndent)(0);
169
- // Wrap each source line independently — code-block semantics demand
170
- // that a "logical line" remains visible as one continued unit even
171
- // when soft-wrapped. The CODE_BG painting closes per VISUAL line so
172
- // a wrap break doesn't bleed bg across the rail of the next row.
155
+ const indent = ' ';
156
+ const hLine = tokens_1.glyphs.chrome.hLine;
157
+ // Top divider: `── <lang> ─────` (lang in brand) OR full-width
158
+ // `────────────` when no language declared.
159
+ const top = langLabel
160
+ ? `${indent}${sk.applyColors(`${hLine.repeat(2)} `, 'muted')}` +
161
+ `${sk.applyColors(langLabel, 'brand')}` +
162
+ ` ${sk.applyColors(hLine.repeat(Math.max(1, width - langLabel.length - 4)), 'muted')}`
163
+ : `${indent}${sk.applyColors(hLine.repeat(width), 'muted')}`;
164
+ // Body content lands at col 4 (4-space indent inside the divider).
165
+ // Width budget: leave room for body indent + CODE_BG envelope spaces.
166
+ const bodyIndent = ' ';
167
+ const contentBudget = Math.max(8, width - 6);
173
168
  const wrappedLines = [];
174
169
  for (const srcLine of body.split('\n')) {
175
170
  const wrapped = (0, frame_1.wrap)(srcLine, contentBudget, { trim: false, hard: true });
176
171
  for (const visualLine of wrapped.split('\n')) {
177
- wrappedLines.push(`${gutter}${rail} ${CODE_BG_ON} ${visualLine} ${CODE_BG_OFF}`);
172
+ wrappedLines.push(`${bodyIndent}${CODE_BG_ON} ${visualLine} ${CODE_BG_OFF}`);
178
173
  }
179
174
  }
180
- const indented = wrappedLines.join('\n');
181
- // Top + bottom fence rules sit at the gutter too — visually anchors
182
- // the block as a unit inside the assistant frame.
183
- return [
184
- `${gutter}${sk.applyColors(top, 'muted')}`,
185
- indented,
186
- `${gutter}${sk.applyColors(bot, 'muted')}`,
187
- '',
188
- ].join('\n') + '\n';
175
+ return [top, ...wrappedLines].join('\n') + '\n';
189
176
  }
190
177
  /**
191
178
  * Render a block quote with a `┃` left rail in muted colour.
@@ -337,7 +324,16 @@ function renderListItemTokens(it, parser) {
337
324
  // block parser handles these via the normal dispatch (which calls
338
325
  // back into our own `renderer.list` override for nested lists —
339
326
  // depth counter is already incremented before we got here).
327
+ //
328
+ // v4.8.0 Slice 8 hotfix — ensure inline text and following block
329
+ // tokens are separated by a newline. Without this, a tight-list
330
+ // item like `- Python` followed by a nested `- Django` collapses
331
+ // to `● Python ○ Django` on a single line because head/tail
332
+ // split in renderer.list takes only the first line as `head`.
340
333
  if (parser.parse) {
334
+ if (out.length > 0 && !out[out.length - 1].endsWith('\n')) {
335
+ out.push('\n');
336
+ }
341
337
  out.push(parser.parse([tk]));
342
338
  continue;
343
339
  }
@@ -535,6 +531,10 @@ function getReplyRenderer() {
535
531
  let isOrdered = false;
536
532
  let startNum = 1;
537
533
  let items;
534
+ // v4.8.0 Slice 8 — task/checked flags collected alongside items so
535
+ // the marker dispatch below can pick ✔ (checked) or ○ (unchecked).
536
+ // Default false (not a task) so the bullet path stays unchanged.
537
+ let itemTasks = [];
538
538
  // CRITICAL: increment depth BEFORE walking items. Item walking via
539
539
  // `parser.parse(it.tokens)` recurses into our own override for any
540
540
  // nested list tokens — those nested calls need to see the parent's
@@ -570,10 +570,14 @@ function getReplyRenderer() {
570
570
  // Confirmed against marked v15 token shapes from `marked.lexer`
571
571
  // (see scripts/smoke-issue-c-tokens.ts).
572
572
  const parser = this.parser;
573
- items = (tok.items ?? []).map((it) => {
574
- if (!parser)
575
- return it.text ?? '';
576
- return renderListItemTokens(it, parser);
573
+ // v4.8.0 Slice 8 capture GFM task/checked flags alongside the
574
+ // rendered text so the marker dispatch below can pick the right
575
+ // glyph (✔ checked / ○ unchecked) for task-list items.
576
+ const rawItems = tok.items ?? [];
577
+ items = rawItems.map((it) => parser ? renderListItemTokens(it, parser) : (it.text ?? ''));
578
+ itemTasks = rawItems.map((it) => {
579
+ const itx = it;
580
+ return { task: itx.task === true, checked: itx.checked === true };
577
581
  });
578
582
  }
579
583
  else {
@@ -587,20 +591,33 @@ function getReplyRenderer() {
587
591
  items = raw.split('\n').filter((ln) => ln.trim().length > 0);
588
592
  }
589
593
  const indent = ' '.repeat(depth);
590
- // Top-level bullet `•` (filled); nested `▸` (arrow-like) for
591
- // visual depth differentiation. Numbered lists override with
592
- // `N.` regardless of depth.
593
- const bulletGlyph = depth === 1 ? '•' : '▸';
594
+ // v4.8.0 Slice 8 — token-sourced bullet glyphs. Top-level (depth 1)
595
+ // uses filled `●`, nested (depth 2) uses hollow `○`. Both painted
596
+ // brand orange to give lists visual identity (was `muted` grey).
597
+ const bulletGlyph = depth === 1 ? tokens_1.glyphs.util.bullet : tokens_1.glyphs.util.bulletOpen;
594
598
  const lines = [];
595
599
  for (let i = 0; i < items.length; i += 1) {
596
600
  const item = items[i];
597
- // Each item may itself contain newlines (nested list output,
598
- // multi-line paragraph). Indent every line of the rendered
599
- // item AFTER the first the first line takes the bullet, the
600
- // continuation lines align under the bullet's content column.
601
- const marker = isOrdered
602
- ? paint('muted')(`${startNum + i}.`)
603
- : paint('muted')(bulletGlyph);
601
+ const task = itemTasks[i] ?? { task: false, checked: false };
602
+ // v4.8.0 Slice 8 marker dispatch:
603
+ // GFM checked task in semantic success (green)
604
+ // GFM unchecked task in tertiary dim (looks incomplete)
605
+ // • Ordered list → `N.` right-padded to 3 cols, brand orange
606
+ // • Bullet ●/○ by depth, brand orange
607
+ let marker;
608
+ if (task.task && task.checked) {
609
+ marker = paint('success')(tokens_1.glyphs.util.check);
610
+ }
611
+ else if (task.task) {
612
+ marker = paint('tertiary')(tokens_1.glyphs.util.bulletOpen);
613
+ }
614
+ else if (isOrdered) {
615
+ const numStr = `${startNum + i}.`.padStart(3);
616
+ marker = paint('brand')(numStr);
617
+ }
618
+ else {
619
+ marker = paint('brand')(bulletGlyph);
620
+ }
604
621
  const itemLines = item.split('\n');
605
622
  const head = itemLines[0] ?? '';
606
623
  const tail = itemLines.slice(1);
@@ -622,6 +639,144 @@ function getReplyRenderer() {
622
639
  const out = lines.join('\n');
623
640
  return proto._listDepth === 0 ? out + '\n' : out + '\n';
624
641
  };
642
+ // ── v4.8.1 Slice 2 — markdown table override ──────────────────────────
643
+ //
644
+ // Why: marked-terminal's default table renderer (cli-table3) auto-
645
+ // wraps cells but doesn't keep wrap-continuation lines aligned to
646
+ // the original row — wide tables with 5+ columns fragment into
647
+ // vertical pipe rails that don't read as rows. The narrow 2-col
648
+ // tables that smoke-tested fine were within the no-wrap budget.
649
+ //
650
+ // Strategy: own the entire render from the marked v15 token object.
651
+ // Use `parser.parseInline(cell.tokens)` to get ANSI-painted cell
652
+ // text, then proportionally distribute the terminal-width budget
653
+ // across columns (clamping to natural max width), wrap each cell
654
+ // to its column width, and render the box with the same row
655
+ // height for every cell in the row so visual rows stay tight.
656
+ //
657
+ // Token-source the box chars from `glyphs.chrome.*` so a single
658
+ // glyph swap propagates here automatically (consistent with the
659
+ // rest of v4.8.x chrome).
660
+ renderer.table = function (header, body) {
661
+ // marked v15 token: { header: [cellTok], rows: [[cellTok]] }.
662
+ // Older string-based API: (headerHtml, bodyHtml) — we fall back
663
+ // to a naive concatenation so the reply isn't lost entirely.
664
+ if (typeof header !== 'object' || header === null) {
665
+ return String(header ?? '') + (body !== undefined ? String(body) : '') + '\n';
666
+ }
667
+ const tok = header;
668
+ const parser = this.parser;
669
+ const renderCell = (c) => {
670
+ if (c.tokens && parser?.parseInline) {
671
+ try {
672
+ return parser.parseInline(c.tokens).trim();
673
+ }
674
+ catch {
675
+ return (c.text ?? '').trim();
676
+ }
677
+ }
678
+ return (c.text ?? '').trim();
679
+ };
680
+ const headers = (tok.header ?? []).map(renderCell);
681
+ const rows = (tok.rows ?? []).map((r) => r.map(renderCell));
682
+ const cols = headers.length;
683
+ if (cols === 0)
684
+ return '';
685
+ // Layout budget. Reply chrome family lives at col 2.
686
+ const indent = ' ';
687
+ const termCols = process.stdout.columns ?? 100;
688
+ const innerBudget = Math.max(40, Math.min(termCols, 110) - indent.length);
689
+ // Chrome per row = `│ ` (2) per col + trailing `│` (1) + 1 trailing
690
+ // space per cell already absorbed in the budget below.
691
+ const chromeCost = 3 * cols + 1;
692
+ const contentBudget = Math.max(cols * 4, innerBudget - chromeCost);
693
+ // Natural width = max(header, body) visible width per column.
694
+ const naturalW = headers.map((h, i) => {
695
+ const hw = (0, box_1.visibleLength)(h);
696
+ const cw = rows.reduce((m, r) => Math.max(m, (0, box_1.visibleLength)(r[i] ?? '')), 0);
697
+ return Math.max(hw, cw, 1);
698
+ });
699
+ // v4.8.1 Slice 2 hotfix #2 — header-floor + proportional allocation.
700
+ //
701
+ // Each column's minimum is `max(headerWidth, MIN_COL_W)` so column
702
+ // headers NEVER wrap — they are the column identifier; wrapping
703
+ // them ("Framework" → "Framew/ork") fragments scanability worse
704
+ // than wrapping body cells. Body content above the header width
705
+ // is what gets compressed under width pressure.
706
+ //
707
+ // Algorithm:
708
+ // 1. Compute `minPerCol = max(headerW[i], MIN_COL_W)` per column.
709
+ // 2. If sum(minPerCol) >= contentBudget (very narrow terminal),
710
+ // use minPerCol as-is — body cells will wrap to fit, headers
711
+ // stay intact.
712
+ // 3. Else if sum(naturalW) <= contentBudget, use natural widths
713
+ // (no wrap needed anywhere).
714
+ // 4. Else: floor at minPerCol, distribute remaining budget
715
+ // proportionally to each column's "extra need above min",
716
+ // then hand rounding leftover to widest-natural cols first.
717
+ const MIN_COL_W = 4;
718
+ const headerW = headers.map((h) => (0, box_1.visibleLength)(h));
719
+ const minPerCol = naturalW.map((_, i) => Math.max(headerW[i], MIN_COL_W));
720
+ const totalMin = minPerCol.reduce((a, b) => a + b, 0);
721
+ const totalNatW = naturalW.reduce((a, b) => a + b, 0);
722
+ let colWidths;
723
+ if (totalMin >= contentBudget) {
724
+ colWidths = minPerCol.slice();
725
+ }
726
+ else if (totalNatW <= contentBudget) {
727
+ colWidths = naturalW.slice();
728
+ }
729
+ else {
730
+ colWidths = minPerCol.slice();
731
+ const extraNeed = naturalW.map((w, i) => Math.max(0, w - minPerCol[i]));
732
+ const totalNeed = extraNeed.reduce((a, b) => a + b, 0);
733
+ const pool = contentBudget - totalMin;
734
+ if (totalNeed > 0) {
735
+ for (let i = 0; i < cols; i += 1) {
736
+ colWidths[i] += Math.floor((extraNeed[i] * pool) / totalNeed);
737
+ }
738
+ }
739
+ let leftover = contentBudget - colWidths.reduce((a, b) => a + b, 0);
740
+ const order = naturalW.map((_, i) => i).sort((a, b) => naturalW[b] - naturalW[a]);
741
+ for (let k = 0; leftover > 0 && k < cols * 2; k += 1) {
742
+ const idx = order[k % cols];
743
+ if (colWidths[idx] < naturalW[idx]) {
744
+ colWidths[idx] += 1;
745
+ leftover -= 1;
746
+ }
747
+ }
748
+ }
749
+ // ANSI-aware cell wrap. frameWrap handles colour-code-aware width.
750
+ const wrapCell = (text, w) => w <= 0 ? [''] : (0, frame_1.wrap)(text, w, { trim: false, hard: true }).split('\n');
751
+ const sk = (0, skinEngine_1.getSkinEngine)();
752
+ const ch = tokens_1.glyphs.chrome;
753
+ const rule = (l, m, r) => indent + sk.applyColors(l + colWidths.map((w) => ch.hLine.repeat(w + 2)).join(m) + r, 'muted');
754
+ const vBar = sk.applyColors(ch.vLine, 'muted');
755
+ const renderRow = (cells) => {
756
+ const height = Math.max(...cells.map((c) => c.length), 1);
757
+ const out = [];
758
+ for (let line = 0; line < height; line += 1) {
759
+ const cellLines = cells.map((cellLines2, ci) => {
760
+ const cellLine = cellLines2[line] ?? '';
761
+ const pad = Math.max(0, colWidths[ci] - (0, box_1.visibleLength)(cellLine));
762
+ return ' ' + cellLine + ' '.repeat(pad) + ' ';
763
+ });
764
+ out.push(indent + vBar + cellLines.join(vBar) + vBar);
765
+ }
766
+ return out.join('\n');
767
+ };
768
+ const wrappedHeader = headers.map((h, i) => wrapCell(h, colWidths[i]));
769
+ const wrappedRows = rows.map((r) => r.map((c, i) => wrapCell(c, colWidths[i])));
770
+ const lines = [rule(ch.topLeft, ch.teeDown, ch.topRight)];
771
+ if (headers.length > 0) {
772
+ lines.push(renderRow(wrappedHeader));
773
+ lines.push(rule(ch.teeRight, ch.cross, ch.teeLeft));
774
+ }
775
+ for (const row of wrappedRows)
776
+ lines.push(renderRow(row));
777
+ lines.push(rule(ch.botLeft, ch.teeUp, ch.botRight));
778
+ return lines.join('\n') + '\n';
779
+ };
625
780
  cachedRenderer = {
626
781
  render(text) {
627
782
  try {
@@ -47,6 +47,8 @@ const box_1 = require("./box");
47
47
  // the cost of broken unit tests under the test runtime.
48
48
  const successScreen_1 = require("./onboarding/successScreen");
49
49
  const providerPicker_1 = require("./onboarding/providerPicker");
50
+ // v4.8.0 Slice 10b — bar + chrome tokens for step headers.
51
+ const tokens_1 = require("./design/tokens");
50
52
  const modelFetch_1 = require("../../core/v4/providers/modelFetch");
51
53
  const probe_1 = require("../../core/v4/providers/probe");
52
54
  // Phase 30.2.1 — provider order optimised for new-user time-to-first-chat.
@@ -537,8 +539,19 @@ async function runSetupWizard(opts = {}) {
537
539
  if (opts.prompts) {
538
540
  display.printBanner();
539
541
  }
540
- display.write('\nWelcomelet\'s pick a provider.\n');
541
- display.write(`${kleur_1.default.dim('(Press Enter to accept Groq free + fastest setup.)')}\n\n`);
542
+ // v4.8.0 Slice 10b step-header helper. Each major wizard step
543
+ // starts with ` ▎ Set up Aiden step N` painted with the orange
544
+ // panel bar so the flow visually consistent with /help and the
545
+ // approval panel. Inquirer widgets render below unchanged.
546
+ const stepHeader = (n) => {
547
+ const bar = display.applyColors(tokens_1.glyphs.panel.bar, 'brand');
548
+ const title = display.applyColors('Set up Aiden', 'heading');
549
+ const sub = display.applyColors(`step ${n}`, 'muted');
550
+ return `\n ${bar} ${title} ${sub}\n`;
551
+ };
552
+ display.write(stepHeader(1));
553
+ display.write(' Welcome — let\'s pick a provider.\n');
554
+ display.write(` ${kleur_1.default.dim('(Press Enter to accept Groq — free + fastest setup.)')}\n\n`);
542
555
  // Phase 30.2.1 — Groq is the new recommended default for first-time
543
556
  // users: free tier, fastest signup, and avoids the surprise charge
544
557
  // path of paid providers. Together AI moved to position [8] paid.
@@ -695,6 +708,9 @@ async function runSetupWizard(opts = {}) {
695
708
  // anyway, so the reorder bought nothing there. Existing test
696
709
  // fixtures provide inputs in legacy order; preserving custom's
697
710
  // order keeps them green.
711
+ if (provider.kind === 'key' || provider.kind === 'subscription' || provider.kind === 'local') {
712
+ display.write(stepHeader(2));
713
+ }
698
714
  let apiKey;
699
715
  let baseUrl;
700
716
  if (provider.kind === 'local') {
@@ -713,6 +729,7 @@ async function runSetupWizard(opts = {}) {
713
729
  }
714
730
  // provider.kind === 'custom' — defer credential prompts until AFTER
715
731
  // the model picker below.
732
+ display.write(stepHeader(3));
716
733
  // Step 3: live model fetch + pick.
717
734
  //
718
735
  // Test-harness gate: when the caller injected `opts.prompts` (only
@@ -66,6 +66,13 @@ const DEFAULT_SKIN = {
66
66
  // (which shares the colour) so callers can differentiate in code
67
67
  // even though they render identically.
68
68
  degraded: [0xff, 0xc1, 0x07],
69
+ // v4.8.0 Slice 7 hotfix #2 — purple accent for the turn-counter
70
+ // segment (⌘) in the status footer. #a48be0 reads as a soft
71
+ // lavender that doesn't compete with brand orange.
72
+ metric_turn: [0xa4, 0x8b, 0xe0],
73
+ // v4.8.0 Slice 8 — tertiary dim grey, dimmer than `muted` (warm
74
+ // tint) for lowest-priority text like unchecked task markers.
75
+ tertiary: [0x6a, 0x6a, 0x6a],
69
76
  },
70
77
  glyphs: {
71
78
  bullet: '•',
@@ -99,6 +106,10 @@ const LIGHT_SKIN = {
99
106
  heading: [0xc4, 0x42, 0x10],
100
107
  session: [0x00, 0x55, 0x88],
101
108
  degraded: [0x80, 0x60, 0x00],
109
+ // Slice 7 hotfix #2 — deeper purple on light bg keeps contrast budget.
110
+ metric_turn: [0x6e, 0x50, 0xaa],
111
+ // Slice 8 — lighter grey on light bg keeps the dim-but-readable feel.
112
+ tertiary: [0x9a, 0x9a, 0x9a],
102
113
  },
103
114
  glyphs: { ...DEFAULT_SKIN.glyphs },
104
115
  };
@@ -118,6 +129,8 @@ const MONOCHROME_SKIN = {
118
129
  heading: null,
119
130
  session: null,
120
131
  degraded: null,
132
+ metric_turn: null,
133
+ tertiary: null,
121
134
  },
122
135
  glyphs: {
123
136
  bullet: '*',
@@ -23,6 +23,7 @@ exports.renderTable = renderTable;
23
23
  const stringWidth = require('string-width');
24
24
  const skinEngine_1 = require("./skinEngine");
25
25
  const box_1 = require("./box");
26
+ const tokens_1 = require("./design/tokens");
26
27
  /**
27
28
  * Visible (post-ANSI-strip) column width. Falls back to
28
29
  * `visibleLength` from box.ts when string-width is unavailable
@@ -167,13 +168,32 @@ function renderTable(rows, cols, opts = {}) {
167
168
  }
168
169
  }
169
170
  }
170
- // Border characters (sharp ASCII).
171
- const TL = '┌', TR = '┐', BL = '└', BR = '┘';
172
- const T = '┬', B = '┴', L = '├', R = '┤';
173
- const X = '┼', H = '─', V = '│';
171
+ // Border characters — token-sourced from design/tokens.ts (v4.8.0 Slice 3).
172
+ const { topLeft: TL, topRight: TR, botLeft: BL, botRight: BR } = tokens_1.glyphs.chrome;
173
+ const { teeDown: T, teeUp: B, teeRight: L, teeLeft: R } = tokens_1.glyphs.chrome;
174
+ const { cross: X, hLine: H, vLine: V } = tokens_1.glyphs.chrome;
174
175
  const ind = ' '.repeat(indent);
175
- // Top border.
176
- const top = TL + widths.map((w) => H.repeat(w + 2)).join(T) + TR;
176
+ // Total inner content width across all cells + inner separators.
177
+ // Used by title-embedded top border + page footer.
178
+ const innerWidth = widths.reduce((s, w) => s + w + 2, 0) + (numCols - 1);
179
+ // v4.8.0 Slice 3 — top border with optional embedded title + count.
180
+ // Format: `┌─ title ──────────── totalCount ──┐`
181
+ // Pads the centre with `─` so the right edge stays aligned regardless
182
+ // of title / count length. Falls back to the legacy plain top border
183
+ // when neither field is supplied.
184
+ let top;
185
+ if (opts.title || opts.totalCount) {
186
+ const titleText = opts.title ? ` ${opts.title} ` : '';
187
+ const countText = opts.totalCount ? ` ${opts.totalCount} ` : '';
188
+ const fixed = vWidth(titleText) + vWidth(countText);
189
+ const filler = Math.max(0, innerWidth - fixed);
190
+ const titlePainted = opts.title ? skin.applyColors(titleText, 'heading') : '';
191
+ const countPainted = opts.totalCount ? skin.applyColors(countText, 'muted') : '';
192
+ top = TL + H + titlePainted + H.repeat(filler) + countPainted + H + TR;
193
+ }
194
+ else {
195
+ top = TL + widths.map((w) => H.repeat(w + 2)).join(T) + TR;
196
+ }
177
197
  // Header row — heading colour, padded. Truncate first if the
178
198
  // header itself is wider than the allocated width (rare, but
179
199
  // keeps borders aligned under aggressive narrow-width pressure).
@@ -203,13 +223,50 @@ function renderTable(rows, cols, opts = {}) {
203
223
  });
204
224
  bodyLines.push(V + cells.join(V) + V);
205
225
  });
206
- // Bottom border.
207
- const bot = BL + widths.map((w) => H.repeat(w + 2)).join(B) + BR;
226
+ // v4.8.0 Slice 3 — pagination footer above the bottom border. Renders
227
+ // `← prev · page X/Y · next →` centred inside the inner width. Side
228
+ // arrows are dim when the page is at the edge so users can read
229
+ // "at-end" cleanly. Caller wires hotkeys; we just paint the chrome.
230
+ let pageFooter = null;
231
+ if (opts.page) {
232
+ const { current, total } = opts.page;
233
+ const atStart = current <= 1;
234
+ const atEnd = current >= total;
235
+ const leftKind = atStart ? 'muted' : 'session';
236
+ const rightKind = atEnd ? 'muted' : 'session';
237
+ const left = skin.applyColors('← prev', leftKind);
238
+ const mid = skin.applyColors(`page ${current}/${total}`, 'muted');
239
+ const right = skin.applyColors('next →', rightKind);
240
+ const sep = skin.applyColors(' · ', 'muted');
241
+ const body = `${left}${sep}${mid}${sep}${right}`;
242
+ const bodyW = vWidth('← prev') + vWidth(` · page ${current}/${total} · `) + vWidth('next →');
243
+ const padW = Math.max(0, innerWidth - bodyW);
244
+ const lpad = Math.floor(padW / 2);
245
+ const rpad = padW - lpad;
246
+ pageFooter = V + ' '.repeat(lpad) + body + ' '.repeat(rpad) + V;
247
+ }
248
+ // Bottom border. Skip the inner tees when the title-style top was
249
+ // used (legacy plain bottom keeps column alignment for un-titled
250
+ // tables; a title-only border on top reads cleanest with a plain
251
+ // bottom mirror).
252
+ const bot = (opts.title || opts.totalCount)
253
+ ? BL + H.repeat(innerWidth) + BR
254
+ : BL + widths.map((w) => H.repeat(w + 2)).join(B) + BR;
255
+ // v4.8.0 Slice 3 — empty-state path. Borders stay so the layout
256
+ // weight matches a populated table; the body is one centered line.
257
+ if (rows.length === 0 && opts.emptyMessage) {
258
+ const msg = skin.applyColors(opts.emptyMessage, 'muted');
259
+ const pad = Math.max(0, innerWidth - vWidth(opts.emptyMessage));
260
+ const lpad = Math.floor(pad / 2);
261
+ const emptyRow = V + ' '.repeat(lpad) + msg + ' '.repeat(pad - lpad) + V;
262
+ return [top, emptyRow, bot].map((l) => ind + l).join('\n') + '\n';
263
+ }
208
264
  const allLines = [
209
265
  top,
210
266
  headerRow,
211
267
  ...(showRule ? [rule] : []),
212
268
  ...bodyLines,
269
+ ...(pageFooter ? [pageFooter] : []),
213
270
  bot,
214
271
  ].map((l) => ind + l);
215
272
  return allLines.join('\n') + '\n';
@@ -166,6 +166,7 @@ class AidenAgent {
166
166
  this.resolveVerifiedFlag = opts.resolveVerifiedFlag;
167
167
  this.resolveToolset = opts.resolveToolset;
168
168
  this.resolveMutates = opts.resolveMutates;
169
+ this.resolveUiOnly = opts.resolveUiOnly;
169
170
  this.promptBuilder = opts.promptBuilder;
170
171
  this.promptBuilderOptions = opts.promptBuilderOptions;
171
172
  this.contextCompressor = opts.contextCompressor;
@@ -839,6 +840,28 @@ class AidenAgent {
839
840
  finalContent = '';
840
841
  break;
841
842
  }
843
+ // v4.8.0 — uiOnly tools are signal channels, not executable
844
+ // tools. The model calls them to communicate render-time
845
+ // state. Dispatch loop skips execute / iteration / mutation
846
+ // marking / verifier / trace / observability hooks and fires
847
+ // onUiEvent on the caller. A '(no output)' tool_result is
848
+ // pushed to satisfy the provider protocol (every tool_call_id
849
+ // needs a matching tool_result). Listener exceptions are
850
+ // swallowed so a bad UI handler cannot break the turn.
851
+ if (this.resolveUiOnly?.(call.name) === true) {
852
+ turnToolMessages.push({
853
+ role: 'tool',
854
+ toolCallId: call.id,
855
+ content: '(no output)',
856
+ });
857
+ try {
858
+ runOptions.onUiEvent?.(call.name, call.arguments);
859
+ }
860
+ catch {
861
+ // defensive — UI listener faults must never break dispatch
862
+ }
863
+ continue;
864
+ }
842
865
  this.onToolCall?.(call, 'before');
843
866
  // v4.2 Phase 4 — mark any active checkpoints as containing a
844
867
  // mutating call BEFORE dispatch. Done pre-dispatch (not post)
@@ -45,19 +45,35 @@ class AuxiliaryClient {
45
45
  return this.opts.adapter;
46
46
  if (!this.opts.resolver)
47
47
  return null;
48
- this.resolveCallCount += 1;
49
- try {
50
- const adapter = await this.opts.resolver.resolve({
51
- providerId: this.opts.defaultProvider,
52
- modelId: this.opts.defaultModel,
53
- });
54
- return adapter;
55
- }
56
- catch (err) {
57
- this.warn(`auxiliary client unavailable (${this.opts.defaultProvider}/${this.opts.defaultModel}): ${err.message}`);
58
- this.adapterUnavailable = true;
59
- return null;
48
+ // v4.8.0 Slice 11 — resolution chain: default first, then each
49
+ // fallback in order. The first attempt that resolves wins. This
50
+ // is the routing-fix entry point for the chatgpt-plus + gpt-5
51
+ // bug: aidenCLI hands us Groq as the default and the parent
52
+ // provider/model as the fallback, so auxiliary calls land on
53
+ // Groq when configured and the parent only sees traffic when
54
+ // Groq is absent.
55
+ const attempts = [
56
+ { providerId: this.opts.defaultProvider, modelId: this.opts.defaultModel },
57
+ ...(this.opts.fallbacks ?? []),
58
+ ];
59
+ const failures = [];
60
+ for (const att of attempts) {
61
+ this.resolveCallCount += 1;
62
+ try {
63
+ const adapter = await this.opts.resolver.resolve({
64
+ providerId: att.providerId,
65
+ modelId: att.modelId,
66
+ });
67
+ this.warn(`auxiliary resolved via ${att.providerId}/${att.modelId}`);
68
+ return adapter;
69
+ }
70
+ catch (err) {
71
+ failures.push(`${att.providerId}/${att.modelId}: ${err.message}`);
72
+ }
60
73
  }
74
+ this.warn(`auxiliary client unavailable (tried ${attempts.length}): ${failures.join('; ')}`);
75
+ this.adapterUnavailable = true;
76
+ return null;
61
77
  }
62
78
  /** Resolve count for tests (verifies single-resolution behaviour). */
63
79
  _resolveCallCount() {
@@ -122,7 +138,24 @@ class AuxiliaryClient {
122
138
  this.usage.set(purpose, cur);
123
139
  }
124
140
  warn(msg) {
125
- (this.opts.warn ?? ((m) => console.warn(`[auxiliary] ${m}`)))(msg);
141
+ // v4.8.0 Slice 5 gate console output behind AIDEN_VERBOSE.
142
+ // Auxiliary failures are recoverable (the main loop continues;
143
+ // result content is just empty), so the warning is pure noise
144
+ // for end users. Power users set AIDEN_VERBOSE=1 to surface them.
145
+ // Inline env-read preserves the core → cli no-import invariant;
146
+ // canonical isVerbose() lives at cli/v4/design/tokens.ts.
147
+ //
148
+ // v4.8.0 Slice 11 — if opts.warn is explicitly injected, always
149
+ // forward (tests + advanced callers register their own sink and
150
+ // expect every message). The AIDEN_VERBOSE gate now applies only
151
+ // to the default console.warn fallback that end-users see.
152
+ if (this.opts.warn) {
153
+ this.opts.warn(msg);
154
+ return;
155
+ }
156
+ if (process.env.AIDEN_VERBOSE !== '1')
157
+ return;
158
+ console.warn(`[auxiliary] ${msg}`);
126
159
  }
127
160
  async withTimeout(p, ms) {
128
161
  return new Promise((resolve, reject) => {