aiden-runtime 4.1.1 → 4.1.3
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/README.md +78 -26
- package/dist/cli/v4/aidenCLI.js +169 -9
- package/dist/cli/v4/callbacks.js +20 -2
- package/dist/cli/v4/chatSession.js +644 -16
- package/dist/cli/v4/commands/auth.js +6 -3
- package/dist/cli/v4/commands/doctor.js +23 -27
- package/dist/cli/v4/commands/help.js +4 -0
- package/dist/cli/v4/commands/index.js +10 -1
- package/dist/cli/v4/commands/model.js +30 -1
- package/dist/cli/v4/commands/reloadSoul.js +37 -0
- package/dist/cli/v4/commands/update.js +102 -0
- package/dist/cli/v4/defaultSoul.js +68 -2
- package/dist/cli/v4/display/capabilityCard.js +135 -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 +492 -142
- package/dist/cli/v4/doctor.js +472 -58
- package/dist/cli/v4/doctorLiveness.js +65 -10
- package/dist/cli/v4/promotionPrompt.js +332 -0
- package/dist/cli/v4/providerBootSelector.js +144 -0
- package/dist/cli/v4/replyRenderer.js +311 -20
- package/dist/cli/v4/sessionSummaryGate.js +66 -0
- package/dist/cli/v4/skinEngine.js +14 -3
- package/dist/cli/v4/toolPreview.js +153 -0
- package/dist/core/tools/nowPlaying.js +7 -15
- package/dist/core/v4/aidenAgent.js +91 -29
- package/dist/core/v4/capabilities.js +89 -0
- package/dist/core/v4/contextCompressor.js +25 -8
- package/dist/core/v4/distillationIndex.js +167 -0
- package/dist/core/v4/distillationStore.js +98 -0
- package/dist/core/v4/logger/logger.js +40 -9
- package/dist/core/v4/promotionCandidates.js +234 -0
- package/dist/core/v4/promptBuilder.js +145 -1
- package/dist/core/v4/sessionDistiller.js +452 -0
- package/dist/core/v4/skillMining/skillMiner.js +43 -6
- package/dist/core/v4/skillOutcomeTracker.js +323 -0
- package/dist/core/v4/subsystemHealth.js +143 -0
- package/dist/core/v4/toolRegistry.js +16 -1
- package/dist/core/v4/update/executeInstall.js +233 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/memoryGuard.js +111 -0
- package/dist/moat/plannerGuard.js +19 -0
- package/dist/moat/skillTeacher.js +14 -5
- package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
- package/dist/providers/v4/errors.js +112 -4
- package/dist/providers/v4/modelDefaults.js +65 -0
- package/dist/providers/v4/registry.js +9 -2
- package/dist/providers/v4/runtimeResolver.js +6 -0
- package/dist/tools/v4/index.js +80 -1
- package/dist/tools/v4/memory/memoryRemove.js +57 -2
- package/dist/tools/v4/memory/sessionSummary.js +151 -0
- package/dist/tools/v4/sessions/recallSession.js +177 -0
- package/dist/tools/v4/sessions/sessionSearch.js +5 -1
- package/dist/tools/v4/system/_psHelpers.js +123 -0
- package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
- package/dist/tools/v4/system/appClose.js +79 -0
- package/dist/tools/v4/system/appInput.js +154 -0
- package/dist/tools/v4/system/appLaunch.js +218 -0
- package/dist/tools/v4/system/clipboardRead.js +54 -0
- package/dist/tools/v4/system/clipboardWrite.js +84 -0
- package/dist/tools/v4/system/mediaKey.js +109 -0
- package/dist/tools/v4/system/mediaSessions.js +163 -0
- package/dist/tools/v4/system/mediaTransport.js +211 -0
- package/dist/tools/v4/system/osProcessList.js +99 -0
- package/dist/tools/v4/system/screenshot.js +106 -0
- package/dist/tools/v4/system/volumeSet.js +157 -0
- package/package.json +4 -1
- package/skills/system_control.md +185 -69
package/dist/cli/v4/display.js
CHANGED
|
@@ -18,13 +18,15 @@
|
|
|
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.countNewlines = countNewlines;
|
|
29
|
+
exports.splitAtUnclosedBold = splitAtUnclosedBold;
|
|
28
30
|
exports.formatToolDuration = formatToolDuration;
|
|
29
31
|
exports.formatCompactTokens = formatCompactTokens;
|
|
30
32
|
exports.formatElapsedShort = formatElapsedShort;
|
|
@@ -36,77 +38,67 @@ const marked_1 = require("marked");
|
|
|
36
38
|
const TerminalRenderer = require('marked-terminal').default ?? require('marked-terminal');
|
|
37
39
|
const skinEngine_1 = require("./skinEngine");
|
|
38
40
|
const box_1 = require("./box");
|
|
41
|
+
const toolTrail_1 = require("./display/toolTrail");
|
|
42
|
+
// v4.1.3-essentials — capability card renderer (auth/platform failures).
|
|
43
|
+
const capabilityCard_1 = require("./display/capabilityCard");
|
|
39
44
|
// Phase v4.1-reply-formatting: skin-aware markdown renderer that
|
|
40
45
|
// replaces marked-terminal's defaults with structured headers, lists,
|
|
41
46
|
// code blocks, blockquotes, and links.
|
|
42
47
|
const replyRenderer_1 = require("./replyRenderer");
|
|
43
48
|
// Optional "Sources" footer when AIDEN_CITATIONS=1 (default off).
|
|
44
49
|
const citationFooter_1 = require("./citationFooter");
|
|
50
|
+
const toolPreview_1 = require("./toolPreview");
|
|
45
51
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* across Windows Terminal / older Console hosts / SSH sessions.
|
|
52
|
+
* v4.1.3-repl-polish — category emoji icons for the tool-row trail.
|
|
53
|
+
* Icons are ON by default (AIDEN_UI_ICONS !== '0'). Set
|
|
54
|
+
* `AIDEN_UI_ICONS=0` to disable them (CI / dumb terminals).
|
|
50
55
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
56
|
+
* Kept as a flat Record for backward-compat with smoke tests that
|
|
57
|
+
* import it directly. The canonical lookup now lives in
|
|
58
|
+
* `./display/toolTrail` (supports verb too); this map is derived
|
|
59
|
+
* from it for legacy reference.
|
|
55
60
|
*
|
|
56
|
-
*
|
|
61
|
+
* Matching order: exact lowercased name, then substring, then 'default'.
|
|
57
62
|
*/
|
|
58
63
|
exports.TOOL_ICONS = {
|
|
59
|
-
// Observe / read
|
|
60
|
-
|
|
61
|
-
read: '👁',
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
skills_list: '📋',
|
|
69
|
-
// Execute / write / run
|
|
70
|
-
execute: '⚡',
|
|
71
|
-
run: '⚡',
|
|
72
|
-
bash: '⚡',
|
|
73
|
-
powershell: '⚡',
|
|
74
|
-
code: '⚡',
|
|
75
|
-
skill_view: '⚡',
|
|
76
|
-
write: '✏',
|
|
77
|
-
edit: '✏',
|
|
64
|
+
// Observe / read
|
|
65
|
+
file_read: '👁', read_file: '👁', file_list: '👁', list_directory: '👁',
|
|
66
|
+
observe: '👁', read: '👁', list: '👁',
|
|
67
|
+
// Write / edit
|
|
68
|
+
file_write: '✏', write_file: '✏', edit_file: '✏',
|
|
69
|
+
write: '✏', edit: '✏', create: '✏',
|
|
70
|
+
// Execute / run
|
|
71
|
+
bash: '⚡', powershell: '⚡', execute_code: '⚡', skill_view: '⚡',
|
|
72
|
+
execute: '⚡', run: '⚡',
|
|
78
73
|
// Web / browse
|
|
79
|
-
web_search: '🌐',
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
74
|
+
web_search: '🌐', web_fetch: '🌐', fetch_url: '🌐', open_url: '🌐',
|
|
75
|
+
navigate: '🌐', browser: '🌐', fetch: '🌐', search: '🌐',
|
|
76
|
+
// Memory / recall
|
|
77
|
+
recall_session: '🧠', session_search: '🧠', memory: '🧠', recall: '🧠',
|
|
78
|
+
// Think
|
|
79
|
+
session_summary: '🧠', analyze: '🧠', think: '🧠',
|
|
80
|
+
// Skills / catalog
|
|
81
|
+
skills_list: '📋', skill: '📋',
|
|
82
|
+
// Screen / capture
|
|
83
|
+
screenshot: '🖥', computer: '🖥',
|
|
84
|
+
// Media / launch
|
|
85
|
+
now_playing: '▶', app_launch: '▶', media: '▶',
|
|
86
|
+
// Deploy / build
|
|
87
|
+
deploy: '📦', build: '📦', push: '📦',
|
|
88
|
+
// Message / send
|
|
89
|
+
send: '💬', message: '💬', notify: '💬',
|
|
86
90
|
// Verify / test
|
|
87
|
-
verify: '🛡',
|
|
88
|
-
|
|
89
|
-
// Default fallback (matches current behaviour).
|
|
91
|
+
verify: '🛡', test: '🛡', doctor: '🛡', health: '🛡',
|
|
92
|
+
// Default fallback
|
|
90
93
|
default: '·',
|
|
91
94
|
};
|
|
92
95
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* insertion order. Pure — exported for smoke testing.
|
|
96
|
+
* Return the category emoji for `toolName`, or '·' when nothing matches.
|
|
97
|
+
* Delegates to the canonical toolTrail lookup (returns icon only).
|
|
98
|
+
* Exported for backward-compat with existing smoke / unit tests.
|
|
97
99
|
*/
|
|
98
100
|
function iconForTool(toolName) {
|
|
99
|
-
|
|
100
|
-
const exact = exports.TOOL_ICONS[lc];
|
|
101
|
-
if (exact)
|
|
102
|
-
return exact;
|
|
103
|
-
for (const [key, glyph] of Object.entries(exports.TOOL_ICONS)) {
|
|
104
|
-
if (key === 'default')
|
|
105
|
-
continue;
|
|
106
|
-
if (lc.includes(key))
|
|
107
|
-
return glyph;
|
|
108
|
-
}
|
|
109
|
-
return exports.TOOL_ICONS.default;
|
|
101
|
+
return (0, toolTrail_1.iconForTool)(toolName).icon;
|
|
110
102
|
}
|
|
111
103
|
/**
|
|
112
104
|
* Phase 26.2.6 — pool of fun spinner phrases that the chat REPL
|
|
@@ -240,6 +232,15 @@ class Display {
|
|
|
240
232
|
// marked-terminal optional — markdown() falls back to raw text below
|
|
241
233
|
}
|
|
242
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* v4.1.3-repl-polish — public colour gate so callers (e.g. session-end
|
|
237
|
+
* card renderer) can colour text without importing SkinEngine directly.
|
|
238
|
+
* Delegates to the active skin's applyColors(). Monochrome mode is
|
|
239
|
+
* respected the same way as internal calls.
|
|
240
|
+
*/
|
|
241
|
+
applyColors(text, kind) {
|
|
242
|
+
return this.skin.applyColors(text, kind);
|
|
243
|
+
}
|
|
243
244
|
/**
|
|
244
245
|
* Build the welcome banner string (does not write).
|
|
245
246
|
*
|
|
@@ -393,13 +394,18 @@ class Display {
|
|
|
393
394
|
const pill = (on, label, value) => `${dot(on)} ${lab(label)} ${val(value)}`;
|
|
394
395
|
const providerOk = args.providerOk !== false;
|
|
395
396
|
const modelValue = providerOk ? args.model : 'not configured';
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
397
|
+
const pills = [
|
|
398
|
+
pill(args.coreOnline, 'core', args.coreOnline ? 'online' : 'starting'),
|
|
399
|
+
pill(true, 'mode', args.mode),
|
|
400
|
+
pill(providerOk, 'model', modelValue),
|
|
401
|
+
pill(args.memoryActive, 'memory', args.memoryActive ? 'active' : 'off'),
|
|
402
|
+
];
|
|
403
|
+
if (args.version) {
|
|
404
|
+
// Version pill: dot + value, no label (the `v` prefix is the label).
|
|
405
|
+
// Always-on dot — informational, not a health indicator.
|
|
406
|
+
pills.push(`${dot(true)} ${val(`v${args.version}`)}`);
|
|
407
|
+
}
|
|
408
|
+
return ' ' + pills.join(' ');
|
|
403
409
|
}
|
|
404
410
|
/**
|
|
405
411
|
* Two-column block (Environment + Capabilities). Side-by-side when
|
|
@@ -732,55 +738,133 @@ class Display {
|
|
|
732
738
|
// ANSI cursor games on a dumb sink.
|
|
733
739
|
toolRow(name, args) {
|
|
734
740
|
const sk = this.skin;
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
741
|
+
// ── Build the fixed left portion (icon + verb + detail) ────────────
|
|
742
|
+
// v4.1.3-repl-polish: icons default ON; set AIDEN_UI_ICONS=0 to
|
|
743
|
+
// disable (CI / dumb terminals / narrow SSH sessions).
|
|
744
|
+
// Read at call-time so env changes take effect without restart.
|
|
745
|
+
const useIcons = process.env.AIDEN_UI_ICONS !== '0';
|
|
746
|
+
const { icon, verb } = (0, toolTrail_1.iconForTool)(name);
|
|
747
|
+
const glyph = useIcons ? icon : sk.applyColors('·', 'muted');
|
|
748
|
+
// Detail field: v4.1.4-media — consult `buildToolPreview` first so
|
|
749
|
+
// tools registered in `TOOL_PRIMARY_ARG` (media_transport → 'target',
|
|
750
|
+
// media_key → 'action', file_read → 'path', etc.) get their
|
|
751
|
+
// meaningful primary-arg preview instead of a JSON blob. Fall back
|
|
752
|
+
// to the generic `previewToolArgs` scan for tools the map doesn't
|
|
753
|
+
// know about so unregistered MCP tools etc. still render readably.
|
|
754
|
+
const mapped = (0, toolPreview_1.buildToolPreview)(name, args);
|
|
755
|
+
const detail = (0, toolTrail_1.truncDetail)(mapped ?? previewToolArgs(args));
|
|
756
|
+
// v4.1.3-essentials: live tool indicator. Capture wall-clock start
|
|
757
|
+
// so the running-row renderer can append an elapsed-time suffix
|
|
758
|
+
// ("running 3s…") after the first second. Sub-second tools render
|
|
759
|
+
// without the suffix — no flash of `running 0s` for fast paths.
|
|
760
|
+
const startedAt = Date.now();
|
|
761
|
+
// Running row — muted pipe, raw icon, tool-colored verb, muted detail.
|
|
762
|
+
// The optional `running Ns…` tail appears once the tool crosses the
|
|
763
|
+
// 1-second mark; the tick interval below redraws this row every 1s.
|
|
764
|
+
const runningRow = () => {
|
|
765
|
+
const elapsed = Date.now() - startedAt;
|
|
766
|
+
const liveSuffix = elapsed >= 1000
|
|
767
|
+
? ` ${sk.applyColors(`running ${formatToolDuration(elapsed)}…`, 'muted')}`
|
|
768
|
+
: '';
|
|
769
|
+
return `${sk.applyColors(toolTrail_1.TRAIL_PIPE, 'muted')} ${glyph} ` +
|
|
770
|
+
`${sk.applyColors((0, toolTrail_1.padVerb)(verb), 'tool')} ` +
|
|
771
|
+
`${sk.applyColors(detail, 'muted')}${liveSuffix}\n`;
|
|
753
772
|
};
|
|
754
|
-
|
|
773
|
+
// Outcome row — entire line colored by outcome kind.
|
|
774
|
+
const outcomeRow = (suffix, kind) => {
|
|
775
|
+
const content = `${toolTrail_1.TRAIL_PIPE} ${glyph} ${(0, toolTrail_1.padVerb)(verb)} ${detail}` +
|
|
776
|
+
(suffix ? ` ${suffix}` : '');
|
|
777
|
+
return `${sk.applyColors(content, kind)}\n`;
|
|
778
|
+
};
|
|
779
|
+
// Capture stream reference so closures don't need `this`.
|
|
780
|
+
const out = this.out;
|
|
781
|
+
const isTty = !!out.isTTY;
|
|
755
782
|
let printed = false;
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
783
|
+
// v4.1.3-essentials: tick handle for the live-elapsed update. Set
|
|
784
|
+
// when we start the interval; cleared by every terminal method
|
|
785
|
+
// (ok / fail / degraded / blocked / emptyFail / emptyRetry) AND by
|
|
786
|
+
// `retry` (retry is a state announcement and should hold static
|
|
787
|
+
// until the next state change — race-free).
|
|
788
|
+
let tickTimer = null;
|
|
789
|
+
const stopTick = () => {
|
|
790
|
+
if (tickTimer !== null) {
|
|
791
|
+
clearInterval(tickTimer);
|
|
792
|
+
tickTimer = null;
|
|
760
793
|
}
|
|
761
|
-
|
|
794
|
+
};
|
|
795
|
+
// Erase the last printed line (TTY only).
|
|
796
|
+
const eraseLast = () => {
|
|
797
|
+
if (isTty && printed)
|
|
798
|
+
out.write('\x1b[1A\x1b[2K\r');
|
|
799
|
+
};
|
|
800
|
+
const writeFinal = (suffix, kind) => {
|
|
801
|
+
stopTick();
|
|
802
|
+
eraseLast();
|
|
803
|
+
out.write(outcomeRow(suffix, kind));
|
|
762
804
|
printed = true;
|
|
763
805
|
};
|
|
764
806
|
if (isTty) {
|
|
765
|
-
|
|
807
|
+
// v4.1.3-essentials (replaces v4.1.3-repl-polish streamInterrupted
|
|
808
|
+
// flag pattern): if a stream is active, fence off the current
|
|
809
|
+
// chunk BEFORE the running row writes. `commitStreamChunk` does
|
|
810
|
+
// its own newline-fencing + in-place rerender of the just-streamed
|
|
811
|
+
// chunk so this row lands cleanly on its own line below.
|
|
812
|
+
this.commitStreamChunk();
|
|
813
|
+
out.write(runningRow());
|
|
766
814
|
printed = true;
|
|
815
|
+
// v4.1.3-essentials: start the live-elapsed ticker. Fires every
|
|
816
|
+
// 1s; first tick at +1s, when `runningRow()` starts emitting the
|
|
817
|
+
// `running 1s…` suffix. Cleared by every terminal method via
|
|
818
|
+
// `stopTick()` — no leaked timers across the tool lifecycle.
|
|
819
|
+
// Tool dispatch in aidenAgent is sequential (one tool at a time
|
|
820
|
+
// per turn) so the assumption "running row is the last written
|
|
821
|
+
// line" holds for the whole tick lifetime; `eraseLast()` is safe.
|
|
822
|
+
tickTimer = setInterval(() => {
|
|
823
|
+
if (!printed)
|
|
824
|
+
return;
|
|
825
|
+
eraseLast();
|
|
826
|
+
out.write(runningRow());
|
|
827
|
+
}, 1000);
|
|
767
828
|
}
|
|
768
|
-
//
|
|
829
|
+
// Non-TTY: hold off until completion (log lines carry final state).
|
|
830
|
+
// No tick — non-TTY sinks (pipes, CI logs) get one line per call
|
|
831
|
+
// with the final state; live updates would be noise in scrollback.
|
|
769
832
|
return {
|
|
770
833
|
ok(durationMs, retries = 0) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
834
|
+
stopTick();
|
|
835
|
+
if (retries > 0) {
|
|
836
|
+
// Showed retries — surface the eventual success in warn so the
|
|
837
|
+
// user knows it took multiple attempts.
|
|
838
|
+
writeFinal(`ok ${formatToolDuration(durationMs)} after ${retries} ${retries === 1 ? 'retry' : 'retries'}`, 'warn');
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
// Clean success — SILENT. Erase on TTY; emit nothing on non-TTY.
|
|
842
|
+
eraseLast();
|
|
843
|
+
}
|
|
775
844
|
},
|
|
776
845
|
fail(durationMs, retries = 0) {
|
|
777
|
-
const
|
|
846
|
+
const suffix = retries > 0
|
|
778
847
|
? `fail ${formatToolDuration(durationMs)} after ${retries} ${retries === 1 ? 'retry' : 'retries'}`
|
|
779
848
|
: `fail ${formatToolDuration(durationMs)}`;
|
|
780
|
-
writeFinal(
|
|
849
|
+
writeFinal(suffix, 'error');
|
|
850
|
+
},
|
|
851
|
+
degraded(durationMs, reason) {
|
|
852
|
+
const suffix = reason
|
|
853
|
+
? `partial ${formatToolDuration(durationMs)} — ${reason}`
|
|
854
|
+
: `partial ${formatToolDuration(durationMs)}`;
|
|
855
|
+
writeFinal(suffix, 'degraded');
|
|
781
856
|
},
|
|
782
857
|
retry(n, m) {
|
|
783
|
-
|
|
858
|
+
// v4.1.3-essentials: retry is a state-change announcement —
|
|
859
|
+
// freeze the row at the retry counter until next state change.
|
|
860
|
+
// Stopping the ticker prevents the next 1s tick from racing
|
|
861
|
+
// back over the retry counter with `running Ns…`.
|
|
862
|
+
stopTick();
|
|
863
|
+
// Update the running row with retry count.
|
|
864
|
+
eraseLast();
|
|
865
|
+
const content = `${toolTrail_1.TRAIL_PIPE} ${glyph} ${(0, toolTrail_1.padVerb)(verb)} ${detail} retry ${n}/${m} …`;
|
|
866
|
+
out.write(sk.applyColors(content, 'warn') + '\n');
|
|
867
|
+
printed = true;
|
|
784
868
|
},
|
|
785
869
|
blocked() {
|
|
786
870
|
writeFinal('blocked', 'warn');
|
|
@@ -794,11 +878,24 @@ class Display {
|
|
|
794
878
|
};
|
|
795
879
|
}
|
|
796
880
|
/**
|
|
797
|
-
* Pretty-print a tool call before it executes.
|
|
798
|
-
*
|
|
881
|
+
* Pretty-print a tool call before it executes. Phase v4.1.2 first
|
|
882
|
+
* consults the `TOOL_PRIMARY_ARG` map in `toolPreview.ts` to render
|
|
883
|
+
* just the meaningful argument (e.g. `terminal: npm test`); falls
|
|
884
|
+
* back to the legacy full-JSON stringification (200-char hard cap)
|
|
885
|
+
* for tools that aren't in the map.
|
|
799
886
|
*/
|
|
800
887
|
toolPreview(name, args) {
|
|
801
888
|
const sk = this.skin;
|
|
889
|
+
const arrow = sk.getActive().glyphs?.arrow ?? '>';
|
|
890
|
+
// Phase v4.1.2: per-tool primary-arg preview.
|
|
891
|
+
const preview = (0, toolPreview_1.buildToolPreview)(name, args);
|
|
892
|
+
if (preview !== null) {
|
|
893
|
+
if (preview === '') {
|
|
894
|
+
return `${sk.applyColors(arrow, 'tool')} ${sk.applyColors(name, 'tool')}`;
|
|
895
|
+
}
|
|
896
|
+
return `${sk.applyColors(arrow, 'tool')} ${sk.applyColors(name, 'tool')} ${sk.applyColors(preview, 'muted')}`;
|
|
897
|
+
}
|
|
898
|
+
// Unknown tool — original behaviour (full JSON, 200-char cap).
|
|
802
899
|
let serialized;
|
|
803
900
|
try {
|
|
804
901
|
serialized = JSON.stringify(args);
|
|
@@ -808,7 +905,6 @@ class Display {
|
|
|
808
905
|
}
|
|
809
906
|
if (serialized.length > 200)
|
|
810
907
|
serialized = `${serialized.slice(0, 197)}...`;
|
|
811
|
-
const arrow = sk.getActive().glyphs?.arrow ?? '>';
|
|
812
908
|
return `${sk.applyColors(arrow, 'tool')} ${sk.applyColors(name, 'tool')} ${sk.applyColors(serialized, 'muted')}`;
|
|
813
909
|
}
|
|
814
910
|
/**
|
|
@@ -955,6 +1051,30 @@ class Display {
|
|
|
955
1051
|
printError(message, suggestion) {
|
|
956
1052
|
this.out.write(this.error(message, suggestion));
|
|
957
1053
|
}
|
|
1054
|
+
/**
|
|
1055
|
+
* v4.1.3-essentials: render a structured capability card. Used when
|
|
1056
|
+
* a tool fails because a capability is missing (platform unsupported,
|
|
1057
|
+
* auth not present, key env-var unset) and a generic error wouldn't
|
|
1058
|
+
* give the user enough signal. The card lists what the user CAN
|
|
1059
|
+
* still do, what's blocked, and a one-line fix.
|
|
1060
|
+
*
|
|
1061
|
+
* Delegates the layout to the pure renderer in `./display/capability-
|
|
1062
|
+
* Card.ts`. This method is the I/O boundary — caller passes data, we
|
|
1063
|
+
* write lines to the configured stdout. Trailing newline ensures the
|
|
1064
|
+
* card sits clean above whatever renders next (typically the prompt).
|
|
1065
|
+
*/
|
|
1066
|
+
capabilityCard(data) {
|
|
1067
|
+
// v4.1.3-essentials: fence off any active stream chunk before the
|
|
1068
|
+
// card writes so the chunk gets rerendered as markdown and the
|
|
1069
|
+
// card lands below it on its own line. Same pattern as toolRow /
|
|
1070
|
+
// streamToolIndicator.
|
|
1071
|
+
this.commitStreamChunk();
|
|
1072
|
+
const lines = (0, capabilityCard_1.renderCapabilityCard)(data, (t, k) => this.applyColors(t, k));
|
|
1073
|
+
for (const line of lines) {
|
|
1074
|
+
this.out.write(line + '\n');
|
|
1075
|
+
}
|
|
1076
|
+
this.out.write('\n');
|
|
1077
|
+
}
|
|
958
1078
|
/**
|
|
959
1079
|
* Append a streamed text fragment. Writes a styled "Aiden" header on
|
|
960
1080
|
* the first call of a turn, then writes raw text directly via the
|
|
@@ -987,51 +1107,59 @@ class Display {
|
|
|
987
1107
|
this.streamLineCount += 1;
|
|
988
1108
|
}
|
|
989
1109
|
/**
|
|
990
|
-
*
|
|
991
|
-
*
|
|
992
|
-
*
|
|
993
|
-
*
|
|
1110
|
+
* v4.1.3-essentials: rerender a buffered stream chunk in place. Walks
|
|
1111
|
+
* the cursor back `lines` rows, erases to end-of-screen, and reprints
|
|
1112
|
+
* the chunk as skin-aware markdown.
|
|
1113
|
+
*
|
|
1114
|
+
* Pure side-effect; returns nothing. Used by:
|
|
1115
|
+
* - `commitStreamChunk()` — when a tool row interrupts the
|
|
1116
|
+
* stream, render the pre-tool
|
|
1117
|
+
* chunk before the row writes.
|
|
1118
|
+
* - `streamComplete()` — final chunk at end of turn.
|
|
1119
|
+
*
|
|
1120
|
+
* Heuristic gate avoids flicker on plain prose (no structure → no
|
|
1121
|
+
* rerender, no eraser fires). The catch block writes the RAW buffered
|
|
1122
|
+
* text as a fallback if `marked` throws — without this the eraser
|
|
1123
|
+
* would already have run and the body would silently vanish.
|
|
1124
|
+
* v4.1.3-essentials raw-text fallback per "make state legible" thesis.
|
|
994
1125
|
*/
|
|
995
|
-
|
|
996
|
-
if (!this.streamHeaderShown)
|
|
997
|
-
return;
|
|
998
|
-
if (!this.streamLastEndedNewline)
|
|
999
|
-
this.out.write('\n');
|
|
1000
|
-
// Phase v4.1-reply-formatting: re-render the buffered stream as
|
|
1001
|
-
// structured markdown — but ONLY when stdout is a TTY and the
|
|
1002
|
-
// buffer actually contains markdown structure worth rendering.
|
|
1003
|
-
// Plain prose with no headers / lists / fences gets left alone
|
|
1004
|
-
// (no flicker, identical output). Otherwise we erase the raw
|
|
1005
|
-
// streamed body via cursor-up + erase-line and reprint via the
|
|
1006
|
-
// skin-aware renderer.
|
|
1007
|
-
const buffered = this.streamBuffer;
|
|
1008
|
-
const lines = this.streamLineCount;
|
|
1009
|
-
this.streamBuffer = '';
|
|
1010
|
-
this.streamLineCount = 0;
|
|
1011
|
-
this.streamHeaderShown = false;
|
|
1012
|
-
this.streamLastEndedNewline = false;
|
|
1126
|
+
tryRerenderInPlace(buffered, lines) {
|
|
1013
1127
|
if (!this.out.isTTY)
|
|
1014
1128
|
return;
|
|
1015
1129
|
if (process.env.AIDEN_NO_REFORMAT === '1')
|
|
1016
1130
|
return;
|
|
1017
|
-
|
|
1018
|
-
|
|
1131
|
+
if (lines === 0)
|
|
1132
|
+
return;
|
|
1133
|
+
// Cheap structural heuristic — only re-render when formatting
|
|
1134
|
+
// actually helps. Plain prose chunks stay raw (no flicker).
|
|
1135
|
+
//
|
|
1136
|
+
// v4.1.3-essentials post-ship: inline `**bold**` and `` `code` ``
|
|
1137
|
+
// added to the heuristic. Before this, a chunk that contained ONLY
|
|
1138
|
+
// inline markdown (no headings / lists / code blocks) skipped
|
|
1139
|
+
// rerender entirely, leaving the literal `**bold**` asterisks in
|
|
1140
|
+
// user-visible output. The `paintBoldWhite` strong renderer was
|
|
1141
|
+
// never invoked for those chunks.
|
|
1142
|
+
//
|
|
1143
|
+
// Patterns:
|
|
1144
|
+
// - `**bold**`: requires non-space immediately after the opening
|
|
1145
|
+
// `**` so `2 ** 3` math expressions don't false-positive.
|
|
1146
|
+
// Tolerates multi-line bold via `[\s\S]*?`.
|
|
1147
|
+
// - `` `code` ``: negative-lookarounds for the triple-backtick
|
|
1148
|
+
// fence so we don't double-trigger when ``` lines are present
|
|
1149
|
+
// (those already match the fence pattern above).
|
|
1019
1150
|
const hasStructure = /^#{1,6}\s/m.test(buffered) ||
|
|
1020
1151
|
/^\s*[-*+]\s/m.test(buffered) ||
|
|
1021
1152
|
/^\s*\d+\.\s/m.test(buffered) ||
|
|
1022
1153
|
/^>\s/m.test(buffered) ||
|
|
1023
|
-
/```/.test(buffered)
|
|
1154
|
+
/```/.test(buffered) ||
|
|
1155
|
+
/\*\*\S[\s\S]*?\*\*/.test(buffered) ||
|
|
1156
|
+
/(?<![`])`[^`\n]+`(?![`])/.test(buffered);
|
|
1024
1157
|
if (!hasStructure)
|
|
1025
1158
|
return;
|
|
1026
1159
|
try {
|
|
1027
|
-
//
|
|
1028
|
-
//
|
|
1029
|
-
|
|
1030
|
-
// `\x1b[<n>F` = cursor-up-and-to-column-0 N times.
|
|
1031
|
-
// `\x1b[J` = erase from cursor to end of screen.
|
|
1032
|
-
if (lines > 0) {
|
|
1033
|
-
this.out.write(`\x1b[${lines}F\x1b[J`);
|
|
1034
|
-
}
|
|
1160
|
+
// \x1b[<n>F = cursor-up-and-to-column-0 N times.
|
|
1161
|
+
// \x1b[J = erase from cursor to end of screen.
|
|
1162
|
+
this.out.write(`\x1b[${lines}F\x1b[J`);
|
|
1035
1163
|
const formatted = this.markdown(buffered).trimEnd();
|
|
1036
1164
|
const indented = formatted
|
|
1037
1165
|
.split('\n')
|
|
@@ -1040,11 +1168,127 @@ class Display {
|
|
|
1040
1168
|
this.out.write(indented + '\n');
|
|
1041
1169
|
}
|
|
1042
1170
|
catch {
|
|
1043
|
-
//
|
|
1044
|
-
//
|
|
1045
|
-
//
|
|
1171
|
+
// Eraser already ran. v4.1.3-essentials: write the raw buffered
|
|
1172
|
+
// text back so the body doesn't vanish silently. The user sees
|
|
1173
|
+
// unformatted markdown rather than a missing reply — the honest
|
|
1174
|
+
// failure mode.
|
|
1175
|
+
this.out.write(buffered);
|
|
1176
|
+
if (!buffered.endsWith('\n'))
|
|
1177
|
+
this.out.write('\n');
|
|
1046
1178
|
}
|
|
1047
1179
|
}
|
|
1180
|
+
/**
|
|
1181
|
+
* v4.1.3-essentials: fence off the current stream chunk before a
|
|
1182
|
+
* non-stream write (tool row, tool indicator, capability card) lands.
|
|
1183
|
+
*
|
|
1184
|
+
* Replaces the v4.1.3-repl-polish `streamInterrupted` flag pattern.
|
|
1185
|
+
* Old pattern: set flag mid-stream → on streamComplete, check flag
|
|
1186
|
+
* and SKIP rerender entirely (lost markdown on every tool-using
|
|
1187
|
+
* turn). New pattern: at each interrupt point, eagerly rerender THIS
|
|
1188
|
+
* chunk in place, then reset the per-chunk window so the next
|
|
1189
|
+
* streamPartial starts a fresh count. The cursor is at the end of
|
|
1190
|
+
* this chunk when commit fires, so `streamLineCount` is correct for
|
|
1191
|
+
* the eraser — tool rows write below without being clobbered.
|
|
1192
|
+
*
|
|
1193
|
+
* Multi-chunk turns (model says X, calls tool, says Y, calls tool,
|
|
1194
|
+
* says Z) get all three chunks rerendered as markdown.
|
|
1195
|
+
*
|
|
1196
|
+
* Idempotent: no-op when no stream cycle is active or when the buffer
|
|
1197
|
+
* is empty (consecutive tool calls). Always ensures the cursor sits
|
|
1198
|
+
* at start-of-line before returning so the caller can write its own
|
|
1199
|
+
* row cleanly.
|
|
1200
|
+
*/
|
|
1201
|
+
commitStreamChunk() {
|
|
1202
|
+
if (!this.streamHeaderShown)
|
|
1203
|
+
return;
|
|
1204
|
+
// Ensure the streamed chunk ends with a newline so the interrupt
|
|
1205
|
+
// row doesn't stick to mid-token text from the prior delta.
|
|
1206
|
+
if (!this.streamLastEndedNewline) {
|
|
1207
|
+
this.out.write('\n');
|
|
1208
|
+
this.streamLastEndedNewline = true;
|
|
1209
|
+
// The trailing newline we just wrote DOES bump the cursor's row,
|
|
1210
|
+
// but only by 1 — and `streamLineCount` should reflect physical
|
|
1211
|
+
// rows of the chunk. Add it so the eraser walks back the right
|
|
1212
|
+
// amount.
|
|
1213
|
+
this.streamLineCount += 1;
|
|
1214
|
+
}
|
|
1215
|
+
// v4.1.3-essentials boldwrap-fix: if the chunk ends mid-bold-pair
|
|
1216
|
+
// (e.g. tool fired between the model emitting `**` and the closing
|
|
1217
|
+
// `**`), splitting here would leave literal asterisks in the
|
|
1218
|
+
// rerendered output and a matching orphan in the next chunk.
|
|
1219
|
+
// `splitAtUnclosedBold` finds the last unmatched `**` and carves
|
|
1220
|
+
// the buffer into two parts: the closed-bold prefix we CAN
|
|
1221
|
+
// rerender now, and the carry tail that we keep for the next
|
|
1222
|
+
// chunk (where the closing `**` will eventually arrive).
|
|
1223
|
+
//
|
|
1224
|
+
// Code-fence safety (carried in the helper): if the would-be
|
|
1225
|
+
// unmatched `**` is inside an open ``` fence, we defer the whole
|
|
1226
|
+
// chunk — bold-syntax inside code blocks isn't markdown bold and
|
|
1227
|
+
// splitting there would corrupt the fence.
|
|
1228
|
+
const split = splitAtUnclosedBold(this.streamBuffer);
|
|
1229
|
+
if (split.carry === '') {
|
|
1230
|
+
// Common case: buffer is balanced (or has no `**` at all).
|
|
1231
|
+
// Same behavior as before — rerender the whole chunk in place
|
|
1232
|
+
// and reset the per-chunk window.
|
|
1233
|
+
this.tryRerenderInPlace(this.streamBuffer, this.streamLineCount);
|
|
1234
|
+
this.streamBuffer = '';
|
|
1235
|
+
this.streamLineCount = 0;
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
// Split path: erase the WHOLE chunk (because the cursor is at the
|
|
1239
|
+
// end of the full buffer), rerender the closed prefix, then
|
|
1240
|
+
// re-emit the carry as raw text. The carry visibly stays on
|
|
1241
|
+
// screen as raw `**Live tool indi`-style text — ugly for the
|
|
1242
|
+
// ~milliseconds until the next streamPartial extends it past the
|
|
1243
|
+
// closing `**`, at which point the next commit will rerender
|
|
1244
|
+
// cleanly.
|
|
1245
|
+
const rerenderableLines = countNewlines(split.rerenderable);
|
|
1246
|
+
const carryLines = this.streamLineCount - rerenderableLines;
|
|
1247
|
+
if (this.out.isTTY && this.streamLineCount > 0) {
|
|
1248
|
+
// \x1b[<n>F = cursor-up-and-to-column-0 N times.
|
|
1249
|
+
// \x1b[J = erase from cursor to end of screen.
|
|
1250
|
+
this.out.write(`\x1b[${this.streamLineCount}F\x1b[J`);
|
|
1251
|
+
}
|
|
1252
|
+
// Rerender the closed prefix (handles its own heuristic gate
|
|
1253
|
+
// internally — a prefix without structure stays raw, which is
|
|
1254
|
+
// identical to the pre-split behavior).
|
|
1255
|
+
this.tryRerenderInPlace(split.rerenderable, rerenderableLines);
|
|
1256
|
+
// Re-emit the carry verbatim. It's intentionally raw because the
|
|
1257
|
+
// unmatched `**` can't be rendered without its closing pair.
|
|
1258
|
+
this.out.write(split.carry);
|
|
1259
|
+
// Reset the per-chunk window to the carry only. Next streamPartial
|
|
1260
|
+
// extends it; when the closing `**` lands, the next commit (or
|
|
1261
|
+
// streamComplete) rerenders cleanly.
|
|
1262
|
+
this.streamBuffer = split.carry;
|
|
1263
|
+
this.streamLineCount = carryLines;
|
|
1264
|
+
this.streamLastEndedNewline = split.carry.endsWith('\n');
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Mark the end of a streaming turn. Adds a trailing newline if the
|
|
1268
|
+
* stream didn't end with one so the next CLI line doesn't visually
|
|
1269
|
+
* butt up against the model's last token. Rerenders the FINAL chunk
|
|
1270
|
+
* (post-last-tool prose, or the whole body if no tools fired this
|
|
1271
|
+
* turn) and resets the per-turn state so the next `streamPartial`
|
|
1272
|
+
* re-emits the header.
|
|
1273
|
+
*/
|
|
1274
|
+
streamComplete() {
|
|
1275
|
+
if (!this.streamHeaderShown)
|
|
1276
|
+
return;
|
|
1277
|
+
if (!this.streamLastEndedNewline) {
|
|
1278
|
+
this.out.write('\n');
|
|
1279
|
+
this.streamLineCount += 1;
|
|
1280
|
+
}
|
|
1281
|
+
// Final chunk: same in-place rerender path as commitStreamChunk
|
|
1282
|
+
// (factored shared helper). Tool-row interrupts have already
|
|
1283
|
+
// committed their preceding chunks; what's left in the buffer here
|
|
1284
|
+
// is the post-final-tool prose — typically the bulk of the
|
|
1285
|
+
// user-visible body in well-behaved turns.
|
|
1286
|
+
this.tryRerenderInPlace(this.streamBuffer, this.streamLineCount);
|
|
1287
|
+
this.streamBuffer = '';
|
|
1288
|
+
this.streamLineCount = 0;
|
|
1289
|
+
this.streamHeaderShown = false;
|
|
1290
|
+
this.streamLastEndedNewline = false;
|
|
1291
|
+
}
|
|
1048
1292
|
/**
|
|
1049
1293
|
* Phase v4.1-reply-formatting: render the optional "Sources"
|
|
1050
1294
|
* footer when AIDEN_CITATIONS=1 and the trace has fetch-class
|
|
@@ -1067,10 +1311,15 @@ class Display {
|
|
|
1067
1311
|
* newline if the prior delta ran past column N without one.
|
|
1068
1312
|
*/
|
|
1069
1313
|
streamToolIndicator(name) {
|
|
1314
|
+
// v4.1.3-essentials (replaces v4.1.3-repl-polish streamInterrupted
|
|
1315
|
+
// flag pattern): fence off the streamed chunk before the indicator
|
|
1316
|
+
// writes. `commitStreamChunk` handles the newline-or-not and
|
|
1317
|
+
// in-place rerenders the pre-indicator chunk so this row lands
|
|
1318
|
+
// below well-formed markdown rather than below raw streamed text.
|
|
1319
|
+
this.commitStreamChunk();
|
|
1070
1320
|
const sk = this.skin;
|
|
1071
1321
|
const arrow = sk.getActive().glyphs?.arrow ?? '>';
|
|
1072
|
-
|
|
1073
|
-
this.out.write(`${prefix}${sk.applyColors(`${arrow} ${name}…`, 'tool')}\n`);
|
|
1322
|
+
this.out.write(`${sk.applyColors(`${arrow} ${name}…`, 'tool')}\n`);
|
|
1074
1323
|
this.streamLastEndedNewline = true;
|
|
1075
1324
|
}
|
|
1076
1325
|
}
|
|
@@ -1126,11 +1375,29 @@ function renderRmsBar(rms) {
|
|
|
1126
1375
|
const filled = Math.round((safe / BAR_FULL_RMS) * BAR_WIDTH);
|
|
1127
1376
|
return '▌'.repeat(filled) + ' '.repeat(BAR_WIDTH - filled);
|
|
1128
1377
|
}
|
|
1129
|
-
// ── Phase 23.5 — tool row helpers
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1378
|
+
// ── Phase 23.5 / v4.1.3-repl-polish — tool row helpers ────────────────
|
|
1379
|
+
//
|
|
1380
|
+
// v4.1.3 changes the row format from:
|
|
1381
|
+
// " {glyph} tool {name:16} {args} [{state}]"
|
|
1382
|
+
// to the compact trail format:
|
|
1383
|
+
// "┊ {icon} {verb:12} {detail:40}"
|
|
1384
|
+
//
|
|
1385
|
+
// Outcome semantics:
|
|
1386
|
+
// ok() → SILENT — running row is erased; nothing persists.
|
|
1387
|
+
// fail() → row persists in error (red).
|
|
1388
|
+
// degraded() → row persists in degraded (yellow). NEW in v4.1.3.
|
|
1389
|
+
// blocked() → row persists in warn.
|
|
1390
|
+
// retry() → running row is updated with retry counter.
|
|
1391
|
+
// emptyFail() → treated as fail (error colour).
|
|
1392
|
+
// emptyRetry()→ treated as retry.
|
|
1393
|
+
/**
|
|
1394
|
+
* Kept for reference / smoke-test backward compat. New code should use
|
|
1395
|
+
* TRAIL_VERB_PAD / TRAIL_DETAIL_CAP from toolTrail.ts instead.
|
|
1396
|
+
* @deprecated
|
|
1397
|
+
*/
|
|
1398
|
+
exports.TOOL_ROW_NAME_PAD = toolTrail_1.TRAIL_VERB_PAD;
|
|
1399
|
+
/** @deprecated Use TRAIL_DETAIL_CAP. */
|
|
1400
|
+
exports.TOOL_ROW_ARG_CAP = toolTrail_1.TRAIL_DETAIL_CAP;
|
|
1134
1401
|
/**
|
|
1135
1402
|
* Build a compact, single-line preview of the tool's arguments. Picks
|
|
1136
1403
|
* the most informative scalar fields when the args are an object, then
|
|
@@ -1166,13 +1433,96 @@ function previewToolArgs(args) {
|
|
|
1166
1433
|
catch {
|
|
1167
1434
|
serialized = String(obj);
|
|
1168
1435
|
}
|
|
1436
|
+
// v4.1.4-media: an empty object serializes to '{}'. Rendering that
|
|
1437
|
+
// literal in the trail row is honest but ugly and reads as "buggy
|
|
1438
|
+
// empty args". When the model legitimately passes an empty args
|
|
1439
|
+
// object (e.g. `media_sessions({})`, `system_info()`), show nothing
|
|
1440
|
+
// rather than the braces — `buildToolPreview` already does this for
|
|
1441
|
+
// tools mapped in `TOOL_PRIMARY_ARG`; here we extend the same UX to
|
|
1442
|
+
// any unmapped-tool fallback that bottoms out at `{}`.
|
|
1443
|
+
if (serialized === '{}')
|
|
1444
|
+
return '';
|
|
1169
1445
|
return truncToolArg(serialized);
|
|
1170
1446
|
}
|
|
1171
1447
|
function truncToolArg(s) {
|
|
1172
1448
|
const flat = s.replace(/\s+/g, ' ').trim();
|
|
1173
|
-
if (flat.length <= TOOL_ROW_ARG_CAP)
|
|
1449
|
+
if (flat.length <= exports.TOOL_ROW_ARG_CAP)
|
|
1174
1450
|
return flat;
|
|
1175
|
-
return flat.slice(0, TOOL_ROW_ARG_CAP - 1) + '…';
|
|
1451
|
+
return flat.slice(0, exports.TOOL_ROW_ARG_CAP - 1) + '…';
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* v4.1.3-essentials boldwrap-fix: count `\n` occurrences in `s`.
|
|
1455
|
+
* Used by `commitStreamChunk` to recompute `streamLineCount` after
|
|
1456
|
+
* splitting a buffer at an unclosed-bold boundary. Pure helper —
|
|
1457
|
+
* exported for unit-test access.
|
|
1458
|
+
*/
|
|
1459
|
+
function countNewlines(s) {
|
|
1460
|
+
let n = 0;
|
|
1461
|
+
for (let i = 0; i < s.length; i += 1)
|
|
1462
|
+
if (s[i] === '\n')
|
|
1463
|
+
n += 1;
|
|
1464
|
+
return n;
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* v4.1.3-essentials boldwrap-fix: split a streamed-chunk buffer at the
|
|
1468
|
+
* last unmatched `**` so the closed-bold prefix can be rerendered now
|
|
1469
|
+
* and the open-bold tail can be carried into the next chunk.
|
|
1470
|
+
*
|
|
1471
|
+
* Returns `{ rerenderable, carry }`:
|
|
1472
|
+
* - rerenderable: the prefix with all `**` pairs balanced
|
|
1473
|
+
* - carry: the suffix starting at the last unmatched `**`
|
|
1474
|
+
*
|
|
1475
|
+
* `carry === ''` signals "balanced — render the whole buffer". Caller
|
|
1476
|
+
* uses this as the fast-path discriminator.
|
|
1477
|
+
*
|
|
1478
|
+
* Code-fence safety: if the buffer contains an UNCLOSED fenced code
|
|
1479
|
+
* block (` ``` ` count is odd), defer the entire chunk by returning
|
|
1480
|
+
* `{ rerenderable: '', carry: buffer }`. Bold-syntax inside code
|
|
1481
|
+
* blocks is literal text — splitting there would corrupt the fence
|
|
1482
|
+
* AND likely produce nonsensical rerender output. Trade-off: a chunk
|
|
1483
|
+
* that ends mid-code-block doesn't rerender at all until the closing
|
|
1484
|
+
* ``` arrives; acceptable because code blocks have their own
|
|
1485
|
+
* styling (dark bg + left rail) that doesn't depend on the markdown
|
|
1486
|
+
* rerender step.
|
|
1487
|
+
*
|
|
1488
|
+
* Pure function. Tested via `tests/v4/cli/display.test.ts`.
|
|
1489
|
+
*/
|
|
1490
|
+
function splitAtUnclosedBold(buffer) {
|
|
1491
|
+
// Fast path: no `**` at all → balanced.
|
|
1492
|
+
if (!buffer.includes('**'))
|
|
1493
|
+
return { rerenderable: buffer, carry: '' };
|
|
1494
|
+
// Code-fence safety: count triple-backtick fences. Odd = open fence,
|
|
1495
|
+
// defer the whole buffer.
|
|
1496
|
+
const fenceMatches = buffer.match(/```/g);
|
|
1497
|
+
if (fenceMatches && fenceMatches.length % 2 === 1) {
|
|
1498
|
+
return { rerenderable: '', carry: buffer };
|
|
1499
|
+
}
|
|
1500
|
+
// Count `**` occurrences. Even → balanced. Odd → there's an
|
|
1501
|
+
// unmatched `**` — find the LAST one (the open).
|
|
1502
|
+
const positions = [];
|
|
1503
|
+
for (let i = 0; i < buffer.length - 1; i += 1) {
|
|
1504
|
+
if (buffer[i] === '*' && buffer[i + 1] === '*') {
|
|
1505
|
+
positions.push(i);
|
|
1506
|
+
i += 1; // skip the second `*` so `***` doesn't double-count
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (positions.length % 2 === 0) {
|
|
1510
|
+
return { rerenderable: buffer, carry: '' };
|
|
1511
|
+
}
|
|
1512
|
+
const lastUnmatched = positions[positions.length - 1];
|
|
1513
|
+
// Inline-backtick safety: if the unmatched `**` sits inside an
|
|
1514
|
+
// open single-backtick span on the same line, the `**` is literal
|
|
1515
|
+
// code, not a bold marker. Defer the whole chunk.
|
|
1516
|
+
const lineStart = buffer.lastIndexOf('\n', lastUnmatched) + 1;
|
|
1517
|
+
const lineUpToBold = buffer.slice(lineStart, lastUnmatched);
|
|
1518
|
+
const backticksOnLine = (lineUpToBold.match(/`/g) ?? []).length;
|
|
1519
|
+
if (backticksOnLine % 2 === 1) {
|
|
1520
|
+
return { rerenderable: '', carry: buffer };
|
|
1521
|
+
}
|
|
1522
|
+
return {
|
|
1523
|
+
rerenderable: buffer.slice(0, lastUnmatched),
|
|
1524
|
+
carry: buffer.slice(lastUnmatched),
|
|
1525
|
+
};
|
|
1176
1526
|
}
|
|
1177
1527
|
/**
|
|
1178
1528
|
* Render a tool-call duration in the bracket cluster. Sub-second
|