pum-agent 0.2.24-beta.1 → 0.2.26-beta.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/package.json +1 -1
- package/src/animation.tsx +135 -0
- package/src/app.tsx +337 -49
- package/src/bash-output.ts +4 -3
- package/src/cli.ts +2 -2
- package/src/commands.ts +26 -0
- package/src/help-popup.tsx +2 -1
- package/src/platform.ts +2 -4
- package/src/providers-autocomplete.ts +59 -0
- package/src/providers-command.ts +77 -0
- package/src/providers-controller.ts +222 -0
- package/src/providers-flow.ts +100 -0
- package/src/providers-popup.tsx +192 -0
- package/src/settings-autocomplete.ts +118 -0
- package/src/settings-command.ts +459 -0
- package/src/subagents/manager.ts +4 -16
- package/src/text-wrap.ts +84 -0
- package/src/transcript.tsx +111 -8
package/package.json
CHANGED
package/src/animation.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type MarkdownRenderable,
|
|
6
6
|
type RGBA,
|
|
7
7
|
type TextChunk,
|
|
8
|
+
type TextareaRenderable,
|
|
8
9
|
type TextRenderable,
|
|
9
10
|
} from "@opentui/core";
|
|
10
11
|
import { useRenderer } from "@opentui/react";
|
|
@@ -324,6 +325,140 @@ export function shimmer(text: string, base: RGBA, hi: RGBA, elapsedMs: number):
|
|
|
324
325
|
return new StyledText(chunks);
|
|
325
326
|
}
|
|
326
327
|
|
|
328
|
+
/**
|
|
329
|
+
* True raised forms, and only those.
|
|
330
|
+
*
|
|
331
|
+
* A terminal cell grid has no half-row offset, so a letter cannot be drawn
|
|
332
|
+
* between two rows. These modifier letters are the same letter cut higher in
|
|
333
|
+
* the cell, which is as close as the grid allows. The map holds no lookalike
|
|
334
|
+
* substitutes: a letter with no raised form of its own — uppercase F, digits,
|
|
335
|
+
* punctuation — is left exactly as it is rather than replaced by a glyph that
|
|
336
|
+
* reads as a different character.
|
|
337
|
+
*/
|
|
338
|
+
const RAISED_LETTERS: Record<string, string> = {
|
|
339
|
+
a: "ᵃ", b: "ᵇ", c: "ᶜ", d: "ᵈ", e: "ᵉ", f: "ᶠ", g: "ᵍ", h: "ʰ", i: "ⁱ", j: "ʲ",
|
|
340
|
+
k: "ᵏ", l: "ˡ", m: "ᵐ", n: "ⁿ", o: "ᵒ", p: "ᵖ", r: "ʳ", s: "ˢ", t: "ᵗ", u: "ᵘ",
|
|
341
|
+
v: "ᵛ", w: "ʷ", x: "ˣ", y: "ʸ", z: "ᶻ",
|
|
342
|
+
A: "ᴬ", B: "ᴮ", D: "ᴰ", E: "ᴱ", G: "ᴳ", H: "ᴴ", I: "ᴵ", J: "ᴶ", K: "ᴷ", L: "ᴸ",
|
|
343
|
+
M: "ᴹ", N: "ᴺ", O: "ᴼ", P: "ᴾ", R: "ᴿ", T: "ᵀ", U: "ᵁ", V: "ⱽ", W: "ᵂ",
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/** How long one crest takes to cross the phrase, and the rest between sweeps. */
|
|
347
|
+
const WAVE_CHARS_PER_MS = 0.022;
|
|
348
|
+
const WAVE_WIDTH = 5;
|
|
349
|
+
const WAVE_REST_CHARS = 90;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* One bright crest travelling across the first `crestEnd` characters, with a
|
|
353
|
+
* long rest between sweeps. The remainder — the steer hint — never moves, so
|
|
354
|
+
* it keeps reading as a fixed note rather than as part of the motion.
|
|
355
|
+
*/
|
|
356
|
+
export function placeholderWave(
|
|
357
|
+
text: string,
|
|
358
|
+
crestEnd: number,
|
|
359
|
+
base: RGBA,
|
|
360
|
+
hi: RGBA,
|
|
361
|
+
elapsedMs: number,
|
|
362
|
+
lift = true,
|
|
363
|
+
): StyledText {
|
|
364
|
+
const characters = Array.from(text);
|
|
365
|
+
const crest = Math.max(0, Math.min(crestEnd, characters.length));
|
|
366
|
+
const bloom = bloomColor(hi);
|
|
367
|
+
const period = crest + WAVE_REST_CHARS;
|
|
368
|
+
const head = ((elapsedMs * WAVE_CHARS_PER_MS) % period) - WAVE_WIDTH;
|
|
369
|
+
// Exactly one letter rides the top of the crest. A strength threshold would
|
|
370
|
+
// lift two or three at once, or none at all between cells.
|
|
371
|
+
const peak = Math.round(head);
|
|
372
|
+
const chunks: TextChunk[] = [];
|
|
373
|
+
let runColor: RGBA | null = null;
|
|
374
|
+
let runText = "";
|
|
375
|
+
const flush = () => {
|
|
376
|
+
if (runColor && runText) chunks.push(fg(runColor)(runText));
|
|
377
|
+
runText = "";
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
for (let index = 0; index < crest; index++) {
|
|
381
|
+
const strength = glowFalloff(index - head, WAVE_WIDTH);
|
|
382
|
+
const color = glowColor(base, hi, bloom, strength);
|
|
383
|
+
const raised = lift && index === peak ? RAISED_LETTERS[characters[index]!] : undefined;
|
|
384
|
+
const glyph = raised ?? characters[index]!;
|
|
385
|
+
if (runColor && sameColor(runColor, color) && !raised) {
|
|
386
|
+
runText += glyph;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
flush();
|
|
390
|
+
runColor = color;
|
|
391
|
+
runText = glyph;
|
|
392
|
+
// A raised cell is one cell wide and never merges with its neighbours.
|
|
393
|
+
if (raised) {
|
|
394
|
+
flush();
|
|
395
|
+
runColor = null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
flush();
|
|
399
|
+
if (crest < characters.length) {
|
|
400
|
+
chunks.push(fg(base)(characters.slice(crest).join("")));
|
|
401
|
+
}
|
|
402
|
+
return new StyledText(chunks);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Animates a textarea placeholder in place.
|
|
407
|
+
*
|
|
408
|
+
* The textarea accepts StyledText for its placeholder, so the wave needs no
|
|
409
|
+
* overlay element. React keeps passing the plain string, which is what shows
|
|
410
|
+
* whenever the wave is inactive — with animations off, or without true colour,
|
|
411
|
+
* the placeholder is exactly the one PUM has always drawn.
|
|
412
|
+
*/
|
|
413
|
+
export function usePlaceholderWave(opts: {
|
|
414
|
+
inputRef: RefObject<TextareaRenderable | null>;
|
|
415
|
+
text: string;
|
|
416
|
+
crestEnd: number;
|
|
417
|
+
color: string;
|
|
418
|
+
highlight: string;
|
|
419
|
+
active: boolean;
|
|
420
|
+
lift?: boolean;
|
|
421
|
+
}) {
|
|
422
|
+
const { inputRef, text, crestEnd, color, highlight, active, lift = true } = opts;
|
|
423
|
+
const { subscribe, enabled } = useClock();
|
|
424
|
+
const latest = useRef({ text, crestEnd });
|
|
425
|
+
latest.current = { text, crestEnd };
|
|
426
|
+
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
if (!active || !enabled) {
|
|
429
|
+
if (inputRef.current) inputRef.current.placeholder = text;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const base = rgba(color);
|
|
433
|
+
const hi = rgba(highlight);
|
|
434
|
+
const stop = subscribe((elapsedMs) => {
|
|
435
|
+
if (!inputRef.current) return;
|
|
436
|
+
inputRef.current.placeholder = placeholderWave(
|
|
437
|
+
latest.current.text,
|
|
438
|
+
latest.current.crestEnd,
|
|
439
|
+
base,
|
|
440
|
+
hi,
|
|
441
|
+
elapsedMs,
|
|
442
|
+
lift,
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
return () => {
|
|
446
|
+
stop();
|
|
447
|
+
// Hand the plain string back, or the last frame would stay frozen on it.
|
|
448
|
+
if (inputRef.current) inputRef.current.placeholder = latest.current.text;
|
|
449
|
+
};
|
|
450
|
+
}, [inputRef, text, active, enabled, color, highlight, lift, subscribe]);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The clock provider sits below App, so the wave runs from a child that draws
|
|
455
|
+
* nothing and only owns the placeholder of the textarea it is given.
|
|
456
|
+
*/
|
|
457
|
+
export function PlaceholderWave(props: Parameters<typeof usePlaceholderWave>[0]): null {
|
|
458
|
+
usePlaceholderWave(props);
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
327
462
|
/**
|
|
328
463
|
* Owns the renderable's `content` outright — pass the returned ref to a bare
|
|
329
464
|
* `<text ref={...} />` and set no `content` prop, or the two will fight.
|