aiden-runtime 4.1.3 → 4.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/v4/callbacks.js +70 -13
- package/dist/cli/v4/chatSession.js +130 -22
- package/dist/cli/v4/defaultSoul.js +69 -2
- package/dist/cli/v4/display/frame.js +234 -0
- package/dist/cli/v4/display/progressBar.js +137 -0
- package/dist/cli/v4/display.js +427 -21
- package/dist/cli/v4/replyRenderer.js +196 -26
- package/dist/cli/v4/skinEngine.js +15 -4
- package/dist/cli/v4/toolPreview.js +68 -19
- package/dist/core/v4/aidenAgent.js +9 -0
- package/dist/core/v4/promptBuilder.js +2 -1
- package/dist/core/version.js +1 -1
- package/dist/providers/v4/anthropicAdapter.js +25 -2
- package/package.json +2 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* cli/v4/display/progressBar.ts — Phase v4.1.4 Part 1.6.
|
|
10
|
+
*
|
|
11
|
+
* Per-turn token progress bar. Renders `▰▰▰▰▰▱▱▱▱▱ 412/4096 tokens`
|
|
12
|
+
* on a single line below the activity indicator (or in place of it,
|
|
13
|
+
* once the stream takes over). Event-driven: each `update(n, max)`
|
|
14
|
+
* call redraws the bar with the new counter — no `setInterval`,
|
|
15
|
+
* because the source of truth is the adapter's incremental
|
|
16
|
+
* `progress` stream events, which already fire at the granularity
|
|
17
|
+
* the model produces tokens.
|
|
18
|
+
*
|
|
19
|
+
* Honest degradation: if the adapter never calls `update()`, the bar
|
|
20
|
+
* never paints. No client-side estimation (per v4.1.4 spec — token
|
|
21
|
+
* count only, no time-based fakery).
|
|
22
|
+
*
|
|
23
|
+
* Visual:
|
|
24
|
+
*
|
|
25
|
+
* ▰▰▰▰▰▱▱▱▱▱ 412/4096 tokens
|
|
26
|
+
* │└────┬───┘ └────┬─────┘
|
|
27
|
+
* │ │ └── current/max (compact via formatCompactTokens)
|
|
28
|
+
* │ └── 10 cells, filled ratio ∝ outputTokens/maxTokens
|
|
29
|
+
* └── leading gutter aligns with frame
|
|
30
|
+
*
|
|
31
|
+
* Bar cells:
|
|
32
|
+
* - filled: ▰ (U+25B0, dark shade block-fill)
|
|
33
|
+
* - empty: ▱ (U+25B1, light shade)
|
|
34
|
+
*
|
|
35
|
+
* Cursor invariant on render: bar OWNS one line. After each `update`
|
|
36
|
+
* the cursor sits at column 0 of the bar line (single-line `\r\x1b[K`
|
|
37
|
+
* overwrite pattern, same as activityIndicator). Callers that want to
|
|
38
|
+
* write OTHER content below MUST call `hide()` first.
|
|
39
|
+
*
|
|
40
|
+
* Non-TTY: completely silent — pipes/CI/MCP serve mode get clean
|
|
41
|
+
* output by default. The handle still accepts updates so callers
|
|
42
|
+
* don't need to branch on TTY-ness.
|
|
43
|
+
*/
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.createProgressBar = createProgressBar;
|
|
46
|
+
const frame_1 = require("./frame");
|
|
47
|
+
const display_1 = require("../display");
|
|
48
|
+
/**
|
|
49
|
+
* Number of bar cells. 10 cells gives clean fractions (every 10% of
|
|
50
|
+
* fill ratio == one cell). Wider bars feel noisy at the standard
|
|
51
|
+
* frame width (75 visible cols on an 80-col terminal).
|
|
52
|
+
*/
|
|
53
|
+
const BAR_CELLS = 10;
|
|
54
|
+
/** Glyphs. Chosen for clean visual weight at the standard mono font. */
|
|
55
|
+
const FILLED = '▰';
|
|
56
|
+
const EMPTY = '▱';
|
|
57
|
+
/**
|
|
58
|
+
* Create a progress-bar handle bound to a writable stream + skin.
|
|
59
|
+
* The bar paints on the next `update` call — there's no initial
|
|
60
|
+
* paint at creation time, because we don't know the token counts yet.
|
|
61
|
+
*
|
|
62
|
+
* `out` is the stream to write on (usually `process.stdout` via
|
|
63
|
+
* `Display.out`). `skin` is the active skin engine. Both are captured
|
|
64
|
+
* by closure — the handle survives skin swaps for the rest of the turn.
|
|
65
|
+
*/
|
|
66
|
+
function createProgressBar(out, skin) {
|
|
67
|
+
const isTty = !!out.isTTY;
|
|
68
|
+
let outputTokens = 0;
|
|
69
|
+
let maxTokens = undefined;
|
|
70
|
+
let printed = false;
|
|
71
|
+
let hidden = false;
|
|
72
|
+
let lastPaintTokens = -1;
|
|
73
|
+
const buildLine = () => {
|
|
74
|
+
// Fill ratio: 0..1, then snap to a cell count 0..BAR_CELLS.
|
|
75
|
+
const denom = maxTokens && maxTokens > 0 ? maxTokens : 0;
|
|
76
|
+
const ratio = denom > 0 ? Math.min(1, outputTokens / denom) : 0;
|
|
77
|
+
const filled = Math.min(BAR_CELLS, Math.round(ratio * BAR_CELLS));
|
|
78
|
+
const empty = BAR_CELLS - filled;
|
|
79
|
+
const bar = skin.applyColors(FILLED.repeat(filled), 'brand') +
|
|
80
|
+
skin.applyColors(EMPTY.repeat(empty), 'muted');
|
|
81
|
+
// Label: compact "412/4096 tokens". If no maxTokens, render
|
|
82
|
+
// "412 tokens" (denominator unknown). The model name doesn't
|
|
83
|
+
// belong here — that's the status footer's job post-turn.
|
|
84
|
+
const left = (0, display_1.formatCompactTokens)(outputTokens);
|
|
85
|
+
const right = denom > 0 ? (0, display_1.formatCompactTokens)(denom) : '?';
|
|
86
|
+
const label = denom > 0
|
|
87
|
+
? skin.applyColors(`${left}/${right} tokens`, 'muted')
|
|
88
|
+
: skin.applyColors(`${left} tokens`, 'muted');
|
|
89
|
+
const gutter = (0, frame_1.getIndent)(0);
|
|
90
|
+
return `${gutter}${bar} ${label}`;
|
|
91
|
+
};
|
|
92
|
+
const paint = () => {
|
|
93
|
+
if (!isTty || hidden)
|
|
94
|
+
return;
|
|
95
|
+
// `\r\x1b[K` — carriage return + erase to end of line, then
|
|
96
|
+
// rewrite. Same single-line overwrite pattern as the activity
|
|
97
|
+
// indicator and tool-row live tick.
|
|
98
|
+
out.write(`\r\x1b[K${buildLine()}`);
|
|
99
|
+
printed = true;
|
|
100
|
+
lastPaintTokens = outputTokens;
|
|
101
|
+
};
|
|
102
|
+
const erase = () => {
|
|
103
|
+
if (isTty && printed)
|
|
104
|
+
out.write('\r\x1b[K');
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
update(n, max) {
|
|
108
|
+
if (hidden)
|
|
109
|
+
return;
|
|
110
|
+
// Coerce + clamp. Non-finite or negative inputs are ignored —
|
|
111
|
+
// never crash the stream consumer with a malformed event.
|
|
112
|
+
if (typeof n === 'number' && Number.isFinite(n) && n >= 0) {
|
|
113
|
+
outputTokens = Math.floor(n);
|
|
114
|
+
}
|
|
115
|
+
if (typeof max === 'number' && Number.isFinite(max) && max > 0) {
|
|
116
|
+
maxTokens = Math.floor(max);
|
|
117
|
+
}
|
|
118
|
+
// Dedup: skip the repaint if the visible state didn't change.
|
|
119
|
+
// Anthropic emits message_delta events with the SAME running
|
|
120
|
+
// counter when no new tokens were produced; without this
|
|
121
|
+
// gate we'd flicker on every duplicate.
|
|
122
|
+
if (outputTokens === lastPaintTokens && printed)
|
|
123
|
+
return;
|
|
124
|
+
paint();
|
|
125
|
+
},
|
|
126
|
+
hide() {
|
|
127
|
+
if (hidden)
|
|
128
|
+
return;
|
|
129
|
+
hidden = true;
|
|
130
|
+
erase();
|
|
131
|
+
},
|
|
132
|
+
isHidden() { return hidden; },
|
|
133
|
+
getTokens() {
|
|
134
|
+
return { output: outputTokens, max: maxTokens };
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
package/dist/cli/v4/display.js
CHANGED
|
@@ -25,6 +25,8 @@ 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;
|
|
28
30
|
exports.countNewlines = countNewlines;
|
|
29
31
|
exports.splitAtUnclosedBold = splitAtUnclosedBold;
|
|
30
32
|
exports.formatToolDuration = formatToolDuration;
|
|
@@ -48,6 +50,12 @@ const replyRenderer_1 = require("./replyRenderer");
|
|
|
48
50
|
// Optional "Sources" footer when AIDEN_CITATIONS=1 (default off).
|
|
49
51
|
const citationFooter_1 = require("./citationFooter");
|
|
50
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");
|
|
51
59
|
/**
|
|
52
60
|
* v4.1.3-repl-polish — category emoji icons for the tool-row trail.
|
|
53
61
|
* Icons are ON by default (AIDEN_UI_ICONS !== '0'). Set
|
|
@@ -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
|
|
@@ -948,15 +1105,57 @@ class Display {
|
|
|
948
1105
|
const sk = this.skin;
|
|
949
1106
|
const useMd = opts.markdown !== false;
|
|
950
1107
|
const rawBody = useMd ? this.markdown(text).trimEnd() : text;
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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);
|
|
955
1120
|
const reasoning = opts.reasoning
|
|
956
|
-
?
|
|
1121
|
+
? `${(0, frame_1.getIndent)(0)}${sk.applyColors(opts.reasoning.trim(), 'muted')}\n`
|
|
957
1122
|
: '';
|
|
958
1123
|
return `${this.agentHeader()}${reasoning}${indented}\n`;
|
|
959
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
|
+
}
|
|
960
1159
|
/**
|
|
961
1160
|
* Format a recoverable error with optional remediation suggestion.
|
|
962
1161
|
* Output goes through the caller (returned as string), not stderr.
|
|
@@ -1075,6 +1274,33 @@ class Display {
|
|
|
1075
1274
|
}
|
|
1076
1275
|
this.out.write('\n');
|
|
1077
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
|
+
}
|
|
1078
1304
|
/**
|
|
1079
1305
|
* Append a streamed text fragment. Writes a styled "Aiden" header on
|
|
1080
1306
|
* the first call of a turn, then writes raw text directly via the
|
|
@@ -1099,12 +1325,83 @@ class Display {
|
|
|
1099
1325
|
this.out.write(text);
|
|
1100
1326
|
this.streamLastEndedNewline = text.endsWith('\n');
|
|
1101
1327
|
// Phase v4.1-reply-formatting: track buffer + line count for the
|
|
1102
|
-
// post-stream re-render.
|
|
1103
|
-
// so the eraser later knows how many rows to clear.
|
|
1328
|
+
// post-stream re-render.
|
|
1104
1329
|
this.streamBuffer += text;
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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);
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
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.
|
|
1373
|
+
*/
|
|
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;
|
|
1108
1405
|
}
|
|
1109
1406
|
/**
|
|
1110
1407
|
* v4.1.3-essentials: rerender a buffered stream chunk in place. Walks
|
|
@@ -1161,10 +1458,13 @@ class Display {
|
|
|
1161
1458
|
// \x1b[J = erase from cursor to end of screen.
|
|
1162
1459
|
this.out.write(`\x1b[${lines}F\x1b[J`);
|
|
1163
1460
|
const formatted = this.markdown(buffered).trimEnd();
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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);
|
|
1168
1468
|
this.out.write(indented + '\n');
|
|
1169
1469
|
}
|
|
1170
1470
|
catch {
|
|
@@ -1450,6 +1750,112 @@ function truncToolArg(s) {
|
|
|
1450
1750
|
return flat;
|
|
1451
1751
|
return flat.slice(0, exports.TOOL_ROW_ARG_CAP - 1) + '…';
|
|
1452
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
|
+
}
|
|
1453
1859
|
/**
|
|
1454
1860
|
* v4.1.3-essentials boldwrap-fix: count `\n` occurrences in `s`.
|
|
1455
1861
|
* Used by `commitStreamChunk` to recompute `streamLineCount` after
|