aiden-runtime 4.7.0 → 4.8.1
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 +12 -1
- package/dist/cli/v4/aidenCLI.js +40 -5
- package/dist/cli/v4/callbacks.js +52 -31
- package/dist/cli/v4/chatSession.js +55 -8
- package/dist/cli/v4/commands/help.js +22 -11
- package/dist/cli/v4/commands/runs.js +42 -24
- package/dist/cli/v4/commands/skills.js +15 -17
- package/dist/cli/v4/commands/update.js +14 -2
- package/dist/cli/v4/commands/usage.js +17 -5
- package/dist/cli/v4/daemonAgentBuilder.js +1 -0
- package/dist/cli/v4/design/tokens.js +265 -0
- package/dist/cli/v4/display/framedPanel.js +116 -0
- package/dist/cli/v4/display/toolTrail.js +2 -2
- package/dist/cli/v4/display.js +489 -164
- package/dist/cli/v4/onboarding/disclaimer.js +42 -10
- package/dist/cli/v4/onboarding/loading.js +24 -1
- package/dist/cli/v4/onboarding/successScreen.js +17 -8
- package/dist/cli/v4/pasteIntercept.js +214 -70
- package/dist/cli/v4/replyRenderer.js +213 -58
- package/dist/cli/v4/setupWizard.js +19 -2
- package/dist/cli/v4/skinEngine.js +13 -0
- package/dist/cli/v4/table.js +65 -8
- package/dist/core/v4/aidenAgent.js +23 -0
- package/dist/core/v4/auxiliaryClient.js +46 -13
- package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +13 -8
- package/dist/core/v4/promptBuilder.js +51 -0
- package/dist/core/v4/subagent/childBuilder.js +1 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +7 -1
- package/dist/core/v4/ui/banner.js +16 -16
- package/dist/core/v4/update/executeInstall.js +10 -6
- package/dist/core/v4/update/installMethodDetect.js +7 -0
- package/dist/core/version.js +67 -2
- package/dist/moat/approvalEngine.js +14 -0
- package/dist/tools/v4/index.js +54 -0
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +23 -0
- package/package.json +1 -3
package/dist/cli/v4/display.js
CHANGED
|
@@ -41,6 +41,7 @@ const marked_1 = require("marked");
|
|
|
41
41
|
const TerminalRenderer = require('marked-terminal').default ?? require('marked-terminal');
|
|
42
42
|
const skinEngine_1 = require("./skinEngine");
|
|
43
43
|
const box_1 = require("./box");
|
|
44
|
+
const tokens_1 = require("./design/tokens");
|
|
44
45
|
const toolTrail_1 = require("./display/toolTrail");
|
|
45
46
|
// v4.1.3-essentials — capability card renderer (auth/platform failures).
|
|
46
47
|
const capabilityCard_1 = require("./display/capabilityCard");
|
|
@@ -231,6 +232,16 @@ class Display {
|
|
|
231
232
|
// reprints the formatted output.
|
|
232
233
|
this.streamBuffer = '';
|
|
233
234
|
this.streamLineCount = 0;
|
|
235
|
+
// v4.8.0 Phase 2.3 — task_id → label map for in-flight ui_task_update
|
|
236
|
+
// rows. ui_task_done looks up the label so the completion row can
|
|
237
|
+
// echo it even when the model only sends task_id + status. Cleared
|
|
238
|
+
// on done. Map is per-Display-instance; one REPL session.
|
|
239
|
+
this.uiTaskRows = new Map();
|
|
240
|
+
// v4.8.0 Phase 2.3 fix — set true by renderUiEvent; tryRerenderInPlace
|
|
241
|
+
// early-returns when set so the cursor-up + erase-to-end-of-screen
|
|
242
|
+
// sequence can't wipe our ui_* rows. Reset at stream lifecycle
|
|
243
|
+
// boundaries (streamPartial first-delta init + streamComplete).
|
|
244
|
+
this.uiEventsFiredThisTurn = false;
|
|
234
245
|
this.skin = opts.skin ?? (0, skinEngine_1.getSkinEngine)();
|
|
235
246
|
this.out = opts.stdout ?? process.stdout;
|
|
236
247
|
this.err = opts.stderr ?? process.stderr;
|
|
@@ -345,12 +356,16 @@ class Display {
|
|
|
345
356
|
* Returns one indented line with a trailing newline.
|
|
346
357
|
*/
|
|
347
358
|
agentHeader() {
|
|
348
|
-
|
|
359
|
+
// v4.8.0 Slice 7 hotfix — replace v4.7 ┃ heavy-vertical with the
|
|
360
|
+
// Slice 4 framedPanel bar `▎` so reply chrome matches /help,
|
|
361
|
+
// approval prompt, and other Slice 4+ surfaces. Trailing `\n\n`
|
|
362
|
+
// (was `\n`) puts one blank between header and first content row.
|
|
363
|
+
const bar = this.skin.applyColors(tokens_1.glyphs.panel.bar, 'brand');
|
|
349
364
|
const head = this.skin.applyColors('Aiden', 'brand');
|
|
350
365
|
if (process.env.AIDEN_UI_TIMESTAMPS === '1') {
|
|
351
|
-
return `${this.timestampPrefix()} ${bar} ${head}\n`;
|
|
366
|
+
return `${this.timestampPrefix()} ${bar} ${head}\n\n`;
|
|
352
367
|
}
|
|
353
|
-
return ` ${bar} ${head}\n`;
|
|
368
|
+
return ` ${bar} ${head}\n\n`;
|
|
354
369
|
}
|
|
355
370
|
/**
|
|
356
371
|
* Phase 26.2.3 — turn boundary marker. Writes a thin muted rule
|
|
@@ -360,7 +375,12 @@ class Display {
|
|
|
360
375
|
* surface.
|
|
361
376
|
*/
|
|
362
377
|
printTurnSeparator() {
|
|
363
|
-
|
|
378
|
+
// v4.8.0 Slice 7 hotfix — drop the trailing blank line. Inquirer's
|
|
379
|
+
// own prompt leading newline + the Aiden header's leading 2-space
|
|
380
|
+
// indent provide enough breathing room; the extra blank was
|
|
381
|
+
// stacking with other emit points to produce 3+ blank lines
|
|
382
|
+
// between user prompt and reply.
|
|
383
|
+
this.out.write(` ${this.rule()}\n`);
|
|
364
384
|
}
|
|
365
385
|
/**
|
|
366
386
|
* Render the v3-style boot status line:
|
|
@@ -500,55 +520,61 @@ class Display {
|
|
|
500
520
|
* across lines.
|
|
501
521
|
*/
|
|
502
522
|
scrollFooter() {
|
|
523
|
+
// v4.8.0 Slice 10d — rounded heavy frame for identity / credits.
|
|
524
|
+
// The Slice 10b/c orange-bar chrome lacked visual containment for
|
|
525
|
+
// an identity surface (the bar reads as panel-content, not as a
|
|
526
|
+
// credits card). This restores a heavy frame — but with rounded
|
|
527
|
+
// corners (╭╮╰╯) sourced from glyphs.box, and muted chrome so the
|
|
528
|
+
// brand `♥` + brand kv labels carry the visual weight inside.
|
|
503
529
|
const sk = this.skin;
|
|
504
530
|
const m = (s) => sk.applyColors(s, 'muted');
|
|
505
531
|
const lab = (s) => sk.applyColors(s, 'brand');
|
|
506
532
|
const val = (s) => sk.applyColors(s, 'agent');
|
|
507
533
|
const heart = sk.applyColors('♥', 'brand');
|
|
508
534
|
if (this.cols() < 80) {
|
|
509
|
-
//
|
|
510
|
-
// card stays compact. The 4-line plain fallback shipped earlier
|
|
511
|
-
// wastes vertical space when terminals already squeeze content.
|
|
535
|
+
// Narrow fallback unchanged — single-line credits stays compact.
|
|
512
536
|
return ` ${heart} ${m('built solo · github.com/taracodlabs/aiden · aiden.taracod.com')}`;
|
|
513
537
|
}
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
const
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
return
|
|
538
|
+
const indent = ' ';
|
|
539
|
+
const innerW = Math.min(this.cols() - 4, 70);
|
|
540
|
+
const tL = m(tokens_1.glyphs.box.topLeft);
|
|
541
|
+
const tR = m(tokens_1.glyphs.box.topRight);
|
|
542
|
+
const bL = m(tokens_1.glyphs.box.bottomLeft);
|
|
543
|
+
const bR = m(tokens_1.glyphs.box.bottomRight);
|
|
544
|
+
const side = m(tokens_1.glyphs.chrome.vLine);
|
|
545
|
+
const hRun = m(tokens_1.glyphs.chrome.hLine.repeat(innerW));
|
|
546
|
+
const pad = (visible, width) => {
|
|
547
|
+
const v = (0, box_1.visibleLength)(visible);
|
|
548
|
+
return visible + ' '.repeat(Math.max(0, width - v));
|
|
525
549
|
};
|
|
526
|
-
|
|
527
|
-
// box has visual breathing room from the lines above/below, and
|
|
528
|
-
// a trailing interior blank so contact info doesn't crowd the
|
|
529
|
-
// bottom border.
|
|
550
|
+
const row = (content) => `${indent}${side} ${pad(content, innerW - 2)} ${side}`;
|
|
530
551
|
return [
|
|
531
552
|
'',
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
wallIndent + pipe + lid + pipe,
|
|
553
|
+
`${indent}${tL}${hRun}${tR}`,
|
|
554
|
+
row(`${heart} ${val('Built solo')}`),
|
|
555
|
+
row(''),
|
|
556
|
+
row(`${lab('GitHub:'.padEnd(10))}${val('github.com/taracodlabs/aiden')}`),
|
|
557
|
+
row(`${lab('Web:'.padEnd(10))}${val('aiden.taracod.com')}`),
|
|
558
|
+
row(`${lab('Contact:'.padEnd(10))}${val('contact@taracod.com')}`),
|
|
559
|
+
`${indent}${bL}${hRun}${bR}`,
|
|
540
560
|
'',
|
|
541
561
|
].join('\n');
|
|
542
562
|
}
|
|
543
563
|
/**
|
|
544
564
|
* Bottom prompt hint that replaces the prior `ready ▸ /help` +
|
|
545
|
-
* `✦ Tip:` lines.
|
|
565
|
+
* `✦ Tip:` lines.
|
|
566
|
+
*
|
|
567
|
+
* v4.8.0 Slice 11 — dropped the leading `▲` glyph. The inquirer
|
|
568
|
+
* prompt that paints immediately below this hint already carries
|
|
569
|
+
* the brand triangle as its input prefix (`display.promptPrefix()`),
|
|
570
|
+
* so the hint's own `▲` read as a duplicate orphan sitting one row
|
|
571
|
+
* above the active cursor. Hint is now text-only-muted; `▲` stays
|
|
572
|
+
* exclusively as the user-input identity glyph.
|
|
546
573
|
*/
|
|
547
574
|
bottomPromptHint() {
|
|
548
575
|
const sk = this.skin;
|
|
549
|
-
const tri = sk.applyColors('▲', 'brand');
|
|
550
576
|
const text = sk.applyColors('Type your message · /help for commands · /skills to add more', 'muted');
|
|
551
|
-
return `
|
|
577
|
+
return ` ${text}`;
|
|
552
578
|
}
|
|
553
579
|
/**
|
|
554
580
|
* v3-style "ready" line:
|
|
@@ -564,33 +590,96 @@ class Display {
|
|
|
564
590
|
return ` ${ready} ${arrow} ${sk.applyColors(hint, 'muted')}`;
|
|
565
591
|
}
|
|
566
592
|
/**
|
|
567
|
-
* v3-style post-turn status footer
|
|
593
|
+
* v3-style post-turn status footer, extended in v4.8.0 Slice 7 with
|
|
594
|
+
* packed info density (turn counter, session uptime, per-turn state
|
|
595
|
+
* dot) and progressive disclosure based on terminal width.
|
|
568
596
|
*
|
|
569
|
-
*
|
|
597
|
+
* Layout tiers:
|
|
598
|
+
* ≥120 cols: ▲ provider · model │ N/M <bar> N% │ ⌘ N │ ⏱ Hms │ ● state
|
|
599
|
+
* ≥100 cols: ▲ provider · model │ N/M <bar> N% │ ⌘ N │ Ns
|
|
600
|
+
* < 100: ▲ provider · model │ <bar> N% │ Ns
|
|
570
601
|
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
* elapsed in muted. Returns string sans trailing newline.
|
|
602
|
+
* `turnCount`, `sessionMs`, `state` are optional for backward compat;
|
|
603
|
+
* old call sites continue to work unchanged.
|
|
574
604
|
*/
|
|
575
605
|
statusFooter(args) {
|
|
576
606
|
const sk = this.skin;
|
|
577
607
|
const SEP = sk.applyColors(' │ ', 'muted');
|
|
578
608
|
const tri = this.triangle();
|
|
609
|
+
// v4.8.0 Slice 7 hotfix #2 — per-metric accent palette.
|
|
610
|
+
// Model: cyan (tool kind). Token counts: amber (warn). Bar/pct:
|
|
611
|
+
// semantic tier. Turn: purple (metric_turn). Timer: teal (success).
|
|
579
612
|
const provModel = `${tri} ${sk.applyColors(args.provider, 'muted')}` +
|
|
580
613
|
`${sk.applyColors(' · ', 'muted')}` +
|
|
581
|
-
sk.applyColors(args.model, '
|
|
614
|
+
sk.applyColors(args.model, 'tool');
|
|
582
615
|
const pct = args.ctxMax > 0
|
|
583
616
|
? Math.min(100, Math.round((args.ctxUsed / args.ctxMax) * 100))
|
|
584
617
|
: 0;
|
|
585
|
-
|
|
618
|
+
// 5-cell bar with single-space separators — reads as discrete dots
|
|
619
|
+
// rather than a continuous line. Total visible width ≈ 9 cells,
|
|
620
|
+
// similar to the prior 10-cell solid bar so 80-col tier stays tight.
|
|
621
|
+
const barW = 5;
|
|
586
622
|
const filled = Math.round((pct / 100) * barW);
|
|
587
623
|
const ctxKind = pct < 60 ? 'success' : pct < 85 ? 'warn' : 'error';
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
const
|
|
592
|
-
const elapsed = sk.applyColors(formatElapsedShort(args.elapsedMs), '
|
|
593
|
-
|
|
624
|
+
const cells = Array.from({ length: barW }, (_, i) => i < filled ? tokens_1.glyphs.bar.filled : tokens_1.glyphs.bar.empty);
|
|
625
|
+
const bar = sk.applyColors(cells.join(' '), ctxKind);
|
|
626
|
+
const ctxRatio = sk.applyColors(`${formatCompactTokens(args.ctxUsed)}/${formatCompactTokens(args.ctxMax)}`, 'warn');
|
|
627
|
+
const ctxPctText = sk.applyColors(`${pct}%`, ctxKind);
|
|
628
|
+
const elapsed = sk.applyColors(formatElapsedShort(args.elapsedMs), 'success');
|
|
629
|
+
// Progressive disclosure: pick layout based on RAW terminal width.
|
|
630
|
+
// `this.cols()` caps at 100 (frame budget for body content), but
|
|
631
|
+
// the footer wants the full physical width to choose its tier.
|
|
632
|
+
const cols = (typeof this.out.columns === 'number' && this.out.columns >= 1)
|
|
633
|
+
? this.out.columns
|
|
634
|
+
: 100;
|
|
635
|
+
// Tier ≥120: full density (ratio + bar + pct + turn + session + state).
|
|
636
|
+
// Tier ≥100: ratio + bar + pct + turn + elapsed.
|
|
637
|
+
// Tier <100: bar + pct + elapsed.
|
|
638
|
+
const stateDot = args.state
|
|
639
|
+
? sk.applyColors(tokens_1.glyphs.status.dot, this.stateKind(args.state))
|
|
640
|
+
: '';
|
|
641
|
+
// v4.8.0 Slice 9 hotfix — turn glyph dropped; bare colored number
|
|
642
|
+
// matches the timer pattern. Color alone (purple metric_turn)
|
|
643
|
+
// carries the semantic.
|
|
644
|
+
const turnSeg = args.turnCount !== undefined
|
|
645
|
+
? sk.applyColors(String(args.turnCount), 'metric_turn')
|
|
646
|
+
: '';
|
|
647
|
+
// v4.8.0 Slice 9 hotfix — ⌛ restored ahead of the bare elapsed
|
|
648
|
+
// string. Wider font support than the retired ⏱. `sessionMs` arg
|
|
649
|
+
// stays plumbed-but-unused for backward compat with the field name.
|
|
650
|
+
const sessionSeg = args.elapsedMs !== undefined
|
|
651
|
+
? `${sk.applyColors(tokens_1.glyphs.status.timer, 'success')} ${sk.applyColors(formatElapsedShort(args.elapsedMs), 'success')}`
|
|
652
|
+
: '';
|
|
653
|
+
// ctxRatio + ctxPctText are pre-painted (warn + ctxKind respectively).
|
|
654
|
+
const ctxSegFull = `${ctxRatio} ${bar} ${ctxPctText}`;
|
|
655
|
+
const ctxSegCompact = `${bar} ${ctxPctText}`;
|
|
656
|
+
let segments;
|
|
657
|
+
if (cols >= 120 && stateDot && turnSeg && sessionSeg) {
|
|
658
|
+
segments = [provModel, ctxSegFull, turnSeg, sessionSeg, stateDot];
|
|
659
|
+
}
|
|
660
|
+
else if (cols >= 100 && turnSeg) {
|
|
661
|
+
// v4.8.1 Slice 2 hotfix — was `elapsed` (bare); now uses
|
|
662
|
+
// `sessionSeg` which includes the ⌛ timer glyph. The previous
|
|
663
|
+
// mid-tier dropped the glyph for "denser" packing, but Shiva's
|
|
664
|
+
// smoke at 80–110 cols showed only ` 5.1s` (leading space, no
|
|
665
|
+
// glyph). The glyph is single-cell, cheap, and load-bearing as
|
|
666
|
+
// the timer's identity affordance.
|
|
667
|
+
segments = [provModel, ctxSegFull, turnSeg, sessionSeg || elapsed];
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
segments = [provModel, ctxSegCompact, sessionSeg || elapsed];
|
|
671
|
+
}
|
|
672
|
+
return ` ${segments.join(SEP)}`;
|
|
673
|
+
}
|
|
674
|
+
/** Map a per-turn outcome to the colour kind used by the state dot. */
|
|
675
|
+
stateKind(state) {
|
|
676
|
+
if (state === 'ok')
|
|
677
|
+
return 'success';
|
|
678
|
+
if (state === 'warn')
|
|
679
|
+
return 'warn';
|
|
680
|
+
if (state === 'error')
|
|
681
|
+
return 'error';
|
|
682
|
+
return 'muted';
|
|
594
683
|
}
|
|
595
684
|
/**
|
|
596
685
|
* Tier-3.1 (v4.1-tier3.1): pre-prompt status line.
|
|
@@ -682,11 +771,15 @@ class Display {
|
|
|
682
771
|
* `12:41:02 ▲ <input>`. Default OFF preserves `▲ <input>`.
|
|
683
772
|
*/
|
|
684
773
|
promptPrefix() {
|
|
774
|
+
// v4.8.0 Slice 7 hotfix #2 — 2-space lead matches the rest of the
|
|
775
|
+
// surface family (▎ Aiden header, status footer, bottom hint).
|
|
776
|
+
// Timestamp variant unchanged — the timestamp gutter already
|
|
777
|
+
// provides its own consistent left edge.
|
|
685
778
|
const tri = this.skin.applyColors('▲', 'brand');
|
|
686
779
|
if (process.env.AIDEN_UI_TIMESTAMPS === '1') {
|
|
687
780
|
return `${this.timestampPrefix()} ${tri} `;
|
|
688
781
|
}
|
|
689
|
-
return
|
|
782
|
+
return ` ${tri} `;
|
|
690
783
|
}
|
|
691
784
|
/**
|
|
692
785
|
* Phase 26.2.6 — pick a random phrase from `SPINNER_PHRASES`,
|
|
@@ -805,6 +898,13 @@ class Display {
|
|
|
805
898
|
let stopped = false;
|
|
806
899
|
let printed = false;
|
|
807
900
|
let tickTimer = null;
|
|
901
|
+
// v4.8.1 Slice 2 hotfix #4 — true once the indicator has paused
|
|
902
|
+
// and resumed at least once (i.e. a tool row interrupted it). When
|
|
903
|
+
// false at stop() time, the indicator is still in its initial-paint
|
|
904
|
+
// row immediately below the leading blank, so stop()'s erase can
|
|
905
|
+
// safely consume BOTH rows. When true, the leading blank is far
|
|
906
|
+
// above and stop() erases only the current indicator row.
|
|
907
|
+
let movedFromInitial = false;
|
|
808
908
|
// Tunable cadence. v4.1.4 Phase 3b' (Issue G): bumped from 400ms
|
|
809
909
|
// to 250ms after visual smoke — 400ms felt sluggish, made the
|
|
810
910
|
// indicator look static between seconds. 250ms gives ~4 dot
|
|
@@ -812,69 +912,65 @@ class Display {
|
|
|
812
912
|
// (Ns) counter hasn't ticked. Slow enough not to flicker on SSH
|
|
813
913
|
// / slow ConPTY refresh.
|
|
814
914
|
const TICK_MS = 250;
|
|
815
|
-
//
|
|
816
|
-
//
|
|
915
|
+
// v4.8.0 Slice 11 — leading glyph is no longer a static `⌛` (or
|
|
916
|
+
// a separate 2nd-row wave bar). Now it's a single-row sliding
|
|
917
|
+
// shimmer: a 4-cell brand-orange `█` segment that scrolls L→R
|
|
918
|
+
// across a muted `─` track, wrapping at the right edge. The dots
|
|
919
|
+
// pulse + (Ns) timer keep their roles as secondary motion cues;
|
|
920
|
+
// the shimmer is the primary "something is happening" affordance
|
|
921
|
+
// in TTFT space. Token-sourced from `glyphs.shimmer` so the glyph
|
|
922
|
+
// pair lives next to the rest of the v4.8.0 design system.
|
|
817
923
|
//
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
// and
|
|
821
|
-
//
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
// 250ms tick as the verb dot pulse — one timer drives both rows.
|
|
827
|
-
const waveBarEnabled = opts.waveBar !== false; // default true
|
|
828
|
-
const WAVE_CELLS = 10;
|
|
829
|
-
const WAVE_BLOCK = 3;
|
|
830
|
-
let waveFrame = 0;
|
|
831
|
-
const buildLine = () => {
|
|
832
|
-
const dots = '.'.repeat(dotFrame); // 0..3 dots
|
|
833
|
-
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
|
834
|
-
const elapsedStr = elapsedSec >= 1
|
|
835
|
-
? ` ${sk.applyColors(`(${elapsedSec}s)`, 'muted')}`
|
|
836
|
-
: '';
|
|
837
|
-
// `▲ {verb}{dots-padded-to-3}{elapsed?}`
|
|
838
|
-
return `${glyph} ${verb}${dots.padEnd(3, ' ')}${elapsedStr}`;
|
|
839
|
-
};
|
|
924
|
+
// `opts.waveBar` is preserved as a back-compat option that maps
|
|
925
|
+
// to "shimmer enabled". Pass `{ waveBar: false }` to drop the
|
|
926
|
+
// shimmer cluster and render the bare verb row (the legacy
|
|
927
|
+
// v4.1.4 single-row indicator). Default ON.
|
|
928
|
+
const shimmerEnabled = opts.waveBar !== false;
|
|
929
|
+
const SHIMMER_CELLS = 10;
|
|
930
|
+
const SHIMMER_BLOCK = 4;
|
|
931
|
+
let shimmerFrame = 0;
|
|
840
932
|
/**
|
|
841
|
-
* v4.
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
933
|
+
* v4.8.0 Slice 11 — render the sliding-block shimmer. A 4-cell
|
|
934
|
+
* `█` (U+2588 FULL BLOCK) segment at positions `[frame,
|
|
935
|
+
* frame+1, frame+2, frame+3]` mod 10, on a muted `─` track.
|
|
936
|
+
* Brand-orange block, muted track. Token-sourced glyphs;
|
|
937
|
+
* cell-by-cell paint keeps glyph order true to position so
|
|
938
|
+
* the wrap visibly slides rather than jumping.
|
|
847
939
|
*
|
|
848
|
-
* Heartbeat semantics: this is NOT progress. The
|
|
849
|
-
* constant 250ms cadence regardless of any backend metric.
|
|
850
|
-
* exists purely so the user sees motion during the
|
|
851
|
-
* TTFT (time-to-first-token) wait. The verb
|
|
852
|
-
*
|
|
940
|
+
* Heartbeat semantics: this is NOT progress. The block moves
|
|
941
|
+
* at a constant 250ms cadence regardless of any backend metric.
|
|
942
|
+
* It exists purely so the user sees motion during the
|
|
943
|
+
* unobservable TTFT (time-to-first-token) wait. The verb +
|
|
944
|
+
* dot pulse + (Ns) timer carry the real lifecycle signal.
|
|
853
945
|
*/
|
|
854
|
-
const
|
|
855
|
-
// v4.1.5 Phase 1d (Q-P1) — glyph palette switch. Was `▰`/`▱`
|
|
856
|
-
// (U+25B0/B1, Geometric Shapes) which legacy Windows console
|
|
857
|
-
// fonts render as tofu. Now `▓`/`░` (U+2593/91, Block Elements
|
|
858
|
-
// — in CP437, universally supported). Matches the existing
|
|
859
|
-
// statusFooter chrome that's shipped since v3 without ever
|
|
860
|
-
// being garbled.
|
|
946
|
+
const buildShimmer = () => {
|
|
861
947
|
const filled = new Set();
|
|
862
|
-
for (let i = 0; i <
|
|
863
|
-
filled.add((
|
|
948
|
+
for (let i = 0; i < SHIMMER_BLOCK; i += 1) {
|
|
949
|
+
filled.add((shimmerFrame + i) % SHIMMER_CELLS);
|
|
864
950
|
}
|
|
865
|
-
// Render cells in order so the snake-scroll visually slides:
|
|
866
|
-
// we paint cell-by-cell with the right color, joined into one
|
|
867
|
-
// string. ANSI runs reset per cell — slight overhead but keeps
|
|
868
|
-
// glyph order true to position. Brand orange filled, warm-muted
|
|
869
|
-
// empty.
|
|
870
951
|
const cells = [];
|
|
871
|
-
for (let c = 0; c <
|
|
952
|
+
for (let c = 0; c < SHIMMER_CELLS; c += 1) {
|
|
872
953
|
cells.push(filled.has(c)
|
|
873
|
-
? sk.applyColors(
|
|
874
|
-
: sk.applyColors(
|
|
954
|
+
? sk.applyColors(tokens_1.glyphs.shimmer.block, 'brand')
|
|
955
|
+
: sk.applyColors(tokens_1.glyphs.shimmer.track, 'muted'));
|
|
875
956
|
}
|
|
876
957
|
return cells.join('');
|
|
877
958
|
};
|
|
959
|
+
const buildLine = () => {
|
|
960
|
+
const dots = '.'.repeat(dotFrame); // 0..3 dots
|
|
961
|
+
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
|
962
|
+
const elapsedStr = elapsedSec >= 1
|
|
963
|
+
? ` ${sk.applyColors(`(${elapsedSec}s)`, 'muted')}`
|
|
964
|
+
: '';
|
|
965
|
+
// Shimmer prefix (or none, when opts.waveBar === false).
|
|
966
|
+
const prefix = shimmerEnabled ? `${buildShimmer()} ` : '';
|
|
967
|
+
// v4.8.1 Slice 2 hotfix #4 — 2-space leading indent so the
|
|
968
|
+
// indicator line aligns at col 2, matching `▎ Aiden`, the
|
|
969
|
+
// user-prompt ` ▲ `, the panel ` │ ` bar, and every other
|
|
970
|
+
// structured surface. Prior buildLine started at col 0 which
|
|
971
|
+
// read as misaligned against the rest of the v4.8 chrome.
|
|
972
|
+
return ` ${prefix}${verb}${dots.padEnd(3, ' ')}${elapsedStr}`;
|
|
973
|
+
};
|
|
878
974
|
// v4.1.5 Part 1a — Issue M (Windows ConPTY buffering fix).
|
|
879
975
|
//
|
|
880
976
|
// Prior pattern wrote `\r\x1b[K{indicator}` with NO trailing
|
|
@@ -905,24 +1001,15 @@ class Display {
|
|
|
905
1001
|
if (stopped || paused || !isTty)
|
|
906
1002
|
return;
|
|
907
1003
|
dotFrame = (dotFrame + 1) % 4;
|
|
908
|
-
// v4.
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
//
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
// cursor lands on the row below the wave bar.
|
|
918
|
-
out.write(`${ANSI_UP_ERASE}${ANSI_UP_ERASE}` +
|
|
919
|
-
`${buildLine()}\n` +
|
|
920
|
-
`${buildWave()}\n`);
|
|
921
|
-
}
|
|
922
|
-
else {
|
|
923
|
-
// Single-row layout (back-compat with v4.1.4 tests).
|
|
924
|
-
out.write(`${ANSI_UP_ERASE}${buildLine()}\n`);
|
|
925
|
-
}
|
|
1004
|
+
// v4.8.0 Slice 11 — shimmer slides 1 cell per tick. Same 250ms
|
|
1005
|
+
// cadence as the dot pulse, so block + dots move in visible
|
|
1006
|
+
// lockstep. Modulo SHIMMER_CELLS wraps the leading block back
|
|
1007
|
+
// to the left edge.
|
|
1008
|
+
shimmerFrame = (shimmerFrame + 1) % SHIMMER_CELLS;
|
|
1009
|
+
// Single-row layout: walk up 1, erase, repaint, newline. Cursor
|
|
1010
|
+
// lands on the row below the indicator, ready for the next
|
|
1011
|
+
// tick to walk back up.
|
|
1012
|
+
out.write(`${ANSI_UP_ERASE}${buildLine()}\n`);
|
|
926
1013
|
};
|
|
927
1014
|
const startTick = () => {
|
|
928
1015
|
if (stopped || !isTty || tickTimer !== null)
|
|
@@ -936,41 +1023,27 @@ class Display {
|
|
|
936
1023
|
}
|
|
937
1024
|
};
|
|
938
1025
|
const eraseLine = () => {
|
|
939
|
-
// Walk up
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
// the indicator had been. v4.1.5 visual smoke flagged the
|
|
945
|
-
// wave-bar→`┃ Aiden` proximity as feeling cramped. The
|
|
946
|
-
// trailing `\n` gains one visible blank row of breathing space
|
|
947
|
-
// AND adds another Windows ConPTY flush trigger (Issue M).
|
|
948
|
-
//
|
|
949
|
-
// v4.1.5 Issue K — with wave bar enabled, walk up 2 rows (two
|
|
950
|
-
// up-1+erase sequences). Without the bar, walk up 1 row.
|
|
1026
|
+
// Walk up 1 row + erase + drop a newline so the cursor lands
|
|
1027
|
+
// on a blank line BELOW the indicator's old footprint. The
|
|
1028
|
+
// trailing `\n` provides one visible blank row of breathing
|
|
1029
|
+
// space and acts as a Windows ConPTY flush trigger (v4.1.5
|
|
1030
|
+
// Issue M). Slice 11 collapsed the prior 2-row layout to 1.
|
|
951
1031
|
if (!isTty || !printed)
|
|
952
1032
|
return;
|
|
953
|
-
|
|
954
|
-
out.write(`${ANSI_UP_ERASE}${ANSI_UP_ERASE}\n`);
|
|
955
|
-
}
|
|
956
|
-
else {
|
|
957
|
-
out.write(`${ANSI_UP_ERASE}\n`);
|
|
958
|
-
}
|
|
1033
|
+
out.write(`${ANSI_UP_ERASE}\n`);
|
|
959
1034
|
};
|
|
960
|
-
// Initial paint — only on TTY.
|
|
961
|
-
// flushes and the cursor sits on the row below, ready for the
|
|
962
|
-
// first tick to walk back up.
|
|
1035
|
+
// Initial paint — only on TTY.
|
|
963
1036
|
//
|
|
964
|
-
// v4.1
|
|
965
|
-
//
|
|
966
|
-
//
|
|
1037
|
+
// v4.8.1 Slice 2 hotfix #4 — leading `\n` restored to give one
|
|
1038
|
+
// blank row between the user-input row and the indicator (hotfix
|
|
1039
|
+
// #3 dropped the dim rule that previously provided that gap).
|
|
1040
|
+
// To keep the post-stop layout at "exactly one blank between
|
|
1041
|
+
// user input and ▎ Aiden", stop() now walks up TWO rows when
|
|
1042
|
+
// the indicator never moved (no pause/resume), consuming both
|
|
1043
|
+
// the indicator row AND the leading blank. The `movedFromInitial`
|
|
1044
|
+
// flag below tracks that state.
|
|
967
1045
|
if (isTty) {
|
|
968
|
-
|
|
969
|
-
out.write(`${buildLine()}\n${buildWave()}\n`);
|
|
970
|
-
}
|
|
971
|
-
else {
|
|
972
|
-
out.write(`${buildLine()}\n`);
|
|
973
|
-
}
|
|
1046
|
+
out.write(`\n${buildLine()}\n`);
|
|
974
1047
|
printed = true;
|
|
975
1048
|
startTick();
|
|
976
1049
|
}
|
|
@@ -980,6 +1053,12 @@ class Display {
|
|
|
980
1053
|
return;
|
|
981
1054
|
paused = true;
|
|
982
1055
|
stopTick();
|
|
1056
|
+
// v4.8.1 Slice 2 hotfix #4 — mark the indicator as "moved" so
|
|
1057
|
+
// a subsequent stop() does NOT walk up 2 rows. The leading
|
|
1058
|
+
// blank from initial paint is now far above the current row
|
|
1059
|
+
// and shouldn't be consumed; doing so would erase tool-row
|
|
1060
|
+
// content instead.
|
|
1061
|
+
movedFromInitial = true;
|
|
983
1062
|
eraseLine();
|
|
984
1063
|
// After erase the cursor is at column 0 of the indicator's
|
|
985
1064
|
// (now empty) line. Caller is expected to write its own
|
|
@@ -998,17 +1077,16 @@ class Display {
|
|
|
998
1077
|
// Caller has just finished writing its own content (typically
|
|
999
1078
|
// ending with `\n`), so the cursor is on a fresh line below
|
|
1000
1079
|
// whatever was there. Paint the indicator + `\n` to claim the
|
|
1001
|
-
// current row
|
|
1080
|
+
// current row and leave the cursor on the row below — same
|
|
1002
1081
|
// invariant the initial paint and tick maintain. Trailing `\n`
|
|
1003
1082
|
// also flushes Windows ConPTY buffering (Issue M).
|
|
1004
1083
|
//
|
|
1005
|
-
// v4.
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1084
|
+
// v4.8.0 Slice 11 — single-row layout: paint one row only.
|
|
1085
|
+
// (Initial paint includes a leading `\n` for breathing space;
|
|
1086
|
+
// resume omits it because the caller has already written its
|
|
1087
|
+
// own content above this point and an extra blank would
|
|
1088
|
+
// double up.)
|
|
1089
|
+
out.write(`${buildLine()}\n`);
|
|
1012
1090
|
printed = true;
|
|
1013
1091
|
startTick();
|
|
1014
1092
|
},
|
|
@@ -1021,7 +1099,20 @@ class Display {
|
|
|
1021
1099
|
return;
|
|
1022
1100
|
stopped = true;
|
|
1023
1101
|
stopTick();
|
|
1024
|
-
|
|
1102
|
+
// v4.8.1 Slice 2 hotfix #4 — when the indicator never moved
|
|
1103
|
+
// (no pause/resume happened during the turn), walk up TWO
|
|
1104
|
+
// rows: erase the indicator row AND the leading blank above
|
|
1105
|
+
// it. The trailing `\n` then lands the cursor exactly one
|
|
1106
|
+
// row below the user-input echo, so the next writer
|
|
1107
|
+
// (agentHeader → ▎ Aiden) produces a clean single-blank gap.
|
|
1108
|
+
if (!printed || !isTty)
|
|
1109
|
+
return;
|
|
1110
|
+
if (movedFromInitial) {
|
|
1111
|
+
out.write(`${ANSI_UP_ERASE}\n`);
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
out.write(`${ANSI_UP_ERASE}${ANSI_UP_ERASE}\n`);
|
|
1115
|
+
}
|
|
1025
1116
|
},
|
|
1026
1117
|
isPaused: () => paused,
|
|
1027
1118
|
isStopped: () => stopped,
|
|
@@ -1088,18 +1179,25 @@ class Display {
|
|
|
1088
1179
|
// Running row — muted pipe, raw icon, tool-colored verb, muted detail.
|
|
1089
1180
|
// The optional `running Ns…` tail appears once the tool crosses the
|
|
1090
1181
|
// 1-second mark; the tick interval below redraws this row every 1s.
|
|
1182
|
+
//
|
|
1183
|
+
// v4.8.0 Slice 11c — double-space between `${glyph}` and `${padVerb}`.
|
|
1184
|
+
// Emoji-class icons (`👁️`, `✏️`, `📋`, `🌐`, etc.) render 2-cell-wide
|
|
1185
|
+
// visually on Windows ConPTY but the cursor/column tracker treats
|
|
1186
|
+
// them as 1 cell, so a single trailing space is visually swallowed
|
|
1187
|
+
// by the emoji's right cell. Two spaces guarantees one visible gap
|
|
1188
|
+
// regardless of how the terminal measures the glyph.
|
|
1091
1189
|
const runningRow = () => {
|
|
1092
1190
|
const elapsed = Date.now() - startedAt;
|
|
1093
1191
|
const liveSuffix = elapsed >= 1000
|
|
1094
1192
|
? ` ${sk.applyColors(`running ${formatToolDuration(elapsed)}…`, 'muted')}`
|
|
1095
1193
|
: '';
|
|
1096
|
-
return `${sk.applyColors(toolTrail_1.TRAIL_PIPE, 'muted')} ${glyph}
|
|
1194
|
+
return `${sk.applyColors(toolTrail_1.TRAIL_PIPE, 'muted')} ${glyph} ` +
|
|
1097
1195
|
`${sk.applyColors((0, toolTrail_1.padVerb)(verb), 'tool')} ` +
|
|
1098
1196
|
`${sk.applyColors(detail, 'muted')}${liveSuffix}\n`;
|
|
1099
1197
|
};
|
|
1100
1198
|
// Outcome row — entire line colored by outcome kind.
|
|
1101
1199
|
const outcomeRow = (suffix, kind) => {
|
|
1102
|
-
const content = `${toolTrail_1.TRAIL_PIPE} ${glyph}
|
|
1200
|
+
const content = `${toolTrail_1.TRAIL_PIPE} ${glyph} ${(0, toolTrail_1.padVerb)(verb)} ${detail}` +
|
|
1103
1201
|
(suffix ? ` ${suffix}` : '');
|
|
1104
1202
|
return `${sk.applyColors(content, kind)}\n`;
|
|
1105
1203
|
};
|
|
@@ -1211,8 +1309,10 @@ class Display {
|
|
|
1211
1309
|
// back over the retry counter with `running Ns…`.
|
|
1212
1310
|
stopTick();
|
|
1213
1311
|
// Update the running row with retry count.
|
|
1312
|
+
// v4.8.0 Slice 11c — double-space between glyph and verb (see
|
|
1313
|
+
// runningRow comment above for the emoji-width rationale).
|
|
1214
1314
|
eraseLast();
|
|
1215
|
-
const content = `${toolTrail_1.TRAIL_PIPE} ${glyph}
|
|
1315
|
+
const content = `${toolTrail_1.TRAIL_PIPE} ${glyph} ${(0, toolTrail_1.padVerb)(verb)} ${detail} retry ${n}/${m} …`;
|
|
1216
1316
|
out.write(sk.applyColors(content, 'warn') + '\n');
|
|
1217
1317
|
printed = true;
|
|
1218
1318
|
},
|
|
@@ -1326,7 +1426,12 @@ class Display {
|
|
|
1326
1426
|
* (post-stream rerender) so both paths produce identical output.
|
|
1327
1427
|
*/
|
|
1328
1428
|
applyFrameToRendered(rawBody) {
|
|
1329
|
-
|
|
1429
|
+
// v4.8.0 Slice 7 hotfix #2 — override frame.GUTTER (3) to 2 cells
|
|
1430
|
+
// locally so Aiden reply prose aligns with the ▎ bar of agentHeader
|
|
1431
|
+
// (col 2). The GUTTER constant stays at 3 for other consumers
|
|
1432
|
+
// (markdown list/blockquote/code-block renderers in frame.ts) where
|
|
1433
|
+
// a 3-cell gutter is part of their own visual algebra.
|
|
1434
|
+
const indent = ' ';
|
|
1330
1435
|
const bw = (0, frame_1.getBodyWidth)(this.out);
|
|
1331
1436
|
return rawBody
|
|
1332
1437
|
.split('\n')
|
|
@@ -1507,15 +1612,31 @@ class Display {
|
|
|
1507
1612
|
if (!text)
|
|
1508
1613
|
return;
|
|
1509
1614
|
if (!this.streamHeaderShown) {
|
|
1510
|
-
// Phase 26.2.3 — share the
|
|
1511
|
-
//
|
|
1512
|
-
// open identically.
|
|
1615
|
+
// Phase 26.2.3 — share the `▎ Aiden` header with non-streaming
|
|
1616
|
+
// agentTurn so streamed + non-streamed responses open identically.
|
|
1513
1617
|
this.out.write(this.agentHeader());
|
|
1514
1618
|
this.streamHeaderShown = true;
|
|
1515
1619
|
this.streamBuffer = '';
|
|
1516
1620
|
this.streamLineCount = 0;
|
|
1621
|
+
this.uiEventsFiredThisTurn = false;
|
|
1622
|
+
// agentHeader emits trailing `\n\n`; cursor is at col 0 of a fresh
|
|
1623
|
+
// line, so the very next chunk needs the leading indent.
|
|
1624
|
+
this.streamLastEndedNewline = true;
|
|
1517
1625
|
}
|
|
1518
|
-
|
|
1626
|
+
// v4.8.0 Slice 7 hotfix #3 — inject a 2-cell indent at every line
|
|
1627
|
+
// start so streamed content aligns with the ▎ bar in agentHeader.
|
|
1628
|
+
// Pre-this-fix the raw chunk wrote at col 0; the post-stream
|
|
1629
|
+
// rerender in applyFrameToRendered would indent eventually, but
|
|
1630
|
+
// mid-stream the user saw col-0 content. streamBuffer stays raw
|
|
1631
|
+
// (the rerender path applies its own indent).
|
|
1632
|
+
const indent = ' ';
|
|
1633
|
+
let toWrite = text;
|
|
1634
|
+
if (this.streamLastEndedNewline)
|
|
1635
|
+
toWrite = indent + toWrite;
|
|
1636
|
+
const endsNl = toWrite.endsWith('\n');
|
|
1637
|
+
const body = endsNl ? toWrite.slice(0, -1) : toWrite;
|
|
1638
|
+
toWrite = body.replace(/\n/g, '\n' + indent) + (endsNl ? '\n' : '');
|
|
1639
|
+
this.out.write(toWrite);
|
|
1519
1640
|
this.streamLastEndedNewline = text.endsWith('\n');
|
|
1520
1641
|
// Phase v4.1-reply-formatting: track buffer + line count for the
|
|
1521
1642
|
// post-stream re-render.
|
|
@@ -1620,6 +1741,15 @@ class Display {
|
|
|
1620
1741
|
return;
|
|
1621
1742
|
if (lines === 0)
|
|
1622
1743
|
return;
|
|
1744
|
+
// v4.8.0 Phase 2.3 fix — when ui_* events painted this turn, skip the
|
|
1745
|
+
// cursor-up + erase-to-end-of-screen rerender. The eraser wipes
|
|
1746
|
+
// anything below where the stream started, including our event rows.
|
|
1747
|
+
// Tradeoff: assistant text on a ui-event turn stays raw (no in-place
|
|
1748
|
+
// markdown beautification). Acceptable — when the model is using
|
|
1749
|
+
// structured ui events, it's signalling state, not relying on prose
|
|
1750
|
+
// formatting.
|
|
1751
|
+
if (this.uiEventsFiredThisTurn)
|
|
1752
|
+
return;
|
|
1623
1753
|
// Cheap structural heuristic — only re-render when formatting
|
|
1624
1754
|
// actually helps. Plain prose chunks stay raw (no flicker).
|
|
1625
1755
|
//
|
|
@@ -1781,6 +1911,8 @@ class Display {
|
|
|
1781
1911
|
this.streamLineCount = 0;
|
|
1782
1912
|
this.streamHeaderShown = false;
|
|
1783
1913
|
this.streamLastEndedNewline = false;
|
|
1914
|
+
// v4.8.0 Phase 2.3 fix — turn ends; clear the ui-fired flag.
|
|
1915
|
+
this.uiEventsFiredThisTurn = false;
|
|
1784
1916
|
}
|
|
1785
1917
|
/**
|
|
1786
1918
|
* Phase v4.1-reply-formatting: render the optional "Sources"
|
|
@@ -1815,6 +1947,199 @@ class Display {
|
|
|
1815
1947
|
this.out.write(`${sk.applyColors(`${arrow} ${name}…`, 'tool')}\n`);
|
|
1816
1948
|
this.streamLastEndedNewline = true;
|
|
1817
1949
|
}
|
|
1950
|
+
/**
|
|
1951
|
+
* v4.8.0 Phase 2.3 — render a semantic ui_* event signalled by the
|
|
1952
|
+
* model via a uiOnly tool call. Append-only: each event paints one
|
|
1953
|
+
* row; in-place mutation is a v4.8.x upgrade if UX demands it.
|
|
1954
|
+
*
|
|
1955
|
+
* Currently handles `ui_task_update` and `ui_task_done`; other 5
|
|
1956
|
+
* names land in Phase 2.4 (silent ignore until then). Non-TTY out
|
|
1957
|
+
* surfaces silent — matches the activityIndicator precedent.
|
|
1958
|
+
*/
|
|
1959
|
+
/**
|
|
1960
|
+
* v4.8.0 Phase 2.3 fix-2 — reset the per-turn ui-event flag. Called
|
|
1961
|
+
* by chatSession at the top of each turn. The existing reset sites
|
|
1962
|
+
* (streamPartial first-delta + streamComplete) only fire when the
|
|
1963
|
+
* turn actually streamed text deltas. Tool-only turns never reset,
|
|
1964
|
+
* leaving the flag sticky into subsequent turns. This is the
|
|
1965
|
+
* authoritative reset for turn-start.
|
|
1966
|
+
*/
|
|
1967
|
+
resetUiTurnState() {
|
|
1968
|
+
this.uiEventsFiredThisTurn = false;
|
|
1969
|
+
}
|
|
1970
|
+
renderUiEvent(name, args) {
|
|
1971
|
+
if (!this.out.isTTY)
|
|
1972
|
+
return;
|
|
1973
|
+
// v4.8.0 Phase 2.3 fix — Option C. The post-stream markdown rerender
|
|
1974
|
+
// (`tryRerenderInPlace`) does `cursor-up-N + erase-to-end-of-screen`,
|
|
1975
|
+
// which wipes anything painted between stream start and stream end —
|
|
1976
|
+
// including our ui_* rows. Mark the turn so the rerender skips this
|
|
1977
|
+
// turn entirely. Resets when the next streaming turn begins (see
|
|
1978
|
+
// streamPartial header init) and on streamComplete cleanup.
|
|
1979
|
+
this.uiEventsFiredThisTurn = true;
|
|
1980
|
+
if (name === 'ui_task_update') {
|
|
1981
|
+
this.renderUiTaskUpdate(args);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
if (name === 'ui_task_done') {
|
|
1985
|
+
this.renderUiTaskDone(args);
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
if (name === 'ui_command_result') {
|
|
1989
|
+
this.renderUiCommandResult(args);
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
if (name === 'ui_test_result') {
|
|
1993
|
+
this.renderUiTestResult(args);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
if (name === 'ui_approval_request') {
|
|
1997
|
+
this.renderUiApprovalRequest(args);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
if (name === 'ui_toast') {
|
|
2001
|
+
this.renderUiToast(args);
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
if (name === 'ui_artifact_created') {
|
|
2005
|
+
this.renderUiArtifactCreated(args);
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
// Unknown event names silent-ignore (defensive — future registrations).
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* v4.8.0 Phase 2.4 polish — build one trail-gutter row matching the
|
|
2012
|
+
* `toolRow` chrome (muted `┊` + space + colored content + `\n`).
|
|
2013
|
+
* Splits on embedded newlines so multi-line surfaces (capped stdout,
|
|
2014
|
+
* preview tails, optional reasons) carry the gutter on every line.
|
|
2015
|
+
*/
|
|
2016
|
+
uiTrailRow(content, kind) {
|
|
2017
|
+
const pipe = this.skin.applyColors(toolTrail_1.TRAIL_PIPE, 'muted');
|
|
2018
|
+
return content.split('\n').map(l => `${pipe} ${this.skin.applyColors(l, kind)}\n`).join('');
|
|
2019
|
+
}
|
|
2020
|
+
renderUiTaskUpdate(args) {
|
|
2021
|
+
const taskId = typeof args.task_id === 'string' ? args.task_id : '';
|
|
2022
|
+
const label = typeof args.label === 'string' ? args.label : '';
|
|
2023
|
+
const status = typeof args.status === 'string' ? args.status : '';
|
|
2024
|
+
const kindArg = typeof args.kind === 'string' ? args.kind : 'task';
|
|
2025
|
+
const depth = typeof args.depth === 'number' && args.depth > 0 ? args.depth : 0;
|
|
2026
|
+
if (!taskId || !label)
|
|
2027
|
+
return;
|
|
2028
|
+
this.commitStreamChunk();
|
|
2029
|
+
const glyph = status === 'paused' ? '⏸' : status === 'blocked' ? '⛔' : '⟳';
|
|
2030
|
+
const colorKind = status === 'running' ? 'tool' : 'warn';
|
|
2031
|
+
this.uiTaskRows.set(taskId, { label });
|
|
2032
|
+
const short = label.length > 80 ? label.slice(0, 79) + '…' : label;
|
|
2033
|
+
// v4.8.0 Phase 2.4 — subagent kind: indent by depth inside the
|
|
2034
|
+
// gutter so nested rows tier below their parent.
|
|
2035
|
+
const indent = kindArg === 'subagent' ? ' '.repeat(depth) : '';
|
|
2036
|
+
this.out.write(this.uiTrailRow(`${indent}${glyph} ${short}`, colorKind));
|
|
2037
|
+
this.streamLastEndedNewline = true;
|
|
2038
|
+
}
|
|
2039
|
+
renderUiTaskDone(args) {
|
|
2040
|
+
const taskId = typeof args.task_id === 'string' ? args.task_id : '';
|
|
2041
|
+
const status = typeof args.status === 'string' ? args.status : '';
|
|
2042
|
+
const summary = typeof args.summary === 'string' ? args.summary : '';
|
|
2043
|
+
if (!taskId)
|
|
2044
|
+
return;
|
|
2045
|
+
this.commitStreamChunk();
|
|
2046
|
+
const tracked = this.uiTaskRows.get(taskId);
|
|
2047
|
+
const label = tracked?.label ?? taskId;
|
|
2048
|
+
this.uiTaskRows.delete(taskId);
|
|
2049
|
+
const glyph = status === 'success' ? '✓' : status === 'failure' ? '✗' : '⊘';
|
|
2050
|
+
const kind = status === 'success' ? 'success' :
|
|
2051
|
+
status === 'failure' ? 'error' : 'warn';
|
|
2052
|
+
const shortLabel = label.length > 80 ? label.slice(0, 79) + '…' : label;
|
|
2053
|
+
const shortSum = summary.length > 120 ? summary.slice(0, 119) + '…' : summary;
|
|
2054
|
+
const tail = shortSum ? ` — ${shortSum}` : '';
|
|
2055
|
+
this.out.write(this.uiTrailRow(`${glyph} ${shortLabel}${tail}`, kind));
|
|
2056
|
+
this.streamLastEndedNewline = true;
|
|
2057
|
+
}
|
|
2058
|
+
renderUiCommandResult(args) {
|
|
2059
|
+
const command = typeof args.command === 'string' ? args.command : '';
|
|
2060
|
+
if (!command)
|
|
2061
|
+
return;
|
|
2062
|
+
const stdout = typeof args.stdout === 'string' ? args.stdout : '';
|
|
2063
|
+
const stderr = typeof args.stderr === 'string' ? args.stderr : '';
|
|
2064
|
+
const exitCode = typeof args.exit_code === 'number' ? args.exit_code : 0;
|
|
2065
|
+
this.commitStreamChunk();
|
|
2066
|
+
const ok = exitCode === 0;
|
|
2067
|
+
const cap = (t) => t.split('\n').slice(0, 5).join('\n');
|
|
2068
|
+
let out = this.uiTrailRow(`▸ ${command}`, ok ? 'success' : 'error');
|
|
2069
|
+
if (stdout)
|
|
2070
|
+
out += this.uiTrailRow(cap(stdout), 'muted');
|
|
2071
|
+
if (stderr)
|
|
2072
|
+
out += this.uiTrailRow(cap(stderr), 'error');
|
|
2073
|
+
if (!ok)
|
|
2074
|
+
out += this.uiTrailRow(`(exit ${exitCode})`, 'error');
|
|
2075
|
+
this.out.write(out);
|
|
2076
|
+
this.streamLastEndedNewline = true;
|
|
2077
|
+
}
|
|
2078
|
+
renderUiTestResult(args) {
|
|
2079
|
+
const framework = typeof args.framework === 'string' ? args.framework : '';
|
|
2080
|
+
if (!framework)
|
|
2081
|
+
return;
|
|
2082
|
+
const passed = typeof args.passed === 'number' ? args.passed : 0;
|
|
2083
|
+
const failed = typeof args.failed === 'number' ? args.failed : 0;
|
|
2084
|
+
const skipped = typeof args.skipped === 'number' ? args.skipped : 0;
|
|
2085
|
+
const durationMs = typeof args.duration_ms === 'number' ? args.duration_ms : 0;
|
|
2086
|
+
this.commitStreamChunk();
|
|
2087
|
+
const ok = failed === 0;
|
|
2088
|
+
const parts = [`${passed} passed`, `${failed} failed`];
|
|
2089
|
+
if (skipped > 0)
|
|
2090
|
+
parts.push(`${skipped} skipped`);
|
|
2091
|
+
const dur = durationMs > 0 ? ` in ${durationMs}ms` : '';
|
|
2092
|
+
this.out.write(this.uiTrailRow(`${ok ? '✓' : '✗'} ${framework}: ${parts.join(', ')}${dur}`, ok ? 'success' : 'error'));
|
|
2093
|
+
this.streamLastEndedNewline = true;
|
|
2094
|
+
}
|
|
2095
|
+
renderUiApprovalRequest(_args) {
|
|
2096
|
+
// v4.8.1 Slice 1 — silent no-op. The Phase 2.5 wiring fires both
|
|
2097
|
+
// `ui_approval_request` (this method) AND `callbacks.promptApproval`
|
|
2098
|
+
// (which paints the framed approval panel via `renderApprovalBox`)
|
|
2099
|
+
// for every single approval request. The intent was complementary —
|
|
2100
|
+
// succinct event row above, structured kv panel below — but in live
|
|
2101
|
+
// smoke the two surfaces stack as a visual duplicate ("Approval
|
|
2102
|
+
// needed: file_write {...}" event row + "│ tool / │ reason / │ args"
|
|
2103
|
+
// panel). The panel is the canonical, information-rich surface; this
|
|
2104
|
+
// event-row paint is redundant.
|
|
2105
|
+
//
|
|
2106
|
+
// Behavioural change is renderer-side only: `approvalEngine` still
|
|
2107
|
+
// fires `onUiEvent('ui_approval_request', ...)` so any future
|
|
2108
|
+
// telemetry / daemon-side run_events subscriber will still see the
|
|
2109
|
+
// event. Nothing paints to the chat surface from this method.
|
|
2110
|
+
//
|
|
2111
|
+
// The `_args` parameter is retained for the dispatch signature
|
|
2112
|
+
// contract (`renderUiEvent` calls it positionally) and for the day
|
|
2113
|
+
// we re-introduce a single-paint surface keyed off args.risk_tier.
|
|
2114
|
+
}
|
|
2115
|
+
renderUiToast(args) {
|
|
2116
|
+
const message = typeof args.message === 'string' ? args.message : '';
|
|
2117
|
+
if (!message)
|
|
2118
|
+
return;
|
|
2119
|
+
const kindArg = typeof args.kind === 'string' ? args.kind : 'info';
|
|
2120
|
+
this.commitStreamChunk();
|
|
2121
|
+
const glyph = kindArg === 'success' ? '✓' : kindArg === 'warning' ? '⚠' : kindArg === 'error' ? '✗' : 'ℹ';
|
|
2122
|
+
const kind = kindArg === 'success' ? 'success' : kindArg === 'warning' ? 'warn' : kindArg === 'error' ? 'error' : 'tool';
|
|
2123
|
+
const short = message.length > 120 ? message.slice(0, 119) + '…' : message;
|
|
2124
|
+
this.out.write(this.uiTrailRow(`${glyph} ${short}`, kind));
|
|
2125
|
+
this.streamLastEndedNewline = true;
|
|
2126
|
+
}
|
|
2127
|
+
renderUiArtifactCreated(args) {
|
|
2128
|
+
const path = typeof args.path === 'string' ? args.path : '';
|
|
2129
|
+
if (!path)
|
|
2130
|
+
return;
|
|
2131
|
+
const kindArg = typeof args.kind === 'string' ? args.kind : 'file';
|
|
2132
|
+
const preview = typeof args.preview === 'string' ? args.preview : '';
|
|
2133
|
+
this.commitStreamChunk();
|
|
2134
|
+
const glyph = kindArg === 'skill' ? '🛠' : kindArg === 'directory' ? '📁' : '📄';
|
|
2135
|
+
let out = this.uiTrailRow(`${glyph} Created: ${path}`, 'accent');
|
|
2136
|
+
if (preview) {
|
|
2137
|
+
const shortP = preview.length > 200 ? preview.slice(0, 199) + '…' : preview;
|
|
2138
|
+
out += this.uiTrailRow(` ${shortP}`, 'muted');
|
|
2139
|
+
}
|
|
2140
|
+
this.out.write(out);
|
|
2141
|
+
this.streamLastEndedNewline = true;
|
|
2142
|
+
}
|
|
1818
2143
|
}
|
|
1819
2144
|
exports.Display = Display;
|
|
1820
2145
|
/**
|