@underactive/pi-topping-moa-fusion 0.4.5 → 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 +29 -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 +7 -23
- package/src/opinion/opinionResults.ts +2 -2
- package/src/opinion/runOpinion.ts +6 -5
- package/src/planning/planMode.ts +5 -3
- package/src/planning/tools/enterPlanMode.ts +3 -3
- package/src/planning/tools/mfPlanSubagent.ts +2 -2
- 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
|
@@ -1,96 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { AssistantMessage, Message, ToolCall } from "@earendil-works/pi-ai";
|
|
3
|
-
import {
|
|
4
|
-
import { StreamingWordCounter } from "../activityMeter.ts";
|
|
5
|
-
import { isNonnegativeFiniteNumber, isValidContentIndex } from "./wire.ts";
|
|
6
|
-
|
|
7
|
-
/** Max length of a one-line tool-activity description surfaced for progress UIs. */
|
|
8
|
-
const ACTIVITY_MAX = 80;
|
|
9
|
-
|
|
10
|
-
/** Collapse all whitespace/newlines to single spaces and trim. */
|
|
11
|
-
function collapseWhitespace(text: string): string {
|
|
12
|
-
return text.replace(/\s+/g, " ").trim();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Pick the most informative string argument for a one-line activity label.
|
|
17
|
-
* Prefers well-known tool arg names (command, pattern, path, …); falls back to
|
|
18
|
-
* the first non-empty string value.
|
|
19
|
-
*/
|
|
20
|
-
function summarizeToolArgs(args: unknown): string {
|
|
21
|
-
if (!args || typeof args !== "object") return "";
|
|
22
|
-
const record = args as Record<string, unknown>;
|
|
23
|
-
const preferred = ["command", "pattern", "query", "path", "file", "glob", "dir", "url", "name"];
|
|
24
|
-
for (const key of preferred) {
|
|
25
|
-
const value = record[key];
|
|
26
|
-
if (typeof value === "string" && value.trim()) return collapseWhitespace(value);
|
|
27
|
-
}
|
|
28
|
-
for (const value of Object.values(record)) {
|
|
29
|
-
if (typeof value === "string" && value.trim()) return collapseWhitespace(value);
|
|
30
|
-
}
|
|
31
|
-
return "";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Format a `tool_execution_start` event into a single-line "toolName arg" label. */
|
|
35
|
-
export function formatToolActivity(toolName: string, args: unknown): string {
|
|
36
|
-
const detail = summarizeToolArgs(args);
|
|
37
|
-
const label = detail ? `${toolName} ${detail}` : String(toolName);
|
|
38
|
-
return stripTerminalSequences(collapseWhitespace(label).slice(0, ACTIVITY_MAX));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Cumulative generated-output reading for the fan-out activity monitor. */
|
|
42
|
-
export interface OutputActivity {
|
|
43
|
-
/** Tokens generated so far this run: confirmed turns plus the in-flight turn. */
|
|
44
|
-
tokens: number;
|
|
45
|
-
/**
|
|
46
|
-
* Bumped whenever exact provider usage replaces a differing estimate.
|
|
47
|
-
* Consumers reset their rate tracker on a change so the correction doesn't
|
|
48
|
-
* register as a burst of generation that never happened.
|
|
49
|
-
*/
|
|
50
|
-
revision: number;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export class OutputActivityTracker {
|
|
54
|
-
#counter = new StreamingWordCounter();
|
|
55
|
-
#confirmed = 0;
|
|
56
|
-
#liveWords = 0;
|
|
57
|
-
#revision = 0;
|
|
58
|
-
|
|
59
|
-
/** Begin a new assistant turn, discarding only the previous turn's live estimate. */
|
|
60
|
-
messageStart(message: { role?: string } | undefined): void {
|
|
61
|
-
if (message?.role !== "assistant") return;
|
|
62
|
-
this.#resetTurn();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
messageUpdate(assistantEvent: { type?: string; delta?: string } | undefined): void {
|
|
66
|
-
if (assistantEvent?.type !== "text_delta" && assistantEvent?.type !== "thinking_delta") return;
|
|
67
|
-
if (typeof assistantEvent.delta !== "string") return;
|
|
68
|
-
// Counted per stream kind so a word split across deltas isn't double
|
|
69
|
-
// counted, and interleaved thinking/text streams don't corrupt each other.
|
|
70
|
-
this.#liveWords += this.#counter.count(assistantEvent.delta, assistantEvent.type);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
messageEnd(message: { role?: string; usage?: { output?: unknown } } | undefined): void {
|
|
74
|
-
if (message?.role !== "assistant") return;
|
|
75
|
-
const exact = message.usage?.output;
|
|
76
|
-
if (isNonnegativeFiniteNumber(exact)) {
|
|
77
|
-
this.#confirmed += exact;
|
|
78
|
-
if (exact !== this.#liveWords) this.#revision++;
|
|
79
|
-
} else {
|
|
80
|
-
this.#confirmed += this.#liveWords;
|
|
81
|
-
}
|
|
82
|
-
this.#resetTurn();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
snapshot(): OutputActivity {
|
|
86
|
-
return { tokens: this.#confirmed + this.#liveWords, revision: this.#revision };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
#resetTurn(): void {
|
|
90
|
-
this.#liveWords = 0;
|
|
91
|
-
this.#counter.reset();
|
|
92
|
-
}
|
|
93
|
-
}
|
|
3
|
+
import { isValidContentIndex } from "./wire.ts";
|
|
94
4
|
|
|
95
5
|
type AssistantContentPart = AssistantMessage["content"][number];
|
|
96
6
|
|
|
@@ -2,34 +2,23 @@
|
|
|
2
2
|
* Capability probing for the pi executable a child process will actually run.
|
|
3
3
|
* The local `@earendil-works/pi-coding-agent` VERSION export describes the
|
|
4
4
|
* package this extension was built against, not the host (or `pi` on PATH)
|
|
5
|
-
* that `
|
|
5
|
+
* that `resolvePiInvocation()` spawns, so flag support is probed on the executable.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { execFile } from "node:child_process";
|
|
9
|
+
import {
|
|
10
|
+
type PiInvocation,
|
|
11
|
+
parsePiVersion,
|
|
12
|
+
versionAtLeast,
|
|
13
|
+
} from "@underactive/pi-topping-primitives/process";
|
|
9
14
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
args: string[];
|
|
13
|
-
}
|
|
15
|
+
export { parsePiVersion, versionAtLeast };
|
|
16
|
+
export type { PiInvocation };
|
|
14
17
|
|
|
15
18
|
/** `--no-mcp` first ships in pi 1.0.4; 1.0.3 rejects it as an unknown option. */
|
|
16
19
|
const NO_MCP_MIN_VERSION = [1, 0, 4] as const;
|
|
17
20
|
const VERSION_PROBE_TIMEOUT_MS = 10_000;
|
|
18
21
|
|
|
19
|
-
export function parsePiVersion(output: string): [number, number, number] | undefined {
|
|
20
|
-
const match = /(\d+)\.(\d+)\.(\d+)/.exec(output);
|
|
21
|
-
if (!match) return undefined;
|
|
22
|
-
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function versionAtLeast(version: readonly number[], minimum: readonly number[]): boolean {
|
|
26
|
-
for (let i = 0; i < minimum.length; i++) {
|
|
27
|
-
const diff = (version[i] ?? 0) - minimum[i];
|
|
28
|
-
if (diff !== 0) return diff > 0;
|
|
29
|
-
}
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
22
|
export function versionSupportsNoMcp(output: string): boolean {
|
|
34
23
|
const version = parsePiVersion(output);
|
|
35
24
|
return version !== undefined && versionAtLeast(version, NO_MCP_MIN_VERSION);
|
|
@@ -1,57 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createProcessTracker,
|
|
3
|
+
escalateKill,
|
|
4
|
+
mapWithConcurrencyLimit,
|
|
5
|
+
} from "@underactive/pi-topping-primitives/process";
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
export const trackedProcesses: Set<ChildProcess> = new Set();
|
|
7
|
+
export { escalateKill, mapWithConcurrencyLimit };
|
|
5
8
|
|
|
6
|
-
/**
|
|
7
|
-
export
|
|
8
|
-
if (proc.exitCode !== null || proc.signalCode !== null) return;
|
|
9
|
-
try { proc.kill("SIGTERM"); } catch { return; }
|
|
10
|
-
setTimeout(() => {
|
|
11
|
-
if (proc.exitCode !== null || proc.signalCode !== null) return;
|
|
12
|
-
try { proc.kill("SIGKILL"); } catch { /* ignore */ }
|
|
13
|
-
}, termTimeoutMs).unref();
|
|
14
|
-
}
|
|
9
|
+
/** This extension's spawned processes, so /reload kills only what moa-fusion started. */
|
|
10
|
+
export const trackedProcesses = createProcessTracker();
|
|
15
11
|
|
|
16
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Kill every tracked process. Safe to call multiple times. The snapshot-then-clear ordering
|
|
14
|
+
* (the delayed SIGKILL pass still reaches the processes that were just signalled) lives in the
|
|
15
|
+
* package's tracker.
|
|
16
|
+
*/
|
|
17
17
|
export function cleanupTrackedProcesses(termTimeoutMs = 5000): void {
|
|
18
|
-
|
|
19
|
-
// reaches the same processes that were just signalled.
|
|
20
|
-
const processes = [...trackedProcesses];
|
|
21
|
-
for (const proc of processes) {
|
|
22
|
-
if (proc.exitCode !== null || proc.signalCode !== null) continue;
|
|
23
|
-
try { proc.kill("SIGTERM"); } catch { /* ignore */ }
|
|
24
|
-
}
|
|
25
|
-
trackedProcesses.clear();
|
|
26
|
-
|
|
27
|
-
// Give SIGTERM a moment, then SIGKILL any process that has not exited.
|
|
28
|
-
setTimeout(() => {
|
|
29
|
-
for (const proc of processes) {
|
|
30
|
-
if (proc.exitCode !== null || proc.signalCode !== null) continue;
|
|
31
|
-
try { proc.kill("SIGKILL"); } catch { /* ignore */ }
|
|
32
|
-
}
|
|
33
|
-
}, termTimeoutMs).unref();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
export async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
38
|
-
items: TIn[],
|
|
39
|
-
concurrency: number,
|
|
40
|
-
fn: (item: TIn, index: number) => Promise<TOut>,
|
|
41
|
-
onStart?: (index: number) => void,
|
|
42
|
-
): Promise<TOut[]> {
|
|
43
|
-
if (items.length === 0) return [];
|
|
44
|
-
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
45
|
-
const results: TOut[] = new Array(items.length);
|
|
46
|
-
let nextIndex = 0;
|
|
47
|
-
const workers = new Array(limit).fill(null).map(async () => {
|
|
48
|
-
while (true) {
|
|
49
|
-
const current = nextIndex++;
|
|
50
|
-
if (current >= items.length) return;
|
|
51
|
-
onStart?.(current);
|
|
52
|
-
results[current] = await fn(items[current], current);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
await Promise.all(workers);
|
|
56
|
-
return results;
|
|
18
|
+
trackedProcesses.cleanup(termTimeoutMs);
|
|
57
19
|
}
|
package/src/runtime/results.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
-
import type {
|
|
2
|
+
import type { OutputTokenSnapshot } from "@underactive/pi-topping-primitives/activity";
|
|
3
3
|
|
|
4
4
|
export interface UsageStats {
|
|
5
5
|
input: number;
|
|
@@ -41,7 +41,7 @@ export interface SingleResult {
|
|
|
41
41
|
* Distinct from `usage.output`, which only advances at turn boundaries and
|
|
42
42
|
* stays at 0 for providers that never report usage.
|
|
43
43
|
*/
|
|
44
|
-
outputActivity?:
|
|
44
|
+
outputActivity?: OutputTokenSnapshot;
|
|
45
45
|
/**
|
|
46
46
|
* True when this agent was aborted (per-agent/user cancellation) and the
|
|
47
47
|
* caller opted into `resolveOnAbort`. Cancelled results still count as
|
|
@@ -85,11 +85,21 @@ export function isFailedResult(result: { exitCode: number; stopReason?: string;
|
|
|
85
85
|
);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Prefixed to a child's output when its final response stopped at the model's
|
|
90
|
+
* output-token limit. Children run in `--mode json`, where pi exits 0 on a
|
|
91
|
+
* `length` stop (only text mode exits 1), so the reply counts as a success and
|
|
92
|
+
* this is the only sign that it was cut off. It leads rather than trails the
|
|
93
|
+
* output so `truncateOutput` cannot cut it away.
|
|
94
|
+
*/
|
|
95
|
+
export const RESPONSE_TRUNCATED_WARNING = "[Response was truncated: the model reached its output token limit.]";
|
|
96
|
+
|
|
88
97
|
export function getResultOutput(result: { exitCode: number; stopReason?: string; errorMessage?: string; stderr?: string; messages: Message[] }): string {
|
|
89
98
|
if (isFailedResult(result)) {
|
|
90
99
|
return result.errorMessage || result.stderr || getFinalOutput(result.messages) || "No agent messages were emitted.";
|
|
91
100
|
}
|
|
92
|
-
|
|
101
|
+
const output = getFinalOutput(result.messages) || "(no output)";
|
|
102
|
+
return result.stopReason === "length" ? `${RESPONSE_TRUNCATED_WARNING}\n\n${output}` : output;
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
/**
|
package/src/runtime/runner.ts
CHANGED
|
@@ -10,10 +10,13 @@ import * as os from "node:os";
|
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
12
12
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { OutputTokenAccount } from "@underactive/pi-topping-primitives/activity";
|
|
14
|
+
import { formatToolActivity } from "@underactive/pi-topping-primitives/activity-tui";
|
|
15
|
+
import { resolvePiInvocation } from "@underactive/pi-topping-primitives/process";
|
|
13
16
|
import type { AgentConfig } from "../agents/discovery.ts";
|
|
14
17
|
import { normalizeMaxConcurrentAgents } from "../config/settings.ts";
|
|
15
18
|
import type { ThinkingLevel } from "../shared/modelRefs.ts";
|
|
16
|
-
import {
|
|
19
|
+
import { PartialAssistantAssembler } from "./activityTracking.ts";
|
|
17
20
|
import { piSupportsNoMcp, type PiInvocation } from "./piCapabilities.ts";
|
|
18
21
|
import { escalateKill, mapWithConcurrencyLimit, trackedProcesses } from "./processPool.ts";
|
|
19
22
|
import { getFinalOutput, type SingleResult } from "./results.ts";
|
|
@@ -71,22 +74,6 @@ export function getReadOnlyAgentTools(configuredTools?: string[]): string[] {
|
|
|
71
74
|
return configuredTools.filter((tool) => READ_ONLY_AGENT_TOOL_SET.has(tool));
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
function getPiInvocation(args: string[]): PiInvocation {
|
|
75
|
-
const currentScript = process.argv[1];
|
|
76
|
-
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
77
|
-
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
|
78
|
-
return { command: process.execPath, args: [currentScript, ...args] };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const execName = path.basename(process.execPath).toLowerCase();
|
|
82
|
-
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
83
|
-
if (!isGenericRuntime) {
|
|
84
|
-
return { command: process.execPath, args };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return { command: "pi", args };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
77
|
export interface RunSingleAgentOptions {
|
|
91
78
|
/**
|
|
92
79
|
* Load all configured pi extensions in the child process. Retained as a
|
|
@@ -201,7 +188,7 @@ export async function runSingleAgent(
|
|
|
201
188
|
return currentResult;
|
|
202
189
|
}
|
|
203
190
|
|
|
204
|
-
const outputActivity = new
|
|
191
|
+
const outputActivity = new OutputTokenAccount();
|
|
205
192
|
const partialMessage = new PartialAssistantAssembler();
|
|
206
193
|
|
|
207
194
|
const emitUpdate = () => {
|
|
@@ -230,7 +217,7 @@ export async function runSingleAgent(
|
|
|
230
217
|
// will run knows the flag. The default and isolated paths already pass
|
|
231
218
|
// --no-extensions, which excludes built-in MCP.
|
|
232
219
|
if (options.loadExtensions && !options.extensionPath) {
|
|
233
|
-
const probe = options.piInvocation ??
|
|
220
|
+
const probe = options.piInvocation ?? resolvePiInvocation([]);
|
|
234
221
|
if (await piSupportsNoMcp(probe)) args.push("--no-mcp");
|
|
235
222
|
}
|
|
236
223
|
|
|
@@ -240,7 +227,7 @@ export async function runSingleAgent(
|
|
|
240
227
|
const exitCode = await new Promise<number>((resolve) => {
|
|
241
228
|
const invocation = options.piInvocation
|
|
242
229
|
? { command: options.piInvocation.command, args: [...options.piInvocation.args, ...args] }
|
|
243
|
-
:
|
|
230
|
+
: resolvePiInvocation(args);
|
|
244
231
|
|
|
245
232
|
const appendDiagnostics = (diagnostics: string) => {
|
|
246
233
|
currentResult.stderr = (currentResult.stderr + diagnostics).slice(-MAX_DIAGNOSTIC_CHARS);
|
package/src/shared/modelRefs.ts
CHANGED
|
@@ -1,42 +1,18 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export {
|
|
2
|
+
isModelRef,
|
|
3
|
+
isThinkingLevel,
|
|
4
|
+
modelRefLabel,
|
|
5
|
+
parseRef,
|
|
6
|
+
shortModelName,
|
|
7
|
+
THINKING_LEVELS,
|
|
8
|
+
type ModelRef,
|
|
9
|
+
type ThinkingLevel,
|
|
10
|
+
} from "@underactive/pi-topping-primitives/models";
|
|
5
11
|
|
|
6
12
|
export type SendUserMessageOptions = { deliverAs?: "steer" | "followUp"; triggerTurn?: boolean };
|
|
7
13
|
export const TRIGGER_TURN: SendUserMessageOptions = { triggerTurn: true };
|
|
8
14
|
export const FOLLOW_UP: SendUserMessageOptions = { deliverAs: "followUp" };
|
|
9
15
|
|
|
10
|
-
/**
|
|
11
|
-
* pi's thinking-level vocabulary, in pi's canonical order (see
|
|
12
|
-
* `pi.getThinkingLevel()` / `--thinking <level>`). `xhigh` and `max` are
|
|
13
|
-
* distinct levels a model may expose independently, so neither is folded
|
|
14
|
-
* into the other.
|
|
15
|
-
*/
|
|
16
|
-
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
17
|
-
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
18
|
-
|
|
19
|
-
export function isThinkingLevel(value: string): value is ThinkingLevel {
|
|
20
|
-
return (THINKING_LEVELS as readonly string[]).includes(value);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function isModelRef(value: unknown): value is ModelRef {
|
|
24
|
-
return !!value
|
|
25
|
-
&& typeof value === "object"
|
|
26
|
-
&& typeof (value as ModelRef).provider === "string"
|
|
27
|
-
&& typeof (value as ModelRef).id === "string";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function modelRefLabel(ref: ModelRef): string {
|
|
31
|
-
return `${ref.provider}/${ref.id}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function parseRef(value: string): ModelRef {
|
|
35
|
-
const idx = value.indexOf("/");
|
|
36
|
-
if (idx === -1) return { provider: value, id: "" };
|
|
37
|
-
return { provider: value.slice(0, idx), id: value.slice(idx + 1) };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
16
|
/**
|
|
41
17
|
* Anonymous slot label substituted for the real `modelRefLabel(ref)` in the
|
|
42
18
|
* synthesizer's input ONLY. Blinding prevents the synthesizer from favoring a
|
|
@@ -52,40 +28,3 @@ export function parseRef(value: string): ModelRef {
|
|
|
52
28
|
export function proposerBlindedLabel(slotIndex: number): string {
|
|
53
29
|
return `Proposer ${slotIndex + 1}`;
|
|
54
30
|
}
|
|
55
|
-
|
|
56
|
-
const MODEL_ATTR_SEGMENTS = new Set([
|
|
57
|
-
"pro", "plus", "max", "ultra", "mini", "flash", "turbo", "coder", "instruct",
|
|
58
|
-
"preview", "lite", "base", "sonnet", "opus", "haiku", "thinking", "reasoning",
|
|
59
|
-
]);
|
|
60
|
-
const CLAUDE_FAMILY_SEGMENTS = new Set(["sonnet", "opus", "haiku"]);
|
|
61
|
-
|
|
62
|
-
function isVersionOrAttrSegment(segment: string): boolean {
|
|
63
|
-
const seg = segment.toLowerCase();
|
|
64
|
-
if (MODEL_ATTR_SEGMENTS.has(seg)) return true;
|
|
65
|
-
if (/^v\d/.test(seg)) return true;
|
|
66
|
-
if (/^k\d/.test(seg)) return true;
|
|
67
|
-
if (/^[a-z]\d+(\.\d+)*$/i.test(seg)) return true;
|
|
68
|
-
if (/^\d+(\.\d+)*$/.test(seg)) return true;
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function modelIdBase(ref: ModelRef): string {
|
|
73
|
-
const slashIdx = ref.id.lastIndexOf("/");
|
|
74
|
-
return slashIdx >= 0 ? ref.id.slice(slashIdx + 1) : ref.id;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Short display name for MoA overlay (e.g. "claude-opus-4-6" → "opus", "deepseek-v4-pro" → "deepseek"). */
|
|
78
|
-
export function shortModelName(ref: ModelRef): string {
|
|
79
|
-
const base = modelIdBase(ref);
|
|
80
|
-
const segments = base.split("-").filter(Boolean);
|
|
81
|
-
// Claude's family is its useful compact identifier; "claude" only names
|
|
82
|
-
// the product and does not distinguish Opus, Sonnet, and Haiku.
|
|
83
|
-
if (segments[0]?.toLowerCase() === "claude") {
|
|
84
|
-
const family = segments.find((segment) => CLAUDE_FAMILY_SEGMENTS.has(segment.toLowerCase()));
|
|
85
|
-
if (family) return family.toLowerCase();
|
|
86
|
-
}
|
|
87
|
-
const kept = segments.filter((s) => !isVersionOrAttrSegment(s));
|
|
88
|
-
let name = kept.length > 0 ? kept.join("-") : base;
|
|
89
|
-
name = name.replace(/\d+(\.\d+)*$/, "");
|
|
90
|
-
return name.toLowerCase() || base;
|
|
91
|
-
}
|
package/src/ui/agentStatus.ts
CHANGED
|
@@ -71,14 +71,8 @@ export function contextPercent(contextTokens: number | undefined, contextWindow:
|
|
|
71
71
|
return `${(ratio * 100).toFixed(1)}%/${formatWindowLabel(contextWindow)}`.padStart(CTX_COL_WIDTH);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/**
|
|
75
|
-
export
|
|
76
|
-
if (costUsd === undefined || !Number.isFinite(costUsd)) return "—";
|
|
77
|
-
return `$${Math.max(0, costUsd).toFixed(3)}`;
|
|
78
|
-
}
|
|
74
|
+
/** Cumulative model cost in the same precision as pi's usage footer; `—` when unknown. Shared from the primitives package. */
|
|
75
|
+
export { formatCostUsd as formatCost } from "@underactive/pi-topping-primitives/text";
|
|
79
76
|
|
|
80
77
|
/** Elapsed time as `M:SS`. Minutes keep counting past 60 rather than rolling into an hours field. */
|
|
81
|
-
export
|
|
82
|
-
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
83
|
-
return `${Math.floor(totalSeconds / 60)}:${String(totalSeconds % 60).padStart(2, "0")}`;
|
|
84
|
-
}
|
|
78
|
+
export { formatElapsedClock as formatElapsed } from "@underactive/pi-topping-primitives/activity";
|
package/src/ui/cancelOverlay.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import { Key, matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
18
|
+
import { highlightActivity } from "@underactive/pi-topping-primitives/activity-tui";
|
|
18
19
|
import type { CancelSession } from "../runtime/cancelRun.ts";
|
|
19
20
|
import { LOOP_THRESHOLD, usageBar } from "./agentStatus.ts";
|
|
20
21
|
import {
|
|
@@ -24,7 +25,6 @@ import {
|
|
|
24
25
|
UI_TICK_MS,
|
|
25
26
|
createFrame,
|
|
26
27
|
} from "./chrome.ts";
|
|
27
|
-
import { highlightActivity } from "./toolActivity.ts";
|
|
28
28
|
|
|
29
29
|
const OVERLAY_MARGIN = 1;
|
|
30
30
|
|
package/src/ui/chrome.ts
CHANGED
|
@@ -3,8 +3,7 @@ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
|
3
3
|
|
|
4
4
|
export const UI_TICK_MS = 100;
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
export const QUADRANT_SPINNER_FRAMES = ["◐", "◓", "◑", "◒"] as const;
|
|
6
|
+
export { SPINNER_FRAMES as BRAILLE_SPINNER_FRAMES, QUADRANT_SPINNER_FRAMES } from "@underactive/pi-topping-primitives/glyphs";
|
|
8
7
|
|
|
9
8
|
export interface BoxGlyphs {
|
|
10
9
|
topLeft: string;
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
ratioViewport,
|
|
11
11
|
} from "./chrome.ts";
|
|
12
12
|
import { getAvailableModelRefs } from "./moaModelPicker.ts";
|
|
13
|
-
import { smartTruncateModelLabel } from "
|
|
14
|
-
import { TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
|
|
13
|
+
import { smartTruncateModelLabel } from "@underactive/pi-topping-primitives/models";
|
|
14
|
+
import { createMoaModelPicker, type TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
|
|
15
15
|
|
|
16
16
|
const HINT_BASE = "type filters models • ↑↓ navigate pane • tab panes/buttons • ←→ switch/select • enter select";
|
|
17
17
|
|
|
@@ -60,7 +60,7 @@ export class DebateModelPickerComponent implements Component {
|
|
|
60
60
|
this.defaults = defaults;
|
|
61
61
|
this.done = done;
|
|
62
62
|
this.rounds = Math.min(MAX_DEBATE_ROUNDS, Math.max(MIN_DEBATE_ROUNDS, config.debateRounds));
|
|
63
|
-
this.twoPane =
|
|
63
|
+
this.twoPane = createMoaModelPicker(tui, theme, availableRefs, config, currentThinking, ctx);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
private assignedCount(): number {
|
package/src/ui/moaModelPicker.ts
CHANGED
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
* while that slot is still empty — they never count as assignments, so
|
|
27
27
|
* readiness always reflects explicit selections.
|
|
28
28
|
*
|
|
29
|
-
* Models and thinking levels both come from the
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Models and thinking levels both come from the shared registry catalogue,
|
|
30
|
+
* so a confirmed selection is usable by construction and nothing verifies
|
|
31
|
+
* it after the picker closes. Whether a model actually answers
|
|
32
32
|
* is decided when it runs.
|
|
33
33
|
*/
|
|
34
34
|
|
|
@@ -43,10 +43,9 @@ import {
|
|
|
43
43
|
wrapWords,
|
|
44
44
|
} from "./chrome.ts";
|
|
45
45
|
import { PLAN_OVERLAY_OPTIONS } from "./menu.ts";
|
|
46
|
-
import { TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
|
|
47
|
-
import { smartTruncateModelLabel } from "
|
|
46
|
+
import { createMoaModelPicker, type TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
|
|
47
|
+
import { getModelCatalogue, smartTruncateModelLabel } from "@underactive/pi-topping-primitives/models";
|
|
48
48
|
import { proposerDiversityWarning } from "../moa/panelDependence.ts";
|
|
49
|
-
import { getModelCatalogue } from "../config/modelCatalogue.ts";
|
|
50
49
|
import {
|
|
51
50
|
loadMoaConfig,
|
|
52
51
|
moaSettingsExist,
|
|
@@ -171,7 +170,7 @@ class MoaModelPickerComponent implements Component {
|
|
|
171
170
|
].filter((slot) => this.availableLabels.has(modelRefLabel(slot.ref))).length,
|
|
172
171
|
}))
|
|
173
172
|
.sort((a, b) => a.roster.name.localeCompare(b.roster.name));
|
|
174
|
-
this.twoPane =
|
|
173
|
+
this.twoPane = createMoaModelPicker(tui, theme, availableRefs, config, currentThinking, ctx);
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
/** The committed model/thinking pair for a slot picker screen, if assigned. */
|
|
@@ -811,7 +810,7 @@ class ImplementingModelPickerComponent implements Component {
|
|
|
811
810
|
private readonly title: string,
|
|
812
811
|
private readonly done: (result: { ref: ModelRef; thinking: ThinkingLevel } | undefined) => void,
|
|
813
812
|
) {
|
|
814
|
-
this.twoPane =
|
|
813
|
+
this.twoPane = createMoaModelPicker(tui, theme, availableRefs, config, currentThinking, ctx);
|
|
815
814
|
this.twoPane.reset(defaultRef, defaultThinking);
|
|
816
815
|
}
|
|
817
816
|
|
|
@@ -31,13 +31,19 @@
|
|
|
31
31
|
import type { Message } from "@earendil-works/pi-ai";
|
|
32
32
|
import type { ExtensionContext, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
33
33
|
import { visibleWidth, wrapTextWithAnsi, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
34
|
-
import { ACTIVITY_METER_WIDTH, ActivityMeter, rateToLevel, TokRateTracker } from "
|
|
34
|
+
import { ACTIVITY_METER_WIDTH, ActivityMeter, rateToLevel, TokRateTracker } from "@underactive/pi-topping-primitives/activity";
|
|
35
|
+
import {
|
|
36
|
+
type AnsiTheme,
|
|
37
|
+
highlightActivity,
|
|
38
|
+
renderIdleMeter,
|
|
39
|
+
renderMeter,
|
|
40
|
+
shimmerString,
|
|
41
|
+
thinkingLevelThemeColor,
|
|
42
|
+
} from "@underactive/pi-topping-primitives/activity-tui";
|
|
35
43
|
import { modelRefLabel, shortModelName, type ModelRef, type ThinkingLevel } from "../shared/modelRefs.ts";
|
|
36
44
|
import { contextPercent, CTX_COL_WIDTH, formatCost, formatElapsed, formatTokens } from "./agentStatus.ts";
|
|
37
45
|
import { formatAgentPreview } from "./agentTranscript.ts";
|
|
38
46
|
import { QUADRANT_SPINNER_FRAMES, UI_TICK_MS, fitVisible, ratioViewport } from "./chrome.ts";
|
|
39
|
-
import { shimmerString, type ShimmerTheme } from "./shimmer.ts";
|
|
40
|
-
import { highlightActivity } from "./toolActivity.ts";
|
|
41
47
|
|
|
42
48
|
const WIDGET_KEY = "mf-plan-moa-status";
|
|
43
49
|
|
|
@@ -140,28 +146,6 @@ const PREVIEW_GUTTER = "│ ";
|
|
|
140
146
|
const PREVIEW_MIN_WIDTH = 16;
|
|
141
147
|
const TRANSCRIPT_MESSAGE_LIMIT = 40;
|
|
142
148
|
|
|
143
|
-
/**
|
|
144
|
-
* MONITOR meter hue per selected thinking level: each row's activity meter is
|
|
145
|
-
* tinted with its level's native theme color (`thinkingOff` … `thinkingMax`),
|
|
146
|
-
* so the table reads the thinking effort at a glance. Rows whose level is
|
|
147
|
-
* unknown fall back to `accent` (see `meterColorFor`). Exhaustive over
|
|
148
|
-
* `ThinkingLevel` so a new level cannot silently render as accent.
|
|
149
|
-
*/
|
|
150
|
-
const THINKING_METER_COLORS: Record<ThinkingLevel, ThemeColor> = {
|
|
151
|
-
off: "thinkingOff",
|
|
152
|
-
minimal: "thinkingMinimal",
|
|
153
|
-
low: "thinkingLow",
|
|
154
|
-
medium: "thinkingMedium",
|
|
155
|
-
high: "thinkingHigh",
|
|
156
|
-
xhigh: "thinkingXhigh",
|
|
157
|
-
max: "thinkingMax",
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
/** Resolve a row's meter hue, defaulting an unknown level to the neutral accent. */
|
|
161
|
-
function meterColorFor(thinking: ThinkingLevel | undefined): ThemeColor {
|
|
162
|
-
return thinking ? THINKING_METER_COLORS[thinking] : "accent";
|
|
163
|
-
}
|
|
164
|
-
|
|
165
149
|
/** Push an activity onto a bounded history buffer (most-recent-last). */
|
|
166
150
|
function pushActivity(history: string[], activity: string): void {
|
|
167
151
|
history.push(activity);
|
|
@@ -747,7 +731,7 @@ export class MoaProgressTableComponent implements Component {
|
|
|
747
731
|
const row = rows[i];
|
|
748
732
|
let entry = this.meters[i];
|
|
749
733
|
if (!entry) {
|
|
750
|
-
entry = { meter: new ActivityMeter("rtl"), tracker: new TokRateTracker(), revision: row.outputRevision };
|
|
734
|
+
entry = { meter: new ActivityMeter({ direction: "rtl" }), tracker: new TokRateTracker(), revision: row.outputRevision };
|
|
751
735
|
this.meters[i] = entry;
|
|
752
736
|
}
|
|
753
737
|
if (entry.revision !== row.outputRevision) {
|
|
@@ -762,14 +746,11 @@ export class MoaProgressTableComponent implements Component {
|
|
|
762
746
|
|
|
763
747
|
private renderMeter(index: number, active: boolean, thinking: ThinkingLevel | undefined): string {
|
|
764
748
|
const meter = this.meters[index]?.meter;
|
|
765
|
-
if (!meter) return this.theme
|
|
749
|
+
if (!meter) return renderIdleMeter(this.theme);
|
|
766
750
|
// Each row's meter carries its selected thinking level's native theme hue;
|
|
767
751
|
// an unknown level falls back to accent. IDLE cells and settled traces keep
|
|
768
|
-
// their existing dimming inside
|
|
769
|
-
|
|
770
|
-
return meter.render((level, char) =>
|
|
771
|
-
ActivityMeter.colorizeCell(level, char, this.theme, color, !active),
|
|
772
|
-
);
|
|
752
|
+
// their existing dimming inside renderMeter regardless of the hue.
|
|
753
|
+
return renderMeter(meter, this.theme, thinkingLevelThemeColor(thinking), !active);
|
|
773
754
|
}
|
|
774
755
|
|
|
775
756
|
/** How many lines the table may use, so it never crowds out the editor below it. */
|
|
@@ -798,7 +779,7 @@ export class MoaProgressTableComponent implements Component {
|
|
|
798
779
|
const th = this.theme;
|
|
799
780
|
if (!lit) return th.fg("dim", text);
|
|
800
781
|
return th.getFgAnsi
|
|
801
|
-
? shimmerString(text, now - this.animationStartedAt, th as
|
|
782
|
+
? shimmerString(text, now - this.animationStartedAt, th as AnsiTheme, "ltr", "normal", true)
|
|
802
783
|
: th.fg("text", text);
|
|
803
784
|
}
|
|
804
785
|
|