@xynogen/pix-subagent 0.1.3 → 0.2.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/package.json +3 -2
- package/src/agent-manager.ts +6 -1
- package/src/index.ts +4 -1
- package/src/model-resolver.ts +67 -2
- package/src/tools.ts +45 -11
- package/src/types.ts +5 -0
- package/src/ui/widget.ts +71 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pi tool — planner-driven sub-agents: spawn, fetch, steer scoped child agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@sinclair/typebox": "^0.34.0"
|
|
39
|
+
"@sinclair/typebox": "^0.34.0",
|
|
40
|
+
"@xynogen/pix-data": "*"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
43
|
"@earendil-works/pi-coding-agent": "*",
|
package/src/agent-manager.ts
CHANGED
|
@@ -180,6 +180,8 @@ export class AgentManager {
|
|
|
180
180
|
abortController,
|
|
181
181
|
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
182
182
|
compactionCount: 0,
|
|
183
|
+
turnCount: 0,
|
|
184
|
+
maxTurns: options.maxTurns,
|
|
183
185
|
invocation: options.invocation,
|
|
184
186
|
};
|
|
185
187
|
this.agents.set(id, record);
|
|
@@ -258,7 +260,10 @@ export class AgentManager {
|
|
|
258
260
|
if (activity.type === "end") record.toolUses++;
|
|
259
261
|
options.onToolActivity?.(activity);
|
|
260
262
|
},
|
|
261
|
-
onTurnEnd:
|
|
263
|
+
onTurnEnd: (turnCount) => {
|
|
264
|
+
record.turnCount = turnCount;
|
|
265
|
+
options.onTurnEnd?.(turnCount);
|
|
266
|
+
},
|
|
262
267
|
onTextDelta: options.onTextDelta,
|
|
263
268
|
onAssistantUsage: (usage) => {
|
|
264
269
|
addUsage(record.lifetimeUsage, usage);
|
package/src/index.ts
CHANGED
|
@@ -135,8 +135,11 @@ export default function registerPixSubagent(pi: ExtensionAPI): void {
|
|
|
135
135
|
widget.update();
|
|
136
136
|
},
|
|
137
137
|
4, // maxConcurrent
|
|
138
|
-
// onStart
|
|
138
|
+
// onStart — re-arm the 80ms spinner loop. update() clears the timer when
|
|
139
|
+
// the last agent finishes; a fresh spawn mid-turn (no new turn_start) must
|
|
140
|
+
// restart it or the spinner freezes on one frame.
|
|
139
141
|
(_record) => {
|
|
142
|
+
widget.ensureTimer();
|
|
140
143
|
widget.update();
|
|
141
144
|
},
|
|
142
145
|
);
|
package/src/model-resolver.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Model resolution: exact match ("provider/modelId") with fuzzy fallback.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { lookupBenchmark, lookupModelsDev } from "@xynogen/pix-data";
|
|
6
|
+
|
|
5
7
|
export interface ModelEntry {
|
|
6
8
|
id: string;
|
|
7
9
|
name: string;
|
|
@@ -91,8 +93,71 @@ export function resolveModel(
|
|
|
91
93
|
return `Model not found: "${input}".\n\nAvailable models:\n${modelList}`;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
// ── Enrichment for orchestrator model choice ────────────────────────────────
|
|
97
|
+
// The orchestrator picks a worker model from the `agent` tool description. Bare
|
|
98
|
+
// ids carry no signal, so each line is annotated with bench score, context,
|
|
99
|
+
// price, and a coarse tier — sourced from pix-data (the shared data layer).
|
|
100
|
+
|
|
101
|
+
const SCORE_FRONTIER = 88;
|
|
102
|
+
const SCORE_STRONG = 75;
|
|
103
|
+
const CHEAP_OUTPUT_PRICE = 3; // $/M output tokens — below this counts as cheap
|
|
104
|
+
|
|
105
|
+
/** Context window → compact "200k" / "1M". */
|
|
106
|
+
function fmtCtx(n: number | undefined): string {
|
|
107
|
+
if (!n || n < 1_000) return n ? `${n}` : "";
|
|
108
|
+
if (n >= 1_000_000) {
|
|
109
|
+
const m = n / 1_000_000;
|
|
110
|
+
return `${Number.isInteger(m) ? m : m.toFixed(1).replace(/\.0$/, "")}M ctx`;
|
|
111
|
+
}
|
|
112
|
+
return `${Math.round(n / 1_000)}k ctx`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Cost → "$3/$15" (input/output per Mtok), "free", or "" when unknown. */
|
|
116
|
+
function fmtCost(input?: number, output?: number): string {
|
|
117
|
+
if (input == null && output == null) return "";
|
|
118
|
+
const i = input ?? 0;
|
|
119
|
+
const o = output ?? 0;
|
|
120
|
+
if (i === 0 && o === 0) return "free";
|
|
121
|
+
return `$${i}/$${o}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Coarse decision tier from score + output price. */
|
|
125
|
+
function tier(score: number | null | undefined, output?: number): string {
|
|
126
|
+
if (typeof score !== "number") return "";
|
|
127
|
+
if (score >= SCORE_FRONTIER) return "frontier";
|
|
128
|
+
if (score >= SCORE_STRONG) return "strong";
|
|
129
|
+
if (output != null && output <= CHEAP_OUTPUT_PRICE) return "fast-cheap";
|
|
130
|
+
return "basic";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One enriched line: "provider/id — ⚡95 · 200k ctx · $3/$15 · frontier". */
|
|
134
|
+
function annotate(m: ModelEntry): { line: string; score: number } {
|
|
135
|
+
const dev = lookupModelsDev(m.provider, m.id);
|
|
136
|
+
const bench = lookupBenchmark(m.name ?? m.id);
|
|
137
|
+
const score = bench?.overallScore ?? null;
|
|
138
|
+
const out = bench?.outputPrice ?? dev?.cost?.output;
|
|
139
|
+
const segs = [
|
|
140
|
+
typeof score === "number" ? `⚡${score}` : "",
|
|
141
|
+
fmtCtx(dev?.limit?.context),
|
|
142
|
+
fmtCost(bench?.inputPrice ?? dev?.cost?.input, out),
|
|
143
|
+
tier(score, out),
|
|
144
|
+
].filter(Boolean);
|
|
145
|
+
const id = `${m.provider}/${m.id}`;
|
|
146
|
+
return {
|
|
147
|
+
line: segs.length ? `${id} — ${segs.join(" · ")}` : id,
|
|
148
|
+
score: score ?? -1,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* List available models, enriched + ranked for orchestrator decisions.
|
|
154
|
+
* Sorted by bench score desc (best first); unscored fall to the bottom
|
|
155
|
+
* alphabetically (preserved by the stable id tiebreak).
|
|
156
|
+
*/
|
|
95
157
|
export function listAvailable(registry: ModelRegistry): string[] {
|
|
96
158
|
const all = (registry.getAvailable?.() ?? registry.getAll()) as ModelEntry[];
|
|
97
|
-
return all
|
|
159
|
+
return all
|
|
160
|
+
.map((m) => ({ ...annotate(m), id: `${m.provider}/${m.id}` }))
|
|
161
|
+
.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
|
|
162
|
+
.map((r) => r.line);
|
|
98
163
|
}
|
package/src/tools.ts
CHANGED
|
@@ -29,7 +29,11 @@ import {
|
|
|
29
29
|
import { resolveAgentInvocationConfig } from "./invocation-config.ts";
|
|
30
30
|
import { resolveModel } from "./model-resolver.ts";
|
|
31
31
|
import type { AgentInvocation, LifetimeUsage } from "./types.ts";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
getLifetimeTotal,
|
|
34
|
+
getSessionContextPercent,
|
|
35
|
+
type SessionLike,
|
|
36
|
+
} from "./usage.ts";
|
|
33
37
|
|
|
34
38
|
// ── Types shared with ui/widget.ts (widget imports from here to avoid circular) ─
|
|
35
39
|
|
|
@@ -54,6 +58,10 @@ export interface AgentDetails {
|
|
|
54
58
|
subagentType: string;
|
|
55
59
|
toolUses: number;
|
|
56
60
|
tokens: string;
|
|
61
|
+
/** Context-window utilization 0–100, or null/undefined when unavailable. */
|
|
62
|
+
contextPercent?: number | null;
|
|
63
|
+
/** Raw output tokens — for t/s = outputTokens / durationMs. */
|
|
64
|
+
outputTokens?: number;
|
|
57
65
|
durationMs: number;
|
|
58
66
|
status:
|
|
59
67
|
| "queued"
|
|
@@ -106,6 +114,15 @@ export function formatMs(ms: number): string {
|
|
|
106
114
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
107
115
|
}
|
|
108
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Output tokens per second over a duration. "" when either input is
|
|
119
|
+
* non-positive (no work / zero elapsed) so callers can skip the segment.
|
|
120
|
+
*/
|
|
121
|
+
export function formatSpeed(outputTokens: number, durationMs: number): string {
|
|
122
|
+
if (outputTokens <= 0 || durationMs <= 0) return "";
|
|
123
|
+
return `${Math.round(outputTokens / (durationMs / 1000))} t/s`;
|
|
124
|
+
}
|
|
125
|
+
|
|
109
126
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
110
127
|
|
|
111
128
|
function textResult(msg: string, details?: AgentDetails) {
|
|
@@ -137,7 +154,13 @@ function buildStats(d: AgentDetails, theme: Theme): string {
|
|
|
137
154
|
parts.push(
|
|
138
155
|
theme.fg("dim", `${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`),
|
|
139
156
|
);
|
|
140
|
-
if (d.tokens)
|
|
157
|
+
if (d.tokens) {
|
|
158
|
+
const pct =
|
|
159
|
+
d.contextPercent != null
|
|
160
|
+
? ` ${theme.fg("dim", `(${Math.round(d.contextPercent)}%)`)}`
|
|
161
|
+
: "";
|
|
162
|
+
parts.push(theme.fg("dim", d.tokens) + pct);
|
|
163
|
+
}
|
|
141
164
|
return parts.join(` ${theme.fg("dim", "·")} `);
|
|
142
165
|
}
|
|
143
166
|
|
|
@@ -314,21 +337,34 @@ export function createAgentTool(
|
|
|
314
337
|
);
|
|
315
338
|
}
|
|
316
339
|
|
|
317
|
-
// Completed / steered
|
|
340
|
+
// Completed / steered. Collapsed view stays empty — the ● Agents widget
|
|
341
|
+
// carries the full finished line, so the inline transcript doesn't echo
|
|
342
|
+
// it (caller shouldn't output the result). Expanded view still shows the
|
|
343
|
+
// summary + full output on demand.
|
|
318
344
|
if (details.status === "completed" || details.status === "steered") {
|
|
345
|
+
if (!expanded) return new Text("", 0, 0);
|
|
319
346
|
const duration = formatMs(details.durationMs);
|
|
320
347
|
const isSteered = details.status === "steered";
|
|
321
348
|
const icon = isSteered
|
|
322
349
|
? theme.fg("warning", "✓")
|
|
323
350
|
: theme.fg("success", "✓");
|
|
351
|
+
const speed = formatSpeed(
|
|
352
|
+
details.outputTokens ?? 0,
|
|
353
|
+
details.durationMs,
|
|
354
|
+
);
|
|
324
355
|
let line =
|
|
325
356
|
icon +
|
|
326
357
|
(stats ? ` ${stats}` : "") +
|
|
327
358
|
" " +
|
|
328
359
|
theme.fg("dim", "·") +
|
|
329
360
|
" " +
|
|
330
|
-
theme.fg("dim", duration)
|
|
361
|
+
theme.fg("dim", duration) +
|
|
362
|
+
(speed ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", speed)}` : "") +
|
|
363
|
+
// Steered = stopped at turn limit; keep that note inline since the
|
|
364
|
+
// stats alone don't say why it ended.
|
|
365
|
+
(isSteered ? ` ${theme.fg("warning", "(turn limit)")}` : "");
|
|
331
366
|
|
|
367
|
+
// Expanded view appends the full result below the one-line summary.
|
|
332
368
|
if (expanded) {
|
|
333
369
|
const resultText =
|
|
334
370
|
result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
@@ -343,13 +379,6 @@ export function createAgentTool(
|
|
|
343
379
|
" … (use agent_result with verbose for full output)",
|
|
344
380
|
);
|
|
345
381
|
}
|
|
346
|
-
} else {
|
|
347
|
-
line +=
|
|
348
|
-
"\n" +
|
|
349
|
-
theme.fg(
|
|
350
|
-
"dim",
|
|
351
|
-
` ⎿ ${isSteered ? "Wrapped up (turn limit)" : "Done"}`,
|
|
352
|
-
);
|
|
353
382
|
}
|
|
354
383
|
return new Text(line, 0, 0);
|
|
355
384
|
}
|
|
@@ -804,10 +833,15 @@ function buildDetails(
|
|
|
804
833
|
activity?: AgentActivity & { durationMs?: number },
|
|
805
834
|
): AgentDetails {
|
|
806
835
|
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
836
|
+
const contextPercent = activity?.session
|
|
837
|
+
? getSessionContextPercent(activity.session as SessionLike)
|
|
838
|
+
: null;
|
|
807
839
|
return {
|
|
808
840
|
...base,
|
|
809
841
|
toolUses: record.toolUses,
|
|
810
842
|
tokens: totalTokens > 0 ? formatTokens(totalTokens) : "",
|
|
843
|
+
contextPercent,
|
|
844
|
+
outputTokens: record.lifetimeUsage.output,
|
|
811
845
|
turnCount: activity?.turnCount,
|
|
812
846
|
maxTurns: activity?.maxTurns,
|
|
813
847
|
durationMs:
|
package/src/types.ts
CHANGED
|
@@ -89,6 +89,11 @@ export interface AgentRecord {
|
|
|
89
89
|
lifetimeUsage: LifetimeUsage;
|
|
90
90
|
/** Number of times this agent's session has compacted. */
|
|
91
91
|
compactionCount: number;
|
|
92
|
+
/** Cumulative agentic turns. Persisted on the record so the finished widget
|
|
93
|
+
* line stays full after agentActivity is cleared on completion. */
|
|
94
|
+
turnCount: number;
|
|
95
|
+
/** Turn cap, if any — for the ↻N≤M display. */
|
|
96
|
+
maxTurns?: number;
|
|
92
97
|
/** Resolved spawn params, captured for UI display. */
|
|
93
98
|
invocation?: AgentInvocation;
|
|
94
99
|
}
|
package/src/ui/widget.ts
CHANGED
|
@@ -12,12 +12,22 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
12
12
|
import type { AgentManager } from "../agent-manager.ts";
|
|
13
13
|
import { getConfig } from "../agent-types.ts";
|
|
14
14
|
import type { AgentActivity, AgentDetails, Theme } from "../tools.ts";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
formatMs,
|
|
17
|
+
formatSpeed,
|
|
18
|
+
formatTokens,
|
|
19
|
+
formatTurns,
|
|
20
|
+
SPINNER,
|
|
21
|
+
} from "../tools.ts";
|
|
16
22
|
import type { AgentInvocation, SubagentType } from "../types.ts";
|
|
17
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
getLifetimeTotal,
|
|
25
|
+
getSessionContextPercent,
|
|
26
|
+
type SessionLike,
|
|
27
|
+
} from "../usage.ts";
|
|
18
28
|
|
|
19
29
|
export type { AgentActivity, AgentDetails, Theme };
|
|
20
|
-
export { formatMs, formatTokens, formatTurns, SPINNER };
|
|
30
|
+
export { formatMs, formatSpeed, formatTokens, formatTurns, SPINNER };
|
|
21
31
|
|
|
22
32
|
// ── constants ─────────────────────────────────────────────────────────────────
|
|
23
33
|
|
|
@@ -97,14 +107,16 @@ export function buildInvocationTags(invocation: AgentInvocation | undefined): {
|
|
|
97
107
|
return { modelName: invocation.modelName, tags };
|
|
98
108
|
}
|
|
99
109
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Live tail of agent output: latest non-empty line, tail-anchored to `len`
|
|
112
|
+
* chars (keeps the moving edge, not the stale first line). Leading … marks
|
|
113
|
+
* the clip. e.g. "…ting batch 6".
|
|
114
|
+
*/
|
|
115
|
+
function truncateLine(text: string, len = 16): string {
|
|
116
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
117
|
+
const line = lines.length ? lines[lines.length - 1].trim() : "";
|
|
106
118
|
if (line.length <= len) return line;
|
|
107
|
-
return
|
|
119
|
+
return `…${line.slice(-len)}`;
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
export function describeActivity(
|
|
@@ -184,12 +196,18 @@ export class AgentWidget {
|
|
|
184
196
|
completedAt?: number;
|
|
185
197
|
error?: string;
|
|
186
198
|
invocation?: AgentInvocation;
|
|
199
|
+
lifetimeUsage?: { input: number; output: number; cacheWrite: number };
|
|
200
|
+
session?: unknown;
|
|
201
|
+
compactionCount?: number;
|
|
202
|
+
turnCount?: number;
|
|
203
|
+
maxTurns?: number;
|
|
187
204
|
},
|
|
188
205
|
theme: Theme,
|
|
189
206
|
): string {
|
|
190
207
|
const name = getDisplayName(a.type);
|
|
191
208
|
const modeLabel = getPromptModeLabel(a.type);
|
|
192
|
-
const
|
|
209
|
+
const durationMs = (a.completedAt ?? Date.now()) - a.startedAt;
|
|
210
|
+
const duration = formatMs(durationMs);
|
|
193
211
|
|
|
194
212
|
// model label (the pix twist — always shown)
|
|
195
213
|
const modelLabel = a.invocation?.modelName
|
|
@@ -218,12 +236,31 @@ export class AgentWidget {
|
|
|
218
236
|
}
|
|
219
237
|
|
|
220
238
|
const parts: string[] = [];
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
239
|
+
// Turns read from the record (a.*), not agentActivity — onComplete deletes
|
|
240
|
+
// the activity entry before this line renders, which had dropped ↻N.
|
|
241
|
+
if (a.turnCount != null && a.turnCount > 0)
|
|
242
|
+
parts.push(formatTurns(a.turnCount, a.maxTurns));
|
|
224
243
|
if (a.toolUses > 0)
|
|
225
244
|
parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
|
|
245
|
+
// Token + context% + speed read from the record (survives the
|
|
246
|
+
// agentActivity delete that fires in onComplete before this renders).
|
|
247
|
+
const tokens = getLifetimeTotal(a.lifetimeUsage);
|
|
248
|
+
if (tokens > 0) {
|
|
249
|
+
const contextPercent = a.session
|
|
250
|
+
? getSessionContextPercent(a.session as SessionLike)
|
|
251
|
+
: null;
|
|
252
|
+
parts.push(
|
|
253
|
+
formatSessionTokens(
|
|
254
|
+
tokens,
|
|
255
|
+
contextPercent,
|
|
256
|
+
theme,
|
|
257
|
+
a.compactionCount ?? 0,
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
226
261
|
parts.push(duration);
|
|
262
|
+
const speed = formatSpeed(a.lifetimeUsage?.output ?? 0, durationMs);
|
|
263
|
+
if (speed) parts.push(speed);
|
|
227
264
|
|
|
228
265
|
return `${icon} ${theme.fg("dim", name)}${modelLabel}${modeTag} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
|
|
229
266
|
}
|
|
@@ -250,7 +287,8 @@ export class AgentWidget {
|
|
|
250
287
|
const truncate = (line: string) => truncateToWidth(line, w);
|
|
251
288
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
252
289
|
const headingColor = hasActive ? "accent" : "dim";
|
|
253
|
-
|
|
290
|
+
// ○ hollow = incomplete (still running), ● filled disk = complete (all done).
|
|
291
|
+
const headingIcon = hasActive ? "○" : "●";
|
|
254
292
|
const frame = SPINNER[this.widgetFrame % SPINNER.length];
|
|
255
293
|
|
|
256
294
|
const finishedLines: string[] = [];
|
|
@@ -262,7 +300,7 @@ export class AgentWidget {
|
|
|
262
300
|
);
|
|
263
301
|
}
|
|
264
302
|
|
|
265
|
-
const runningLines: string[]
|
|
303
|
+
const runningLines: string[] = [];
|
|
266
304
|
for (const a of running) {
|
|
267
305
|
const name = getDisplayName(a.type);
|
|
268
306
|
const modeLabel = getPromptModeLabel(a.type);
|
|
@@ -297,19 +335,26 @@ export class AgentWidget {
|
|
|
297
335
|
parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
|
|
298
336
|
if (tokenText) parts.push(tokenText);
|
|
299
337
|
parts.push(elapsed);
|
|
338
|
+
const liveSpeed = formatSpeed(
|
|
339
|
+
bg?.lifetimeUsage.output ?? 0,
|
|
340
|
+
Date.now() - a.startedAt,
|
|
341
|
+
);
|
|
342
|
+
if (liveSpeed) parts.push(liveSpeed);
|
|
300
343
|
const statsText = parts.join(" · ");
|
|
301
344
|
|
|
345
|
+
// Activity leads (next to the spinner — the moving part by the moving
|
|
346
|
+
// part), then static identity + cumulative stats. One line per agent so
|
|
347
|
+
// many concurrent workers stay readable instead of doubling the height.
|
|
302
348
|
const activity = bg
|
|
303
349
|
? describeActivity(bg.activeTools, bg.responseText)
|
|
304
350
|
: "thinking…";
|
|
305
351
|
|
|
306
|
-
runningLines.push(
|
|
352
|
+
runningLines.push(
|
|
307
353
|
truncate(
|
|
308
354
|
theme.fg("dim", "├─") +
|
|
309
|
-
` ${theme.fg("accent", frame)} ${theme.bold(name)}${modelLabel}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`,
|
|
355
|
+
` ${theme.fg("accent", frame)} ${theme.fg("dim", activity)} ${theme.fg("dim", "·")} ${theme.bold(name)}${modelLabel}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`,
|
|
310
356
|
),
|
|
311
|
-
|
|
312
|
-
]);
|
|
357
|
+
);
|
|
313
358
|
}
|
|
314
359
|
|
|
315
360
|
const queuedLine =
|
|
@@ -322,7 +367,7 @@ export class AgentWidget {
|
|
|
322
367
|
|
|
323
368
|
const maxBody = MAX_WIDGET_LINES - 1;
|
|
324
369
|
const totalBody =
|
|
325
|
-
finishedLines.length + runningLines.length
|
|
370
|
+
finishedLines.length + runningLines.length + (queuedLine ? 1 : 0);
|
|
326
371
|
|
|
327
372
|
const lines: string[] = [
|
|
328
373
|
truncate(
|
|
@@ -334,27 +379,23 @@ export class AgentWidget {
|
|
|
334
379
|
|
|
335
380
|
if (totalBody <= maxBody) {
|
|
336
381
|
lines.push(...finishedLines);
|
|
337
|
-
|
|
382
|
+
lines.push(...runningLines);
|
|
338
383
|
if (queuedLine) lines.push(queuedLine);
|
|
339
384
|
|
|
340
385
|
// Fix last connector ├─ → └─
|
|
341
386
|
if (lines.length > 1) {
|
|
342
387
|
const last = lines.length - 1;
|
|
343
388
|
lines[last] = lines[last].replace("├─", "└─");
|
|
344
|
-
if (runningLines.length > 0 && !queuedLine && last >= 2) {
|
|
345
|
-
lines[last - 1] = lines[last - 1].replace("├─", "└─");
|
|
346
|
-
lines[last] = lines[last].replace("│ ", " ");
|
|
347
|
-
}
|
|
348
389
|
}
|
|
349
390
|
} else {
|
|
350
391
|
let budget = maxBody - 1;
|
|
351
392
|
let hiddenRunning = 0;
|
|
352
393
|
let hiddenFinished = 0;
|
|
353
394
|
|
|
354
|
-
for (const
|
|
355
|
-
if (budget >=
|
|
356
|
-
lines.push(
|
|
357
|
-
budget
|
|
395
|
+
for (const line of runningLines) {
|
|
396
|
+
if (budget >= 1) {
|
|
397
|
+
lines.push(line);
|
|
398
|
+
budget--;
|
|
358
399
|
} else {
|
|
359
400
|
hiddenRunning++;
|
|
360
401
|
}
|