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
package/README.md CHANGED
@@ -242,7 +242,18 @@ Remove-Item -Recurse -Force $env:LOCALAPPDATA\aiden
242
242
 
243
243
  <br>
244
244
 
245
- <img width="938" height="1049" alt="preview (3)" src="https://github.com/user-attachments/assets/93af497d-0fe3-4e2b-aa73-7739682c18b1" />
245
+ <img width="938" height="1049" alt="preview (3)" src="https://github.com/user-attachments/assets/4e32ae38-74ad-433d-b986-0a15bc2dffec" />
246
+
247
+
248
+ ## Recommended terminal setup
249
+
250
+ For best visual rendering, Aiden looks crispest with:
251
+
252
+ - **Font:** Cascadia Code, JetBrains Mono, or Fira Code at 13–14pt
253
+ - **Terminal:** Windows Terminal, iTerm2, or a modern emulator with truecolor support
254
+ - **Color depth:** truecolor (24-bit) — most modern terminals support this
255
+
256
+ Aiden works on any terminal but glyphs and color depth may degrade gracefully on older / minimal setups.
246
257
 
247
258
 
248
259
  ## Setup wizard
@@ -1089,12 +1089,30 @@ async function buildAgentRuntime(cliOpts, opts) {
1089
1089
  catch {
1090
1090
  // Missing or unreadable file is fine — no permanent allowlist yet.
1091
1091
  }
1092
- // Auxiliary client (compression / risk-assessment cheap LLM). Default to
1093
- // the same provider/model as the main loopthe resolver hands the
1094
- // auxiliary client a separately-configured cheap model later (v4.1).
1092
+ // Auxiliary client (compression / risk-assessment / session-summary
1093
+ // / skill-describe). v4.8.0 Slice 11route through Groq's cheap
1094
+ // 8B model as the default, with the parent provider/model as the
1095
+ // fallback when Groq isn't configured. Fixes the ChatGPT Plus +
1096
+ // gpt-5 routing bug: pre-Slice-11 auxiliary inherited the parent
1097
+ // (codex backend, accepts only `gpt-5-codex`/`gpt-4.1-mini`/etc.)
1098
+ // and every aux call returned a 400 model-not-supported. Groq is
1099
+ // cheap, fast, and reliable for the cheap classify/summarise jobs
1100
+ // auxiliary is designed for. If the user has no GROQ_API_KEY, the
1101
+ // resolver throws and we fall through to the parent — no regression
1102
+ // for non-codex users.
1103
+ //
1104
+ // Skip the parent fallback when the parent IS already the Groq cheap
1105
+ // model — same identity attempt twice is just noise in the verbose
1106
+ // log. (Resolver dedup; the auxiliary client itself doesn't filter.)
1107
+ const AUX_DEFAULT_PROVIDER = 'groq';
1108
+ const AUX_DEFAULT_MODEL = 'llama-3.1-8b-instant';
1109
+ const parentSameAsDefault = providerId === AUX_DEFAULT_PROVIDER && modelId === AUX_DEFAULT_MODEL;
1095
1110
  const auxiliaryClient = new auxiliaryClient_1.AuxiliaryClient({
1096
- defaultProvider: providerId,
1097
- defaultModel: modelId,
1111
+ defaultProvider: AUX_DEFAULT_PROVIDER,
1112
+ defaultModel: AUX_DEFAULT_MODEL,
1113
+ fallbacks: parentSameAsDefault
1114
+ ? []
1115
+ : [{ providerId, modelId }],
1098
1116
  // Phase 21 #5: ensure the auxiliary path also honors entry.oauth →
1099
1117
  // tokenStore. If a user runs the auxiliary cheap LLM through an
1100
1118
  // OAuth-only provider, omitting `paths` would skip the fast-path
@@ -1113,6 +1131,9 @@ async function buildAgentRuntime(cliOpts, opts) {
1113
1131
  approvalEngine['callbacks'] = {
1114
1132
  promptUser: callbacks.promptApproval,
1115
1133
  riskAssess: callbacks.riskAssess,
1134
+ // v4.8.0 Phase 2.5 — paint the structured approval row before the
1135
+ // existing y/n prompt runs. Additive; promptApproval flow unchanged.
1136
+ onUiEvent: (name, args) => display.renderUiEvent(name, args),
1116
1137
  // Phase 16f: append-on-disk for "Allow always" choices. Single-process
1117
1138
  // REPL — atomic write via tmp-then-rename.
1118
1139
  persistAllow: (tool, signature) => {
@@ -1237,6 +1258,12 @@ async function buildAgentRuntime(cliOpts, opts) {
1237
1258
  // tools return undefined → agent treats them as non-mutating (no
1238
1259
  // checkpoint flag); plugin authors must declare `mutates` honestly.
1239
1260
  const resolveMutates = (name) => toolRegistry.get(name)?.mutates;
1261
+ // v4.8.0 Phase 2.1 — resolver for the uiOnly flag. The dispatch
1262
+ // loop branches on `=== true` so any non-true value (undefined for
1263
+ // unknown tools, false for explicit executables) keeps the normal
1264
+ // executable path. Closure-captures the live registry, same as
1265
+ // resolveMutates / resolveToolset.
1266
+ const resolveUiOnly = (name) => toolRegistry.get(name)?.uiOnly;
1240
1267
  // ── Phase 16b.4: assemble system-prompt context ─────────────────────
1241
1268
  // PromptBuilder needs SOUL.md (read at build time from `paths.soulMd`),
1242
1269
  // a frozen MemorySnapshot (loaded once at boot — same lifecycle as
@@ -1379,6 +1406,7 @@ async function buildAgentRuntime(cliOpts, opts) {
1379
1406
  resolveVerifiedFlag,
1380
1407
  resolveToolset,
1381
1408
  resolveMutates,
1409
+ resolveUiOnly,
1382
1410
  providerId,
1383
1411
  modelId,
1384
1412
  // Phase 16b.4: wire PromptBuilder so SOUL.md actually reaches the LLM.
@@ -1470,6 +1498,7 @@ async function buildAgentRuntime(cliOpts, opts) {
1470
1498
  resolveVerifiedFlag,
1471
1499
  resolveToolset,
1472
1500
  resolveMutates,
1501
+ resolveUiOnly,
1473
1502
  // v4.7.0 Phase 2.4 — share the REPL's config-resolved honesty mode
1474
1503
  // with daemon-built agents so autonomous turns honour the same
1475
1504
  // setting interactive turns do.
@@ -1576,6 +1605,7 @@ async function buildAgentRuntime(cliOpts, opts) {
1576
1605
  resolveVerifiedFlag,
1577
1606
  resolveToolset,
1578
1607
  resolveMutates,
1608
+ resolveUiOnly,
1579
1609
  runStore: replRunStore,
1580
1610
  instanceId: replInstanceId,
1581
1611
  logger: bootLogger.child('subagent'),
@@ -1650,6 +1680,11 @@ async function buildAgentRuntime(cliOpts, opts) {
1650
1680
  resolveVerifiedFlag,
1651
1681
  resolveToolset,
1652
1682
  resolveMutates,
1683
+ resolveUiOnly,
1684
+ // v4.8.0 Phase 2.5 — emit ui_task_update/ui_task_done events so
1685
+ // subagent activity surfaces as gutter-indented trail rows in the
1686
+ // parent's chat surface alongside its own tool trail.
1687
+ onUiEvent: (name, args) => display.renderUiEvent(name, args),
1653
1688
  runStore: replRunStore,
1654
1689
  instanceId: replInstanceId,
1655
1690
  // v4.6 Phase 2Q-B — REPL parent-run wiring. Reads the same
@@ -21,7 +21,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.CliCallbacks = void 0;
22
22
  exports.mapBlockerToCard = mapBlockerToCard;
23
23
  exports.renderApprovalBox = renderApprovalBox;
24
- const box_1 = require("./box");
24
+ // v4.8.0 Slice 5 — verbose-mode gate for internal-telemetry dim lines.
25
+ const tokens_1 = require("./design/tokens");
25
26
  async function defaultPrompts() {
26
27
  // eslint-disable-next-line @typescript-eslint/no-var-requires
27
28
  const inq = require('@inquirer/prompts');
@@ -379,11 +380,12 @@ Reply with ONE word: safe, caution, or dangerous.`;
379
380
  * own SKILL.md (best-effort parse; falls back to id slice).
380
381
  */
381
382
  this.onSkillCandidate = (candidate) => {
383
+ // v4.8.0 Slice 5 — internal-telemetry cue; user already discovers
384
+ // candidates via /skills review. Surface only in verbose mode.
385
+ if (!(0, tokens_1.isVerbose)())
386
+ return;
382
387
  let name = candidate.id.slice(0, 8);
383
388
  try {
384
- // Tier-3.1c sweep: do not import here — chatSession's display
385
- // wraps strings, and the SKILL.md frontmatter is plain enough
386
- // that a quick regex is fine for the cue line.
387
389
  const m = /\bname\s*:\s*([^\n]+)/.exec(candidate.skillContent);
388
390
  if (m)
389
391
  name = m[1].trim();
@@ -402,6 +404,11 @@ Reply with ONE word: safe, caution, or dangerous.`;
402
404
  this.display.warn('[compress] auxiliary call failed; history unchanged');
403
405
  return;
404
406
  }
407
+ // v4.8.0 Slice 5 — successful auto-compress is technical telemetry
408
+ // (refused / failed variants stay visible since they explain action
409
+ // outcomes; success is just bookkeeping).
410
+ if (!(0, tokens_1.isVerbose)())
411
+ return;
405
412
  this.display.dim(`[compress] removed ${result.removedMessageCount} msgs, kept ${result.preservedRecentCount} recent (~${result.summaryTokens} tok)`);
406
413
  };
407
414
  /** Budget warning sink. Caution = dim line, warning = visible warn. */
@@ -410,7 +417,9 @@ Reply with ONE word: safe, caution, or dangerous.`;
410
417
  if (level === 'warning') {
411
418
  this.display.warn(`Budget: ${msg} — approaching the cap.`);
412
419
  }
413
- else {
420
+ else if ((0, tokens_1.isVerbose)()) {
421
+ // v4.8.0 Slice 5 — caution-level per-turn dim line is verbose-only;
422
+ // the actionable 'warning' tier above continues to fire unchanged.
414
423
  this.display.dim(`[budget] ${msg}`);
415
424
  }
416
425
  };
@@ -424,6 +433,10 @@ Reply with ONE word: safe, caution, or dangerous.`;
424
433
  this.onMemoryRefresh = (files) => {
425
434
  // Phase v4.1.2: argument switched from single-string-or-'both' to
426
435
  // the full sorted set of dirty files (SOUL.md joined the rotation).
436
+ // v4.8.0 Slice 5 — internal cache refresh; the user's "✓ Saved"
437
+ // confirmation lands separately when memory_add returns verified=true.
438
+ if (!(0, tokens_1.isVerbose)())
439
+ return;
427
440
  const label = files.length > 0 ? files.join(', ') : 'none';
428
441
  this.display.dim(`[memory] refreshed system prompt (${label})`);
429
442
  };
@@ -616,23 +629,29 @@ const APPROVAL_BOX_WIDTH = 64;
616
629
  // and the user wouldn't see they were viewing a partial value.
617
630
  const APPROVAL_ARGS_LIMIT = 50;
618
631
  /**
619
- * Render an approval request as a yellow-bordered rounded box. Pure —
620
- * returns the multi-line string; caller writes it. Args are truncated
621
- * to APPROVAL_ARGS_LIMIT chars for display only; the full args stay
622
- * with the tool call.
632
+ * Render an approval request with the Aiden-native framed-panel chrome
633
+ * (Slice 6) orange left bar, no closing corners, footer hint always
634
+ * present. Token-sourced from cli/v4/design/tokens.ts. Returns the
635
+ * multi-line string; caller writes it. Args are truncated to
636
+ * APPROVAL_ARGS_LIMIT chars for display only; the full args stay with
637
+ * the tool call. Colour discipline: brand (bar + title + key glyphs),
638
+ * tier-semantic (badge), muted (everything else) — ≤3 distinct colours.
623
639
  */
624
640
  function renderApprovalBox(req, display) {
625
- const W = APPROVAL_BOX_WIDTH;
626
- const top = display.paint((0, box_1.boxTopTitled)('Approval required', W), 'warn');
627
- const bot = display.paint((0, box_1.boxBottom)(W), 'warn');
628
- const side = (content) => {
629
- const raw = (0, box_1.boxLine)(content, W);
630
- const left = raw.slice(0, 1);
631
- const inner = raw.slice(1, raw.length - 1);
632
- const right = raw.slice(raw.length - 1);
633
- return `${display.paint(left, 'warn')}${inner}${display.paint(right, 'warn')}`;
634
- };
635
- const tierBadge = badgeForTier(req.riskTier);
641
+ // v4.8.0 Slice 6 hotfix:
642
+ // - Drop panel title + tier badge (Phase 2.5 ui_approval_request event
643
+ // row above already announces the headline + tier-by-colour).
644
+ // - Lead with structured key/value rows; unify inner-padding so keys
645
+ // and dividers share the same left edge.
646
+ // - Footer hint matches the actual inquirer select() mechanic
647
+ // (arrow-key navigation), not fictional y/a/n keystrokes.
648
+ // - Leading + trailing blank lines for vertical breathing room
649
+ // between the event row above and the inquirer picker below.
650
+ const indent = ' ';
651
+ const innerW = APPROVAL_BOX_WIDTH;
652
+ const bar = display.applyColors(tokens_1.glyphs.panel.bar, 'brand');
653
+ const line = (content) => `${indent}${bar} ${content}`;
654
+ const divider = display.muted(tokens_1.glyphs.chrome.hLine.repeat(innerW - 2));
636
655
  let argsPreview = '';
637
656
  try {
638
657
  argsPreview = JSON.stringify(req.args);
@@ -643,17 +662,19 @@ function renderApprovalBox(req, display) {
643
662
  if (argsPreview.length > APPROVAL_ARGS_LIMIT) {
644
663
  argsPreview = argsPreview.slice(0, APPROVAL_ARGS_LIMIT - 1) + '…';
645
664
  }
665
+ // Key-value rows. Key column padded to 12 cells for vertical alignment.
666
+ const KEY_W = 12;
667
+ const kv = (k, v) => `${display.muted(k.padEnd(KEY_W))}${v}`;
646
668
  const lines = [
647
- top,
648
- side(''),
649
- side(` ${display.muted('Tool:')} ${req.toolName}${tierBadge ? ' ' + tierBadge : ''}`),
669
+ line(kv('tool', req.toolName)),
650
670
  ];
651
- if (req.reason) {
652
- lines.push(side(` ${display.muted('Reason:')} ${req.reason}`));
653
- }
654
- lines.push(side(` ${display.muted('Args:')} ${argsPreview}`));
655
- lines.push(side(''));
656
- lines.push(side(` ${display.brand('[y]')} allow once ${display.brand('[a]')} allow always ${display.brand('[n]')} deny`));
657
- lines.push(bot);
658
- return lines.join('\n');
671
+ if (req.reason)
672
+ lines.push(line(kv('reason', req.reason)));
673
+ lines.push(line(kv('args', argsPreview)));
674
+ lines.push(line(divider));
675
+ lines.push(line(display.muted('↑↓ navigate · enter select · esc cancel')));
676
+ // Leading + trailing blank lines: caller already adds one trailing
677
+ // newline, so producing '\n<panel>\n' yields one blank above + one
678
+ // blank below once the caller's own '\n' lands.
679
+ return '\n' + lines.join('\n') + '\n';
659
680
  }
@@ -234,6 +234,11 @@ class ChatSession {
234
234
  // turn's elapsed ms (rendered in the trailing footer) and the
235
235
  // provider used last turn (so a switch surfaces as `groq ──→ together`).
236
236
  this.lastTurnElapsedMs = 0;
237
+ // v4.8.0 Slice 7 — status-footer telemetry. turnCount increments on
238
+ // every completed turn (success OR failure paths); lastTurnOutcome
239
+ // maps result.finishReason to a colour-kind hint for the state dot.
240
+ this.turnCount = 0;
241
+ this.lastTurnOutcome = 'ok';
237
242
  this.lastFooterProvider = null;
238
243
  /**
239
244
  * Phase v4.1.2-memory-AB:
@@ -902,13 +907,15 @@ class ChatSession {
902
907
  // Phase 22 Task 4: status bar reflects the live phase. Set on
903
908
  // entry, cleared in both success and error paths below.
904
909
  this.setStatusState({ kind: 'generating', sinceMs: Date.now() });
905
- // Tier-3.1a: dim full-width rule between the user input echo and
906
- // the agent reply for clean visual rhythm.
907
- this.opts.display.write(` ${this.opts.display.rule()}\n`);
908
- // Phase 26.2.3 blank line between the user-input echo and the
909
- // spinner / response so the eye sees user agent as separate
910
- // beats instead of butting together.
911
- this.opts.display.write('\n');
910
+ // v4.8.1 Slice 2 hotfix #3 — removed the prior Tier-3.1a dim
911
+ // rule between the user input echo and the agent reply. The dim
912
+ // colour read as a near-blank row in live smoke, and stacked
913
+ // with the indicator's erase-blank residue produced two visible
914
+ // separator rows above `▎ Aiden`. With the rule gone, the layout
915
+ // is:
916
+ // user input → [indicator paints, erases — 1 blank row] → ▎ Aiden
917
+ // = exactly one blank row between user input and Aiden header,
918
+ // matching the rhythm Shiva flagged in smoke.
912
919
  const turnStartedAt = Date.now();
913
920
  const userMsg = { role: 'user', content: userInput };
914
921
  // Apply any queued system prompts (from skill slash commands) by
@@ -991,6 +998,11 @@ class ChatSession {
991
998
  const indicator = this.opts.display.activityIndicator('thinking');
992
999
  let indicatorStopped = false;
993
1000
  let streamingActive = false;
1001
+ // v4.8.0 Phase 2.3 fix-2 — clear the ui-event flag at turn-start.
1002
+ // The existing reset sites in Display (streamPartial first-delta +
1003
+ // streamComplete) only fire on text-streaming turns; tool-only
1004
+ // turns leave the flag sticky. This is the authoritative reset.
1005
+ this.opts.display.resetUiTurnState();
994
1006
  // v4.1.5 Issue O — track whether this turn had any tool calls so
995
1007
  // we can emit a single muted rule between the tool trail and the
996
1008
  // reply header. Set true when the first tool's `before` phase
@@ -1187,6 +1199,19 @@ class ChatSession {
1187
1199
  this.opts.display.streamToolIndicator(call.name);
1188
1200
  }
1189
1201
  : undefined,
1202
+ // v4.8.0 Phase 2.3 fix-2 — uiOnly events route to the display
1203
+ // layer. The Phase 2.1 dispatch branch in aidenAgent.ts skips
1204
+ // onToolCall('before'), which is what normally fires
1205
+ // beforeFirstToolHook → stopIndicatorOnce. Without this stop
1206
+ // call, the 250ms indicator tick walks up 2 lines and erases
1207
+ // our paint within a quarter-second. Stop the indicator here,
1208
+ // mirroring how a first regular tool call stops it. Phase 2.3
1209
+ // handles ui_task_update + ui_task_done; the other 5 event
1210
+ // names land in Phase 2.4 (renderer silent-ignores them).
1211
+ onUiEvent: (name, args) => {
1212
+ stopIndicatorOnce();
1213
+ this.opts.display.renderUiEvent(name, args);
1214
+ },
1190
1215
  onProgress: streamingEnabled
1191
1216
  ? (outputTokens, maxTokens) => {
1192
1217
  if (indicatorStopped === false)
@@ -1316,6 +1341,12 @@ class ChatSession {
1316
1341
  }
1317
1342
  this.setStatusState({ kind: 'ready' });
1318
1343
  this.lastTurnElapsedMs = Date.now() - turnStartedAt;
1344
+ // v4.8.0 Slice 7 — record per-turn outcome for the status dot.
1345
+ this.turnCount += 1;
1346
+ this.lastTurnOutcome =
1347
+ result.finishReason === 'stop' ? 'ok' :
1348
+ result.finishReason === 'budget_exhausted' ? 'warn' :
1349
+ result.finishReason === 'interrupted' ? 'muted' : 'error';
1319
1350
  // v4.5 Phase 8b — surface a deferred daemon-scheduling tip
1320
1351
  // queued at turn start. Renders AFTER the agent's response per
1321
1352
  // Q-P8b-3(b) — the user reads the answer first, then sees the
@@ -1414,6 +1445,10 @@ class ChatSession {
1414
1445
  }
1415
1446
  this.setStatusState({ kind: 'ready' });
1416
1447
  this.lastTurnElapsedMs = Date.now() - turnStartedAt;
1448
+ // v4.8.0 Slice 7 — error path also bumps the turn counter and
1449
+ // records a red state-dot outcome for the next footer render.
1450
+ this.turnCount += 1;
1451
+ this.lastTurnOutcome = 'error';
1417
1452
  // v4.1.5+ Path A — finalize the loop trace on the error path
1418
1453
  // too. Loop patterns that ended in an error are exactly the
1419
1454
  // ones most worth capturing for diagnosis.
@@ -1614,6 +1649,12 @@ class ChatSession {
1614
1649
  }
1615
1650
  catch { /* never let a missing marker crash boot */ }
1616
1651
  // Bottom prompt hint — final line of the boot card.
1652
+ // v4.8.0 Slice 10d — full-width muted `─` divider between the
1653
+ // boot card (parchment / capability card / two-column block) and
1654
+ // the input hint prevents the boot chrome from merging visually
1655
+ // with the active prompt. Pattern: blank · rule · blank · hint.
1656
+ display.write('\n');
1657
+ display.write(` ${display.rule()}\n`);
1617
1658
  display.write('\n');
1618
1659
  display.write(display.bottomPromptHint() + '\n');
1619
1660
  }
@@ -1694,12 +1735,18 @@ class ChatSession {
1694
1735
  }
1695
1736
  const usedTokens = this.modelMetadata.estimateMessageTokens(this.history);
1696
1737
  const maxTokens = limits.contextLength;
1697
- display.write(display.statusFooter({
1738
+ // v4.8.0 Slice 7 hotfix — predictable 1-blank-line rhythm: one
1739
+ // blank above the footer (visual breath after the reply), one
1740
+ // blank below (before the next prompt).
1741
+ display.write('\n' + display.statusFooter({
1698
1742
  provider,
1699
1743
  model,
1700
1744
  ctxUsed: usedTokens,
1701
1745
  ctxMax: maxTokens,
1702
1746
  elapsedMs: this.lastTurnElapsedMs,
1747
+ turnCount: this.turnCount,
1748
+ sessionMs: Date.now() - this.startedAt,
1749
+ state: this.lastTurnOutcome,
1703
1750
  }) + '\n\n');
1704
1751
  }
1705
1752
  // ── Input ──────────────────────────────────────────────────────────
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.help = exports.SUBSECTION_MAP = exports.SUBSECTION_ORDER = void 0;
4
4
  exports.subsectionFor = subsectionFor;
5
+ const framedPanel_1 = require("../display/framedPanel");
5
6
  /**
6
7
  * Order matters: sections render in this order. Commands within a
7
8
  * section render in registration (alphabetical via registry.list)
@@ -98,25 +99,35 @@ exports.help = {
98
99
  for (const c of system) {
99
100
  buckets.get(subsectionFor(c.name)).push(c);
100
101
  }
101
- // Tier-3.1: gate icon column on AIDEN_UI_ICONS=1 (default OFF).
102
+ // v4.8.0 Slice 4 every section renders as an Aiden-native framed
103
+ // panel: left orange accent bar, title + count subtitle, command
104
+ // rows, footer hint always present. AIDEN_UI_ICONS still respected
105
+ // for the inline glyph column. Sections stack vertically; one
106
+ // blank line between them comes from the panel's trailing newline.
102
107
  const showIcons = process.env.AIDEN_UI_ICONS === '1';
108
+ const toRows = (cmds) => cmds.map((c) => ({
109
+ command: `${showIcons ? `${c.icon ?? ' '} ` : ''}/${c.name}`,
110
+ description: c.description,
111
+ }));
103
112
  for (const sec of exports.SUBSECTION_ORDER) {
104
113
  const cmds = buckets.get(sec);
105
114
  if (cmds.length === 0)
106
115
  continue;
107
- ctx.display.dim(`── ${sec} ──`);
108
- for (const c of cmds) {
109
- const prefix = showIcons ? `${c.icon ?? ' '} ` : '';
110
- ctx.display.write(` ${prefix}/${c.name.padEnd(14)} ${c.description}\n`);
111
- }
116
+ ctx.display.write((0, framedPanel_1.renderFramedPanel)({
117
+ title: sec,
118
+ subtitle: `${cmds.length} ${cmds.length === 1 ? 'command' : 'commands'}`,
119
+ rows: toRows(cmds),
120
+ footer: 'type /<name> to run · /help for this list',
121
+ }));
112
122
  ctx.display.write('\n');
113
123
  }
114
124
  if (skill.length > 0) {
115
- ctx.display.dim('── Skills ──');
116
- for (const c of skill) {
117
- const prefix = showIcons ? '' : '';
118
- ctx.display.write(` ${prefix}/${c.name.padEnd(14)} ${c.description}\n`);
119
- }
125
+ ctx.display.write((0, framedPanel_1.renderFramedPanel)({
126
+ title: 'Skills',
127
+ subtitle: `${skill.length} ${skill.length === 1 ? 'command' : 'commands'}`,
128
+ rows: toRows(skill),
129
+ footer: 'type /<name> to run · /skills list to browse installed skills',
130
+ }));
120
131
  }
121
132
  return {};
122
133
  },
@@ -35,6 +35,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
35
35
  const node_path_1 = __importDefault(require("node:path"));
36
36
  const daemon_1 = require("../../../core/v4/daemon");
37
37
  const paths_1 = require("../../../core/v4/paths");
38
+ const table_1 = require("../table");
38
39
  const noopOut = (s) => { process.stdout.write(s); };
39
40
  const noopErr = (s) => { process.stderr.write(s); };
40
41
  async function runRunsSubcommand(action, args, argv, opts = {}) {
@@ -74,18 +75,12 @@ function cmdList(runStore, argv, out) {
74
75
  sessionIdPrefix: argv.trigger,
75
76
  topLevelOnly: !includeChildren,
76
77
  });
77
- if (rows.length === 0) {
78
- out('No runs match the filter.\n');
79
- return 0;
80
- }
81
- out(`${'runId'.padEnd(6)} ${'status'.padEnd(11)} ${'finish'.padEnd(11)} ${'started'.padEnd(20)} sessionId\n`);
82
- for (const r of rows) {
83
- const started = new Date(r.startedAt).toISOString().slice(0, 19) + 'Z';
84
- const finish = r.finishReason ?? '-';
85
- // v4.6 Phase 2Q-B — child-count badge. Only relevant for the
86
- // top-level view (when --include-children is OFF). Skipped on
87
- // the flat view to avoid double-counting visual weight: in flat
88
- // mode the children are already on screen as their own rows.
78
+ // v4.8.0 Slice 3 — migrate from padEnd string concatenation to the
79
+ // framed table primitive. Title + count in the top border; empty
80
+ // state paints a framed message so layout weight matches populated
81
+ // runs. The trigger-badge (child-count summary) becomes part of the
82
+ // sessionId cell's `format` so column widths still auto-fit.
83
+ const tableRows = rows.map((r) => {
89
84
  let badge = '';
90
85
  if (!includeChildren) {
91
86
  const { total, completed } = runStore.countChildren(r.id);
@@ -93,12 +88,31 @@ function cmdList(runStore, argv, out) {
93
88
  badge = ` (${total} ${total === 1 ? 'child' : 'children'}, ${completed} OK)`;
94
89
  }
95
90
  }
96
- out(`${String(r.id).padEnd(6)} ${r.status.padEnd(11)} ${finish.padEnd(11)} ${started.padEnd(20)} ${r.sessionId}${badge}\n`);
91
+ return {
92
+ runId: String(r.id),
93
+ status: r.status,
94
+ finish: r.finishReason ?? '-',
95
+ started: new Date(r.startedAt).toISOString().slice(0, 19) + 'Z',
96
+ sessionId: r.sessionId + badge,
97
+ };
98
+ });
99
+ out((0, table_1.renderTable)(tableRows, [
100
+ { key: 'runId', header: 'runId', align: 'left' },
101
+ { key: 'status', header: 'status', align: 'left' },
102
+ { key: 'finish', header: 'finish', align: 'left' },
103
+ { key: 'started', header: 'started', align: 'left' },
104
+ { key: 'sessionId', header: 'sessionId', align: 'left', flex: true },
105
+ ], {
106
+ title: 'Recent runs',
107
+ totalCount: `${rows.length} ${rows.length === 1 ? 'run' : 'runs'}`,
108
+ emptyMessage: 'no runs match the filter',
109
+ }));
110
+ if (rows.length > 0) {
111
+ const hint = includeChildren
112
+ ? '(parents + sub-agent children)'
113
+ : '(top-level; use --include-children for sub-agents)';
114
+ out(` ${hint}\n`);
97
115
  }
98
- const hint = includeChildren
99
- ? ' (parents + sub-agent children)'
100
- : ' (top-level; use --include-children for sub-agents)';
101
- out(`\n${rows.length} run${rows.length === 1 ? '' : 's'} shown${hint}\n`);
102
116
  return 0;
103
117
  }
104
118
  // ── show ──────────────────────────────────────────────────────────────────
@@ -205,13 +219,17 @@ function cmdStats(db, out) {
205
219
  COUNT(*) AS n
206
220
  FROM runs
207
221
  WHERE status = 'completed' AND completed_at IS NOT NULL`).get();
208
- out('Run status counts:\n');
209
- if (counts.length === 0) {
210
- out(' (no runs recorded)\n');
211
- }
212
- for (const r of counts) {
213
- out(` ${r.status.padEnd(12)} ${r.c}\n`);
214
- }
222
+ // v4.8.0 Slice 3 — framed table replaces the padEnd block. Right-
223
+ // align the count column so multi-digit totals don't break visual
224
+ // rhythm. Empty state paints a framed message.
225
+ out((0, table_1.renderTable)(counts.map((r) => ({ status: r.status, count: String(r.c) })), [
226
+ { key: 'status', header: 'status', align: 'left' },
227
+ { key: 'count', header: 'count', align: 'right' },
228
+ ], {
229
+ title: 'Run status counts',
230
+ totalCount: `${counts.length} ${counts.length === 1 ? 'status' : 'statuses'}`,
231
+ emptyMessage: 'no runs recorded',
232
+ }));
215
233
  if (completed.n > 0 && completed.mean !== null) {
216
234
  out('\nCompleted-run duration (ms):\n');
217
235
  out(` mean ${Math.round(completed.mean)}\n`);
@@ -35,17 +35,17 @@ exports.skills = {
35
35
  return {};
36
36
  }
37
37
  const skills = await ctx.skillLoader.list();
38
- if (skills.length === 0) {
39
- ctx.display.dim('(no skills installed)');
40
- return {};
41
- }
42
- ctx.display.info(`Installed skills (${skills.length}):`);
38
+ // v4.8.0 Slice 3 — title + count in the top border replaces the
39
+ // separate `Installed skills (N):` info line. Empty state paints
40
+ // a framed message so layout weight matches populated tables.
43
41
  ctx.display.write((0, table_1.renderTable)(skills.map((s) => ({ name: s.name, description: s.description ?? '' })), [
44
42
  { key: 'name', header: 'Name', align: 'left', minWidth: 16 },
45
- // Tier-3.1b: drop the legacy `truncate: 60` cap so the
46
- // description column flexes to fill available width.
47
43
  { key: 'description', header: 'Description', align: 'left', flex: true },
48
- ]));
44
+ ], {
45
+ title: 'Skills',
46
+ totalCount: `${skills.length} installed`,
47
+ emptyMessage: 'no skills installed',
48
+ }));
49
49
  return {};
50
50
  }
51
51
  if (sub === 'view') {
@@ -102,15 +102,7 @@ exports.skills = {
102
102
  const store = new candidateStore_1.CandidateStore();
103
103
  if (sub === 'review') {
104
104
  const candidates = await store.list();
105
- if (candidates.length === 0) {
106
- ctx.display.dim('(no pending candidates — mined skills appear here after a successful 3+ tool turn)');
107
- return {};
108
- }
109
- ctx.display.info(`Pending mined candidates (${candidates.length}):`);
110
105
  ctx.display.write((0, table_1.renderTable)(candidates.map((c) => {
111
- // Pull the name + 1-line description from the candidate's
112
- // own SKILL.md so the table reflects what the user will
113
- // accept verbatim.
114
106
  let name = '(unparsed)';
115
107
  let description = '';
116
108
  try {
@@ -134,7 +126,13 @@ exports.skills = {
134
126
  { key: 'session', header: 'Session', align: 'left' },
135
127
  { key: 'created', header: 'Created', align: 'left' },
136
128
  { key: 'description', header: 'Description', align: 'left', flex: true },
137
- ]));
129
+ ], {
130
+ title: 'Pending skill candidates',
131
+ totalCount: `${candidates.length} pending`,
132
+ emptyMessage: 'no pending candidates — mined skills appear here after a successful 3+ tool turn',
133
+ }));
134
+ if (candidates.length === 0)
135
+ return {};
138
136
  ctx.display.dim('Use `/skills view-candidate <id-prefix>` to preview, `/skills accept <id>` to promote, `/skills reject <id> [reason]` to dismiss.');
139
137
  return {};
140
138
  }
@@ -78,14 +78,26 @@ async function runInstall(ctx) {
78
78
  return;
79
79
  }
80
80
  ctx.display.write(`Installing aiden-runtime v${status.latest} (current: v${status.installed})…\n`);
81
- const result = await (0, executeInstall_1.executeInstall)();
81
+ // v4.8.1 Slice 2 — reuse the v4.8.0 sliding-block shimmer indicator
82
+ // so the user sees motion while npm install runs (typically 5–15s
83
+ // on a warm cache, longer on cold). The indicator paints to a TTY
84
+ // only — non-TTY callers (CI, pipes) see the static "Installing…"
85
+ // line above and the result row below, no shimmer.
86
+ const indicator = ctx.display.activityIndicator('updating');
87
+ let result;
88
+ try {
89
+ result = await (0, executeInstall_1.executeInstall)();
90
+ }
91
+ finally {
92
+ indicator.stop();
93
+ }
82
94
  if (result.success) {
83
95
  const v = result.installedVersion ?? status.latest;
84
96
  ctx.display.write(`\n ✓ aiden-runtime v${v} installed.\n`);
85
97
  ctx.display.dim('Restart Aiden to apply: type /quit then re-run `aiden`.');
86
98
  return;
87
99
  }
88
- ctx.display.warn(result.error ?? 'Install failed (no error message).');
100
+ ctx.display.write(`\n ✗ Update failed: ${result.error ?? 'no error message'}\n`);
89
101
  }
90
102
  // ── v4.5 update system — skip + auto subcommands ───────────────────────────
91
103
  async function runSkip(ctx) {