@oh-my-pi/pi-coding-agent 15.2.2 → 15.2.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/CHANGELOG.md +28 -1
- package/dist/types/cli/worktree-cli.d.ts +26 -0
- package/dist/types/commands/worktree.d.ts +34 -0
- package/dist/types/config/settings-schema.d.ts +23 -0
- package/dist/types/modes/theme/shimmer.d.ts +15 -7
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/yield-queue.d.ts +24 -0
- package/dist/types/slash-commands/helpers/format.d.ts +1 -1
- package/dist/types/task/worktree.d.ts +0 -1
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +7 -7
- package/src/autoresearch/storage.ts +14 -2
- package/src/cli/worktree-cli.ts +291 -0
- package/src/cli.ts +1 -0
- package/src/commands/worktree.ts +56 -0
- package/src/config/settings-schema.ts +16 -0
- package/src/modes/components/mcp-add-wizard.ts +4 -3
- package/src/modes/components/settings-selector.ts +23 -10
- package/src/modes/components/welcome.ts +77 -35
- package/src/modes/controllers/mcp-command-controller.ts +4 -3
- package/src/modes/theme/shimmer.ts +161 -30
- package/src/modes/utils/ui-helpers.ts +31 -13
- package/src/prompts/tools/async-result.md +5 -2
- package/src/sdk.ts +95 -21
- package/src/session/agent-session.ts +22 -0
- package/src/session/yield-queue.ts +155 -0
- package/src/slash-commands/helpers/format.ts +4 -1
- package/src/task/worktree.ts +2 -7
- package/src/tools/gh.ts +35 -32
- package/src/utils/git.ts +4 -0
|
@@ -601,6 +601,22 @@ export const SETTINGS_SCHEMA = {
|
|
|
601
601
|
default: 3,
|
|
602
602
|
},
|
|
603
603
|
|
|
604
|
+
"display.shimmer": {
|
|
605
|
+
type: "enum",
|
|
606
|
+
values: ["classic", "kitt", "disabled"] as const,
|
|
607
|
+
default: "classic",
|
|
608
|
+
ui: {
|
|
609
|
+
tab: "appearance",
|
|
610
|
+
label: "Shimmer",
|
|
611
|
+
description: "Animation style for working/loading messages",
|
|
612
|
+
options: [
|
|
613
|
+
{ value: "classic", label: "Classic", description: "Soft cosine wave sweeping across the text" },
|
|
614
|
+
{ value: "kitt", label: "KITT Scanner", description: "Knight Rider 1982 red light bouncing left-right" },
|
|
615
|
+
{ value: "disabled", label: "Disabled", description: "No animation; static muted text" },
|
|
616
|
+
],
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
|
|
604
620
|
"display.showTokenUsage": {
|
|
605
621
|
type: "boolean",
|
|
606
622
|
default: false,
|
|
@@ -1157,10 +1157,11 @@ export class MCPAddWizard extends Container {
|
|
|
1157
1157
|
this.#contentContainer.addChild(new Text(theme.fg("success", "✓ Authentication successful!"), 0, 0));
|
|
1158
1158
|
this.#contentContainer.addChild(new Spacer(1));
|
|
1159
1159
|
this.#contentContainer.addChild(new Text(theme.fg("muted", "Running connection health check..."), 0, 0));
|
|
1160
|
-
const
|
|
1160
|
+
const spinnerFrames = theme.spinnerFrames;
|
|
1161
|
+
const initialFrame = spinnerFrames[0] ?? "|";
|
|
1162
|
+
const healthText = new Text(theme.fg("muted", `${initialFrame} Checking server connection...`), 0, 0);
|
|
1161
1163
|
this.#contentContainer.addChild(healthText);
|
|
1162
1164
|
|
|
1163
|
-
const spinnerFrames = ["|", "/", "-", "\\"];
|
|
1164
1165
|
let spinnerIndex = 0;
|
|
1165
1166
|
const spinner = setInterval(() => {
|
|
1166
1167
|
healthText.setText(
|
|
@@ -1168,7 +1169,7 @@ export class MCPAddWizard extends Container {
|
|
|
1168
1169
|
);
|
|
1169
1170
|
spinnerIndex++;
|
|
1170
1171
|
this.#requestRender();
|
|
1171
|
-
},
|
|
1172
|
+
}, 80);
|
|
1172
1173
|
|
|
1173
1174
|
let healthPassed = true;
|
|
1174
1175
|
let healthError = "";
|
|
@@ -495,14 +495,6 @@ export class SettingsSelectorComponent extends Container {
|
|
|
495
495
|
*/
|
|
496
496
|
#showSettingsTab(tabId: SettingTab): void {
|
|
497
497
|
const defs = getSettingsForTab(tabId);
|
|
498
|
-
const items: SettingItem[] = [];
|
|
499
|
-
|
|
500
|
-
for (const def of defs) {
|
|
501
|
-
const item = this.#defToItem(def);
|
|
502
|
-
if (item) {
|
|
503
|
-
items.push(item);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
498
|
|
|
507
499
|
// Add status line preview for appearance tab
|
|
508
500
|
if (tabId === "appearance") {
|
|
@@ -516,7 +508,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
516
508
|
}
|
|
517
509
|
|
|
518
510
|
this.#currentList = new SettingsList(
|
|
519
|
-
|
|
511
|
+
this.#buildItemsForDefs(defs),
|
|
520
512
|
10,
|
|
521
513
|
getSettingsListTheme(),
|
|
522
514
|
(id, newValue) => {
|
|
@@ -537,7 +529,12 @@ export class SettingsSelectorComponent extends Container {
|
|
|
537
529
|
settings.set(path, newValue as never);
|
|
538
530
|
this.callbacks.onChange(path, newValue);
|
|
539
531
|
}
|
|
540
|
-
// Submenu types
|
|
532
|
+
// Submenu/text types already persisted the value inside their own
|
|
533
|
+
// done callbacks before SettingsList re-dispatches here. Re-run the
|
|
534
|
+
// definition-to-item mapping so condition-gated settings (e.g. the
|
|
535
|
+
// Hindsight cluster guarded by memory.backend) appear/disappear
|
|
536
|
+
// immediately instead of waiting for the next tab switch.
|
|
537
|
+
this.#refreshCurrentTabItems(defs);
|
|
541
538
|
},
|
|
542
539
|
() => this.callbacks.onCancel(),
|
|
543
540
|
);
|
|
@@ -545,6 +542,22 @@ export class SettingsSelectorComponent extends Container {
|
|
|
545
542
|
this.addChild(this.#currentList);
|
|
546
543
|
}
|
|
547
544
|
|
|
545
|
+
/** Map a definition list to UI items, dropping any whose condition is false. */
|
|
546
|
+
#buildItemsForDefs(defs: SettingDef[]): SettingItem[] {
|
|
547
|
+
const items: SettingItem[] = [];
|
|
548
|
+
for (const def of defs) {
|
|
549
|
+
const item = this.#defToItem(def);
|
|
550
|
+
if (item) items.push(item);
|
|
551
|
+
}
|
|
552
|
+
return items;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Re-evaluate condition gates against the current settings and refresh the active list. */
|
|
556
|
+
#refreshCurrentTabItems(defs: SettingDef[]): void {
|
|
557
|
+
if (this.#currentTabId === "plugins" || !this.#currentList) return;
|
|
558
|
+
this.#currentList.setItems(this.#buildItemsForDefs(defs));
|
|
559
|
+
}
|
|
560
|
+
|
|
548
561
|
/**
|
|
549
562
|
* Get the status line preview string.
|
|
550
563
|
*/
|
|
@@ -45,7 +45,7 @@ export class WelcomeComponent implements Component {
|
|
|
45
45
|
this.#stopAnimation();
|
|
46
46
|
}
|
|
47
47
|
requestRender();
|
|
48
|
-
},
|
|
48
|
+
}, INTRO_TICK_MS);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
#stopAnimation(): void {
|
|
@@ -253,55 +253,97 @@ export class WelcomeComponent implements Component {
|
|
|
253
253
|
|
|
254
254
|
/** Pick the logo frame for the current intro phase, or the resting frame. */
|
|
255
255
|
#currentLogoFrame(): readonly string[] {
|
|
256
|
-
if (this.#animStart == null) return
|
|
256
|
+
if (this.#animStart == null) return REST_FRAME;
|
|
257
257
|
const elapsed = performance.now() - this.#animStart;
|
|
258
|
-
if (elapsed >= INTRO_MS) return
|
|
259
|
-
// Ease-out cubic so the
|
|
260
|
-
// stopping abruptly. Sweeps backward through the phase ring → lands on 0.
|
|
258
|
+
if (elapsed >= INTRO_MS) return REST_FRAME;
|
|
259
|
+
// Ease-out cubic so the spin decelerates into the resting state.
|
|
261
260
|
const progress = elapsed / INTRO_MS;
|
|
262
261
|
const eased = 1 - (1 - progress) ** 3;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
262
|
+
// Sweep backward through INTRO_SWEEPS full rotations so the gradient
|
|
263
|
+
// visibly spins multiple times. `eased == 1` → phase = 0 = resting frame.
|
|
264
|
+
const phase = ((((1 - eased) * INTRO_SWEEPS) % 1) + 1) % 1;
|
|
265
|
+
// Shine traverses the diagonal at a steady pace, decoupled from the
|
|
266
|
+
// gradient phase so the two layers parallax. Strength fades out with
|
|
267
|
+
// the same ease-out curve so the highlight is gone by the resting frame.
|
|
268
|
+
const shinePos = (((progress * INTRO_SHINE_TRAVERSALS) % 1) + 1) % 1;
|
|
269
|
+
const shineStrength = (1 - eased) ** 1.5;
|
|
270
|
+
return gradientLogo(PI_LOGO, phase, { strength: shineStrength, pos: shinePos });
|
|
266
271
|
}
|
|
267
272
|
}
|
|
268
273
|
|
|
269
274
|
// biome-ignore format: preserve ASCII art layout
|
|
270
275
|
const PI_LOGO = ["▀██████████▀", " ╘██ ██ ", " ██ ██ ", " ██ ██ ", " ▄██▄ ▄██▄ "];
|
|
271
276
|
|
|
277
|
+
/** Multi-stop palette for the diagonal gradient. */
|
|
278
|
+
const GRADIENT_STOPS: ReadonlyArray<readonly [number, number, number]> = [
|
|
279
|
+
[255, 92, 200], // hot pink
|
|
280
|
+
[200, 110, 255], // violet
|
|
281
|
+
[120, 130, 255], // periwinkle
|
|
282
|
+
[60, 200, 255], // bright cyan
|
|
283
|
+
[120, 255, 220], // mint
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
/** 256-color ramp fallback when truecolor isn't available. */
|
|
287
|
+
const GRADIENT_RAMP_256 = [199, 171, 135, 99, 75, 51, 87];
|
|
288
|
+
|
|
289
|
+
/** Half-width of the shine highlight band, expressed in gradient-t units. */
|
|
290
|
+
const SHINE_HALF_WIDTH = 0.18;
|
|
291
|
+
|
|
292
|
+
interface ShineConfig {
|
|
293
|
+
/** Overall opacity of the shine overlay, in [0, 1]. */
|
|
294
|
+
strength: number;
|
|
295
|
+
/** Center of the shine band along the diagonal, in [0, 1]. */
|
|
296
|
+
pos: number;
|
|
297
|
+
}
|
|
298
|
+
|
|
272
299
|
/**
|
|
273
|
-
* Apply
|
|
274
|
-
* `phase` (0..1) shifts the
|
|
300
|
+
* Apply a multi-stop diagonal gradient (bottom-left → top-right) plus an
|
|
301
|
+
* optional sliding shine band across multi-line art. `phase` (0..1) shifts the
|
|
302
|
+
* gradient along the diagonal, wrapping at 1. When `shine` is provided, a soft
|
|
303
|
+
* white highlight is composited on top, centered at `shine.pos`.
|
|
275
304
|
*/
|
|
276
|
-
function gradientLogo(lines: readonly string[], phase = 0): string[] {
|
|
305
|
+
function gradientLogo(lines: readonly string[], phase = 0, shine?: ShineConfig): string[] {
|
|
277
306
|
const reset = "\x1b[0m";
|
|
278
307
|
const rows = lines.length;
|
|
279
308
|
const cols = Math.max(...lines.map(l => l.length));
|
|
280
309
|
// span+1 so `base` stays strictly < 1: avoids the wrap-around at the
|
|
281
|
-
// far corner mapping back to t=0 (
|
|
310
|
+
// far corner mapping back to t=0 (hot pink) on the resting frame.
|
|
282
311
|
const span = Math.max(1, cols + rows - 1);
|
|
312
|
+
const shineStrength = shine && shine.strength > 0 ? shine.strength : 0;
|
|
313
|
+
const shinePos = shine ? shine.pos : 0;
|
|
283
314
|
const colorAt = TERMINAL.trueColor
|
|
284
315
|
? (t: number): string => {
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
const stops
|
|
288
|
-
[255, 62, 201], // hot magenta-pink
|
|
289
|
-
[180, 120, 255], // light violet
|
|
290
|
-
[62, 230, 255], // bright cyan
|
|
291
|
-
];
|
|
316
|
+
// 5-stop palette widens the visible color range and avoids the
|
|
317
|
+
// deep-blue valley a naive HSL lerp falls into.
|
|
318
|
+
const stops = GRADIENT_STOPS;
|
|
292
319
|
const seg = t * (stops.length - 1);
|
|
293
320
|
const i = Math.min(stops.length - 2, Math.floor(seg));
|
|
294
321
|
const f = seg - i;
|
|
295
322
|
const a = stops[i];
|
|
296
323
|
const b = stops[i + 1];
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
324
|
+
let r = a[0] + (b[0] - a[0]) * f;
|
|
325
|
+
let g = a[1] + (b[1] - a[1]) * f;
|
|
326
|
+
let bl = a[2] + (b[2] - a[2]) * f;
|
|
327
|
+
if (shineStrength > 0) {
|
|
328
|
+
const dist = Math.abs(t - shinePos);
|
|
329
|
+
const intensity = Math.max(0, 1 - dist / SHINE_HALF_WIDTH) * shineStrength;
|
|
330
|
+
if (intensity > 0) {
|
|
331
|
+
r += (255 - r) * intensity;
|
|
332
|
+
g += (255 - g) * intensity;
|
|
333
|
+
bl += (255 - bl) * intensity;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return `\x1b[38;2;${Math.round(r)};${Math.round(g)};${Math.round(bl)}m`;
|
|
301
337
|
}
|
|
302
338
|
: (t: number): string => {
|
|
303
|
-
const ramp =
|
|
304
|
-
|
|
339
|
+
const ramp = GRADIENT_RAMP_256;
|
|
340
|
+
let idx = Math.min(ramp.length - 1, Math.max(0, Math.floor(t * (ramp.length - 1) + 0.5)));
|
|
341
|
+
if (shineStrength > 0) {
|
|
342
|
+
const dist = Math.abs(t - shinePos);
|
|
343
|
+
const intensity = Math.max(0, 1 - dist / SHINE_HALF_WIDTH) * shineStrength;
|
|
344
|
+
// Promote to the brightest ramp slot when the shine band peaks here.
|
|
345
|
+
if (intensity > 0.5) idx = ramp.length - 1;
|
|
346
|
+
}
|
|
305
347
|
return `\x1b[38;5;${ramp[idx]}m`;
|
|
306
348
|
};
|
|
307
349
|
return lines.map((line, y) => {
|
|
@@ -321,14 +363,14 @@ function gradientLogo(lines: readonly string[], phase = 0): string[] {
|
|
|
321
363
|
});
|
|
322
364
|
}
|
|
323
365
|
|
|
324
|
-
/**
|
|
325
|
-
const
|
|
326
|
-
|
|
366
|
+
/** Total length of the intro animation. */
|
|
367
|
+
const INTRO_MS = 3000;
|
|
368
|
+
/** Render cadence during the intro (~30fps). */
|
|
369
|
+
const INTRO_TICK_MS = 33;
|
|
370
|
+
/** Number of full gradient rotations the sweep performs before settling. */
|
|
371
|
+
const INTRO_SWEEPS = 2.5;
|
|
372
|
+
/** Number of times the shine highlight crosses the diagonal across the intro. */
|
|
373
|
+
const INTRO_SHINE_TRAVERSALS = 3;
|
|
327
374
|
|
|
328
|
-
/**
|
|
329
|
-
|
|
330
|
-
* the intro sweeps frames in reverse so it lands on frame 0.
|
|
331
|
-
*/
|
|
332
|
-
const LOGO_FRAMES: readonly (readonly string[])[] = Array.from({ length: INTRO_PHASES }, (_, i) =>
|
|
333
|
-
gradientLogo(PI_LOGO, i / INTRO_PHASES),
|
|
334
|
-
);
|
|
375
|
+
/** Resting gradient frame, cached for re-renders outside of the intro. */
|
|
376
|
+
const REST_FRAME = gradientLogo(PI_LOGO, 0);
|
|
@@ -763,17 +763,18 @@ export class MCPCommandController {
|
|
|
763
763
|
if (!this.ctx.mcpManager) return "disconnected";
|
|
764
764
|
|
|
765
765
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
766
|
-
const
|
|
766
|
+
const frames = theme.spinnerFrames;
|
|
767
|
+
const initialFrame = frames[0] ?? "|";
|
|
768
|
+
const statusText = new Text(theme.fg("muted", `${initialFrame} Connecting to "${name}"...`), 1, 0);
|
|
767
769
|
this.ctx.chatContainer.addChild(statusText);
|
|
768
770
|
this.ctx.ui.requestRender();
|
|
769
771
|
|
|
770
|
-
const frames = ["|", "/", "-", "\\"];
|
|
771
772
|
let frame = 0;
|
|
772
773
|
const interval = setInterval(() => {
|
|
773
774
|
statusText.setText(theme.fg("muted", `${frames[frame % frames.length]} Connecting to "${name}"...`));
|
|
774
775
|
frame++;
|
|
775
776
|
this.ctx.ui.requestRender();
|
|
776
|
-
},
|
|
777
|
+
}, 80);
|
|
777
778
|
|
|
778
779
|
try {
|
|
779
780
|
try {
|
|
@@ -1,18 +1,36 @@
|
|
|
1
|
+
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
1
2
|
import type { Theme, ThemeColor } from "./theme";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
// ─── Classic sweep tunables ──────────────────────────────────────────────────
|
|
5
|
+
const CLASSIC_PADDING = 10;
|
|
6
|
+
const CLASSIC_SWEEP_MS = 1400;
|
|
7
|
+
const CLASSIC_BAND_HALF_WIDTH = 6;
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
// ─── KITT scanner tunables ───────────────────────────────────────────────────
|
|
10
|
+
// 1.5s round trip ≈ classic 1982 K.I.T.T. scanner cadence (~0.75s per direction).
|
|
11
|
+
const KITT_CYCLE_MS = 1500;
|
|
12
|
+
const KITT_HEAD_HALF = 0.6;
|
|
13
|
+
const KITT_TRAIL_LEN = 7;
|
|
14
|
+
|
|
15
|
+
// ─── Tier thresholds ─────────────────────────────────────────────────────────
|
|
16
|
+
const TIER_HIGH = 0.65;
|
|
17
|
+
const TIER_MID = 0.22;
|
|
18
|
+
|
|
19
|
+
// ─── Raw ANSI codes ──────────────────────────────────────────────────────────
|
|
20
|
+
const FG_RESET = "\x1b[39m";
|
|
21
|
+
const BOLD_OPEN = "\x1b[1m";
|
|
22
|
+
const BOLD_CLOSE = "\x1b[22m";
|
|
23
|
+
|
|
24
|
+
type ShimmerTheme = Pick<Theme, "bold" | "fg" | "getFgAnsi">;
|
|
25
|
+
type ShimmerMode = "classic" | "kitt" | "disabled";
|
|
8
26
|
|
|
9
27
|
/** Three-tier color stack a shimmer character cycles through as the band sweeps. */
|
|
10
28
|
export interface ShimmerPalette {
|
|
11
|
-
/** Color for chars outside / at the edge of the band (intensity < 0.
|
|
29
|
+
/** Color for chars outside / at the edge of the band (intensity < ~0.22). */
|
|
12
30
|
low: ThemeColor;
|
|
13
|
-
/** Color for chars approaching the crest (0.
|
|
31
|
+
/** Color for chars approaching the crest (~0.22 ≤ intensity < ~0.65). */
|
|
14
32
|
mid: ThemeColor;
|
|
15
|
-
/** Color at the band's crest (intensity
|
|
33
|
+
/** Color at the band's crest (intensity ≥ ~0.65). */
|
|
16
34
|
high: ThemeColor;
|
|
17
35
|
/** Whether to bold the crest tier. Default `false`. */
|
|
18
36
|
bold?: boolean;
|
|
@@ -31,47 +49,160 @@ export const DEFAULT_SHIMMER_PALETTE: ShimmerPalette = {
|
|
|
31
49
|
bold: true,
|
|
32
50
|
};
|
|
33
51
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
// ─── Palette compilation cache ───────────────────────────────────────────────
|
|
53
|
+
// Resolving ANSI codes for every character was the dominant per-frame cost.
|
|
54
|
+
// We resolve once per (theme, palette) pair into ready-to-concat prefix/suffix
|
|
55
|
+
// strings, then coalesce same-tier runs at render time so each frame emits a
|
|
56
|
+
// handful of escape sequences instead of one per code point.
|
|
57
|
+
//
|
|
58
|
+
// The cache is stashed as a Symbol-keyed slot directly on the palette object
|
|
59
|
+
// — no module-level sidecar — and invalidates when the active Theme changes.
|
|
60
|
+
interface TierSeq {
|
|
61
|
+
open: string;
|
|
62
|
+
close: string;
|
|
63
|
+
}
|
|
64
|
+
interface CompiledPalette {
|
|
65
|
+
low: TierSeq;
|
|
66
|
+
mid: TierSeq;
|
|
67
|
+
high: TierSeq;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const kCompiledFor = Symbol("shimmer.compiledFor");
|
|
71
|
+
const kCompiled = Symbol("shimmer.compiled");
|
|
72
|
+
interface PaletteCache {
|
|
73
|
+
[kCompiledFor]?: ShimmerTheme;
|
|
74
|
+
[kCompiled]?: CompiledPalette;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compile(theme: ShimmerTheme, palette: ShimmerPalette): CompiledPalette {
|
|
78
|
+
const p = palette as ShimmerPalette & PaletteCache;
|
|
79
|
+
const cached = p[kCompiled];
|
|
80
|
+
if (cached && p[kCompiledFor] === theme) return cached;
|
|
81
|
+
const highOpen = palette.bold ? `${BOLD_OPEN}${theme.getFgAnsi(palette.high)}` : theme.getFgAnsi(palette.high);
|
|
82
|
+
const highClose = palette.bold ? `${BOLD_CLOSE}${FG_RESET}` : FG_RESET;
|
|
83
|
+
const out: CompiledPalette = {
|
|
84
|
+
low: { open: theme.getFgAnsi(palette.low), close: FG_RESET },
|
|
85
|
+
mid: { open: theme.getFgAnsi(palette.mid), close: FG_RESET },
|
|
86
|
+
high: { open: highOpen, close: highClose },
|
|
87
|
+
};
|
|
88
|
+
p[kCompiledFor] = theme;
|
|
89
|
+
p[kCompiled] = out;
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Intensity profiles ──────────────────────────────────────────────────────
|
|
94
|
+
/** Smooth cosine bump sweeping left → right with edge padding. */
|
|
95
|
+
function classicIntensity(time: number, index: number, length: number): number {
|
|
96
|
+
const period = length + CLASSIC_PADDING * 2;
|
|
97
|
+
// Fractional position — kept un-floored so the band glides at the host's
|
|
98
|
+
// frame rate instead of stepping discretely.
|
|
99
|
+
const pos = ((time % CLASSIC_SWEEP_MS) / CLASSIC_SWEEP_MS) * period;
|
|
100
|
+
const dist = Math.abs(index + CLASSIC_PADDING - pos);
|
|
101
|
+
if (dist >= CLASSIC_BAND_HALF_WIDTH) return 0;
|
|
102
|
+
return 0.5 * (1 + Math.cos((Math.PI * dist) / CLASSIC_BAND_HALF_WIDTH));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Knight Rider K.I.T.T. scanner: a single bright head ping-pongs across the
|
|
107
|
+
* bar with a quadratic-decay trail behind it. No leading glow — LEDs don't
|
|
108
|
+
* predict the future.
|
|
109
|
+
*/
|
|
110
|
+
function kittIntensity(time: number, index: number, length: number): number {
|
|
111
|
+
const range = length - 1;
|
|
112
|
+
if (range <= 0) return 1;
|
|
113
|
+
const phase = (time % KITT_CYCLE_MS) / KITT_CYCLE_MS;
|
|
114
|
+
const goingRight = phase < 0.5;
|
|
115
|
+
const head = goingRight ? phase * 2 * range : (1 - phase) * 2 * range;
|
|
116
|
+
const delta = index - head;
|
|
117
|
+
const abs = delta < 0 ? -delta : delta;
|
|
118
|
+
if (abs <= KITT_HEAD_HALF) return 1;
|
|
119
|
+
// Only chars *behind* the head light up — direction-dependent.
|
|
120
|
+
const behind = goingRight ? -delta : delta;
|
|
121
|
+
if (behind <= KITT_HEAD_HALF) return 0;
|
|
122
|
+
const t = (behind - KITT_HEAD_HALF) / KITT_TRAIL_LEN;
|
|
123
|
+
if (t >= 1) return 0;
|
|
124
|
+
const f = 1 - t;
|
|
125
|
+
return f * f;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type Tier = "low" | "mid" | "high";
|
|
39
129
|
|
|
40
|
-
|
|
41
|
-
|
|
130
|
+
function tierFor(intensity: number): Tier {
|
|
131
|
+
if (intensity >= TIER_HIGH) return "high";
|
|
132
|
+
if (intensity >= TIER_MID) return "mid";
|
|
133
|
+
return "low";
|
|
42
134
|
}
|
|
43
135
|
|
|
44
|
-
function
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
const styled = theme.fg(palette.high, ch);
|
|
48
|
-
return palette.bold ? theme.bold(styled) : styled;
|
|
136
|
+
function resolveMode(): ShimmerMode {
|
|
137
|
+
if (!isSettingsInitialized()) return "classic";
|
|
138
|
+
return settings.get("display.shimmer");
|
|
49
139
|
}
|
|
50
140
|
|
|
51
141
|
/**
|
|
52
|
-
* Apply a shimmer sweep across one or more segments, treating them as a
|
|
53
|
-
* continuous string for band positioning. Each segment can supply
|
|
54
|
-
* palette so the gradient stays in lockstep while the colors
|
|
142
|
+
* Apply a shimmer sweep across one or more segments, treating them as a
|
|
143
|
+
* single continuous string for band positioning. Each segment can supply
|
|
144
|
+
* its own palette so the gradient stays in lockstep while the colors
|
|
145
|
+
* differ.
|
|
146
|
+
*
|
|
147
|
+
* Performance shape (per call, dominant cost):
|
|
148
|
+
* - One `Date.now()` read.
|
|
149
|
+
* - One `compile()` lookup per segment (Symbol-keyed cache slot, hot path
|
|
150
|
+
* skipped after first frame).
|
|
151
|
+
* - One ANSI open/close pair per **run of same-tier chars**, not per char.
|
|
152
|
+
* - No per-char allocations beyond the run buffer.
|
|
55
153
|
*/
|
|
56
154
|
export function shimmerSegments(segments: readonly ShimmerSegment[], theme: ShimmerTheme): string {
|
|
155
|
+
const mode = resolveMode();
|
|
156
|
+
|
|
157
|
+
// Pre-scan: total code-point count (positions the band) and resolved palette.
|
|
57
158
|
let total = 0;
|
|
58
|
-
const
|
|
159
|
+
const perSeg: { chars: string[]; palette: ShimmerPalette }[] = [];
|
|
59
160
|
for (const seg of segments) {
|
|
60
|
-
const chars =
|
|
161
|
+
const chars = Array.from(seg.text);
|
|
61
162
|
total += chars.length;
|
|
62
|
-
|
|
163
|
+
perSeg.push({ chars, palette: seg.palette ?? DEFAULT_SHIMMER_PALETTE });
|
|
63
164
|
}
|
|
64
165
|
if (total === 0) return "";
|
|
65
166
|
|
|
66
|
-
|
|
167
|
+
// Disabled: no animation, no per-char work. Paint each segment in its mid
|
|
168
|
+
// tier so the working line stays legible without movement.
|
|
169
|
+
if (mode === "disabled") {
|
|
170
|
+
let out = "";
|
|
171
|
+
for (const { chars, palette } of perSeg) {
|
|
172
|
+
const seq = compile(theme, palette).mid;
|
|
173
|
+
out += `${seq.open}${chars.join("")}${seq.close}`;
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const time = Date.now();
|
|
179
|
+
const intensityFn = mode === "kitt" ? kittIntensity : classicIntensity;
|
|
180
|
+
|
|
181
|
+
let out = "";
|
|
67
182
|
let index = 0;
|
|
68
|
-
for (const { chars, palette } of
|
|
69
|
-
|
|
70
|
-
|
|
183
|
+
for (const { chars, palette } of perSeg) {
|
|
184
|
+
const compiled = compile(theme, palette);
|
|
185
|
+
let runTier: Tier | null = null;
|
|
186
|
+
let runBuf = "";
|
|
187
|
+
for (let i = 0; i < chars.length; i++) {
|
|
188
|
+
const tier = tierFor(intensityFn(time, index, total));
|
|
189
|
+
if (tier !== runTier) {
|
|
190
|
+
if (runTier !== null) {
|
|
191
|
+
const seq = compiled[runTier];
|
|
192
|
+
out += `${seq.open}${runBuf}${seq.close}`;
|
|
193
|
+
runBuf = "";
|
|
194
|
+
}
|
|
195
|
+
runTier = tier;
|
|
196
|
+
}
|
|
197
|
+
runBuf += chars[i];
|
|
71
198
|
index++;
|
|
72
199
|
}
|
|
200
|
+
if (runTier !== null && runBuf.length > 0) {
|
|
201
|
+
const seq = compiled[runTier];
|
|
202
|
+
out += `${seq.open}${runBuf}${seq.close}`;
|
|
203
|
+
}
|
|
73
204
|
}
|
|
74
|
-
return out
|
|
205
|
+
return out;
|
|
75
206
|
}
|
|
76
207
|
|
|
77
208
|
export function shimmerText(text: string, theme: ShimmerTheme, palette?: ShimmerPalette): string {
|
|
@@ -113,21 +113,39 @@ export class UiHelpers {
|
|
|
113
113
|
type?: "bash" | "task";
|
|
114
114
|
label?: string;
|
|
115
115
|
durationMs?: number;
|
|
116
|
+
jobs?: Array<{
|
|
117
|
+
jobId?: string;
|
|
118
|
+
type?: "bash" | "task";
|
|
119
|
+
label?: string;
|
|
120
|
+
durationMs?: number;
|
|
121
|
+
}>;
|
|
116
122
|
}>
|
|
117
123
|
).details;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
const jobs =
|
|
125
|
+
details?.jobs && details.jobs.length > 0
|
|
126
|
+
? details.jobs
|
|
127
|
+
: [
|
|
128
|
+
{
|
|
129
|
+
jobId: details?.jobId,
|
|
130
|
+
type: details?.type,
|
|
131
|
+
label: details?.label,
|
|
132
|
+
durationMs: details?.durationMs,
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
for (const job of jobs) {
|
|
136
|
+
const jobId = job.jobId ?? "unknown";
|
|
137
|
+
const typeLabel = job.type ? `[${job.type}]` : "[job]";
|
|
138
|
+
const duration = typeof job.durationMs === "number" ? formatDuration(job.durationMs) : undefined;
|
|
139
|
+
const line = [
|
|
140
|
+
theme.fg("success", `${theme.status.success} Background job completed`),
|
|
141
|
+
theme.fg("dim", typeLabel),
|
|
142
|
+
theme.fg("accent", jobId),
|
|
143
|
+
duration ? theme.fg("dim", `(${duration})`) : undefined,
|
|
144
|
+
]
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.join(" ");
|
|
147
|
+
this.ctx.chatContainer.addChild(new Text(line, 1, 0));
|
|
148
|
+
}
|
|
131
149
|
break;
|
|
132
150
|
}
|
|
133
151
|
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<system-notice>
|
|
2
|
-
|
|
2
|
+
{{#if multiple}}{{jobs.length}} background jobs have completed. Resume your work using the results below.
|
|
3
3
|
|
|
4
|
-
{{
|
|
4
|
+
{{else}}Background job {{jobs.[0].jobId}} has completed. Resume your work using the result below.
|
|
5
|
+
{{/if}}{{#each jobs}}{{#if @root.multiple}}── Job {{this.jobId}}{{#if this.label}} ({{this.label}}){{/if}} ──
|
|
6
|
+
{{/if}}{{this.result}}{{#unless @last}}
|
|
7
|
+
{{/unless}}{{/each}}
|
|
5
8
|
</system-notice>
|