aiden-runtime 4.1.2 → 4.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/cli/v4/aidenCLI.js +10 -0
  2. package/dist/cli/v4/callbacks.js +85 -13
  3. package/dist/cli/v4/chatSession.js +250 -24
  4. package/dist/cli/v4/commands/doctor.js +23 -27
  5. package/dist/cli/v4/commands/model.js +30 -1
  6. package/dist/cli/v4/defaultSoul.js +69 -2
  7. package/dist/cli/v4/display/capabilityCard.js +135 -0
  8. package/dist/cli/v4/display/frame.js +234 -0
  9. package/dist/cli/v4/display/progressBar.js +137 -0
  10. package/dist/cli/v4/display/sessionEndCard.js +127 -0
  11. package/dist/cli/v4/display/toolTrail.js +172 -0
  12. package/dist/cli/v4/display.js +891 -153
  13. package/dist/cli/v4/doctor.js +377 -75
  14. package/dist/cli/v4/promotionPrompt.js +135 -5
  15. package/dist/cli/v4/replyRenderer.js +487 -26
  16. package/dist/cli/v4/skinEngine.js +26 -4
  17. package/dist/cli/v4/toolPreview.js +82 -19
  18. package/dist/core/tools/nowPlaying.js +7 -15
  19. package/dist/core/v4/aidenAgent.js +9 -0
  20. package/dist/core/v4/promptBuilder.js +2 -1
  21. package/dist/core/v4/sessionDistiller.js +48 -1
  22. package/dist/core/v4/toolRegistry.js +16 -1
  23. package/dist/core/version.js +1 -1
  24. package/dist/moat/plannerGuard.js +19 -0
  25. package/dist/providers/v4/anthropicAdapter.js +25 -2
  26. package/dist/providers/v4/errors.js +92 -0
  27. package/dist/tools/v4/index.js +24 -1
  28. package/dist/tools/v4/sessions/recallSession.js +14 -0
  29. package/dist/tools/v4/system/_psHelpers.js +70 -2
  30. package/dist/tools/v4/system/appInput.js +154 -0
  31. package/dist/tools/v4/system/appLaunch.js +136 -10
  32. package/dist/tools/v4/system/mediaKey.js +35 -4
  33. package/dist/tools/v4/system/mediaSessions.js +163 -0
  34. package/dist/tools/v4/system/mediaTransport.js +211 -0
  35. package/package.json +2 -1
  36. package/skills/system_control.md +56 -6
@@ -1449,6 +1449,12 @@ async function buildAgentRuntime(cliOpts, opts) {
1449
1449
  mcpClient,
1450
1450
  providerId,
1451
1451
  modelId,
1452
+ // v4.1.3-prebump: forward the precedence-case label so the boot
1453
+ // card can render a "where this choice came from" annotation.
1454
+ // The case-3 (persisted-config) branch was confusing users who
1455
+ // expected auto-pick to kick in — surfacing the source closes the
1456
+ // information asymmetry.
1457
+ bootSource,
1452
1458
  resumeSessionId,
1453
1459
  fallbackAdapter,
1454
1460
  personalityManager,
@@ -1475,6 +1481,10 @@ async function runInteractiveChat(cliOpts, opts) {
1475
1481
  config: runtime.config,
1476
1482
  initialProviderId: runtime.providerId,
1477
1483
  initialModelId: runtime.modelId,
1484
+ // v4.1.3-prebump: pass through the precedence-case label so the
1485
+ // boot card can render a dim source annotation under the version
1486
+ // pill ("persisted from prior session" / "auto-picked" / …).
1487
+ initialBootSource: runtime.bootSource,
1478
1488
  resumeSessionId: runtime.resumeSessionId,
1479
1489
  yoloMode: !!cliOpts.yolo,
1480
1490
  fallbackAdapter: runtime.fallbackAdapter,
@@ -82,6 +82,15 @@ class CliCallbacks {
82
82
  }
83
83
  this.beforeFirstToolHook = undefined;
84
84
  }
85
+ // v4.1.4 reply-quality polish — Part 1.6. Pause activity
86
+ // indicator BEFORE the tool row writes so the indicator's line
87
+ // is clean when the row lands. Fires for every tool, not just
88
+ // the first. Defensive try/catch — a misbehaving hook must not
89
+ // block tool dispatch.
90
+ try {
91
+ this.beforeToolHook?.();
92
+ }
93
+ catch { /* defensive */ }
85
94
  const handle = this.display.toolRow(call.name, call.arguments);
86
95
  this.toolRows.set(call.id, handle);
87
96
  this.toolStartTimes.set(call.id, Date.now());
@@ -92,19 +101,61 @@ class CliCallbacks {
92
101
  const startedAt = this.toolStartTimes.get(call.id);
93
102
  this.toolRows.delete(call.id);
94
103
  this.toolStartTimes.delete(call.id);
95
- if (!handle || startedAt === undefined)
104
+ if (!handle || startedAt === undefined) {
105
+ // Even if we lost the handle, the indicator may still need to
106
+ // be re-armed so the next gap shows activity. Tool-name-aware
107
+ // verb selection happens in the hook itself.
108
+ try {
109
+ this.afterEachToolHook?.(call.name);
110
+ }
111
+ catch { /* defensive */ }
96
112
  return;
113
+ }
97
114
  const ms = Date.now() - startedAt;
98
115
  const err = result?.error;
99
116
  if (typeof err === 'string' && err.includes('URL provenance gate')) {
100
117
  handle.blocked();
101
118
  return;
102
119
  }
120
+ // v4.1.4 reply-quality polish — Part 1.6. Helper used by ALL
121
+ // outcome branches below so the activity indicator gets re-armed
122
+ // for the gap that follows this tool (next tool, or final reply).
123
+ // Tool-name-aware verb selection happens in the hook (chatSession
124
+ // wires it through `verbForActivity`).
125
+ const fireAfter = () => {
126
+ try {
127
+ this.afterEachToolHook?.(call.name);
128
+ }
129
+ catch { /* defensive */ }
130
+ };
131
+ if (typeof err === 'string' && err.includes('URL provenance gate')) {
132
+ handle.blocked();
133
+ fireAfter();
134
+ return;
135
+ }
103
136
  if (err) {
104
137
  handle.fail(ms);
138
+ // v4.1.3-essentials: when the tool's failure payload includes a
139
+ // structured capability card (auth missing, platform unsupported),
140
+ // render the card immediately after the fail row. The card sits
141
+ // on its own multi-line block — the fail row is still useful as
142
+ // the action timeline anchor; the card adds the state assessment
143
+ // the user actually needs. No card → plain failure surface.
144
+ if (result?.capabilityCard) {
145
+ this.display.capabilityCard(result.capabilityCard);
146
+ }
147
+ fireAfter();
148
+ return;
149
+ }
150
+ // v4.1.3-repl-polish: degraded outcome — tool completed but with a
151
+ // partial / best-effort result. Show in trail yellow instead of silent.
152
+ if (result?.degraded) {
153
+ handle.degraded(ms, result.degradedReason);
154
+ fireAfter();
105
155
  return;
106
156
  }
107
157
  handle.ok(ms);
158
+ fireAfter();
108
159
  };
109
160
  /** ApprovalEngine.callbacks.promptUser */
110
161
  this.promptApproval = async (req) => {
@@ -171,23 +222,29 @@ Reply with ONE word: safe, caution, or dangerous.`;
171
222
  return false;
172
223
  }
173
224
  };
174
- /** PlannerGuard sink. Quiet in compact mode. */
225
+ /**
226
+ * PlannerGuard sink. v4.1.4 Phase 3b' (Q-Planner): moved to
227
+ * verbose-only. The default `normal` mode previously emitted
228
+ * `[planner] kept N tools (reason)` mid-execution, which collided
229
+ * visually with the activity indicator's single-line paint and
230
+ * with streamed deltas. Users running with the default verbose
231
+ * level should see a clean execution surface — planner-guard
232
+ * decisions are useful for debugging but noise during normal use.
233
+ *
234
+ * `verbose` mode keeps the full breakdown for debugging. `compact`
235
+ * stays silent (unchanged).
236
+ */
175
237
  this.onPlannerGuardDecision = (decision) => {
176
238
  if (this.verboseMode === 'compact')
177
239
  return;
178
- if (decision.reason === 'no_filter')
240
+ if (this.verboseMode !== 'verbose')
179
241
  return;
180
- if (this.verboseMode === 'verbose') {
181
- const conf = decision.confidence !== undefined
182
- ? ` (conf ${decision.confidence.toFixed(2)})`
183
- : '';
184
- this.display.dim(`[planner] ${decision.reason}${conf}: kept ${decision.selectedTools.length} / dropped ${decision.excludedTools.length}`);
242
+ if (decision.reason === 'no_filter')
185
243
  return;
186
- }
187
- // normal
188
- if (decision.excludedTools.length > 0) {
189
- this.display.dim(`[planner] kept ${decision.selectedTools.length} tools (${decision.reason})`);
190
- }
244
+ const conf = decision.confidence !== undefined
245
+ ? ` (conf ${decision.confidence.toFixed(2)})`
246
+ : '';
247
+ this.display.dim(`[planner] ${decision.reason}${conf}: kept ${decision.selectedTools.length} / dropped ${decision.excludedTools.length}`);
191
248
  };
192
249
  /**
193
250
  * Phase v4.1-skill-mining — post-turn cue when the miner has
@@ -265,6 +322,21 @@ Reply with ONE word: safe, caution, or dangerous.`;
265
322
  this.beforeFirstToolHook = fn;
266
323
  this.firstToolFiredThisTurn = false;
267
324
  }
325
+ /**
326
+ * v4.1.4 reply-quality polish — Part 1.6.
327
+ *
328
+ * Register paired hooks so chatSession can pause the activity
329
+ * indicator while a tool row writes, and resume it (with a fresh
330
+ * verb derived from the just-completed tool) in the gap before the
331
+ * next tool fires or the final reply arrives.
332
+ *
333
+ * Both fire for EVERY tool, not just the first. Either can be
334
+ * omitted independently. Cleared between turns by passing `undefined`.
335
+ */
336
+ setActivityIndicatorHooks(opts) {
337
+ this.beforeToolHook = opts.beforeTool;
338
+ this.afterEachToolHook = opts.afterEachTool;
339
+ }
268
340
  }
269
341
  exports.CliCallbacks = CliCallbacks;
270
342
  // Tier-3.1 (v4.1-tier3.1): replaced 🟢/🟡/🔴 emoji badges with
@@ -59,6 +59,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
59
59
  exports.BOOT_TRY_HINT = exports.ChatSession = void 0;
60
60
  exports.parseSessionBulletsResponse = parseSessionBulletsResponse;
61
61
  exports.renderCommandLabel = renderCommandLabel;
62
+ exports.bootSourceLabel = bootSourceLabel;
62
63
  exports.detectOS = detectOS;
63
64
  exports.detectShell = detectShell;
64
65
  exports.formatStatusState = formatStatusState;
@@ -69,12 +70,21 @@ exports.formatTokens = formatTokens;
69
70
  exports.formatDuration = formatDuration;
70
71
  exports.renderMemoryConfirmations = renderMemoryConfirmations;
71
72
  const display_1 = require("./display");
73
+ // v4.1.4 Part 1.6 — per-turn token progress bar. Fed by `onProgress`
74
+ // events from the streaming adapter; hidden when the adapter doesn't
75
+ // emit progress (honest degradation).
76
+ const progressBar_1 = require("./display/progressBar");
72
77
  const uiBuild_1 = require("./uiBuild");
73
78
  const sessionSummaryGate_1 = require("./sessionSummaryGate");
74
79
  const aidenPrompt_1 = __importDefault(require("./aidenPrompt"));
75
80
  const historyStore_1 = require("./historyStore");
76
81
  const modelMetadata_1 = require("../../core/v4/modelMetadata");
82
+ // v4.1.3-prebump: classify provider errors so the catch path can show
83
+ // a tailored action hint (e.g. groq 413 → "switch to chatgpt-plus")
84
+ // instead of the generic "/model or aiden doctor" line.
85
+ const errors_1 = require("../../providers/v4/errors");
77
86
  const sessionDistiller_1 = require("../../core/v4/sessionDistiller");
87
+ const sessionEndCard_1 = require("./display/sessionEndCard");
78
88
  const version_1 = require("../../core/version");
79
89
  const distillationStore_1 = require("../../core/v4/distillationStore");
80
90
  const promotionCandidates_1 = require("../../core/v4/promotionCandidates");
@@ -154,7 +164,15 @@ const STATUS_BAR_WIDTH = 10;
154
164
  * Above this we abandon the LLM half (still write a deterministic-
155
165
  * only distillation so the session isn't lost) and exit honestly.
156
166
  */
157
- const SUMMARY_TIMEOUT_MS_DEFAULT = 4000;
167
+ /**
168
+ * v4.1.3-essentials distillation-fix: bumped 4000 → 12000ms in
169
+ * lockstep with `sessionDistiller.DEFAULT_TIMEOUT_MS`. Same
170
+ * rationale — chatgpt-plus Codex cold-start latency for 800-token
171
+ * summaries regularly exceeds 4s, killing the distillation +
172
+ * promotion-prompt path. Env override `AIDEN_SUMMARY_TIMEOUT_MS`
173
+ * still respected.
174
+ */
175
+ const SUMMARY_TIMEOUT_MS_DEFAULT = 12000;
158
176
  function resolveSummaryTimeoutMs() {
159
177
  const raw = process.env.AIDEN_SUMMARY_TIMEOUT_MS;
160
178
  if (!raw)
@@ -162,6 +180,35 @@ function resolveSummaryTimeoutMs() {
162
180
  const parsed = Number.parseInt(raw, 10);
163
181
  return Number.isFinite(parsed) && parsed > 0 ? parsed : SUMMARY_TIMEOUT_MS_DEFAULT;
164
182
  }
183
+ /**
184
+ * v4.1.3-prebump: map a providerBootSelector precedence-case label to
185
+ * a human-readable hint rendered under the boot card's status pills.
186
+ *
187
+ * Returns `null` for the explicit-selection cases (`cli-flag`, with-or-
188
+ * without -partial) where the source isn't surprising. Annotates the
189
+ * persisted-config / auto-priority / hardcoded-fallback paths so users
190
+ * understand "why this provider, why now".
191
+ *
192
+ * Pure helper — exported for unit testing.
193
+ */
194
+ function bootSourceLabel(source) {
195
+ switch (source) {
196
+ case 'persisted-config':
197
+ return '(persisted from prior session — /model to change)';
198
+ case 'config-partial':
199
+ return '(partial config + auto-resolved companion)';
200
+ case 'auto-priority':
201
+ return '(auto-picked — first authed provider)';
202
+ case 'hardcoded-fallback':
203
+ return '(no authed providers — using legacy default)';
204
+ case 'cli-flag':
205
+ case 'cli-flag-partial':
206
+ // Explicit CLI override — user knows why; no annotation.
207
+ return null;
208
+ default:
209
+ return null;
210
+ }
211
+ }
165
212
  class ChatSession {
166
213
  constructor(opts) {
167
214
  this.opts = opts;
@@ -208,6 +255,13 @@ class ChatSession {
208
255
  * populated alongside it after a verified write.
209
256
  */
210
257
  this.lastDistillation = null;
258
+ /**
259
+ * Absolute path the most recent distillation JSON was written to.
260
+ * Captured at write-time and surfaced in the session-end card so the
261
+ * user has a concrete artifact to inspect or feed to recall_session.
262
+ * Null when the write failed or no distillation has been produced.
263
+ */
264
+ this.lastDistillationPath = null;
211
265
  this.currentProviderId = opts.initialProviderId;
212
266
  this.currentModelId = opts.initialModelId;
213
267
  this.modelMetadata = opts.modelMetadata ?? new modelMetadata_1.ModelMetadata();
@@ -298,6 +352,14 @@ class ChatSession {
298
352
  catch (err) {
299
353
  this.opts.display.warn(`Session summary skipped on ${sig}: ${err.message}`);
300
354
  }
355
+ // v4.1.3-repl-polish: render session-end card before farewell when
356
+ // a distillation was written this session. Pass the on-disk path
357
+ // so the card surfaces the artifact location to the user.
358
+ if (this.lastDistillation) {
359
+ for (const line of (0, sessionEndCard_1.renderSessionEndCard)(this.lastDistillation, (t, k) => this.opts.display.applyColors(t, k), this.lastDistillationPath)) {
360
+ this.opts.display.write(line + '\n');
361
+ }
362
+ }
301
363
  this.opts.display.dim('Goodbye.');
302
364
  process.exit(0);
303
365
  };
@@ -356,9 +418,22 @@ class ChatSession {
356
418
  // Tier-3-essentials: hard-clear the screen on terminal resize so
357
419
  // dropdown re-renders + previous prompt frames don't ghost into
358
420
  // the new viewport. No-op on non-TTY / MCP serve mode.
421
+ //
422
+ // v4.1.4 reply-quality polish: also drop the per-chunk stream row
423
+ // counter so a mid-stream resize doesn't try to erase rows that
424
+ // the hard-clear already removed. See `resetStreamFrameForResize`
425
+ // in display.ts for the rationale.
359
426
  const restoreResizeGuard = this.opts.promptApi
360
427
  ? () => { }
361
- : (0, resizeGuard_1.installResizeGuard)();
428
+ : (0, resizeGuard_1.installResizeGuard)({
429
+ onCleared: () => {
430
+ try {
431
+ this.opts.display
432
+ .resetStreamFrameForResize?.();
433
+ }
434
+ catch { /* defensive — never break the resize listener */ }
435
+ },
436
+ });
362
437
  try {
363
438
  while (iter < max) {
364
439
  iter += 1;
@@ -430,6 +505,12 @@ class ChatSession {
430
505
  // is resumed in the same process (not today's behavior),
431
506
  // otherwise they're skipped — documented in commit.
432
507
  await this.maybeRunPromotion(promptApi);
508
+ // v4.1.3-repl-polish: session-end card before farewell.
509
+ if (this.lastDistillation) {
510
+ for (const line of (0, sessionEndCard_1.renderSessionEndCard)(this.lastDistillation, (t, k) => this.opts.display.applyColors(t, k), this.lastDistillationPath)) {
511
+ this.opts.display.write(line + '\n');
512
+ }
513
+ }
433
514
  break;
434
515
  }
435
516
  if (result.clearHistory)
@@ -555,6 +636,15 @@ class ChatSession {
555
636
  toolTrace: this.sessionToolTrace,
556
637
  auxiliaryClient: this.opts.auxiliaryClient,
557
638
  timeoutMs,
639
+ // v4.1.3-essentials distillation-fix: route the new
640
+ // diagnostic signal to a dim line so the user can see WHICH
641
+ // of the three failure classes fired (timeout / call-fail /
642
+ // unparseable JSON). Before this hook, all three converged
643
+ // on a silent `partial:true` and the downstream "no bullets"
644
+ // warning didn't distinguish them.
645
+ onDiagnostic: (msg) => {
646
+ this.opts.display.dim(`[distill] ${msg}`);
647
+ },
558
648
  });
559
649
  }
560
650
  catch (err) {
@@ -569,6 +659,7 @@ class ChatSession {
569
659
  const dir = node_path_1.default.join(this.opts.paths.root, 'distillations');
570
660
  try {
571
661
  const file = await (0, distillationStore_1.writeDistillation)(dir, dist);
662
+ this.lastDistillationPath = file;
572
663
  this.opts.display.dim(`Session distillation${dist.partial ? ' (partial)' : ''} saved to ${file}`);
573
664
  }
574
665
  catch (err) {
@@ -811,40 +902,105 @@ class ChatSession {
811
902
  const baseHistory = newHistory.length > 0
812
903
  ? [...this.history, ...newHistory, userMsg]
813
904
  : [...this.history, userMsg];
814
- // Phase 16c: streaming gated on display.streaming config (default off).
815
- // Defensive: tests sometimes pass partial config stubs without the
816
- // ConfigManager API; treat that as "streaming disabled".
905
+ // Phase 16c: streaming gated on display.streaming config.
906
+ // v4.1.4 Part 1.6: PRODUCTION DEFAULT FLIPPED FROM FALSE TO TRUE.
907
+ // Streaming delivers the activity indicator, tool-row live tick,
908
+ // and token progress bar that the user feedback ("after prompt i
909
+ // just see output") was specifically asking for. Users who
910
+ // explicitly set `display.streaming: false` in config still opt
911
+ // out; the change affects only the default for users who never
912
+ // touched the flag.
913
+ //
914
+ // Test-stub fallback (no ConfigManager) stays at `false` so
915
+ // existing tests that depended on the non-streaming code path
916
+ // don't have to be rewritten in this slice — they exercise the
917
+ // batch-call path that production users on Ollama / non-streaming
918
+ // adapters still hit naturally.
817
919
  const streamingEnabled = typeof this.opts.config?.getValue === 'function'
818
- ? this.opts.config.getValue('display.streaming', false) === true
920
+ ? this.opts.config.getValue('display.streaming', true) === true
819
921
  : false;
820
- // Phase 26.2.6 random thinking phrase per turn, already wrapped
821
- // in brand orange by Display.thinkingPhrase().
822
- const spinner = this.opts.display.startSpinner(this.opts.display.thinkingPhrase());
823
- let spinnerStopped = false;
922
+ // v4.1.4 reply-quality polish Part 1.6. Activity indicator
923
+ // replaces the prior single-shot spinner. Pause/resume hooks make
924
+ // the indicator cooperate with tool rows: it pauses before each
925
+ // tool row writes and resumes (with a tool-aware verb) in the
926
+ // gap that follows, so the user always sees activity feedback
927
+ // during model-thinking time — not just the pre-first-token gap.
928
+ //
929
+ // Initial verb is "thinking" (pre-tools phase). After each tool
930
+ // completes, `verbForActivity(toolName, 'post-tool')` picks a
931
+ // category-aware verb (reading / searching / analyzing / drafting).
932
+ // When the first stream delta arrives OR the final agentTurn is
933
+ // about to write, the indicator stops permanently.
934
+ const indicator = this.opts.display.activityIndicator('thinking');
935
+ let indicatorStopped = false;
824
936
  let streamingActive = false;
825
- const stopSpinnerOnce = () => {
826
- if (spinnerStopped)
937
+ const stopIndicatorOnce = () => {
938
+ if (indicatorStopped)
827
939
  return;
828
- spinnerStopped = true;
829
- spinner.stop();
940
+ indicatorStopped = true;
941
+ indicator.stop();
942
+ // Clear the per-turn pause/resume hooks so they don't fire
943
+ // against a stopped indicator on a subsequent turn. The next
944
+ // turn re-registers fresh hooks.
945
+ try {
946
+ this.opts.callbacks.setActivityIndicatorHooks?.({});
947
+ }
948
+ catch { /* defensive */ }
830
949
  };
831
- // Phase 23.5: stop the "thinking…" spinner the moment the first
832
- // tool row prints. The event rows are the user-facing indicator
833
- // from that point on; a spinner painting `\r` over the same line
834
- // would corrupt our row-overwrite when the row mutates to its
835
- // final bracket state.
836
- this.opts.callbacks.setBeforeFirstToolHook?.(stopSpinnerOnce);
950
+ // Phase 23.5 carried forward: stop the indicator the moment the
951
+ // first tool row prints the row itself is the activity surface
952
+ // during a tool. Part 1.6 then resumes via `afterEachTool` so the
953
+ // post-tool gap has its own indicator paint.
954
+ this.opts.callbacks.setBeforeFirstToolHook?.(stopIndicatorOnce);
955
+ // Part 1.6: pause/resume hooks around every tool row. The
956
+ // `beforeTool` hook fires before EACH tool row writes (not just
957
+ // the first), so multi-tool sequences also keep the indicator
958
+ // off the tool-row line. `afterEachTool` resumes with a verb
959
+ // chosen from the just-completed tool's category — best guess
960
+ // for "what the model is doing next". `lastToolName` is captured
961
+ // for tests / observability; the verb decision happens inline.
962
+ this.opts.callbacks.setActivityIndicatorHooks?.({
963
+ beforeTool: () => {
964
+ if (indicatorStopped)
965
+ return;
966
+ indicator.pause();
967
+ // v4.1.4 Part 1.6: hide the progress bar while the tool row
968
+ // owns the screen. The bar paints below the indicator, so
969
+ // it'd otherwise sit between the tool row and any subsequent
970
+ // stream output — visual clutter for tool-heavy turns. The
971
+ // bar is per-turn, not per-stream-segment; once hidden it
972
+ // stays hidden until the next turn's bar is created.
973
+ progressBar?.hide();
974
+ },
975
+ afterEachTool: (toolName) => {
976
+ if (indicatorStopped)
977
+ return;
978
+ indicator.resume((0, display_1.verbForActivity)(toolName, 'post-tool'));
979
+ },
980
+ });
981
+ // v4.1.4 Part 1.6: per-turn progress bar. Created lazily on the
982
+ // first `onProgress` event from the streaming adapter so the bar
983
+ // line doesn't paint until there's something to show. Adapters
984
+ // that don't emit progress (Ollama, most OpenAI-compat) never
985
+ // trigger creation — honest degradation, no fake estimates.
986
+ let progressBar = null;
837
987
  try {
838
988
  const result = await this.opts.agent.runConversation(baseHistory, {
839
989
  stream: streamingEnabled,
840
990
  onFirstDelta: streamingEnabled
841
991
  ? () => {
842
- stopSpinnerOnce();
992
+ stopIndicatorOnce();
843
993
  streamingActive = true;
844
994
  }
845
995
  : undefined,
846
996
  onDelta: streamingEnabled
847
997
  ? (text) => {
998
+ // v4.1.4 Part 1.6: bar lives ABOVE streamed text. Hide
999
+ // it before each delta writes so the stream output
1000
+ // doesn't land on the bar's line. The bar repaints on
1001
+ // the next `onProgress` event (which Anthropic emits
1002
+ // frequently enough that the bar stays usefully visible).
1003
+ progressBar?.hide();
848
1004
  this.opts.display.streamPartial(text);
849
1005
  }
850
1006
  : undefined,
@@ -853,8 +1009,30 @@ class ChatSession {
853
1009
  this.opts.display.streamToolIndicator(call.name);
854
1010
  }
855
1011
  : undefined,
1012
+ onProgress: streamingEnabled
1013
+ ? (outputTokens, maxTokens) => {
1014
+ if (indicatorStopped === false)
1015
+ return;
1016
+ // Lazy-create on first event. The indicator must already
1017
+ // be stopped (first delta arrived) so the bar paints on
1018
+ // its own line below where the indicator was. If the
1019
+ // indicator is still up, skip — the bar would land on
1020
+ // the indicator line and get clobbered by the next tick.
1021
+ if (!progressBar) {
1022
+ progressBar = (0, progressBar_1.createProgressBar)(process.stdout,
1023
+ // Display exposes its skin via getter on the
1024
+ // implementation; cast to any to avoid widening
1025
+ // the public Display surface for one-shot use.
1026
+ this.opts.display.skin);
1027
+ }
1028
+ progressBar.update(outputTokens, maxTokens);
1029
+ }
1030
+ : undefined,
856
1031
  });
857
- stopSpinnerOnce();
1032
+ stopIndicatorOnce();
1033
+ // Hide the progress bar before any post-stream content
1034
+ // (statusFooter, the next prompt) lands on its line.
1035
+ progressBar?.hide();
858
1036
  if (streamingActive)
859
1037
  this.opts.display.streamComplete();
860
1038
  this.history = result.messages;
@@ -892,11 +1070,50 @@ class ChatSession {
892
1070
  this.renderStatusLine();
893
1071
  }
894
1072
  catch (err) {
895
- stopSpinnerOnce();
1073
+ stopIndicatorOnce();
1074
+ // v4.1.4 Part 1.6: error path must also hide the progress bar
1075
+ // so it doesn't leak across the boundary into the error chrome
1076
+ // or the next prompt.
1077
+ progressBar?.hide();
896
1078
  if (streamingActive)
897
1079
  this.opts.display.streamComplete();
898
1080
  const msg = err?.message ?? String(err);
899
- this.opts.display.printError(msg, 'Run `/model` to switch providers or `aiden doctor` to diagnose.');
1081
+ // v4.1.3-prebump: classify the error so the suggestion below
1082
+ // points at the actual fix instead of the generic "/model or
1083
+ // doctor" line. 413 / 429 / auth get tailored hints; everything
1084
+ // else keeps the legacy fallback. Use the live providerId so
1085
+ // the user sees WHICH provider blew up (matters when fallback
1086
+ // adapters rotate slots mid-turn).
1087
+ const cls = (0, errors_1.classifyProviderError)(err);
1088
+ const tailored = (0, errors_1.suggestForErrorClass)(cls, this.currentProviderId);
1089
+ // v4.1.3-essentials: on `auth` class errors we have enough state
1090
+ // (which provider, what to run) to render a capability card —
1091
+ // structured "what auth's missing, what you can still do, how to
1092
+ // fix" is more useful than the bare message + one-line hint.
1093
+ // Other classes keep the printError single-line surface; their
1094
+ // hints are already specific.
1095
+ if (cls === 'auth') {
1096
+ const p = this.currentProviderId;
1097
+ this.opts.display.printError(msg);
1098
+ this.opts.display.capabilityCard({
1099
+ title: `${p} authentication required`,
1100
+ canStill: [
1101
+ 'Continue chatting if a non-auth provider is configured (run `/model`)',
1102
+ 'Run `/auth status` to see which providers are signed in',
1103
+ 'Run `aiden doctor --providers` for a fuller liveness probe',
1104
+ ],
1105
+ cannotReliably: [
1106
+ `Call ${p} until credentials are refreshed`,
1107
+ 'Trust any cached responses that depended on this provider',
1108
+ ],
1109
+ fix: `Run \`/auth login ${p}\` if it's an OAuth provider, or set the ` +
1110
+ `relevant API key env var. Then retry — no need to restart Aiden.`,
1111
+ });
1112
+ }
1113
+ else {
1114
+ this.opts.display.printError(msg, tailored
1115
+ ?? 'Run `/model` to switch providers or `aiden doctor` to diagnose.');
1116
+ }
900
1117
  this.setStatusState({ kind: 'ready' });
901
1118
  this.lastTurnElapsedMs = Date.now() - turnStartedAt;
902
1119
  }
@@ -979,6 +1196,15 @@ class ChatSession {
979
1196
  providerOk: !this.opts.unconfigured,
980
1197
  version: version_1.VERSION,
981
1198
  }) + '\n');
1199
+ // v4.1.3-prebump: dim source annotation under the pills row so the
1200
+ // user can see WHY this provider/model was chosen — closes the
1201
+ // information gap that made Case 3 (persisted-config) look like a
1202
+ // bug ("why is it still on groq when I auth'd chatgpt-plus?"). One
1203
+ // line, dim, only when the source is informative.
1204
+ const sourceLabel = bootSourceLabel(this.opts.initialBootSource);
1205
+ if (sourceLabel) {
1206
+ display.write(` ${display.muted(sourceLabel)}\n`);
1207
+ }
982
1208
  // Tier-3.1b: rule + environment/capabilities block + rule + scroll
983
1209
  // + bottom prompt hint. Skipped at <70 cols to keep the narrow
984
1210
  // boot card from wrapping into noise.
@@ -32,35 +32,31 @@ exports.doctor = {
32
32
  }
33
33
  ctx.display.info('Running diagnostic checks...');
34
34
  const report = await (0, doctor_1.runDoctor)({ paths: ctx.paths });
35
- // Phase 22 Task 5A: orange-bordered rounded box; rows + summary
36
- // assembled by renderHealthBox so the slash command stays a thin
37
- // adapter and the same renderer can be reused by `aiden doctor`
38
- // CLI in a future polish pass.
39
- ctx.display.write((0, doctor_1.renderHealthBox)(report, ctx.display) + '\n');
40
- // Phase 23.1: surface session-scoped skill-enforcement counters.
41
- // Lives only on the live agent (process-scoped, no persistence) so
42
- // `aiden doctor` CLI subcommand correctly omits this — the
43
- // counters would always be zero there.
35
+ // v4.1.3-essentials doctor-polish: pull in-process subsystem
36
+ // health + skill-outcome data into the same report so they
37
+ // render as additional grouped sections inside the health box,
38
+ // not as disconnected blocks below it. `subsystemHealthResults`
39
+ // / `skillOutcomeResults` return empty arrays when their
40
+ // sources are unavailable so the grouped-renderer simply drops
41
+ // those sections.
44
42
  if (ctx.agent) {
45
- const m = ctx.agent.getSkillEnforcementMetrics();
46
- ctx.display.write(
47
- // Phase 23.4b: surface the Stage-0 intent pre-arm counter so
48
- // smoke runs can confirm the regex fired on bug-Y queries.
49
- `[skill-enforcement] armed=${m.armed} pre-armed=${m.preArmed} recovered=${m.recovered} failed=${m.failed} (session)\n`);
50
- // Phase 23.4a: same shape, different concern URL provenance
51
- // gate counters. blocked = open_url calls rejected for unknown
52
- // YouTube ids; recovered = corrective retry produced a real
53
- // youtube_search; failed = retry cap exceeded and the turn
54
- // ended with an honest-failure message.
55
- const u = ctx.agent.getUrlProvenanceMetrics();
56
- ctx.display.write(`[url-provenance] blocked=${u.blocked} recovered=${u.recovered} failed=${u.failed} (session)\n`);
57
- // Phase 23.4a-fix2: empty-response counters. detected =
58
- // Codex backend completed a turn with no content and no tool
59
- // calls; retried = corrective system message injected (cap
60
- // 1/turn); recovered = retry yielded a non-empty reply.
61
- const e = ctx.agent.getEmptyResponseMetrics();
62
- ctx.display.write(`[empty-response] detected=${e.detected} retried=${e.retried} recovered=${e.recovered} (session)\n`);
43
+ const a = ctx.agent;
44
+ report.results.push(...(0, doctor_1.subsystemHealthResults)(a.subsystemHealthRegistry));
45
+ report.results.push(...(0, doctor_1.skillOutcomeResults)(a.skillOutcomeTracker));
46
+ // v4.1.3-essentials doctor-polish: session-scoped counters
47
+ // (skill enforcement / URL provenance / empty response) now
48
+ // fold into the same report so they render as a "Session
49
+ // counters" group INSIDE the box instead of as orphan
50
+ // `display.write` lines below it. Previous code emitted them
51
+ // as 3 separate `[bracket-prefix] key=N ...` lines after
52
+ // renderHealthBox closed visually disconnected.
53
+ report.results.push(...(0, doctor_1.sessionCounterResults)(ctx.agent));
63
54
  }
55
+ // v4.1.3-essentials doctor-polish: renderHealthBox now groups
56
+ // results by section header with a top summary. Same renderer
57
+ // is used by `aiden doctor` CLI path so both surfaces stay in
58
+ // visual sync (Path-A unification).
59
+ ctx.display.write((0, doctor_1.renderHealthBox)(report, ctx.display) + '\n');
64
60
  return {};
65
61
  },
66
62
  };