aiden-runtime 4.1.3 → 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.
@@ -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,14 +101,38 @@ 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);
105
138
  // v4.1.3-essentials: when the tool's failure payload includes a
@@ -111,15 +144,18 @@ class CliCallbacks {
111
144
  if (result?.capabilityCard) {
112
145
  this.display.capabilityCard(result.capabilityCard);
113
146
  }
147
+ fireAfter();
114
148
  return;
115
149
  }
116
150
  // v4.1.3-repl-polish: degraded outcome — tool completed but with a
117
151
  // partial / best-effort result. Show in trail yellow instead of silent.
118
152
  if (result?.degraded) {
119
153
  handle.degraded(ms, result.degradedReason);
154
+ fireAfter();
120
155
  return;
121
156
  }
122
157
  handle.ok(ms);
158
+ fireAfter();
123
159
  };
124
160
  /** ApprovalEngine.callbacks.promptUser */
125
161
  this.promptApproval = async (req) => {
@@ -186,23 +222,29 @@ Reply with ONE word: safe, caution, or dangerous.`;
186
222
  return false;
187
223
  }
188
224
  };
189
- /** 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
+ */
190
237
  this.onPlannerGuardDecision = (decision) => {
191
238
  if (this.verboseMode === 'compact')
192
239
  return;
193
- if (decision.reason === 'no_filter')
240
+ if (this.verboseMode !== 'verbose')
194
241
  return;
195
- if (this.verboseMode === 'verbose') {
196
- const conf = decision.confidence !== undefined
197
- ? ` (conf ${decision.confidence.toFixed(2)})`
198
- : '';
199
- this.display.dim(`[planner] ${decision.reason}${conf}: kept ${decision.selectedTools.length} / dropped ${decision.excludedTools.length}`);
242
+ if (decision.reason === 'no_filter')
200
243
  return;
201
- }
202
- // normal
203
- if (decision.excludedTools.length > 0) {
204
- this.display.dim(`[planner] kept ${decision.selectedTools.length} tools (${decision.reason})`);
205
- }
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}`);
206
248
  };
207
249
  /**
208
250
  * Phase v4.1-skill-mining — post-turn cue when the miner has
@@ -280,6 +322,21 @@ Reply with ONE word: safe, caution, or dangerous.`;
280
322
  this.beforeFirstToolHook = fn;
281
323
  this.firstToolFiredThisTurn = false;
282
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
+ }
283
340
  }
284
341
  exports.CliCallbacks = CliCallbacks;
285
342
  // Tier-3.1 (v4.1-tier3.1): replaced 🟢/🟡/🔴 emoji badges with
@@ -70,6 +70,10 @@ exports.formatTokens = formatTokens;
70
70
  exports.formatDuration = formatDuration;
71
71
  exports.renderMemoryConfirmations = renderMemoryConfirmations;
72
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");
73
77
  const uiBuild_1 = require("./uiBuild");
74
78
  const sessionSummaryGate_1 = require("./sessionSummaryGate");
75
79
  const aidenPrompt_1 = __importDefault(require("./aidenPrompt"));
@@ -414,9 +418,22 @@ class ChatSession {
414
418
  // Tier-3-essentials: hard-clear the screen on terminal resize so
415
419
  // dropdown re-renders + previous prompt frames don't ghost into
416
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.
417
426
  const restoreResizeGuard = this.opts.promptApi
418
427
  ? () => { }
419
- : (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
+ });
420
437
  try {
421
438
  while (iter < max) {
422
439
  iter += 1;
@@ -885,40 +902,105 @@ class ChatSession {
885
902
  const baseHistory = newHistory.length > 0
886
903
  ? [...this.history, ...newHistory, userMsg]
887
904
  : [...this.history, userMsg];
888
- // Phase 16c: streaming gated on display.streaming config (default off).
889
- // Defensive: tests sometimes pass partial config stubs without the
890
- // 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.
891
919
  const streamingEnabled = typeof this.opts.config?.getValue === 'function'
892
- ? this.opts.config.getValue('display.streaming', false) === true
920
+ ? this.opts.config.getValue('display.streaming', true) === true
893
921
  : false;
894
- // Phase 26.2.6 random thinking phrase per turn, already wrapped
895
- // in brand orange by Display.thinkingPhrase().
896
- const spinner = this.opts.display.startSpinner(this.opts.display.thinkingPhrase());
897
- 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;
898
936
  let streamingActive = false;
899
- const stopSpinnerOnce = () => {
900
- if (spinnerStopped)
937
+ const stopIndicatorOnce = () => {
938
+ if (indicatorStopped)
901
939
  return;
902
- spinnerStopped = true;
903
- 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 */ }
904
949
  };
905
- // Phase 23.5: stop the "thinking…" spinner the moment the first
906
- // tool row prints. The event rows are the user-facing indicator
907
- // from that point on; a spinner painting `\r` over the same line
908
- // would corrupt our row-overwrite when the row mutates to its
909
- // final bracket state.
910
- 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;
911
987
  try {
912
988
  const result = await this.opts.agent.runConversation(baseHistory, {
913
989
  stream: streamingEnabled,
914
990
  onFirstDelta: streamingEnabled
915
991
  ? () => {
916
- stopSpinnerOnce();
992
+ stopIndicatorOnce();
917
993
  streamingActive = true;
918
994
  }
919
995
  : undefined,
920
996
  onDelta: streamingEnabled
921
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();
922
1004
  this.opts.display.streamPartial(text);
923
1005
  }
924
1006
  : undefined,
@@ -927,8 +1009,30 @@ class ChatSession {
927
1009
  this.opts.display.streamToolIndicator(call.name);
928
1010
  }
929
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,
930
1031
  });
931
- 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();
932
1036
  if (streamingActive)
933
1037
  this.opts.display.streamComplete();
934
1038
  this.history = result.messages;
@@ -966,7 +1070,11 @@ class ChatSession {
966
1070
  this.renderStatusLine();
967
1071
  }
968
1072
  catch (err) {
969
- 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();
970
1078
  if (streamingActive)
971
1079
  this.opts.display.streamComplete();
972
1080
  const msg = err?.message ?? String(err);
@@ -30,7 +30,7 @@ exports.PREVIOUS_BUNDLED_SOULS = exports.DEFAULT_SOUL_MD = exports.BUNDLED_SOUL_
30
30
  // <act_dont_ask>. ensureSoulMdSeeded compares this against the user's
31
31
  // on-disk SOUL.md to decide whether to silent-replace (matches a prior
32
32
  // bundled default) or preserve+notify (user-edited).
33
- exports.BUNDLED_SOUL_VERSION = 'v4.1.2';
33
+ exports.BUNDLED_SOUL_VERSION = 'v4.1.4';
34
34
  exports.DEFAULT_SOUL_MD = `You are Aiden — a local-first AI agent built by Taracod.
35
35
 
36
36
  Identity:
@@ -40,7 +40,8 @@ Identity:
40
40
  - You have 40 tools spanning files, browser, terminal, web, memory.
41
41
 
42
42
  Voice:
43
- - Direct. No fluff. Match the user's energy.
43
+ - Match the user's energy. When the user asks a thoughtful question (opinion, exploration, comparison), engage thoughtfully. When the user asks transactionally, stay tight.
44
+ - On thoughtful questions, share the reasoning before the answer — what you considered, what you discarded, why.
44
45
  - Honest above all — if you didn't do something, say so. If you're not sure, say so.
45
46
  - You never claim to "have run" a tool unless the trace shows it.
46
47
 
@@ -254,5 +255,71 @@ Limits:
254
255
  - You're a CLI agent in v4.0.0. No voice, no scheduled jobs, no messaging gateway yet — those are v4.1.
255
256
  - You can't bypass approval prompts for dangerous commands.
256
257
  - You don't lie to look smart. If you don't know, you say so.
258
+ `,
259
+ // v4.1.2 default — shipped through v4.1.3 (no SOUL change in v4.1.3).
260
+ // v4.1.4 rewrites the Voice block to make conciseness conditional:
261
+ // thoughtful questions get thoughtful engagement, transactional
262
+ // questions stay tight. Adds an explicit reasoning-visibility line.
263
+ // Users on the v4.1.2 / v4.1.3 install have this verbatim text on
264
+ // disk; silent-upgrade picks them up here.
265
+ `You are Aiden — a local-first AI agent built by Taracod.
266
+
267
+ Identity:
268
+ - You run on the user's machine, native Windows/Linux/macOS (not WSL2).
269
+ - You have 72 bundled skills + access to install more via skills.sh.
270
+ - You remember past sessions via persistent storage.
271
+ - You have 40 tools spanning files, browser, terminal, web, memory.
272
+
273
+ Voice:
274
+ - Direct. No fluff. Match the user's energy.
275
+ - Honest above all — if you didn't do something, say so. If you're not sure, say so.
276
+ - You never claim to "have run" a tool unless the trace shows it.
277
+
278
+ Behavior:
279
+ - Default to action over discussion. The user wants results.
280
+ - When asked who you are, identify as Aiden. Not "a large language model."
281
+ - When asked what you can do, mention specific skills/tools, not generic capabilities.
282
+ - If user mentions trading/NSE/markets, you have specialized skills for that.
283
+
284
+ <act_dont_ask>
285
+ When a request has an obvious default interpretation, act on it
286
+ immediately instead of asking for clarification. Examples:
287
+ - "play me a popular song" / "play X on youtube" → load skill_view(media-search)
288
+ and follow it. Substitute fuzzy phrases ("popular song") with a specific
289
+ chart-topper BEFORE searching, then open_url a /watch?v= URL once.
290
+ NEVER search verbatim "popular song" — that returns articles, not music.
291
+ - "what files are in my Downloads?" → file_list on Downloads. Don't ask
292
+ "which user?" — it's the current user.
293
+ - "is port 443 open?" → check this machine. Don't ask "open where?"
294
+ Only ask for clarification when the ambiguity genuinely changes which
295
+ tool you would call.
296
+ </act_dont_ask>
297
+
298
+ <prerequisite_checks>
299
+ Before acting, check whether prerequisite discovery, lookup, or
300
+ context-gathering steps are needed. If a step depends on output from a
301
+ prior step, resolve that dependency first. Don't skip prerequisite
302
+ steps just because the final action seems obvious.
303
+ </prerequisite_checks>
304
+
305
+ <missing_context>
306
+ If required context is missing, do NOT guess or hallucinate. Use the
307
+ appropriate lookup tool when missing information is retrievable
308
+ (file_read, file_list, web_search, fetch_url, session_search,
309
+ system_info). Ask a clarifying question ONLY when no tool can resolve
310
+ the ambiguity.
311
+ </missing_context>
312
+
313
+ <keep_going>
314
+ Work autonomously until the task is fully resolved. Don't stop with a
315
+ plan — execute it. Multi-step tasks (open browser → search → click
316
+ result; or list files → read each → summarise) are expected; chain
317
+ the tool calls within a single turn instead of returning halfway and
318
+ asking the user what to do next.
319
+ </keep_going>
320
+
321
+ Limits:
322
+ - You can't bypass approval prompts for dangerous commands.
323
+ - You don't lie to look smart. If you don't know, you say so.
257
324
  `,
258
325
  ];
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * cli/v4/display/frame.ts — Phase v4.1.4 reply-quality polish (Part 1.5).
10
+ *
11
+ * Single source of truth for the reply-frame math: terminal width,
12
+ * left gutter, body width, indent-by-depth, and ANSI-aware soft wrap.
13
+ *
14
+ * Before this module the same width formula
15
+ * (`Math.min(out.columns ?? 80, 100)`) lived at 4 different sites with
16
+ * inconsistent offsets, no shared floor, and no actual wrap engine —
17
+ * `marked-terminal` has `reflowText: false` so long prose just spilled
18
+ * to terminal-natural wrap (continuation lines at column 0, not at the
19
+ * gutter). frame.ts replaces all four with one formula and one wrapper.
20
+ *
21
+ * Public surface:
22
+ * - GUTTER 3-col left gutter (assistant body)
23
+ * - BODY_WIDTH_MAX 100 (tunable cap; export so future skin/theme
24
+ * work can override without touching consumers)
25
+ * - getTerminalCols() live `process.stdout.columns` with 80 fallback
26
+ * - getBodyWidth() `max(20, cols - gutter - 2)` capped at
27
+ * `BODY_WIDTH_MAX - gutter - 2`
28
+ * - getIndent(depth) `' '.repeat(GUTTER + depth*2)` — gutter +
29
+ * depth-aware. depth=0 = bare gutter, depth=1 =
30
+ * gutter+2, etc.
31
+ * - wrap(text, opts) ANSI-aware soft wrap via wrap-ansi defaults
32
+ * `{ trim: false, hard: true }`. Returns string
33
+ * with embedded `\n` per wrap point.
34
+ * - applyFrame(body) convenience: indents every line of `body` by
35
+ * GUTTER. No wrap (caller pre-wraps to bodyWidth).
36
+ *
37
+ * Wrap engine: wrap-ansi@9 (ESM-only). Loaded via cached dynamic
38
+ * `import()` so a CJS TypeScript build can still consume it. Until the
39
+ * first wrap finishes resolving, `wrap()` falls back to a passthrough
40
+ * that preserves the input verbatim — wrong visual but never incorrect
41
+ * data. Boot-time prime via `primeFrameAsync()` (best-effort) so the
42
+ * first user-visible wrap call already has the module loaded.
43
+ */
44
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
45
+ if (k2 === undefined) k2 = k;
46
+ var desc = Object.getOwnPropertyDescriptor(m, k);
47
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
48
+ desc = { enumerable: true, get: function() { return m[k]; } };
49
+ }
50
+ Object.defineProperty(o, k2, desc);
51
+ }) : (function(o, m, k, k2) {
52
+ if (k2 === undefined) k2 = k;
53
+ o[k2] = m[k];
54
+ }));
55
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
56
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
57
+ }) : function(o, v) {
58
+ o["default"] = v;
59
+ });
60
+ var __importStar = (this && this.__importStar) || (function () {
61
+ var ownKeys = function(o) {
62
+ ownKeys = Object.getOwnPropertyNames || function (o) {
63
+ var ar = [];
64
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
65
+ return ar;
66
+ };
67
+ return ownKeys(o);
68
+ };
69
+ return function (mod) {
70
+ if (mod && mod.__esModule) return mod;
71
+ var result = {};
72
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
73
+ __setModuleDefault(result, mod);
74
+ return result;
75
+ };
76
+ })();
77
+ Object.defineProperty(exports, "__esModule", { value: true });
78
+ exports.BODY_WIDTH_MIN = exports.BODY_WIDTH_MAX = exports.GUTTER = void 0;
79
+ exports.getTerminalCols = getTerminalCols;
80
+ exports.getBodyWidth = getBodyWidth;
81
+ exports.getIndent = getIndent;
82
+ exports.primeFrameAsync = primeFrameAsync;
83
+ exports.wrap = wrap;
84
+ exports.applyFrame = applyFrame;
85
+ exports._resetForTests = _resetForTests;
86
+ exports._injectWrapForTests = _injectWrapForTests;
87
+ // ── Tunable constants ─────────────────────────────────────────────────
88
+ /**
89
+ * Left gutter for assistant body. 3 columns matches the visual rhythm
90
+ * established by the boot card, tool trail, and status footer once
91
+ * Part 1.5 lands. Was 2 before this slice — bumped one column so the
92
+ * body breathes against the left edge.
93
+ */
94
+ exports.GUTTER = 3;
95
+ /**
96
+ * Maximum body width before the visual frame stops growing. Wide
97
+ * terminals (150+ cols) get a body capped at this minus gutter+2
98
+ * because long lines (~120 chars) are harder to read than mid-length
99
+ * (~70-90 chars). Tunable: skin or theme code can override.
100
+ */
101
+ exports.BODY_WIDTH_MAX = 100;
102
+ /**
103
+ * Hard floor on body width. Below this we render at 20 cols and let
104
+ * the terminal-natural wrap pick up the rest — better than crashing
105
+ * with a negative-width wrap-ansi call on a 5-col terminal.
106
+ */
107
+ exports.BODY_WIDTH_MIN = 20;
108
+ // ── Width helpers ─────────────────────────────────────────────────────
109
+ /**
110
+ * Live terminal column count. Reads `process.stdout.columns` on every
111
+ * call so resize events propagate without us needing a cache. Falls
112
+ * back to 80 when the stream is non-TTY or hasn't reported a size yet
113
+ * (pipes, CI logs, MCP serve).
114
+ *
115
+ * `out` override exists for testability — display.test.ts injects a
116
+ * fake stream with explicit `columns` to assert various widths.
117
+ */
118
+ function getTerminalCols(out = process.stdout) {
119
+ const c = out.columns;
120
+ if (typeof c !== 'number' || !Number.isFinite(c) || c < 1)
121
+ return 80;
122
+ return c;
123
+ }
124
+ /**
125
+ * Computed body width — the safe horizontal space inside the frame.
126
+ * Math: `min(BODY_WIDTH_MAX, cols) - GUTTER - 2`. The trailing `-2`
127
+ * leaves visual breathing room on the right margin (mirrors the boot
128
+ * card / tool-row right-pad convention). Floored at BODY_WIDTH_MIN so
129
+ * pathological narrow terminals still get a usable wrap.
130
+ */
131
+ function getBodyWidth(out = process.stdout) {
132
+ const cols = Math.min(getTerminalCols(out), exports.BODY_WIDTH_MAX);
133
+ const raw = cols - exports.GUTTER - 2;
134
+ return Math.max(exports.BODY_WIDTH_MIN, raw);
135
+ }
136
+ /**
137
+ * Indent string for the given nesting depth. Depth 0 = bare gutter
138
+ * (3 spaces). Each additional level adds 2 spaces. Used by list,
139
+ * blockquote, and code-block renderers so every element shares one
140
+ * indent algebra.
141
+ */
142
+ function getIndent(depth = 0) {
143
+ const d = Math.max(0, Math.floor(depth));
144
+ return ' '.repeat(exports.GUTTER + d * 2);
145
+ }
146
+ let cachedWrap = null;
147
+ let primePromise = null;
148
+ /**
149
+ * Best-effort load of wrap-ansi. Idempotent. Safe to call from boot.
150
+ * Returns a promise that resolves once the module is loaded (or
151
+ * rejects silently — wrap() will just keep using the passthrough).
152
+ */
153
+ function primeFrameAsync() {
154
+ if (cachedWrap)
155
+ return Promise.resolve();
156
+ if (primePromise)
157
+ return primePromise;
158
+ primePromise = (async () => {
159
+ try {
160
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
161
+ const mod = await Promise.resolve().then(() => __importStar(require('wrap-ansi')));
162
+ const fn = (mod.default ?? mod);
163
+ if (typeof fn === 'function')
164
+ cachedWrap = fn;
165
+ }
166
+ catch {
167
+ // Swallow — fallback is a passthrough, never crashes.
168
+ }
169
+ })();
170
+ return primePromise;
171
+ }
172
+ // Kick off the import at module load. Best effort — if it fails (e.g.
173
+ // missing dep, broken install) we degrade to passthrough.
174
+ primeFrameAsync();
175
+ /**
176
+ * ANSI-aware soft wrap. Defaults `{ trim: false, hard: true }`:
177
+ * - trim: false → preserves leading/trailing whitespace on each
178
+ * visual line (important for code-block indent + alignment).
179
+ * - hard: true → break extremely long words mid-character at width
180
+ * instead of overflowing. Code blocks especially need this.
181
+ *
182
+ * Pure with respect to ANSI: escape sequences pass through wrap-ansi
183
+ * untouched and don't count toward the column budget.
184
+ *
185
+ * Synchronous. When wrap-ansi hasn't finished loading yet (the rare
186
+ * boot-race window), returns `text` unchanged. The user sees the
187
+ * un-wrapped paint exactly once; by the next render the cache is hot.
188
+ */
189
+ function wrap(text, cols, options = {}) {
190
+ const w = cachedWrap;
191
+ if (!w)
192
+ return text;
193
+ const opts = { trim: options.trim ?? false, hard: options.hard ?? true };
194
+ try {
195
+ return w(text, cols, opts);
196
+ }
197
+ catch {
198
+ return text;
199
+ }
200
+ }
201
+ /**
202
+ * Indent every line of `body` by the bare gutter. Empty lines are
203
+ * passed through unindented so blank visual rows don't carry
204
+ * trailing whitespace into the transcript.
205
+ *
206
+ * Caller is responsible for pre-wrapping to `getBodyWidth()` — this
207
+ * function is purely the indent step, not the wrap step. Keeping them
208
+ * separate so callers that already have their own indent (lists,
209
+ * code blocks) can opt out of this and still consume `wrap()`.
210
+ */
211
+ function applyFrame(body) {
212
+ const ind = getIndent(0);
213
+ return body
214
+ .split('\n')
215
+ .map((ln) => (ln.length === 0 ? '' : `${ind}${ln}`))
216
+ .join('\n');
217
+ }
218
+ /**
219
+ * Test reset — drops the cached wrap function so a fresh prime can be
220
+ * forced. Used by unit tests to exercise the fallback path AND to
221
+ * confirm post-prime behaviour.
222
+ */
223
+ function _resetForTests() {
224
+ cachedWrap = null;
225
+ primePromise = null;
226
+ }
227
+ /**
228
+ * Test injection — set the wrap function explicitly. Used by tests
229
+ * that want deterministic behaviour without depending on dynamic
230
+ * `import()` resolution timing.
231
+ */
232
+ function _injectWrapForTests(fn) {
233
+ cachedWrap = fn;
234
+ }