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.
- package/dist/cli/v4/aidenCLI.js +10 -0
- package/dist/cli/v4/callbacks.js +85 -13
- package/dist/cli/v4/chatSession.js +250 -24
- package/dist/cli/v4/commands/doctor.js +23 -27
- package/dist/cli/v4/commands/model.js +30 -1
- package/dist/cli/v4/defaultSoul.js +69 -2
- package/dist/cli/v4/display/capabilityCard.js +135 -0
- package/dist/cli/v4/display/frame.js +234 -0
- package/dist/cli/v4/display/progressBar.js +137 -0
- package/dist/cli/v4/display/sessionEndCard.js +127 -0
- package/dist/cli/v4/display/toolTrail.js +172 -0
- package/dist/cli/v4/display.js +891 -153
- package/dist/cli/v4/doctor.js +377 -75
- package/dist/cli/v4/promotionPrompt.js +135 -5
- package/dist/cli/v4/replyRenderer.js +487 -26
- package/dist/cli/v4/skinEngine.js +26 -4
- package/dist/cli/v4/toolPreview.js +82 -19
- package/dist/core/tools/nowPlaying.js +7 -15
- package/dist/core/v4/aidenAgent.js +9 -0
- package/dist/core/v4/promptBuilder.js +2 -1
- package/dist/core/v4/sessionDistiller.js +48 -1
- package/dist/core/v4/toolRegistry.js +16 -1
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +19 -0
- package/dist/providers/v4/anthropicAdapter.js +25 -2
- package/dist/providers/v4/errors.js +92 -0
- package/dist/tools/v4/index.js +24 -1
- package/dist/tools/v4/sessions/recallSession.js +14 -0
- package/dist/tools/v4/system/_psHelpers.js +70 -2
- package/dist/tools/v4/system/appInput.js +154 -0
- package/dist/tools/v4/system/appLaunch.js +136 -10
- package/dist/tools/v4/system/mediaKey.js +35 -4
- package/dist/tools/v4/system/mediaSessions.js +163 -0
- package/dist/tools/v4/system/mediaTransport.js +211 -0
- package/package.json +2 -1
- package/skills/system_control.md +56 -6
package/dist/cli/v4/display.js
CHANGED
|
@@ -18,13 +18,17 @@
|
|
|
18
18
|
*
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.Display = exports.SPINNER_PHRASES = exports.TOOL_ICONS = void 0;
|
|
21
|
+
exports.TOOL_ROW_ARG_CAP = exports.TOOL_ROW_NAME_PAD = exports.Display = exports.SPINNER_PHRASES = exports.TOOL_ICONS = void 0;
|
|
22
22
|
exports.iconForTool = iconForTool;
|
|
23
23
|
exports.detectConfiguredChannels = detectConfiguredChannels;
|
|
24
24
|
exports.summarizeConfiguredChannels = summarizeConfiguredChannels;
|
|
25
25
|
exports.summarizeChannelState = summarizeChannelState;
|
|
26
26
|
exports.voiceIndicator = voiceIndicator;
|
|
27
27
|
exports.previewToolArgs = previewToolArgs;
|
|
28
|
+
exports.verbForActivity = verbForActivity;
|
|
29
|
+
exports.isPreFramedLine = isPreFramedLine;
|
|
30
|
+
exports.countNewlines = countNewlines;
|
|
31
|
+
exports.splitAtUnclosedBold = splitAtUnclosedBold;
|
|
28
32
|
exports.formatToolDuration = formatToolDuration;
|
|
29
33
|
exports.formatCompactTokens = formatCompactTokens;
|
|
30
34
|
exports.formatElapsedShort = formatElapsedShort;
|
|
@@ -36,6 +40,9 @@ const marked_1 = require("marked");
|
|
|
36
40
|
const TerminalRenderer = require('marked-terminal').default ?? require('marked-terminal');
|
|
37
41
|
const skinEngine_1 = require("./skinEngine");
|
|
38
42
|
const box_1 = require("./box");
|
|
43
|
+
const toolTrail_1 = require("./display/toolTrail");
|
|
44
|
+
// v4.1.3-essentials — capability card renderer (auth/platform failures).
|
|
45
|
+
const capabilityCard_1 = require("./display/capabilityCard");
|
|
39
46
|
// Phase v4.1-reply-formatting: skin-aware markdown renderer that
|
|
40
47
|
// replaces marked-terminal's defaults with structured headers, lists,
|
|
41
48
|
// code blocks, blockquotes, and links.
|
|
@@ -43,71 +50,63 @@ const replyRenderer_1 = require("./replyRenderer");
|
|
|
43
50
|
// Optional "Sources" footer when AIDEN_CITATIONS=1 (default off).
|
|
44
51
|
const citationFooter_1 = require("./citationFooter");
|
|
45
52
|
const toolPreview_1 = require("./toolPreview");
|
|
53
|
+
// v4.1.4 reply-quality polish: shared frame math for width + indent.
|
|
54
|
+
// `cols()`, `rule()`, `agentTurn`, and `tryRerenderInPlace` all route
|
|
55
|
+
// through frame helpers so the visible left edge / right margin / wrap
|
|
56
|
+
// targets are consistent across streaming, rerender, and one-shot
|
|
57
|
+
// reply paths. See `cli/v4/display/frame.ts` for the math.
|
|
58
|
+
const frame_1 = require("./display/frame");
|
|
46
59
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* across Windows Terminal / older Console hosts / SSH sessions.
|
|
60
|
+
* v4.1.3-repl-polish — category emoji icons for the tool-row trail.
|
|
61
|
+
* Icons are ON by default (AIDEN_UI_ICONS !== '0'). Set
|
|
62
|
+
* `AIDEN_UI_ICONS=0` to disable them (CI / dumb terminals).
|
|
51
63
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
64
|
+
* Kept as a flat Record for backward-compat with smoke tests that
|
|
65
|
+
* import it directly. The canonical lookup now lives in
|
|
66
|
+
* `./display/toolTrail` (supports verb too); this map is derived
|
|
67
|
+
* from it for legacy reference.
|
|
56
68
|
*
|
|
57
|
-
*
|
|
69
|
+
* Matching order: exact lowercased name, then substring, then 'default'.
|
|
58
70
|
*/
|
|
59
71
|
exports.TOOL_ICONS = {
|
|
60
|
-
// Observe / read
|
|
61
|
-
|
|
62
|
-
read: '👁',
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
skills_list: '📋',
|
|
70
|
-
// Execute / write / run
|
|
71
|
-
execute: '⚡',
|
|
72
|
-
run: '⚡',
|
|
73
|
-
bash: '⚡',
|
|
74
|
-
powershell: '⚡',
|
|
75
|
-
code: '⚡',
|
|
76
|
-
skill_view: '⚡',
|
|
77
|
-
write: '✏',
|
|
78
|
-
edit: '✏',
|
|
72
|
+
// Observe / read
|
|
73
|
+
file_read: '👁', read_file: '👁', file_list: '👁', list_directory: '👁',
|
|
74
|
+
observe: '👁', read: '👁', list: '👁',
|
|
75
|
+
// Write / edit
|
|
76
|
+
file_write: '✏', write_file: '✏', edit_file: '✏',
|
|
77
|
+
write: '✏', edit: '✏', create: '✏',
|
|
78
|
+
// Execute / run
|
|
79
|
+
bash: '⚡', powershell: '⚡', execute_code: '⚡', skill_view: '⚡',
|
|
80
|
+
execute: '⚡', run: '⚡',
|
|
79
81
|
// Web / browse
|
|
80
|
-
web_search: '🌐',
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
web_search: '🌐', web_fetch: '🌐', fetch_url: '🌐', open_url: '🌐',
|
|
83
|
+
navigate: '🌐', browser: '🌐', fetch: '🌐', search: '🌐',
|
|
84
|
+
// Memory / recall
|
|
85
|
+
recall_session: '🧠', session_search: '🧠', memory: '🧠', recall: '🧠',
|
|
86
|
+
// Think
|
|
87
|
+
session_summary: '🧠', analyze: '🧠', think: '🧠',
|
|
88
|
+
// Skills / catalog
|
|
89
|
+
skills_list: '📋', skill: '📋',
|
|
90
|
+
// Screen / capture
|
|
91
|
+
screenshot: '🖥', computer: '🖥',
|
|
92
|
+
// Media / launch
|
|
93
|
+
now_playing: '▶', app_launch: '▶', media: '▶',
|
|
94
|
+
// Deploy / build
|
|
95
|
+
deploy: '📦', build: '📦', push: '📦',
|
|
96
|
+
// Message / send
|
|
97
|
+
send: '💬', message: '💬', notify: '💬',
|
|
87
98
|
// Verify / test
|
|
88
|
-
verify: '🛡',
|
|
89
|
-
|
|
90
|
-
// Default fallback (matches current behaviour).
|
|
99
|
+
verify: '🛡', test: '🛡', doctor: '🛡', health: '🛡',
|
|
100
|
+
// Default fallback
|
|
91
101
|
default: '·',
|
|
92
102
|
};
|
|
93
103
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* insertion order. Pure — exported for smoke testing.
|
|
104
|
+
* Return the category emoji for `toolName`, or '·' when nothing matches.
|
|
105
|
+
* Delegates to the canonical toolTrail lookup (returns icon only).
|
|
106
|
+
* Exported for backward-compat with existing smoke / unit tests.
|
|
98
107
|
*/
|
|
99
108
|
function iconForTool(toolName) {
|
|
100
|
-
|
|
101
|
-
const exact = exports.TOOL_ICONS[lc];
|
|
102
|
-
if (exact)
|
|
103
|
-
return exact;
|
|
104
|
-
for (const [key, glyph] of Object.entries(exports.TOOL_ICONS)) {
|
|
105
|
-
if (key === 'default')
|
|
106
|
-
continue;
|
|
107
|
-
if (lc.includes(key))
|
|
108
|
-
return glyph;
|
|
109
|
-
}
|
|
110
|
-
return exports.TOOL_ICONS.default;
|
|
109
|
+
return (0, toolTrail_1.iconForTool)(toolName).icon;
|
|
111
110
|
}
|
|
112
111
|
/**
|
|
113
112
|
* Phase 26.2.6 — pool of fun spinner phrases that the chat REPL
|
|
@@ -241,6 +240,15 @@ class Display {
|
|
|
241
240
|
// marked-terminal optional — markdown() falls back to raw text below
|
|
242
241
|
}
|
|
243
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* v4.1.3-repl-polish — public colour gate so callers (e.g. session-end
|
|
245
|
+
* card renderer) can colour text without importing SkinEngine directly.
|
|
246
|
+
* Delegates to the active skin's applyColors(). Monochrome mode is
|
|
247
|
+
* respected the same way as internal calls.
|
|
248
|
+
*/
|
|
249
|
+
applyColors(text, kind) {
|
|
250
|
+
return this.skin.applyColors(text, kind);
|
|
251
|
+
}
|
|
244
252
|
/**
|
|
245
253
|
* Build the welcome banner string (does not write).
|
|
246
254
|
*
|
|
@@ -263,18 +271,25 @@ class Display {
|
|
|
263
271
|
// ── Phase 23.6 — v3 visual primitives ──────────────────────────────────
|
|
264
272
|
// Pure renderers (return strings, don't write) so chatSession can
|
|
265
273
|
// compose the boot card and turn rhythm without owning ANSI escapes.
|
|
266
|
-
/**
|
|
274
|
+
/**
|
|
275
|
+
* Terminal column count. v4.1.4 reply-quality polish: delegates to
|
|
276
|
+
* `frame.getTerminalCols()` so all width math shares one formula.
|
|
277
|
+
* Retains the 100-col cap via `Math.min` so existing callers that
|
|
278
|
+
* paint full-width chrome (boot card, footer) keep their visual
|
|
279
|
+
* identity — `frame.BODY_WIDTH_MAX` is the tunable.
|
|
280
|
+
*/
|
|
267
281
|
cols() {
|
|
268
|
-
return Math.min(this.out
|
|
282
|
+
return Math.min((0, frame_1.getTerminalCols)(this.out), 100);
|
|
269
283
|
}
|
|
270
284
|
/**
|
|
271
|
-
* Thin horizontal rule (`──…──`) in muted colour, full
|
|
272
|
-
*
|
|
273
|
-
* the
|
|
274
|
-
*
|
|
285
|
+
* Thin horizontal rule (`──…──`) in muted colour, full body width.
|
|
286
|
+
* v4.1.4 reply-quality polish: width sourced from `frame.getBodyWidth()`
|
|
287
|
+
* so the rule sits at the same right margin as wrapped prose and
|
|
288
|
+
* code blocks. Returns the line WITHOUT a trailing newline; caller
|
|
289
|
+
* adds one + the leading gutter.
|
|
275
290
|
*/
|
|
276
291
|
rule(width) {
|
|
277
|
-
const w = Math.max(8,
|
|
292
|
+
const w = Math.max(8, width ?? (0, frame_1.getBodyWidth)(this.out));
|
|
278
293
|
return this.skin.applyColors('─'.repeat(w), 'muted');
|
|
279
294
|
}
|
|
280
295
|
/** Render `▲` (brand-orange filled triangle) — Aiden's identity motif. */
|
|
@@ -725,6 +740,148 @@ class Display {
|
|
|
725
740
|
},
|
|
726
741
|
};
|
|
727
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* v4.1.4 reply-quality polish — Part 1.6 activity indicator.
|
|
745
|
+
*
|
|
746
|
+
* Renders `▲ {verb}{dots} (Ns) ▸▸ Ctrl+C cancel` on a single
|
|
747
|
+
* line. `verb` is the activity label; the dots pulse 0→1→2→3→0
|
|
748
|
+
* every 400ms; elapsed time `(Ns)` appears only once N >= 1 (avoids
|
|
749
|
+
* the `(0s)` flash). The "▸▸ Ctrl+C cancel" hint is folded into the
|
|
750
|
+
* same line so cursor management stays simple (single-line write,
|
|
751
|
+
* single-line erase).
|
|
752
|
+
*
|
|
753
|
+
* Pause/resume semantics:
|
|
754
|
+
* - `pause()` erases the line + stops the tick + sets paused=true.
|
|
755
|
+
* Elapsed time keeps accumulating wall-clock — when a later
|
|
756
|
+
* `resume()` re-renders, the indicator shows the TOTAL elapsed
|
|
757
|
+
* since the original `activityIndicator()` call, not just since
|
|
758
|
+
* the last resume.
|
|
759
|
+
* - `resume(verb?)` re-renders on a fresh line below the current
|
|
760
|
+
* cursor and restarts the tick. Optional `verb` swap is the
|
|
761
|
+
* supported way to transition phases ("thinking" → "drafting").
|
|
762
|
+
* - `stop()` is terminal — erases the line, marks stopped, refuses
|
|
763
|
+
* further pause/resume.
|
|
764
|
+
*
|
|
765
|
+
* Non-TTY: completely silent. No initial paint, no ticks, no erases.
|
|
766
|
+
* Pipes / CI / MCP serve mode get clean output by default.
|
|
767
|
+
*
|
|
768
|
+
* Cursor invariant on render: the indicator OWNS one line. After
|
|
769
|
+
* each render the cursor sits at column 0 of the indicator line
|
|
770
|
+
* (NOT a new line below it) — that way the next render erases the
|
|
771
|
+
* line and rewrites in place. Callers that want to write OTHER
|
|
772
|
+
* content below MUST call `pause()` first; otherwise their content
|
|
773
|
+
* lands on the indicator line and the next tick clobbers it.
|
|
774
|
+
*/
|
|
775
|
+
activityIndicator(initialVerb = 'thinking') {
|
|
776
|
+
const sk = this.skin;
|
|
777
|
+
const out = this.out;
|
|
778
|
+
const isTty = !!out.isTTY;
|
|
779
|
+
const startTime = Date.now();
|
|
780
|
+
let verb = initialVerb;
|
|
781
|
+
let dotFrame = 0;
|
|
782
|
+
let paused = !isTty; // non-TTY = effectively pre-paused (silent)
|
|
783
|
+
let stopped = false;
|
|
784
|
+
let printed = false;
|
|
785
|
+
let tickTimer = null;
|
|
786
|
+
// Tunable cadence. v4.1.4 Phase 3b' (Issue G): bumped from 400ms
|
|
787
|
+
// to 250ms after visual smoke — 400ms felt sluggish, made the
|
|
788
|
+
// indicator look static between seconds. 250ms gives ~4 dot
|
|
789
|
+
// updates per second so motion is always visible even when the
|
|
790
|
+
// (Ns) counter hasn't ticked. Slow enough not to flicker on SSH
|
|
791
|
+
// / slow ConPTY refresh.
|
|
792
|
+
const TICK_MS = 250;
|
|
793
|
+
// ▲ glyph in brand orange — the user's primary motif. Dots and
|
|
794
|
+
// elapsed counter paint muted to keep visual weight on the verb.
|
|
795
|
+
//
|
|
796
|
+
// v4.1.4 Phase 3b' (Issue F): the inline "▸▸ Ctrl+C cancel" hint
|
|
797
|
+
// shipped with Phase 3a was visually noisy on the activity line
|
|
798
|
+
// and collided with planner-debug dim writes. Dropped per user
|
|
799
|
+
// feedback; a separate bottom-of-screen footer can be added in
|
|
800
|
+
// v4.1.5 if wanted, but it must NOT be glued to the indicator.
|
|
801
|
+
const glyph = sk.applyColors('▲', 'brand');
|
|
802
|
+
const buildLine = () => {
|
|
803
|
+
const dots = '.'.repeat(dotFrame); // 0..3 dots
|
|
804
|
+
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
|
805
|
+
const elapsedStr = elapsedSec >= 1
|
|
806
|
+
? ` ${sk.applyColors(`(${elapsedSec}s)`, 'muted')}`
|
|
807
|
+
: '';
|
|
808
|
+
// `▲ {verb}{dots-padded-to-3}{elapsed?}`
|
|
809
|
+
return `${glyph} ${verb}${dots.padEnd(3, ' ')}${elapsedStr}`;
|
|
810
|
+
};
|
|
811
|
+
const renderTick = () => {
|
|
812
|
+
if (stopped || paused || !isTty)
|
|
813
|
+
return;
|
|
814
|
+
dotFrame = (dotFrame + 1) % 4;
|
|
815
|
+
// `\r\x1b[K` — carriage return + clear line — then write the
|
|
816
|
+
// fresh indicator. No newline at end: cursor stays at end of
|
|
817
|
+
// the indicator line, ready for the next overwrite.
|
|
818
|
+
out.write(`\r\x1b[K${buildLine()}`);
|
|
819
|
+
};
|
|
820
|
+
const startTick = () => {
|
|
821
|
+
if (stopped || !isTty || tickTimer !== null)
|
|
822
|
+
return;
|
|
823
|
+
tickTimer = setInterval(renderTick, TICK_MS);
|
|
824
|
+
};
|
|
825
|
+
const stopTick = () => {
|
|
826
|
+
if (tickTimer !== null) {
|
|
827
|
+
clearInterval(tickTimer);
|
|
828
|
+
tickTimer = null;
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
const eraseLine = () => {
|
|
832
|
+
if (isTty && printed)
|
|
833
|
+
out.write('\r\x1b[K');
|
|
834
|
+
};
|
|
835
|
+
// Initial paint — only on TTY.
|
|
836
|
+
if (isTty) {
|
|
837
|
+
out.write(buildLine());
|
|
838
|
+
printed = true;
|
|
839
|
+
startTick();
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
pause: () => {
|
|
843
|
+
if (stopped || paused)
|
|
844
|
+
return;
|
|
845
|
+
paused = true;
|
|
846
|
+
stopTick();
|
|
847
|
+
eraseLine();
|
|
848
|
+
// After erase the cursor is at column 0 of the indicator's
|
|
849
|
+
// (now empty) line. Caller is expected to write its own
|
|
850
|
+
// content next; that content lands cleanly on this line.
|
|
851
|
+
},
|
|
852
|
+
resume: (newVerb) => {
|
|
853
|
+
if (stopped)
|
|
854
|
+
return;
|
|
855
|
+
if (typeof newVerb === 'string' && newVerb.length > 0)
|
|
856
|
+
verb = newVerb;
|
|
857
|
+
if (!paused)
|
|
858
|
+
return;
|
|
859
|
+
paused = false;
|
|
860
|
+
if (!isTty)
|
|
861
|
+
return;
|
|
862
|
+
// Caller has just finished writing its own content (typically
|
|
863
|
+
// ending with `\n`), so the cursor is on a fresh line below
|
|
864
|
+
// whatever was there. Render the indicator there and arm the
|
|
865
|
+
// tick again.
|
|
866
|
+
out.write(buildLine());
|
|
867
|
+
printed = true;
|
|
868
|
+
startTick();
|
|
869
|
+
},
|
|
870
|
+
setVerb: (newVerb) => {
|
|
871
|
+
if (typeof newVerb === 'string' && newVerb.length > 0)
|
|
872
|
+
verb = newVerb;
|
|
873
|
+
},
|
|
874
|
+
stop: () => {
|
|
875
|
+
if (stopped)
|
|
876
|
+
return;
|
|
877
|
+
stopped = true;
|
|
878
|
+
stopTick();
|
|
879
|
+
eraseLine();
|
|
880
|
+
},
|
|
881
|
+
isPaused: () => paused,
|
|
882
|
+
isStopped: () => stopped,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
728
885
|
// ── Phase 23.5 — tool event row ───────────────────────────────────────
|
|
729
886
|
// One line per tool call: a "·" gutter, the keyword `tool`, the
|
|
730
887
|
// tool name (soft cyan, padded), a brief truncated arg preview, and
|
|
@@ -738,55 +895,133 @@ class Display {
|
|
|
738
895
|
// ANSI cursor games on a dumb sink.
|
|
739
896
|
toolRow(name, args) {
|
|
740
897
|
const sk = this.skin;
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
898
|
+
// ── Build the fixed left portion (icon + verb + detail) ────────────
|
|
899
|
+
// v4.1.3-repl-polish: icons default ON; set AIDEN_UI_ICONS=0 to
|
|
900
|
+
// disable (CI / dumb terminals / narrow SSH sessions).
|
|
901
|
+
// Read at call-time so env changes take effect without restart.
|
|
902
|
+
const useIcons = process.env.AIDEN_UI_ICONS !== '0';
|
|
903
|
+
const { icon, verb } = (0, toolTrail_1.iconForTool)(name);
|
|
904
|
+
const glyph = useIcons ? icon : sk.applyColors('·', 'muted');
|
|
905
|
+
// Detail field: v4.1.4-media — consult `buildToolPreview` first so
|
|
906
|
+
// tools registered in `TOOL_PRIMARY_ARG` (media_transport → 'target',
|
|
907
|
+
// media_key → 'action', file_read → 'path', etc.) get their
|
|
908
|
+
// meaningful primary-arg preview instead of a JSON blob. Fall back
|
|
909
|
+
// to the generic `previewToolArgs` scan for tools the map doesn't
|
|
910
|
+
// know about so unregistered MCP tools etc. still render readably.
|
|
911
|
+
const mapped = (0, toolPreview_1.buildToolPreview)(name, args);
|
|
912
|
+
const detail = (0, toolTrail_1.truncDetail)(mapped ?? previewToolArgs(args));
|
|
913
|
+
// v4.1.3-essentials: live tool indicator. Capture wall-clock start
|
|
914
|
+
// so the running-row renderer can append an elapsed-time suffix
|
|
915
|
+
// ("running 3s…") after the first second. Sub-second tools render
|
|
916
|
+
// without the suffix — no flash of `running 0s` for fast paths.
|
|
917
|
+
const startedAt = Date.now();
|
|
918
|
+
// Running row — muted pipe, raw icon, tool-colored verb, muted detail.
|
|
919
|
+
// The optional `running Ns…` tail appears once the tool crosses the
|
|
920
|
+
// 1-second mark; the tick interval below redraws this row every 1s.
|
|
921
|
+
const runningRow = () => {
|
|
922
|
+
const elapsed = Date.now() - startedAt;
|
|
923
|
+
const liveSuffix = elapsed >= 1000
|
|
924
|
+
? ` ${sk.applyColors(`running ${formatToolDuration(elapsed)}…`, 'muted')}`
|
|
925
|
+
: '';
|
|
926
|
+
return `${sk.applyColors(toolTrail_1.TRAIL_PIPE, 'muted')} ${glyph} ` +
|
|
927
|
+
`${sk.applyColors((0, toolTrail_1.padVerb)(verb), 'tool')} ` +
|
|
928
|
+
`${sk.applyColors(detail, 'muted')}${liveSuffix}\n`;
|
|
759
929
|
};
|
|
760
|
-
|
|
930
|
+
// Outcome row — entire line colored by outcome kind.
|
|
931
|
+
const outcomeRow = (suffix, kind) => {
|
|
932
|
+
const content = `${toolTrail_1.TRAIL_PIPE} ${glyph} ${(0, toolTrail_1.padVerb)(verb)} ${detail}` +
|
|
933
|
+
(suffix ? ` ${suffix}` : '');
|
|
934
|
+
return `${sk.applyColors(content, kind)}\n`;
|
|
935
|
+
};
|
|
936
|
+
// Capture stream reference so closures don't need `this`.
|
|
937
|
+
const out = this.out;
|
|
938
|
+
const isTty = !!out.isTTY;
|
|
761
939
|
let printed = false;
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
940
|
+
// v4.1.3-essentials: tick handle for the live-elapsed update. Set
|
|
941
|
+
// when we start the interval; cleared by every terminal method
|
|
942
|
+
// (ok / fail / degraded / blocked / emptyFail / emptyRetry) AND by
|
|
943
|
+
// `retry` (retry is a state announcement and should hold static
|
|
944
|
+
// until the next state change — race-free).
|
|
945
|
+
let tickTimer = null;
|
|
946
|
+
const stopTick = () => {
|
|
947
|
+
if (tickTimer !== null) {
|
|
948
|
+
clearInterval(tickTimer);
|
|
949
|
+
tickTimer = null;
|
|
766
950
|
}
|
|
767
|
-
|
|
951
|
+
};
|
|
952
|
+
// Erase the last printed line (TTY only).
|
|
953
|
+
const eraseLast = () => {
|
|
954
|
+
if (isTty && printed)
|
|
955
|
+
out.write('\x1b[1A\x1b[2K\r');
|
|
956
|
+
};
|
|
957
|
+
const writeFinal = (suffix, kind) => {
|
|
958
|
+
stopTick();
|
|
959
|
+
eraseLast();
|
|
960
|
+
out.write(outcomeRow(suffix, kind));
|
|
768
961
|
printed = true;
|
|
769
962
|
};
|
|
770
963
|
if (isTty) {
|
|
771
|
-
|
|
964
|
+
// v4.1.3-essentials (replaces v4.1.3-repl-polish streamInterrupted
|
|
965
|
+
// flag pattern): if a stream is active, fence off the current
|
|
966
|
+
// chunk BEFORE the running row writes. `commitStreamChunk` does
|
|
967
|
+
// its own newline-fencing + in-place rerender of the just-streamed
|
|
968
|
+
// chunk so this row lands cleanly on its own line below.
|
|
969
|
+
this.commitStreamChunk();
|
|
970
|
+
out.write(runningRow());
|
|
772
971
|
printed = true;
|
|
972
|
+
// v4.1.3-essentials: start the live-elapsed ticker. Fires every
|
|
973
|
+
// 1s; first tick at +1s, when `runningRow()` starts emitting the
|
|
974
|
+
// `running 1s…` suffix. Cleared by every terminal method via
|
|
975
|
+
// `stopTick()` — no leaked timers across the tool lifecycle.
|
|
976
|
+
// Tool dispatch in aidenAgent is sequential (one tool at a time
|
|
977
|
+
// per turn) so the assumption "running row is the last written
|
|
978
|
+
// line" holds for the whole tick lifetime; `eraseLast()` is safe.
|
|
979
|
+
tickTimer = setInterval(() => {
|
|
980
|
+
if (!printed)
|
|
981
|
+
return;
|
|
982
|
+
eraseLast();
|
|
983
|
+
out.write(runningRow());
|
|
984
|
+
}, 1000);
|
|
773
985
|
}
|
|
774
|
-
//
|
|
986
|
+
// Non-TTY: hold off until completion (log lines carry final state).
|
|
987
|
+
// No tick — non-TTY sinks (pipes, CI logs) get one line per call
|
|
988
|
+
// with the final state; live updates would be noise in scrollback.
|
|
775
989
|
return {
|
|
776
990
|
ok(durationMs, retries = 0) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
991
|
+
stopTick();
|
|
992
|
+
if (retries > 0) {
|
|
993
|
+
// Showed retries — surface the eventual success in warn so the
|
|
994
|
+
// user knows it took multiple attempts.
|
|
995
|
+
writeFinal(`ok ${formatToolDuration(durationMs)} after ${retries} ${retries === 1 ? 'retry' : 'retries'}`, 'warn');
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
// Clean success — SILENT. Erase on TTY; emit nothing on non-TTY.
|
|
999
|
+
eraseLast();
|
|
1000
|
+
}
|
|
781
1001
|
},
|
|
782
1002
|
fail(durationMs, retries = 0) {
|
|
783
|
-
const
|
|
1003
|
+
const suffix = retries > 0
|
|
784
1004
|
? `fail ${formatToolDuration(durationMs)} after ${retries} ${retries === 1 ? 'retry' : 'retries'}`
|
|
785
1005
|
: `fail ${formatToolDuration(durationMs)}`;
|
|
786
|
-
writeFinal(
|
|
1006
|
+
writeFinal(suffix, 'error');
|
|
1007
|
+
},
|
|
1008
|
+
degraded(durationMs, reason) {
|
|
1009
|
+
const suffix = reason
|
|
1010
|
+
? `partial ${formatToolDuration(durationMs)} — ${reason}`
|
|
1011
|
+
: `partial ${formatToolDuration(durationMs)}`;
|
|
1012
|
+
writeFinal(suffix, 'degraded');
|
|
787
1013
|
},
|
|
788
1014
|
retry(n, m) {
|
|
789
|
-
|
|
1015
|
+
// v4.1.3-essentials: retry is a state-change announcement —
|
|
1016
|
+
// freeze the row at the retry counter until next state change.
|
|
1017
|
+
// Stopping the ticker prevents the next 1s tick from racing
|
|
1018
|
+
// back over the retry counter with `running Ns…`.
|
|
1019
|
+
stopTick();
|
|
1020
|
+
// Update the running row with retry count.
|
|
1021
|
+
eraseLast();
|
|
1022
|
+
const content = `${toolTrail_1.TRAIL_PIPE} ${glyph} ${(0, toolTrail_1.padVerb)(verb)} ${detail} retry ${n}/${m} …`;
|
|
1023
|
+
out.write(sk.applyColors(content, 'warn') + '\n');
|
|
1024
|
+
printed = true;
|
|
790
1025
|
},
|
|
791
1026
|
blocked() {
|
|
792
1027
|
writeFinal('blocked', 'warn');
|
|
@@ -870,15 +1105,57 @@ class Display {
|
|
|
870
1105
|
const sk = this.skin;
|
|
871
1106
|
const useMd = opts.markdown !== false;
|
|
872
1107
|
const rawBody = useMd ? this.markdown(text).trimEnd() : text;
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1108
|
+
// v4.1.4 reply-quality polish — F1 detect-and-skip indent + wrap.
|
|
1109
|
+
//
|
|
1110
|
+
// Walks the rendered markdown line-by-line, but applies frame
|
|
1111
|
+
// indent/wrap ONLY to plain prose lines. Lines that already carry
|
|
1112
|
+
// structural chrome (code-block rail+bg, blockquote rail,
|
|
1113
|
+
// pre-indented list bullets) pass through untouched — `renderCode-
|
|
1114
|
+
// Block`, `renderBlockquote`, and the list override already own
|
|
1115
|
+
// their own gutter + per-line wrap. Double-applying the gutter
|
|
1116
|
+
// shifts content right by 3 cols; double-wrapping breaks the rail
|
|
1117
|
+
// off the wrap-continuation row. See `isPreFramedLine` for the
|
|
1118
|
+
// detection rules.
|
|
1119
|
+
const indented = this.applyFrameToRendered(rawBody);
|
|
877
1120
|
const reasoning = opts.reasoning
|
|
878
|
-
?
|
|
1121
|
+
? `${(0, frame_1.getIndent)(0)}${sk.applyColors(opts.reasoning.trim(), 'muted')}\n`
|
|
879
1122
|
: '';
|
|
880
1123
|
return `${this.agentHeader()}${reasoning}${indented}\n`;
|
|
881
1124
|
}
|
|
1125
|
+
/**
|
|
1126
|
+
* v4.1.4 reply-quality polish — F1 shared helper.
|
|
1127
|
+
*
|
|
1128
|
+
* Apply frame indent + soft-wrap to the prose lines of a rendered
|
|
1129
|
+
* markdown body, but pass structural lines (code-block rail+bg,
|
|
1130
|
+
* blockquote rail, pre-indented list bullets) through unchanged.
|
|
1131
|
+
*
|
|
1132
|
+
* Shared by `agentTurn` (one-shot reply) and `tryRerenderInPlace`
|
|
1133
|
+
* (post-stream rerender) so both paths produce identical output.
|
|
1134
|
+
*/
|
|
1135
|
+
applyFrameToRendered(rawBody) {
|
|
1136
|
+
const indent = (0, frame_1.getIndent)(0);
|
|
1137
|
+
const bw = (0, frame_1.getBodyWidth)(this.out);
|
|
1138
|
+
return rawBody
|
|
1139
|
+
.split('\n')
|
|
1140
|
+
.map((ln) => {
|
|
1141
|
+
if (ln.length === 0)
|
|
1142
|
+
return '';
|
|
1143
|
+
// F1 detect-and-skip: pre-framed lines (code-block chrome, list
|
|
1144
|
+
// bullets, blockquote rails) own their own gutter + wrap. Don't
|
|
1145
|
+
// re-indent or re-wrap them — that double-applies the gutter
|
|
1146
|
+
// and breaks the rail off wrap-continuation rows.
|
|
1147
|
+
if (isPreFramedLine(ln))
|
|
1148
|
+
return ln;
|
|
1149
|
+
// Plain prose: indent + wrap to bodyWidth. wrap-ansi handles
|
|
1150
|
+
// ANSI-aware width counting so bold/heading paint survives.
|
|
1151
|
+
const wrapped = (0, frame_1.wrap)(ln, bw, { trim: false, hard: true });
|
|
1152
|
+
return wrapped
|
|
1153
|
+
.split('\n')
|
|
1154
|
+
.map((vln) => `${indent}${vln}`)
|
|
1155
|
+
.join('\n');
|
|
1156
|
+
})
|
|
1157
|
+
.join('\n');
|
|
1158
|
+
}
|
|
882
1159
|
/**
|
|
883
1160
|
* Format a recoverable error with optional remediation suggestion.
|
|
884
1161
|
* Output goes through the caller (returned as string), not stderr.
|
|
@@ -973,6 +1250,57 @@ class Display {
|
|
|
973
1250
|
printError(message, suggestion) {
|
|
974
1251
|
this.out.write(this.error(message, suggestion));
|
|
975
1252
|
}
|
|
1253
|
+
/**
|
|
1254
|
+
* v4.1.3-essentials: render a structured capability card. Used when
|
|
1255
|
+
* a tool fails because a capability is missing (platform unsupported,
|
|
1256
|
+
* auth not present, key env-var unset) and a generic error wouldn't
|
|
1257
|
+
* give the user enough signal. The card lists what the user CAN
|
|
1258
|
+
* still do, what's blocked, and a one-line fix.
|
|
1259
|
+
*
|
|
1260
|
+
* Delegates the layout to the pure renderer in `./display/capability-
|
|
1261
|
+
* Card.ts`. This method is the I/O boundary — caller passes data, we
|
|
1262
|
+
* write lines to the configured stdout. Trailing newline ensures the
|
|
1263
|
+
* card sits clean above whatever renders next (typically the prompt).
|
|
1264
|
+
*/
|
|
1265
|
+
capabilityCard(data) {
|
|
1266
|
+
// v4.1.3-essentials: fence off any active stream chunk before the
|
|
1267
|
+
// card writes so the chunk gets rerendered as markdown and the
|
|
1268
|
+
// card lands below it on its own line. Same pattern as toolRow /
|
|
1269
|
+
// streamToolIndicator.
|
|
1270
|
+
this.commitStreamChunk();
|
|
1271
|
+
const lines = (0, capabilityCard_1.renderCapabilityCard)(data, (t, k) => this.applyColors(t, k));
|
|
1272
|
+
for (const line of lines) {
|
|
1273
|
+
this.out.write(line + '\n');
|
|
1274
|
+
}
|
|
1275
|
+
this.out.write('\n');
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* v4.1.4 reply-quality polish (Q-ResizeReflow Option B): zero the
|
|
1279
|
+
* per-chunk row counter when the terminal resizes mid-stream.
|
|
1280
|
+
*
|
|
1281
|
+
* Why: the resize guard hard-clears the viewport (`\x1b[2J\x1b[H`)
|
|
1282
|
+
* which removes ALL rows from the screen — but our `streamLineCount`
|
|
1283
|
+
* still believes those rows are there. The next `tryRerenderInPlace`
|
|
1284
|
+
* would walk the cursor back N rows that no longer exist, leaving a
|
|
1285
|
+
* ghost gap at the top of the new viewport. Zeroing the count makes
|
|
1286
|
+
* the next eraser a no-op (which is correct — there's nothing left
|
|
1287
|
+
* to erase).
|
|
1288
|
+
*
|
|
1289
|
+
* Idempotent: no-op when no stream is active. Safe to call from a
|
|
1290
|
+
* resize callback that fires unconditionally on every viewport
|
|
1291
|
+
* change. Also resets `streamBuffer` so the next commit doesn't try
|
|
1292
|
+
* to rerender content that was already wiped.
|
|
1293
|
+
*/
|
|
1294
|
+
resetStreamFrameForResize() {
|
|
1295
|
+
if (!this.streamHeaderShown)
|
|
1296
|
+
return;
|
|
1297
|
+
this.streamLineCount = 0;
|
|
1298
|
+
this.streamBuffer = '';
|
|
1299
|
+
// Header was wiped by the hard-clear too — let the next
|
|
1300
|
+
// streamPartial / agentTurn write a fresh one.
|
|
1301
|
+
this.streamHeaderShown = false;
|
|
1302
|
+
this.streamLastEndedNewline = false;
|
|
1303
|
+
}
|
|
976
1304
|
/**
|
|
977
1305
|
* Append a streamed text fragment. Writes a styled "Aiden" header on
|
|
978
1306
|
* the first call of a turn, then writes raw text directly via the
|
|
@@ -997,72 +1325,270 @@ class Display {
|
|
|
997
1325
|
this.out.write(text);
|
|
998
1326
|
this.streamLastEndedNewline = text.endsWith('\n');
|
|
999
1327
|
// Phase v4.1-reply-formatting: track buffer + line count for the
|
|
1000
|
-
// post-stream re-render.
|
|
1001
|
-
// so the eraser later knows how many rows to clear.
|
|
1328
|
+
// post-stream re-render.
|
|
1002
1329
|
this.streamBuffer += text;
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1330
|
+
// v4.1.4 reply-quality polish — F-B1 wrap-aware row count.
|
|
1331
|
+
//
|
|
1332
|
+
// Prior counter just `streamLineCount += text.match(/\n/g)?.length`
|
|
1333
|
+
// — counted `\n` chars only. When the model emits a long single
|
|
1334
|
+
// line (e.g. a 100-char bullet on an 80-col terminal), the terminal
|
|
1335
|
+
// naturally wraps it across multiple screen rows, but the old
|
|
1336
|
+
// counter would still think it's 1 row. At streamComplete the
|
|
1337
|
+
// eraser walked back N rows that didn't match the wrapped row
|
|
1338
|
+
// count → raw `**markup**` from the streaming phase remained
|
|
1339
|
+
// visible above the rerendered output.
|
|
1340
|
+
//
|
|
1341
|
+
// Confirmed undercount via scripts/smoke-stream-wrap-count.ts:
|
|
1342
|
+
// 3 long bullets on 80-col counted 3, actually 6. Multi-chunk
|
|
1343
|
+
// preamble + bullets on 40-col counted 4, actually 8.
|
|
1344
|
+
//
|
|
1345
|
+
// Fix: count `ceil(visibleWidth / cols)` rows per `\n`-delimited
|
|
1346
|
+
// segment, then add 1 for the `\n` itself (cursor advances to
|
|
1347
|
+
// next row when newline is emitted). Visible width strips ANSI.
|
|
1348
|
+
this.streamLineCount += this.countStreamRows(text);
|
|
1006
1349
|
}
|
|
1007
1350
|
/**
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1351
|
+
* v4.1.4 reply-quality polish — F-B1 helper.
|
|
1352
|
+
*
|
|
1353
|
+
* Estimate how many screen rows `text` consumes when written to a
|
|
1354
|
+
* terminal of width `this.out.columns`. Counts terminal-natural-wrap
|
|
1355
|
+
* rows for each logical line, plus one row per `\n`.
|
|
1356
|
+
*
|
|
1357
|
+
* Falls back to a sane count when columns is undefined (non-TTY or
|
|
1358
|
+
* pre-resize): in that case the eraser won't fire anyway
|
|
1359
|
+
* (`tryRerenderInPlace` gates on `out.isTTY`), so the count is
|
|
1360
|
+
* effectively ignored. We still compute a defensible value so any
|
|
1361
|
+
* future TTY-detection change doesn't silently regress.
|
|
1362
|
+
*
|
|
1363
|
+
* Pure with respect to ANSI: escape sequences pass through
|
|
1364
|
+
* `visibleLength` and don't inflate the row count.
|
|
1365
|
+
*
|
|
1366
|
+
* Edge cases:
|
|
1367
|
+
* - Empty text → 0 rows (consistent with the prior counter).
|
|
1368
|
+
* - Text without `\n` → ceil(visibleLen / cols) rows.
|
|
1369
|
+
* - Trailing `\n` → counts the prior content row + 1 for the
|
|
1370
|
+
* newline. Cursor is now at the start of the next row, which is
|
|
1371
|
+
* correct screen-state — the next streamPartial extends from
|
|
1372
|
+
* col 0 of that row.
|
|
1012
1373
|
*/
|
|
1013
|
-
|
|
1014
|
-
if (
|
|
1015
|
-
return;
|
|
1016
|
-
|
|
1017
|
-
this.out.
|
|
1018
|
-
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1374
|
+
countStreamRows(text) {
|
|
1375
|
+
if (text.length === 0)
|
|
1376
|
+
return 0;
|
|
1377
|
+
const cols = (typeof this.out.columns === 'number' && this.out.columns >= 1)
|
|
1378
|
+
? this.out.columns
|
|
1379
|
+
: 80;
|
|
1380
|
+
// Semantics: counter tracks ROW BOUNDARIES CROSSED during
|
|
1381
|
+
// emission, not "rows occupied". The eraser uses `\x1b[<N>F`
|
|
1382
|
+
// which moves the cursor up N rows; if N matches the number of
|
|
1383
|
+
// boundaries crossed from start-of-stream to current-cursor, the
|
|
1384
|
+
// eraser lands at the start row and `\x1b[J` clears the rest.
|
|
1385
|
+
//
|
|
1386
|
+
// For a segment of visible width V on a terminal of width C:
|
|
1387
|
+
// - V == 0 → 0 wrap boundaries
|
|
1388
|
+
// - V <= C → 0 wrap boundaries (single row)
|
|
1389
|
+
// - C < V <= 2C → 1 wrap boundary
|
|
1390
|
+
// - General → floor((V - 1) / C) wrap boundaries
|
|
1391
|
+
//
|
|
1392
|
+
// Each `\n` between segments crosses one boundary regardless of
|
|
1393
|
+
// visible width — that's the newline advancing the cursor.
|
|
1394
|
+
let rows = 0;
|
|
1395
|
+
const segments = text.split('\n');
|
|
1396
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
1397
|
+
const seg = segments[i] ?? '';
|
|
1398
|
+
const visible = (0, box_1.visibleLength)(seg);
|
|
1399
|
+
if (visible > 0)
|
|
1400
|
+
rows += Math.floor((visible - 1) / cols);
|
|
1401
|
+
if (i < segments.length - 1)
|
|
1402
|
+
rows += 1;
|
|
1403
|
+
}
|
|
1404
|
+
return rows;
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* v4.1.3-essentials: rerender a buffered stream chunk in place. Walks
|
|
1408
|
+
* the cursor back `lines` rows, erases to end-of-screen, and reprints
|
|
1409
|
+
* the chunk as skin-aware markdown.
|
|
1410
|
+
*
|
|
1411
|
+
* Pure side-effect; returns nothing. Used by:
|
|
1412
|
+
* - `commitStreamChunk()` — when a tool row interrupts the
|
|
1413
|
+
* stream, render the pre-tool
|
|
1414
|
+
* chunk before the row writes.
|
|
1415
|
+
* - `streamComplete()` — final chunk at end of turn.
|
|
1416
|
+
*
|
|
1417
|
+
* Heuristic gate avoids flicker on plain prose (no structure → no
|
|
1418
|
+
* rerender, no eraser fires). The catch block writes the RAW buffered
|
|
1419
|
+
* text as a fallback if `marked` throws — without this the eraser
|
|
1420
|
+
* would already have run and the body would silently vanish.
|
|
1421
|
+
* v4.1.3-essentials raw-text fallback per "make state legible" thesis.
|
|
1422
|
+
*/
|
|
1423
|
+
tryRerenderInPlace(buffered, lines) {
|
|
1031
1424
|
if (!this.out.isTTY)
|
|
1032
1425
|
return;
|
|
1033
1426
|
if (process.env.AIDEN_NO_REFORMAT === '1')
|
|
1034
1427
|
return;
|
|
1035
|
-
|
|
1036
|
-
|
|
1428
|
+
if (lines === 0)
|
|
1429
|
+
return;
|
|
1430
|
+
// Cheap structural heuristic — only re-render when formatting
|
|
1431
|
+
// actually helps. Plain prose chunks stay raw (no flicker).
|
|
1432
|
+
//
|
|
1433
|
+
// v4.1.3-essentials post-ship: inline `**bold**` and `` `code` ``
|
|
1434
|
+
// added to the heuristic. Before this, a chunk that contained ONLY
|
|
1435
|
+
// inline markdown (no headings / lists / code blocks) skipped
|
|
1436
|
+
// rerender entirely, leaving the literal `**bold**` asterisks in
|
|
1437
|
+
// user-visible output. The `paintBoldWhite` strong renderer was
|
|
1438
|
+
// never invoked for those chunks.
|
|
1439
|
+
//
|
|
1440
|
+
// Patterns:
|
|
1441
|
+
// - `**bold**`: requires non-space immediately after the opening
|
|
1442
|
+
// `**` so `2 ** 3` math expressions don't false-positive.
|
|
1443
|
+
// Tolerates multi-line bold via `[\s\S]*?`.
|
|
1444
|
+
// - `` `code` ``: negative-lookarounds for the triple-backtick
|
|
1445
|
+
// fence so we don't double-trigger when ``` lines are present
|
|
1446
|
+
// (those already match the fence pattern above).
|
|
1037
1447
|
const hasStructure = /^#{1,6}\s/m.test(buffered) ||
|
|
1038
1448
|
/^\s*[-*+]\s/m.test(buffered) ||
|
|
1039
1449
|
/^\s*\d+\.\s/m.test(buffered) ||
|
|
1040
1450
|
/^>\s/m.test(buffered) ||
|
|
1041
|
-
/```/.test(buffered)
|
|
1451
|
+
/```/.test(buffered) ||
|
|
1452
|
+
/\*\*\S[\s\S]*?\*\*/.test(buffered) ||
|
|
1453
|
+
/(?<![`])`[^`\n]+`(?![`])/.test(buffered);
|
|
1042
1454
|
if (!hasStructure)
|
|
1043
1455
|
return;
|
|
1044
1456
|
try {
|
|
1045
|
-
//
|
|
1046
|
-
//
|
|
1047
|
-
|
|
1048
|
-
// `\x1b[<n>F` = cursor-up-and-to-column-0 N times.
|
|
1049
|
-
// `\x1b[J` = erase from cursor to end of screen.
|
|
1050
|
-
if (lines > 0) {
|
|
1051
|
-
this.out.write(`\x1b[${lines}F\x1b[J`);
|
|
1052
|
-
}
|
|
1457
|
+
// \x1b[<n>F = cursor-up-and-to-column-0 N times.
|
|
1458
|
+
// \x1b[J = erase from cursor to end of screen.
|
|
1459
|
+
this.out.write(`\x1b[${lines}F\x1b[J`);
|
|
1053
1460
|
const formatted = this.markdown(buffered).trimEnd();
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1461
|
+
// v4.1.4 reply-quality polish: same detect-and-skip indent + wrap
|
|
1462
|
+
// as agentTurn so streamed and one-shot replies share the visible
|
|
1463
|
+
// frame. wrap-ansi handles ANSI-aware width counting for prose;
|
|
1464
|
+
// structural lines (code-block chrome, list bullets, blockquote
|
|
1465
|
+
// rails) pass through unchanged so their own gutter + wrap stays
|
|
1466
|
+
// intact.
|
|
1467
|
+
const indented = this.applyFrameToRendered(formatted);
|
|
1058
1468
|
this.out.write(indented + '\n');
|
|
1059
1469
|
}
|
|
1060
1470
|
catch {
|
|
1061
|
-
//
|
|
1062
|
-
//
|
|
1063
|
-
//
|
|
1471
|
+
// Eraser already ran. v4.1.3-essentials: write the raw buffered
|
|
1472
|
+
// text back so the body doesn't vanish silently. The user sees
|
|
1473
|
+
// unformatted markdown rather than a missing reply — the honest
|
|
1474
|
+
// failure mode.
|
|
1475
|
+
this.out.write(buffered);
|
|
1476
|
+
if (!buffered.endsWith('\n'))
|
|
1477
|
+
this.out.write('\n');
|
|
1064
1478
|
}
|
|
1065
1479
|
}
|
|
1480
|
+
/**
|
|
1481
|
+
* v4.1.3-essentials: fence off the current stream chunk before a
|
|
1482
|
+
* non-stream write (tool row, tool indicator, capability card) lands.
|
|
1483
|
+
*
|
|
1484
|
+
* Replaces the v4.1.3-repl-polish `streamInterrupted` flag pattern.
|
|
1485
|
+
* Old pattern: set flag mid-stream → on streamComplete, check flag
|
|
1486
|
+
* and SKIP rerender entirely (lost markdown on every tool-using
|
|
1487
|
+
* turn). New pattern: at each interrupt point, eagerly rerender THIS
|
|
1488
|
+
* chunk in place, then reset the per-chunk window so the next
|
|
1489
|
+
* streamPartial starts a fresh count. The cursor is at the end of
|
|
1490
|
+
* this chunk when commit fires, so `streamLineCount` is correct for
|
|
1491
|
+
* the eraser — tool rows write below without being clobbered.
|
|
1492
|
+
*
|
|
1493
|
+
* Multi-chunk turns (model says X, calls tool, says Y, calls tool,
|
|
1494
|
+
* says Z) get all three chunks rerendered as markdown.
|
|
1495
|
+
*
|
|
1496
|
+
* Idempotent: no-op when no stream cycle is active or when the buffer
|
|
1497
|
+
* is empty (consecutive tool calls). Always ensures the cursor sits
|
|
1498
|
+
* at start-of-line before returning so the caller can write its own
|
|
1499
|
+
* row cleanly.
|
|
1500
|
+
*/
|
|
1501
|
+
commitStreamChunk() {
|
|
1502
|
+
if (!this.streamHeaderShown)
|
|
1503
|
+
return;
|
|
1504
|
+
// Ensure the streamed chunk ends with a newline so the interrupt
|
|
1505
|
+
// row doesn't stick to mid-token text from the prior delta.
|
|
1506
|
+
if (!this.streamLastEndedNewline) {
|
|
1507
|
+
this.out.write('\n');
|
|
1508
|
+
this.streamLastEndedNewline = true;
|
|
1509
|
+
// The trailing newline we just wrote DOES bump the cursor's row,
|
|
1510
|
+
// but only by 1 — and `streamLineCount` should reflect physical
|
|
1511
|
+
// rows of the chunk. Add it so the eraser walks back the right
|
|
1512
|
+
// amount.
|
|
1513
|
+
this.streamLineCount += 1;
|
|
1514
|
+
}
|
|
1515
|
+
// v4.1.3-essentials boldwrap-fix: if the chunk ends mid-bold-pair
|
|
1516
|
+
// (e.g. tool fired between the model emitting `**` and the closing
|
|
1517
|
+
// `**`), splitting here would leave literal asterisks in the
|
|
1518
|
+
// rerendered output and a matching orphan in the next chunk.
|
|
1519
|
+
// `splitAtUnclosedBold` finds the last unmatched `**` and carves
|
|
1520
|
+
// the buffer into two parts: the closed-bold prefix we CAN
|
|
1521
|
+
// rerender now, and the carry tail that we keep for the next
|
|
1522
|
+
// chunk (where the closing `**` will eventually arrive).
|
|
1523
|
+
//
|
|
1524
|
+
// Code-fence safety (carried in the helper): if the would-be
|
|
1525
|
+
// unmatched `**` is inside an open ``` fence, we defer the whole
|
|
1526
|
+
// chunk — bold-syntax inside code blocks isn't markdown bold and
|
|
1527
|
+
// splitting there would corrupt the fence.
|
|
1528
|
+
const split = splitAtUnclosedBold(this.streamBuffer);
|
|
1529
|
+
if (split.carry === '') {
|
|
1530
|
+
// Common case: buffer is balanced (or has no `**` at all).
|
|
1531
|
+
// Same behavior as before — rerender the whole chunk in place
|
|
1532
|
+
// and reset the per-chunk window.
|
|
1533
|
+
this.tryRerenderInPlace(this.streamBuffer, this.streamLineCount);
|
|
1534
|
+
this.streamBuffer = '';
|
|
1535
|
+
this.streamLineCount = 0;
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
// Split path: erase the WHOLE chunk (because the cursor is at the
|
|
1539
|
+
// end of the full buffer), rerender the closed prefix, then
|
|
1540
|
+
// re-emit the carry as raw text. The carry visibly stays on
|
|
1541
|
+
// screen as raw `**Live tool indi`-style text — ugly for the
|
|
1542
|
+
// ~milliseconds until the next streamPartial extends it past the
|
|
1543
|
+
// closing `**`, at which point the next commit will rerender
|
|
1544
|
+
// cleanly.
|
|
1545
|
+
const rerenderableLines = countNewlines(split.rerenderable);
|
|
1546
|
+
const carryLines = this.streamLineCount - rerenderableLines;
|
|
1547
|
+
if (this.out.isTTY && this.streamLineCount > 0) {
|
|
1548
|
+
// \x1b[<n>F = cursor-up-and-to-column-0 N times.
|
|
1549
|
+
// \x1b[J = erase from cursor to end of screen.
|
|
1550
|
+
this.out.write(`\x1b[${this.streamLineCount}F\x1b[J`);
|
|
1551
|
+
}
|
|
1552
|
+
// Rerender the closed prefix (handles its own heuristic gate
|
|
1553
|
+
// internally — a prefix without structure stays raw, which is
|
|
1554
|
+
// identical to the pre-split behavior).
|
|
1555
|
+
this.tryRerenderInPlace(split.rerenderable, rerenderableLines);
|
|
1556
|
+
// Re-emit the carry verbatim. It's intentionally raw because the
|
|
1557
|
+
// unmatched `**` can't be rendered without its closing pair.
|
|
1558
|
+
this.out.write(split.carry);
|
|
1559
|
+
// Reset the per-chunk window to the carry only. Next streamPartial
|
|
1560
|
+
// extends it; when the closing `**` lands, the next commit (or
|
|
1561
|
+
// streamComplete) rerenders cleanly.
|
|
1562
|
+
this.streamBuffer = split.carry;
|
|
1563
|
+
this.streamLineCount = carryLines;
|
|
1564
|
+
this.streamLastEndedNewline = split.carry.endsWith('\n');
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Mark the end of a streaming turn. Adds a trailing newline if the
|
|
1568
|
+
* stream didn't end with one so the next CLI line doesn't visually
|
|
1569
|
+
* butt up against the model's last token. Rerenders the FINAL chunk
|
|
1570
|
+
* (post-last-tool prose, or the whole body if no tools fired this
|
|
1571
|
+
* turn) and resets the per-turn state so the next `streamPartial`
|
|
1572
|
+
* re-emits the header.
|
|
1573
|
+
*/
|
|
1574
|
+
streamComplete() {
|
|
1575
|
+
if (!this.streamHeaderShown)
|
|
1576
|
+
return;
|
|
1577
|
+
if (!this.streamLastEndedNewline) {
|
|
1578
|
+
this.out.write('\n');
|
|
1579
|
+
this.streamLineCount += 1;
|
|
1580
|
+
}
|
|
1581
|
+
// Final chunk: same in-place rerender path as commitStreamChunk
|
|
1582
|
+
// (factored shared helper). Tool-row interrupts have already
|
|
1583
|
+
// committed their preceding chunks; what's left in the buffer here
|
|
1584
|
+
// is the post-final-tool prose — typically the bulk of the
|
|
1585
|
+
// user-visible body in well-behaved turns.
|
|
1586
|
+
this.tryRerenderInPlace(this.streamBuffer, this.streamLineCount);
|
|
1587
|
+
this.streamBuffer = '';
|
|
1588
|
+
this.streamLineCount = 0;
|
|
1589
|
+
this.streamHeaderShown = false;
|
|
1590
|
+
this.streamLastEndedNewline = false;
|
|
1591
|
+
}
|
|
1066
1592
|
/**
|
|
1067
1593
|
* Phase v4.1-reply-formatting: render the optional "Sources"
|
|
1068
1594
|
* footer when AIDEN_CITATIONS=1 and the trace has fetch-class
|
|
@@ -1085,10 +1611,15 @@ class Display {
|
|
|
1085
1611
|
* newline if the prior delta ran past column N without one.
|
|
1086
1612
|
*/
|
|
1087
1613
|
streamToolIndicator(name) {
|
|
1614
|
+
// v4.1.3-essentials (replaces v4.1.3-repl-polish streamInterrupted
|
|
1615
|
+
// flag pattern): fence off the streamed chunk before the indicator
|
|
1616
|
+
// writes. `commitStreamChunk` handles the newline-or-not and
|
|
1617
|
+
// in-place rerenders the pre-indicator chunk so this row lands
|
|
1618
|
+
// below well-formed markdown rather than below raw streamed text.
|
|
1619
|
+
this.commitStreamChunk();
|
|
1088
1620
|
const sk = this.skin;
|
|
1089
1621
|
const arrow = sk.getActive().glyphs?.arrow ?? '>';
|
|
1090
|
-
|
|
1091
|
-
this.out.write(`${prefix}${sk.applyColors(`${arrow} ${name}…`, 'tool')}\n`);
|
|
1622
|
+
this.out.write(`${sk.applyColors(`${arrow} ${name}…`, 'tool')}\n`);
|
|
1092
1623
|
this.streamLastEndedNewline = true;
|
|
1093
1624
|
}
|
|
1094
1625
|
}
|
|
@@ -1144,11 +1675,29 @@ function renderRmsBar(rms) {
|
|
|
1144
1675
|
const filled = Math.round((safe / BAR_FULL_RMS) * BAR_WIDTH);
|
|
1145
1676
|
return '▌'.repeat(filled) + ' '.repeat(BAR_WIDTH - filled);
|
|
1146
1677
|
}
|
|
1147
|
-
// ── Phase 23.5 — tool row helpers
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1678
|
+
// ── Phase 23.5 / v4.1.3-repl-polish — tool row helpers ────────────────
|
|
1679
|
+
//
|
|
1680
|
+
// v4.1.3 changes the row format from:
|
|
1681
|
+
// " {glyph} tool {name:16} {args} [{state}]"
|
|
1682
|
+
// to the compact trail format:
|
|
1683
|
+
// "┊ {icon} {verb:12} {detail:40}"
|
|
1684
|
+
//
|
|
1685
|
+
// Outcome semantics:
|
|
1686
|
+
// ok() → SILENT — running row is erased; nothing persists.
|
|
1687
|
+
// fail() → row persists in error (red).
|
|
1688
|
+
// degraded() → row persists in degraded (yellow). NEW in v4.1.3.
|
|
1689
|
+
// blocked() → row persists in warn.
|
|
1690
|
+
// retry() → running row is updated with retry counter.
|
|
1691
|
+
// emptyFail() → treated as fail (error colour).
|
|
1692
|
+
// emptyRetry()→ treated as retry.
|
|
1693
|
+
/**
|
|
1694
|
+
* Kept for reference / smoke-test backward compat. New code should use
|
|
1695
|
+
* TRAIL_VERB_PAD / TRAIL_DETAIL_CAP from toolTrail.ts instead.
|
|
1696
|
+
* @deprecated
|
|
1697
|
+
*/
|
|
1698
|
+
exports.TOOL_ROW_NAME_PAD = toolTrail_1.TRAIL_VERB_PAD;
|
|
1699
|
+
/** @deprecated Use TRAIL_DETAIL_CAP. */
|
|
1700
|
+
exports.TOOL_ROW_ARG_CAP = toolTrail_1.TRAIL_DETAIL_CAP;
|
|
1152
1701
|
/**
|
|
1153
1702
|
* Build a compact, single-line preview of the tool's arguments. Picks
|
|
1154
1703
|
* the most informative scalar fields when the args are an object, then
|
|
@@ -1184,13 +1733,202 @@ function previewToolArgs(args) {
|
|
|
1184
1733
|
catch {
|
|
1185
1734
|
serialized = String(obj);
|
|
1186
1735
|
}
|
|
1736
|
+
// v4.1.4-media: an empty object serializes to '{}'. Rendering that
|
|
1737
|
+
// literal in the trail row is honest but ugly and reads as "buggy
|
|
1738
|
+
// empty args". When the model legitimately passes an empty args
|
|
1739
|
+
// object (e.g. `media_sessions({})`, `system_info()`), show nothing
|
|
1740
|
+
// rather than the braces — `buildToolPreview` already does this for
|
|
1741
|
+
// tools mapped in `TOOL_PRIMARY_ARG`; here we extend the same UX to
|
|
1742
|
+
// any unmapped-tool fallback that bottoms out at `{}`.
|
|
1743
|
+
if (serialized === '{}')
|
|
1744
|
+
return '';
|
|
1187
1745
|
return truncToolArg(serialized);
|
|
1188
1746
|
}
|
|
1189
1747
|
function truncToolArg(s) {
|
|
1190
1748
|
const flat = s.replace(/\s+/g, ' ').trim();
|
|
1191
|
-
if (flat.length <= TOOL_ROW_ARG_CAP)
|
|
1749
|
+
if (flat.length <= exports.TOOL_ROW_ARG_CAP)
|
|
1192
1750
|
return flat;
|
|
1193
|
-
return flat.slice(0, TOOL_ROW_ARG_CAP - 1) + '…';
|
|
1751
|
+
return flat.slice(0, exports.TOOL_ROW_ARG_CAP - 1) + '…';
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* v4.1.4 reply-quality polish — Part 1.6 tool-aware verb mapper.
|
|
1755
|
+
*
|
|
1756
|
+
* Picks the activity-indicator verb for the gap that follows a given
|
|
1757
|
+
* tool's completion. The verb reflects "what the model is likely doing
|
|
1758
|
+
* next" rather than "what just happened" — so a `file_read` completing
|
|
1759
|
+
* leads to "reading" (model is digesting the contents) rather than
|
|
1760
|
+
* "drafted" (which would imply done). Tested in display.test.ts.
|
|
1761
|
+
*
|
|
1762
|
+
* Categories (matches against the tool-name substring, lowercased):
|
|
1763
|
+
* - read/list/view/get/inspect → 'reading'
|
|
1764
|
+
* - search/web/fetch_url/scrape → 'searching'
|
|
1765
|
+
* - shell/exec/run/compute/system → 'analyzing'
|
|
1766
|
+
* - write/edit/patch/save → 'drafting'
|
|
1767
|
+
* - everything else (or undefined) → 'thinking'
|
|
1768
|
+
*
|
|
1769
|
+
* Special caller-supplied phase override:
|
|
1770
|
+
* - When the caller knows "all tools are done, reply about to start"
|
|
1771
|
+
* they pass `phase: 'post-all'` → verb defaults to 'drafting'
|
|
1772
|
+
* regardless of the last tool name.
|
|
1773
|
+
*
|
|
1774
|
+
* Pure; exported for unit-test access.
|
|
1775
|
+
*/
|
|
1776
|
+
function verbForActivity(toolName, phase = 'post-tool') {
|
|
1777
|
+
if (phase === 'pre-tools')
|
|
1778
|
+
return 'thinking';
|
|
1779
|
+
if (phase === 'post-all')
|
|
1780
|
+
return 'drafting';
|
|
1781
|
+
const t = (toolName ?? '').toLowerCase();
|
|
1782
|
+
if (t.length === 0)
|
|
1783
|
+
return 'thinking';
|
|
1784
|
+
// Match in priority order so 'web_search' hits 'searching' (search)
|
|
1785
|
+
// before 'reading' (a hypothetical 'web_search_read' would still
|
|
1786
|
+
// map to 'searching' since search hits first).
|
|
1787
|
+
if (/(^|_)(search|web|fetch_url|scrape|crawl)(_|$)/.test(t))
|
|
1788
|
+
return 'searching';
|
|
1789
|
+
if (/(^|_)(read|list|view|get|inspect|info|status)(_|$)/.test(t))
|
|
1790
|
+
return 'reading';
|
|
1791
|
+
if (/(^|_)(write|edit|patch|save|create|append|delete|remove)(_|$)/.test(t))
|
|
1792
|
+
return 'drafting';
|
|
1793
|
+
if (/(^|_)(shell|exec|execute|run|compute|process|system|launch)(_|$)/.test(t))
|
|
1794
|
+
return 'analyzing';
|
|
1795
|
+
return 'thinking';
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* v4.1.4 reply-quality polish — F1 detect-and-skip predicate.
|
|
1799
|
+
*
|
|
1800
|
+
* Returns true when `line` is a structural / pre-framed line emitted
|
|
1801
|
+
* by replyRenderer (code-block chrome, blockquote rail, indented list
|
|
1802
|
+
* bullet, fence rules). These lines OWN their own gutter and per-line
|
|
1803
|
+
* wrap; `agentTurn` and `tryRerenderInPlace` MUST pass them through
|
|
1804
|
+
* unchanged so the post-render indent+wrap pass doesn't:
|
|
1805
|
+
* - Double the gutter (content drifts 3 cols right per pass)
|
|
1806
|
+
* - Re-wrap an already-wrapped code line (rail/bg breaks across
|
|
1807
|
+
* the new wrap continuation row)
|
|
1808
|
+
*
|
|
1809
|
+
* Detection rules (all on the ANSI-bearing line as emitted by marked
|
|
1810
|
+
* via our renderer overrides):
|
|
1811
|
+
* - Contains `\x1b[48;` anywhere → 24-bit bg paint = code-block
|
|
1812
|
+
* line. Always pre-framed (renderCodeBlock applies gutter + rail).
|
|
1813
|
+
* - Starts with ` │ ` or ` ┃ ` → explicit pre-framed rail
|
|
1814
|
+
* (code or blockquote at the frame gutter).
|
|
1815
|
+
* - Matches `^\s{2,}(•|▸|\d+\.)\s` (depth-indented list bullet) →
|
|
1816
|
+
* the list override already applied the per-depth indent.
|
|
1817
|
+
* - Matches `^\s{0,4}─{8,}` (horizontal-rule run or fence) → render-
|
|
1818
|
+
* specific divider already styled.
|
|
1819
|
+
*
|
|
1820
|
+
* Pure; exported for unit-test access.
|
|
1821
|
+
*/
|
|
1822
|
+
function isPreFramedLine(line) {
|
|
1823
|
+
if (line.length === 0)
|
|
1824
|
+
return false;
|
|
1825
|
+
// eslint-disable-next-line no-control-regex
|
|
1826
|
+
const stripped = line.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
1827
|
+
// v4.1.4 reply-quality polish — Fix D (tightened predicate).
|
|
1828
|
+
//
|
|
1829
|
+
// Code-block body lines start with the frame gutter + rail. The
|
|
1830
|
+
// 24-bit bg paint (`\x1b[48;…`) also appears on these lines, but
|
|
1831
|
+
// we MUST NOT use bg-presence alone as the trigger: the `codespan`
|
|
1832
|
+
// renderer wraps inline `` `code` `` with the same bg envelope, so
|
|
1833
|
+
// any prose line containing inline code would be wrongly classified
|
|
1834
|
+
// as a code-block line and would bypass the indent + wrap pass
|
|
1835
|
+
// (Issue D from visual smoke — prose with inline codespans
|
|
1836
|
+
// terminal-natural-wrapped past bodyWidth).
|
|
1837
|
+
//
|
|
1838
|
+
// Rail prefix (after ANSI strip) is the reliable signal: only
|
|
1839
|
+
// `renderCodeBlock` emits ` │ ` and only `renderBlockquote` emits
|
|
1840
|
+
// `┃ ` at line start (with display-layer gutter prepended).
|
|
1841
|
+
if (/^ │ /.test(stripped))
|
|
1842
|
+
return true;
|
|
1843
|
+
if (/^ ┃ /.test(stripped) || /^┃ /.test(stripped))
|
|
1844
|
+
return true;
|
|
1845
|
+
// Depth-indented list bullets emitted by the renderer.list override:
|
|
1846
|
+
// ` • prose…` (depth 1, 2-space indent)
|
|
1847
|
+
// ` ▸ prose…` (depth 2, 4-space indent)
|
|
1848
|
+
// ` 1. prose…` (numbered, depth 1)
|
|
1849
|
+
if (/^\s{2,}(•|▸|\d+\.)\s/.test(stripped))
|
|
1850
|
+
return true;
|
|
1851
|
+
// Code-block fence rules (long runs of `─` with optional leading
|
|
1852
|
+
// gutter + optional language label from renderCodeBlock). Match
|
|
1853
|
+
// ANYWHERE in the line so the language-tagged top rule
|
|
1854
|
+
// (` ── lang ──────…──`) trips alongside the unlabeled bottom rule.
|
|
1855
|
+
if (/─{8,}/.test(stripped))
|
|
1856
|
+
return true;
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* v4.1.3-essentials boldwrap-fix: count `\n` occurrences in `s`.
|
|
1861
|
+
* Used by `commitStreamChunk` to recompute `streamLineCount` after
|
|
1862
|
+
* splitting a buffer at an unclosed-bold boundary. Pure helper —
|
|
1863
|
+
* exported for unit-test access.
|
|
1864
|
+
*/
|
|
1865
|
+
function countNewlines(s) {
|
|
1866
|
+
let n = 0;
|
|
1867
|
+
for (let i = 0; i < s.length; i += 1)
|
|
1868
|
+
if (s[i] === '\n')
|
|
1869
|
+
n += 1;
|
|
1870
|
+
return n;
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* v4.1.3-essentials boldwrap-fix: split a streamed-chunk buffer at the
|
|
1874
|
+
* last unmatched `**` so the closed-bold prefix can be rerendered now
|
|
1875
|
+
* and the open-bold tail can be carried into the next chunk.
|
|
1876
|
+
*
|
|
1877
|
+
* Returns `{ rerenderable, carry }`:
|
|
1878
|
+
* - rerenderable: the prefix with all `**` pairs balanced
|
|
1879
|
+
* - carry: the suffix starting at the last unmatched `**`
|
|
1880
|
+
*
|
|
1881
|
+
* `carry === ''` signals "balanced — render the whole buffer". Caller
|
|
1882
|
+
* uses this as the fast-path discriminator.
|
|
1883
|
+
*
|
|
1884
|
+
* Code-fence safety: if the buffer contains an UNCLOSED fenced code
|
|
1885
|
+
* block (` ``` ` count is odd), defer the entire chunk by returning
|
|
1886
|
+
* `{ rerenderable: '', carry: buffer }`. Bold-syntax inside code
|
|
1887
|
+
* blocks is literal text — splitting there would corrupt the fence
|
|
1888
|
+
* AND likely produce nonsensical rerender output. Trade-off: a chunk
|
|
1889
|
+
* that ends mid-code-block doesn't rerender at all until the closing
|
|
1890
|
+
* ``` arrives; acceptable because code blocks have their own
|
|
1891
|
+
* styling (dark bg + left rail) that doesn't depend on the markdown
|
|
1892
|
+
* rerender step.
|
|
1893
|
+
*
|
|
1894
|
+
* Pure function. Tested via `tests/v4/cli/display.test.ts`.
|
|
1895
|
+
*/
|
|
1896
|
+
function splitAtUnclosedBold(buffer) {
|
|
1897
|
+
// Fast path: no `**` at all → balanced.
|
|
1898
|
+
if (!buffer.includes('**'))
|
|
1899
|
+
return { rerenderable: buffer, carry: '' };
|
|
1900
|
+
// Code-fence safety: count triple-backtick fences. Odd = open fence,
|
|
1901
|
+
// defer the whole buffer.
|
|
1902
|
+
const fenceMatches = buffer.match(/```/g);
|
|
1903
|
+
if (fenceMatches && fenceMatches.length % 2 === 1) {
|
|
1904
|
+
return { rerenderable: '', carry: buffer };
|
|
1905
|
+
}
|
|
1906
|
+
// Count `**` occurrences. Even → balanced. Odd → there's an
|
|
1907
|
+
// unmatched `**` — find the LAST one (the open).
|
|
1908
|
+
const positions = [];
|
|
1909
|
+
for (let i = 0; i < buffer.length - 1; i += 1) {
|
|
1910
|
+
if (buffer[i] === '*' && buffer[i + 1] === '*') {
|
|
1911
|
+
positions.push(i);
|
|
1912
|
+
i += 1; // skip the second `*` so `***` doesn't double-count
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
if (positions.length % 2 === 0) {
|
|
1916
|
+
return { rerenderable: buffer, carry: '' };
|
|
1917
|
+
}
|
|
1918
|
+
const lastUnmatched = positions[positions.length - 1];
|
|
1919
|
+
// Inline-backtick safety: if the unmatched `**` sits inside an
|
|
1920
|
+
// open single-backtick span on the same line, the `**` is literal
|
|
1921
|
+
// code, not a bold marker. Defer the whole chunk.
|
|
1922
|
+
const lineStart = buffer.lastIndexOf('\n', lastUnmatched) + 1;
|
|
1923
|
+
const lineUpToBold = buffer.slice(lineStart, lastUnmatched);
|
|
1924
|
+
const backticksOnLine = (lineUpToBold.match(/`/g) ?? []).length;
|
|
1925
|
+
if (backticksOnLine % 2 === 1) {
|
|
1926
|
+
return { rerenderable: '', carry: buffer };
|
|
1927
|
+
}
|
|
1928
|
+
return {
|
|
1929
|
+
rerenderable: buffer.slice(0, lastUnmatched),
|
|
1930
|
+
carry: buffer.slice(lastUnmatched),
|
|
1931
|
+
};
|
|
1194
1932
|
}
|
|
1195
1933
|
/**
|
|
1196
1934
|
* Render a tool-call duration in the bracket cluster. Sub-second
|