@underactive/pi-topping-moa-fusion 0.4.4 → 0.5.0
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 +41 -0
- package/README.md +0 -3
- package/package.json +9 -6
- package/src/agents/discovery.ts +24 -88
- package/src/config/settings.ts +79 -69
- package/src/debate/runDebate.ts +6 -5
- package/src/moa/modelRuntime.ts +36 -24
- package/src/opinion/opinionResults.ts +2 -2
- package/src/opinion/runOpinion.ts +6 -5
- package/src/planning/planMode.ts +15 -4
- package/src/planning/tools/enterPlanMode.ts +3 -3
- package/src/planning/tools/mfPlanSubagent.ts +13 -4
- package/src/runtime/activityTracking.ts +1 -91
- package/src/runtime/piCapabilities.ts +8 -19
- package/src/runtime/processPool.ts +14 -52
- package/src/runtime/results.ts +13 -3
- package/src/runtime/runner.ts +7 -20
- package/src/shared/modelRefs.ts +10 -71
- package/src/ui/agentStatus.ts +3 -9
- package/src/ui/cancelOverlay.ts +1 -1
- package/src/ui/chrome.ts +1 -2
- package/src/ui/debateModelPicker.ts +3 -3
- package/src/ui/moaModelPicker.ts +7 -8
- package/src/ui/moaProgressWidget.ts +14 -33
- package/src/ui/moaSetupOverlay.ts +13 -9
- package/src/ui/opinionModelPicker.ts +3 -3
- package/src/ui/twoPaneModelThinking.ts +31 -268
- package/src/ui/workingOverlay.ts +2 -2
- package/src/activityMeter.ts +0 -193
- package/src/config/modelCatalogue.ts +0 -54
- package/src/ui/modelLabel.ts +0 -61
- package/src/ui/shimmer.ts +0 -77
- package/src/ui/toolActivity.ts +0 -35
package/src/activityMeter.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output-token activity meter.
|
|
3
|
-
*
|
|
4
|
-
* Vendored from pi-topping (`src/activity-meter.ts` for the meter/rate
|
|
5
|
-
* tracker, `src/format.ts` for StreamingWordCounter) rather than depended on,
|
|
6
|
-
* so the two extensions stay independently installable. Behavior is meant to
|
|
7
|
-
* stay identical — glyph ramp, thresholds, EMA coefficient, direction and
|
|
8
|
-
* colouring are all load-bearing for visual parity, so port changes upstream
|
|
9
|
-
* rather than tuning them here. test/activity-meter.test.mjs mirrors
|
|
10
|
-
* pi-topping's tests/activity-meter.test.ts to catch drift.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
|
|
15
|
-
export const ActivityMeterLevel = {
|
|
16
|
-
IDLE: 0,
|
|
17
|
-
LOW: 1,
|
|
18
|
-
MEDIUM: 2,
|
|
19
|
-
HIGH: 3,
|
|
20
|
-
PEAK_1: 4,
|
|
21
|
-
PEAK_2: 5,
|
|
22
|
-
PEAK_3: 6,
|
|
23
|
-
FULL: 7,
|
|
24
|
-
} as const;
|
|
25
|
-
|
|
26
|
-
export type ActivityMeterLevel = (typeof ActivityMeterLevel)[keyof typeof ActivityMeterLevel];
|
|
27
|
-
|
|
28
|
-
const EMA_ALPHA = 0.4;
|
|
29
|
-
const BRAILLE: Record<ActivityMeterLevel, string> = {
|
|
30
|
-
[ActivityMeterLevel.IDLE]: "⢀",
|
|
31
|
-
[ActivityMeterLevel.LOW]: "⣀",
|
|
32
|
-
[ActivityMeterLevel.MEDIUM]: "⣠",
|
|
33
|
-
[ActivityMeterLevel.HIGH]: "⣤",
|
|
34
|
-
[ActivityMeterLevel.PEAK_1]: "⣴",
|
|
35
|
-
[ActivityMeterLevel.PEAK_2]: "⣶",
|
|
36
|
-
[ActivityMeterLevel.PEAK_3]: "⣾",
|
|
37
|
-
[ActivityMeterLevel.FULL]: "⣿",
|
|
38
|
-
};
|
|
39
|
-
export const ACTIVITY_METER_WIDTH = 8;
|
|
40
|
-
type CellColorizer = (level: ActivityMeterLevel, char: string) => string;
|
|
41
|
-
|
|
42
|
-
/** Convert an estimated output-token rate to a display level. */
|
|
43
|
-
export function rateToLevel(tokensPerSecond: number): ActivityMeterLevel {
|
|
44
|
-
const THRESHOLDS = [0, 5, 10, 15, 22, 30, 40];
|
|
45
|
-
for (let i = THRESHOLDS.length - 1; i >= 0; i--) {
|
|
46
|
-
if (tokensPerSecond > THRESHOLDS[i]) return (i + 1) as ActivityMeterLevel;
|
|
47
|
-
}
|
|
48
|
-
return 0;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** EMA-smoothed rate tracker for a cumulative output-token estimate. */
|
|
52
|
-
export class TokRateTracker {
|
|
53
|
-
#lastTotal = 0;
|
|
54
|
-
#lastTime = 0;
|
|
55
|
-
#rate = 0;
|
|
56
|
-
#hasSample = false;
|
|
57
|
-
#pendingTokens = 0;
|
|
58
|
-
|
|
59
|
-
sample(totalTokens: number, now: number): number {
|
|
60
|
-
if (!this.#hasSample) {
|
|
61
|
-
this.#lastTotal = totalTokens;
|
|
62
|
-
this.#hasSample = true;
|
|
63
|
-
this.#lastTime = now;
|
|
64
|
-
return this.#rate;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const elapsedSeconds = (now - this.#lastTime) / 1_000;
|
|
68
|
-
if (elapsedSeconds <= 0) {
|
|
69
|
-
this.#pendingTokens += Math.max(0, totalTokens - this.#lastTotal);
|
|
70
|
-
this.#lastTotal = totalTokens;
|
|
71
|
-
return this.#rate;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const totalDelta = this.#pendingTokens + Math.max(0, totalTokens - this.#lastTotal);
|
|
75
|
-
const instantRate = totalDelta / elapsedSeconds;
|
|
76
|
-
this.#rate = EMA_ALPHA * instantRate + (1 - EMA_ALPHA) * this.#rate;
|
|
77
|
-
this.#lastTotal = totalTokens;
|
|
78
|
-
this.#lastTime = now;
|
|
79
|
-
this.#pendingTokens = 0;
|
|
80
|
-
return this.#rate;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
reset(): void {
|
|
84
|
-
this.#lastTotal = 0;
|
|
85
|
-
this.#lastTime = 0;
|
|
86
|
-
this.#rate = 0;
|
|
87
|
-
this.#hasSample = false;
|
|
88
|
-
this.#pendingTokens = 0;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Eight-column scrolling activity meter, scrolling either left-to-right or right-to-left. */
|
|
93
|
-
export class ActivityMeter {
|
|
94
|
-
#levels: ActivityMeterLevel[] = Array<ActivityMeterLevel>(ACTIVITY_METER_WIDTH).fill(0);
|
|
95
|
-
#direction: "ltr" | "rtl";
|
|
96
|
-
|
|
97
|
-
constructor(direction: "ltr" | "rtl" = "ltr") {
|
|
98
|
-
this.#direction = direction;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
setDirection(direction: "ltr" | "rtl"): void {
|
|
102
|
-
if (direction !== this.#direction) {
|
|
103
|
-
this.#direction = direction;
|
|
104
|
-
// Reverse the existing data so the visual flow instantly flips.
|
|
105
|
-
this.#levels.reverse();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
push(level: ActivityMeterLevel): void {
|
|
110
|
-
if (this.#direction === "rtl") {
|
|
111
|
-
this.#levels.shift();
|
|
112
|
-
this.#levels.push(level);
|
|
113
|
-
} else {
|
|
114
|
-
this.#levels.pop();
|
|
115
|
-
this.#levels.unshift(level);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
render(colorize?: CellColorizer): string {
|
|
120
|
-
return this.#levels
|
|
121
|
-
.map((level) => {
|
|
122
|
-
const char = BRAILLE[level];
|
|
123
|
-
return colorize ? colorize(level, char) : char;
|
|
124
|
-
})
|
|
125
|
-
.join("");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
reset(): void {
|
|
129
|
-
this.#levels.fill(0);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Colorize a meter cell using the default theme mapping (dim at IDLE, `color` otherwise). */
|
|
133
|
-
static colorizeCell(
|
|
134
|
-
level: ActivityMeterLevel,
|
|
135
|
-
char: string,
|
|
136
|
-
theme: { fg: (style: ThemeColor, s: string) => string },
|
|
137
|
-
color: ThemeColor = "accent",
|
|
138
|
-
dimmed?: boolean,
|
|
139
|
-
): string {
|
|
140
|
-
if (level === ActivityMeterLevel.IDLE) return theme.fg("dim", char);
|
|
141
|
-
const colored = theme.fg(color, char);
|
|
142
|
-
// ANSI dim (SGR 2) reduces color brightness; SGR 22 resets dim/bold.
|
|
143
|
-
// This is necessary because theme.fg("dim", ...) sets a gray color instead
|
|
144
|
-
// of applying the ANSI dim attribute, so nesting would overwrite the accent color.
|
|
145
|
-
return dimmed ? `\x1b[2m${colored}\x1b[22m` : colored;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Counts words across a split stream without re-scanning earlier chunks: each
|
|
151
|
-
* stream remembers whether it ended mid-word, so a word broken across two
|
|
152
|
-
* deltas is counted once. Used as the output-token estimate for providers that
|
|
153
|
-
* never report usage.
|
|
154
|
-
*/
|
|
155
|
-
export class StreamingWordCounter {
|
|
156
|
-
#inWordByStream = new Map<string, boolean>();
|
|
157
|
-
|
|
158
|
-
count(text: string, stream = "default"): number {
|
|
159
|
-
let inWord = this.#inWordByStream.get(stream) ?? false;
|
|
160
|
-
let count = 0;
|
|
161
|
-
for (let i = 0; i < text.length; i++) {
|
|
162
|
-
const code = text.charCodeAt(i);
|
|
163
|
-
if (isWhitespace(code)) {
|
|
164
|
-
inWord = false;
|
|
165
|
-
} else if (!inWord) {
|
|
166
|
-
count++;
|
|
167
|
-
inWord = true;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
this.#inWordByStream.set(stream, inWord);
|
|
171
|
-
return count;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
reset(): void {
|
|
175
|
-
this.#inWordByStream.clear();
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function isWhitespace(code: number): boolean {
|
|
180
|
-
return (
|
|
181
|
-
code === 32 ||
|
|
182
|
-
(code >= 9 && code <= 13) ||
|
|
183
|
-
code === 160 ||
|
|
184
|
-
code === 0x1680 ||
|
|
185
|
-
(code >= 0x2000 && code <= 0x200a) ||
|
|
186
|
-
code === 0x2028 ||
|
|
187
|
-
code === 0x2029 ||
|
|
188
|
-
code === 0x202f ||
|
|
189
|
-
code === 0x205f ||
|
|
190
|
-
code === 0x3000 ||
|
|
191
|
-
code === 0xfeff
|
|
192
|
-
);
|
|
193
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cached view of the models pi reports as usable, with the thinking levels the
|
|
3
|
-
* registry declares for each.
|
|
4
|
-
*
|
|
5
|
-
* The registry is the only authority the pickers consult: it decides both which
|
|
6
|
-
* models may be selected and which thinking levels each one supports, so a
|
|
7
|
-
* confirmed selection needs no further verification. Whether a model actually
|
|
8
|
-
* answers is decided when it runs, not here.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { getSupportedThinkingLevels, type Api, type Model } from "@earendil-works/pi-ai";
|
|
12
|
-
import { isThinkingLevel, modelRefLabel, parseRef, type ModelRef, type ThinkingLevel } from "../shared/modelRefs.ts";
|
|
13
|
-
|
|
14
|
-
export interface ModelCatalogue {
|
|
15
|
-
/** Selectable models, sorted by `provider/id`. */
|
|
16
|
-
availableRefs(): ModelRef[];
|
|
17
|
-
/** Levels the registry declares for a model, in pi's canonical order. */
|
|
18
|
-
thinkingLevelsFor(ref: ModelRef): ThinkingLevel[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** The slice of pi's `ModelRegistry` a catalogue is built from. */
|
|
22
|
-
export interface CatalogueRegistry {
|
|
23
|
-
getAvailable(): Model<Api>[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Lives for as long as the registry object itself: pi hands extensions a stable
|
|
27
|
-
// registry per session, and dropping the registry drops its catalogue with it.
|
|
28
|
-
const CATALOGUES = new WeakMap<CatalogueRegistry, ModelCatalogue>();
|
|
29
|
-
|
|
30
|
-
export function getModelCatalogue(registry: CatalogueRegistry): ModelCatalogue {
|
|
31
|
-
const cached = CATALOGUES.get(registry);
|
|
32
|
-
if (cached) return cached;
|
|
33
|
-
const built = buildCatalogue(registry);
|
|
34
|
-
CATALOGUES.set(registry, built);
|
|
35
|
-
return built;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function buildCatalogue(registry: CatalogueRegistry): ModelCatalogue {
|
|
39
|
-
const levels = new Map<string, ThinkingLevel[]>();
|
|
40
|
-
for (const model of registry.getAvailable()) {
|
|
41
|
-
const key = `${model.provider}/${model.id}`;
|
|
42
|
-
if (levels.has(key)) continue;
|
|
43
|
-
levels.set(key, getSupportedThinkingLevels(model).filter(isThinkingLevel));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const refs = [...levels.keys()]
|
|
47
|
-
.sort((a, b) => a.localeCompare(b))
|
|
48
|
-
.map((key) => parseRef(key));
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
availableRefs: () => [...refs],
|
|
52
|
-
thinkingLevelsFor: (ref) => levels.get(modelRefLabel(ref)) ?? [],
|
|
53
|
-
};
|
|
54
|
-
}
|
package/src/ui/modelLabel.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { truncateToWidth, visibleWidth, type SelectListLayoutOptions } from "@earendil-works/pi-tui";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Truncates a slash-namespaced model ref (`provider/accounts/.../model-name`)
|
|
5
|
-
* so the parts that matter to identify the model survive first. Priority:
|
|
6
|
-
* the model name (last segment) is mandatory, then the first segment (the
|
|
7
|
-
* namespace root), then the segment right before the model name, then
|
|
8
|
-
* whatever else still fits. Skipped runs collapse to a single ellipsis.
|
|
9
|
-
*/
|
|
10
|
-
export function smartTruncateModelLabel(text: string, maxWidth: number): string {
|
|
11
|
-
const ELLIPSIS = "…";
|
|
12
|
-
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
13
|
-
if (visibleWidth(text) <= maxWidth) return text;
|
|
14
|
-
const segments = text.split("/");
|
|
15
|
-
const n = segments.length;
|
|
16
|
-
if (n === 1) return truncateToWidth(text, maxWidth, ELLIPSIS).replace(ANSI_RE, "");
|
|
17
|
-
|
|
18
|
-
const lastIndex = n - 1;
|
|
19
|
-
const kept = new Set<number>([lastIndex]);
|
|
20
|
-
const priority: number[] = [];
|
|
21
|
-
priority.push(0);
|
|
22
|
-
if (n - 2 > 0) priority.push(n - 2);
|
|
23
|
-
for (let i = 1; i <= n - 3; i++) priority.push(i);
|
|
24
|
-
|
|
25
|
-
const render = () => {
|
|
26
|
-
const parts: string[] = [];
|
|
27
|
-
let i = 0;
|
|
28
|
-
while (i < n) {
|
|
29
|
-
if (kept.has(i)) {
|
|
30
|
-
parts.push(segments[i]!);
|
|
31
|
-
i++;
|
|
32
|
-
} else {
|
|
33
|
-
while (i < n && !kept.has(i)) i++;
|
|
34
|
-
parts.push(ELLIPSIS);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return parts.join("/");
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
let current = render();
|
|
41
|
-
if (visibleWidth(current) > maxWidth) {
|
|
42
|
-
const skeleton = `${ELLIPSIS}/`;
|
|
43
|
-
const budget = Math.max(0, maxWidth - visibleWidth(skeleton));
|
|
44
|
-
return (skeleton + truncateToWidth(segments[lastIndex]!, budget, ELLIPSIS)).replace(ANSI_RE, "");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
for (const index of priority) {
|
|
48
|
-
kept.add(index);
|
|
49
|
-
const candidate = render();
|
|
50
|
-
if (visibleWidth(candidate) > maxWidth) {
|
|
51
|
-
kept.delete(index);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
current = candidate;
|
|
55
|
-
}
|
|
56
|
-
return current;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const MODEL_LIST_LAYOUT: SelectListLayoutOptions = {
|
|
60
|
-
truncatePrimary: ({ text, maxWidth }) => smartTruncateModelLabel(text, maxWidth),
|
|
61
|
-
};
|
package/src/ui/shimmer.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codex-style light-sweep text shimmer.
|
|
3
|
-
*
|
|
4
|
-
* Vendored from pi-topping-persona-audit (`src/shimmer.ts`) rather than
|
|
5
|
-
* depended on, so the extensions stay independently installable. The sweep
|
|
6
|
-
* geometry — band half-width, padding, speed scaling — is load-bearing for
|
|
7
|
-
* visual parity, so port changes upstream rather than tuning them here.
|
|
8
|
-
* test/shimmer.test.mjs mirrors the upstream tests to catch drift.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
|
|
13
|
-
const SHIMMER_SWEEP_S = 2.0;
|
|
14
|
-
const SHIMMER_BAND_HALF = 5.0;
|
|
15
|
-
const SHIMMER_PADDING = 10;
|
|
16
|
-
|
|
17
|
-
/** The slice of pi's `Theme` shimmerString needs. Structural so callers can supply a stub. */
|
|
18
|
-
export interface ShimmerTheme {
|
|
19
|
-
getFgAnsi(color: ThemeColor): string;
|
|
20
|
-
fg(color: ThemeColor, text: string): string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* With `invert=true`, the text color is used at rest and the sweep moves toward
|
|
25
|
-
* the theme's dim color.
|
|
26
|
-
*/
|
|
27
|
-
export function shimmerString(
|
|
28
|
-
text: string,
|
|
29
|
-
elapsedMs: number,
|
|
30
|
-
theme: ShimmerTheme,
|
|
31
|
-
direction: "ltr" | "rtl" = "ltr",
|
|
32
|
-
speed: "slow" | "normal" | "fast" = "normal",
|
|
33
|
-
invert = false,
|
|
34
|
-
): string {
|
|
35
|
-
const chars = [...text];
|
|
36
|
-
if (chars.length === 0) return "";
|
|
37
|
-
const shimmerBase = ansiToRgb(theme.getFgAnsi(invert ? "text" : "dim"));
|
|
38
|
-
const shimmerHighlight = ansiToRgb(theme.getFgAnsi(invert ? "dim" : "text"));
|
|
39
|
-
if (!shimmerBase || !shimmerHighlight) return theme.fg("text", text);
|
|
40
|
-
const period = chars.length + SHIMMER_PADDING * 2;
|
|
41
|
-
const unitsPerS = period / SHIMMER_SWEEP_S;
|
|
42
|
-
// The band crosses padding at either end while every character is still dim. Scaling only
|
|
43
|
-
// the stretch where it actually overlaps the text keeps that dark pause identical at every
|
|
44
|
-
// speed, so `speed` changes the sweep alone rather than the whole cycle.
|
|
45
|
-
const litEnter = SHIMMER_PADDING - SHIMMER_BAND_HALF;
|
|
46
|
-
const litExit = SHIMMER_PADDING + chars.length - 1 + SHIMMER_BAND_HALF;
|
|
47
|
-
const enterS = litEnter / unitsPerS;
|
|
48
|
-
const litS = (litExit - litEnter) / unitsPerS / (speed === "slow" ? 0.5 : speed === "fast" ? 2 : 1);
|
|
49
|
-
const phase = (elapsedMs / 1000) % (enterS + litS + (period - litExit) / unitsPerS);
|
|
50
|
-
const linear =
|
|
51
|
-
phase < enterS
|
|
52
|
-
? phase * unitsPerS
|
|
53
|
-
: phase < enterS + litS
|
|
54
|
-
? litEnter + ((phase - enterS) / litS) * (litExit - litEnter)
|
|
55
|
-
: litExit + (phase - enterS - litS) * unitsPerS;
|
|
56
|
-
const pos = direction === "rtl" ? period - linear : linear;
|
|
57
|
-
|
|
58
|
-
let out = "";
|
|
59
|
-
for (let i = 0; i < chars.length; i++) {
|
|
60
|
-
const ch = chars[i]!;
|
|
61
|
-
const dist = Math.abs(i + SHIMMER_PADDING - pos);
|
|
62
|
-
const t = dist <= SHIMMER_BAND_HALF ? 0.5 * (1 + Math.cos((Math.PI * dist) / SHIMMER_BAND_HALF)) : 0;
|
|
63
|
-
const alpha = t * 0.9;
|
|
64
|
-
const r = Math.round(shimmerHighlight[0] * alpha + shimmerBase[0] * (1 - alpha));
|
|
65
|
-
const g = Math.round(shimmerHighlight[1] * alpha + shimmerBase[1] * (1 - alpha));
|
|
66
|
-
const b = Math.round(shimmerHighlight[2] * alpha + shimmerBase[2] * (1 - alpha));
|
|
67
|
-
const bold = !invert && t > 0.2 ? "\x1b[1m" : "";
|
|
68
|
-
out += `${bold}\x1b[38;2;${r};${g};${b}m${ch}\x1b[22m`;
|
|
69
|
-
}
|
|
70
|
-
return out + "\x1b[0m";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function ansiToRgb(ansi: string): [number, number, number] | null {
|
|
74
|
-
const match = ansi.match(/^\x1b\[38;2;(\d+);(\d+);(\d+)m$/);
|
|
75
|
-
if (!match) return null;
|
|
76
|
-
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
77
|
-
}
|
package/src/ui/toolActivity.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared renderer for one-line tool-activity labels (`formatToolActivity`
|
|
3
|
-
* output), so the MoA progress table and the cancel overlay color a tool
|
|
4
|
-
* name and its argument identically, following pi's own read/grep/edit/write
|
|
5
|
-
* renderers (bold `toolTitle` name, `accent` argument).
|
|
6
|
-
*
|
|
7
|
-
* Splits only at the first run of whitespace — the boundary
|
|
8
|
-
* `formatToolActivity` itself produces between a tool name and its argument
|
|
9
|
-
* — and never rewrites the text on either side, so activity strings stay
|
|
10
|
-
* byte-identical for the runtime's equality-based loop detection.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
|
|
15
|
-
/** The slice of pi's `Theme` this renderer needs. */
|
|
16
|
-
export interface ActivityTheme {
|
|
17
|
-
fg(color: ThemeColor, text: string): string;
|
|
18
|
-
bold(text: string): string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const ACTIVITY_SPLIT = /^(\s*)(\S+)(\s*)([\s\S]*)$/;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Highlight a plain activity string as a bold `toolTitle` tool name followed
|
|
25
|
-
* by its `accent` argument, preserving every character — including leading
|
|
26
|
-
* and repeated whitespace — exactly. Falls back to the raw string when no
|
|
27
|
-
* tool token can be found at all (empty or whitespace-only input).
|
|
28
|
-
*/
|
|
29
|
-
export function highlightActivity(theme: ActivityTheme, activity: string): string {
|
|
30
|
-
const match = activity.match(ACTIVITY_SPLIT);
|
|
31
|
-
if (!match) return activity;
|
|
32
|
-
const [, leading, tool, sep, detail] = match;
|
|
33
|
-
const styledTool = theme.fg("toolTitle", theme.bold(tool));
|
|
34
|
-
return detail ? `${leading}${styledTool}${sep}${theme.fg("accent", detail)}` : `${leading}${styledTool}${sep}`;
|
|
35
|
-
}
|