pi-cockpit 0.1.2 → 0.3.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 -3
- package/src/agents-store.ts +46 -0
- package/src/bash-bg-widget.ts +2 -22
- package/src/footer.ts +15 -13
- package/src/index.ts +55 -42
- package/src/render.ts +27 -2
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cockpit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Agent cockpit for Pi: a list-mode status stack (live teammates + todo plan) pinned above the editor, plus a Starship-style footer. Renders only via public extension APIs (setWidget/setFooter).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@earendil-works/pi-ai": "*",
|
|
35
35
|
"@earendil-works/pi-coding-agent": "*",
|
|
36
36
|
"@earendil-works/pi-tui": "*",
|
|
37
|
-
"pi-maestro-teammate": "^0.
|
|
37
|
+
"pi-maestro-teammate": "^1.0.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@earendil-works/pi-agent-core": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@earendil-works/pi-coding-agent": "0.82.1",
|
|
60
60
|
"@earendil-works/pi-tui": "0.82.1",
|
|
61
61
|
"@types/node": "^22.0.0",
|
|
62
|
-
"pi-maestro-teammate": "0.
|
|
62
|
+
"pi-maestro-teammate": "1.0.0",
|
|
63
63
|
"typescript": "^5.5.0"
|
|
64
64
|
},
|
|
65
65
|
"main": "./src/index.ts",
|
package/src/agents-store.ts
CHANGED
|
@@ -41,11 +41,17 @@ export interface ProgressPayload {
|
|
|
41
41
|
recentTools?: Array<string | { name?: string; status?: string }>;
|
|
42
42
|
toolCount?: number;
|
|
43
43
|
tokens?: number;
|
|
44
|
+
inputTokens?: number;
|
|
45
|
+
outputTokens?: number;
|
|
44
46
|
lastMessage?: string;
|
|
45
47
|
}
|
|
46
48
|
export interface MessagePayload {
|
|
47
49
|
correlationId: string;
|
|
48
50
|
taskCorrelationId?: string;
|
|
51
|
+
// Present on progress/interaction deltas (teammate/.../index.ts publishProgress);
|
|
52
|
+
// absent on send deltas. Used to self-heal a row when `started` was missed.
|
|
53
|
+
agent?: string;
|
|
54
|
+
name?: string;
|
|
49
55
|
taskIndex?: number;
|
|
50
56
|
dependencies?: number[];
|
|
51
57
|
message?: string;
|
|
@@ -53,6 +59,8 @@ export interface MessagePayload {
|
|
|
53
59
|
recentTools?: Array<string | { name?: string; status?: string }>;
|
|
54
60
|
toolCount?: number;
|
|
55
61
|
tokens?: number;
|
|
62
|
+
inputTokens?: number;
|
|
63
|
+
outputTokens?: number;
|
|
56
64
|
status?: string;
|
|
57
65
|
lastActivityAt?: number | string;
|
|
58
66
|
progress?: ProgressPayload[];
|
|
@@ -116,6 +124,9 @@ function latestTool(tools: MessagePayload["recentTools"]): string | undefined {
|
|
|
116
124
|
// Self-accumulating roster. The teammate extension only broadcasts deltas
|
|
117
125
|
// (started/message/complete), never a full snapshot, so we rebuild the list here.
|
|
118
126
|
// Cold start is empty by design — we only reflect activity observed after load.
|
|
127
|
+
// The roster is self-healing: message/progress deltas for an agent whose
|
|
128
|
+
// `started` delta was missed still materialize a row, so a running agent is
|
|
129
|
+
// always represented as long as it keeps emitting activity.
|
|
119
130
|
export class AgentsStore {
|
|
120
131
|
private readonly roster = new Map<string, AgentRow>();
|
|
121
132
|
|
|
@@ -146,6 +157,22 @@ export class AgentsStore {
|
|
|
146
157
|
for (const progress of p.progress) this.applyProgress(p.correlationId, progress, now);
|
|
147
158
|
}
|
|
148
159
|
const targetId = p.taskCorrelationId ?? p.correlationId;
|
|
160
|
+
if (!this.roster.has(targetId)) {
|
|
161
|
+
// Self-heal the roster: a running agent must stay visible even when its
|
|
162
|
+
// `started` delta was missed (cold start, event reorder, or a foreground
|
|
163
|
+
// dispatch that detached to background). Materialize the row from this
|
|
164
|
+
// event instead of dropping it; later deltas refine the placeholder.
|
|
165
|
+
this.applyStarted({
|
|
166
|
+
correlationId: targetId,
|
|
167
|
+
agent: p.agent ?? "",
|
|
168
|
+
name: p.name,
|
|
169
|
+
status: p.status,
|
|
170
|
+
lastActivityAt: p.lastActivityAt,
|
|
171
|
+
...(p.taskCorrelationId && p.taskCorrelationId !== p.correlationId
|
|
172
|
+
? { spawnedBy: p.correlationId }
|
|
173
|
+
: {}),
|
|
174
|
+
}, now);
|
|
175
|
+
}
|
|
149
176
|
const row = this.roster.get(targetId);
|
|
150
177
|
if (!row) return;
|
|
151
178
|
const progressActivity = p.progress?.find((progress) => progress.correlationId === targetId)?.lastActivityAt;
|
|
@@ -159,6 +186,8 @@ export class AgentsStore {
|
|
|
159
186
|
}
|
|
160
187
|
if (typeof p.toolCount === "number") row.toolCount = p.toolCount;
|
|
161
188
|
if (typeof p.tokens === "number") row.tokens = p.tokens;
|
|
189
|
+
if (typeof p.inputTokens === "number") row.inputTokens = p.inputTokens;
|
|
190
|
+
if (typeof p.outputTokens === "number") row.outputTokens = p.outputTokens;
|
|
162
191
|
if (typeof p.status === "string") {
|
|
163
192
|
row.taskStatus = p.status;
|
|
164
193
|
row.status = mapAgentStatus(p.status);
|
|
@@ -217,6 +246,21 @@ export class AgentsStore {
|
|
|
217
246
|
}
|
|
218
247
|
|
|
219
248
|
private applyProgress(parentCorrelationId: string, p: ProgressPayload, now: number): void {
|
|
249
|
+
if (!this.roster.has(p.correlationId)) {
|
|
250
|
+
// Self-heal: see applyMessage. A graph child whose `started` delta was
|
|
251
|
+
// missed still materializes from its first progress event.
|
|
252
|
+
this.applyStarted({
|
|
253
|
+
correlationId: p.correlationId,
|
|
254
|
+
agent: p.agent,
|
|
255
|
+
name: p.name,
|
|
256
|
+
status: p.status,
|
|
257
|
+
startedAt: p.startedAt,
|
|
258
|
+
lastActivityAt: p.lastActivityAt,
|
|
259
|
+
...(parentCorrelationId !== p.correlationId
|
|
260
|
+
? { spawnedBy: parentCorrelationId }
|
|
261
|
+
: {}),
|
|
262
|
+
}, now);
|
|
263
|
+
}
|
|
220
264
|
const row = this.roster.get(p.correlationId);
|
|
221
265
|
if (!row) return;
|
|
222
266
|
row.parentCorrelationId = parentCorrelationId === p.correlationId
|
|
@@ -241,6 +285,8 @@ export class AgentsStore {
|
|
|
241
285
|
}
|
|
242
286
|
if (typeof p.toolCount === "number") row.toolCount = p.toolCount;
|
|
243
287
|
if (typeof p.tokens === "number") row.tokens = p.tokens;
|
|
288
|
+
if (typeof p.inputTokens === "number") row.inputTokens = p.inputTokens;
|
|
289
|
+
if (typeof p.outputTokens === "number") row.outputTokens = p.outputTokens;
|
|
244
290
|
if (typeof p.lastMessage === "string" && p.lastMessage.length > 0) {
|
|
245
291
|
row.tail = truncateTail(p.lastMessage);
|
|
246
292
|
}
|
package/src/bash-bg-widget.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { IconGlyphs } from "./icons.ts";
|
|
3
|
-
import { sanitizeExtensionStatusText } from "./extension-status.ts";
|
|
4
3
|
import { fitLineByPriority, type PrioritizedSegment } from "./layout.ts";
|
|
5
4
|
import { formatDuration, type PaintTheme, type WidthUtils } from "./render.ts";
|
|
6
5
|
import type { BashBgJob } from "./types.ts";
|
|
@@ -35,10 +34,7 @@ export function renderBashBgSummary(
|
|
|
35
34
|
].filter(Boolean).join(" · ");
|
|
36
35
|
const statusColor: ThemeColor = stopping > 0 ? "warning" : "accent";
|
|
37
36
|
const elapsed = formatDuration(options.now - current.startedAt);
|
|
38
|
-
const preview = lastOutputLine(current.outputTail);
|
|
39
37
|
const g = options.glyphs;
|
|
40
|
-
// The Alt+J hint is the only advertised route into the job detail overlay, so
|
|
41
|
-
// it outranks the command preview instead of being the first thing clipped.
|
|
42
38
|
const segments: PrioritizedSegment[] = [
|
|
43
39
|
{
|
|
44
40
|
text: theme.fg(statusColor, current.status === "stopping" ? g.blocked : options.spin),
|
|
@@ -47,24 +43,8 @@ export function renderBashBgSummary(
|
|
|
47
43
|
},
|
|
48
44
|
{ text: theme.fg("muted", "BG"), priority: 95, clippable: false },
|
|
49
45
|
{ text: theme.fg(statusColor, counts), priority: 90, clippable: false },
|
|
50
|
-
{ text: theme.fg("
|
|
51
|
-
{ text: theme.fg("dim", `${options.hint ?? DEFAULT_BASH_BG_HINT} details`), priority:
|
|
52
|
-
{ text: theme.fg("dim", elapsed), priority: 50, clippable: false },
|
|
53
|
-
{ text: theme.fg("mdLink", current.id), priority: 30, clippable: false },
|
|
46
|
+
{ text: theme.fg("dim", elapsed), priority: 80, clippable: false },
|
|
47
|
+
{ text: theme.fg("dim", `${options.hint ?? DEFAULT_BASH_BG_HINT} details`), priority: 70, clippable: false },
|
|
54
48
|
];
|
|
55
|
-
if (preview) segments.push({ text: theme.fg("dim", `${g.arrow} ${preview}`), priority: 20, minWidth: 8 });
|
|
56
49
|
return [fitLineByPriority(segments, width, utils, theme.fg("dim", g.separator), g.ellipsis)];
|
|
57
50
|
}
|
|
58
|
-
|
|
59
|
-
function lastOutputLine(output: string): string {
|
|
60
|
-
const lines = output.replace(/\r/g, "").split("\n");
|
|
61
|
-
for (let index = lines.length - 1; index >= 0; index--) {
|
|
62
|
-
const line = lines[index]?.trim();
|
|
63
|
-
if (line) return oneLine(line);
|
|
64
|
-
}
|
|
65
|
-
return "";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function oneLine(value: string): string {
|
|
69
|
-
return sanitizeExtensionStatusText(value);
|
|
70
|
-
}
|
package/src/footer.ts
CHANGED
|
@@ -27,10 +27,10 @@ export interface FooterParts {
|
|
|
27
27
|
ctxTokens: number;
|
|
28
28
|
ctxWindow: number;
|
|
29
29
|
totals: UsageTotals;
|
|
30
|
-
git?: string;
|
|
31
|
-
agentSummary?: string;
|
|
32
|
-
bashBgStatus?: string;
|
|
33
|
-
workflowStatus?: string;
|
|
30
|
+
git?: string;
|
|
31
|
+
agentSummary?: string;
|
|
32
|
+
bashBgStatus?: string;
|
|
33
|
+
workflowStatus?: string;
|
|
34
34
|
extensionStatuses?: readonly ExtensionStatusSegment[];
|
|
35
35
|
width: number;
|
|
36
36
|
glyphs: IconGlyphs;
|
|
@@ -207,7 +207,9 @@ export function renderFooter(p: FooterParts): string[] {
|
|
|
207
207
|
(status) => isVisibleExtensionStatus(status, p.thinking),
|
|
208
208
|
);
|
|
209
209
|
const controlStatuses = visibleStatuses.filter(
|
|
210
|
-
(status) => status.key === "
|
|
210
|
+
(status) => status.key === "mode"
|
|
211
|
+
|| status.key === "approval-mode"
|
|
212
|
+
|| status.key === "maestro-auto-compact-mode",
|
|
211
213
|
);
|
|
212
214
|
|
|
213
215
|
// line 1: prioritized identity and control segments
|
|
@@ -323,14 +325,14 @@ export function renderFooter(p: FooterParts): string[] {
|
|
|
323
325
|
}
|
|
324
326
|
if (!selected) fittedLeft = fitLeftBeside("");
|
|
325
327
|
|
|
326
|
-
const line1 = alignRight(fittedLeft.join(identitySeparator), right1, width, utils.measure);
|
|
327
|
-
const lines = [utils.clip(line1, width, ell)];
|
|
328
|
-
if (p.bashBgStatus) {
|
|
329
|
-
lines.push(utils.clip(p.bashBgStatus, width, ell));
|
|
330
|
-
}
|
|
331
|
-
if (p.workflowStatus) {
|
|
332
|
-
lines.push(utils.clip(theme.fg("muted", p.workflowStatus), width, ell));
|
|
333
|
-
}
|
|
328
|
+
const line1 = alignRight(fittedLeft.join(identitySeparator), right1, width, utils.measure);
|
|
329
|
+
const lines = [utils.clip(line1, width, ell)];
|
|
330
|
+
if (p.bashBgStatus) {
|
|
331
|
+
lines.push(utils.clip(p.bashBgStatus, width, ell));
|
|
332
|
+
}
|
|
333
|
+
if (p.workflowStatus) {
|
|
334
|
+
lines.push(utils.clip(theme.fg("muted", p.workflowStatus), width, ell));
|
|
335
|
+
}
|
|
334
336
|
const statuses = visibleStatuses.filter((status) => !controlStatuses.includes(status));
|
|
335
337
|
if (statuses.length > 0) {
|
|
336
338
|
const statusSeparator = ` ${sep} `;
|
package/src/index.ts
CHANGED
|
@@ -2,17 +2,17 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
4
4
|
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
-
import { AgentsStore, type CompletePayload, type MessagePayload, type StartedPayload } from "./agents-store.ts";
|
|
6
|
-
import { statusText, titleFor, workingMessage, type AmbientState } from "./ambient.ts";
|
|
7
|
-
import { BashBgStore } from "./bash-bg-store.ts";
|
|
8
|
-
import { BashBgOverlay } from "./bash-bg-overlay.ts";
|
|
9
|
-
import { renderBashBgSummary } from "./bash-bg-widget.ts";
|
|
10
|
-
import { TodoStore } from "./todo-store.ts";
|
|
5
|
+
import { AgentsStore, type CompletePayload, type MessagePayload, type StartedPayload } from "./agents-store.ts";
|
|
6
|
+
import { statusText, titleFor, workingMessage, type AmbientState } from "./ambient.ts";
|
|
7
|
+
import { BashBgStore } from "./bash-bg-store.ts";
|
|
8
|
+
import { BashBgOverlay } from "./bash-bg-overlay.ts";
|
|
9
|
+
import { renderBashBgSummary } from "./bash-bg-widget.ts";
|
|
10
|
+
import { TodoStore } from "./todo-store.ts";
|
|
11
11
|
import { makeTodoWidget, makeAgentWidget, terminalRows } from "./stack-widget.ts";
|
|
12
12
|
import { activeThemeName, ThemePicker } from "./theme-picker.ts";
|
|
13
13
|
import { getUsageTotals, invalidateUsageCache, renderFooter, type PaintTheme, type WidthUtils } from "./footer.ts";
|
|
14
14
|
import { collectExtensionStatuses } from "./extension-status.ts";
|
|
15
|
-
import { ANIMATION_PERIOD_MS, resolveGlyphs, spinFrame } from "./icons.ts";
|
|
15
|
+
import { ANIMATION_PERIOD_MS, resolveGlyphs, spinFrame } from "./icons.ts";
|
|
16
16
|
import { ensureConfigExists, loadConfig, saveConfig } from "./config.ts";
|
|
17
17
|
import { applyRow, buildRows, rowKeyForAccel, type SaveState } from "./settings-view.ts";
|
|
18
18
|
import {
|
|
@@ -133,6 +133,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
133
133
|
pi.events.emit(COCKPIT_UI_OWNERSHIP_EVENT, {
|
|
134
134
|
todo: config.enabled,
|
|
135
135
|
agents: config.enabled && config.hideNativeAgents,
|
|
136
|
+
footer: config.enabled,
|
|
136
137
|
todoExpanded: config.todoExpanded,
|
|
137
138
|
});
|
|
138
139
|
};
|
|
@@ -181,12 +182,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
181
182
|
ctx.ui.setWidget(
|
|
182
183
|
AGENT_WIDGET_KEY,
|
|
183
184
|
(tui, theme) => {
|
|
184
|
-
capturedTui = tui;
|
|
185
|
-
return makeAgentWidget({
|
|
186
|
-
getAgents: () => agents.snapshot(),
|
|
187
|
-
getConfig: () => config,
|
|
188
|
-
isRunning: () => running,
|
|
189
|
-
isAnimating,
|
|
185
|
+
capturedTui = tui;
|
|
186
|
+
return makeAgentWidget({
|
|
187
|
+
getAgents: () => agents.snapshot(),
|
|
188
|
+
getConfig: () => config,
|
|
189
|
+
isRunning: () => running,
|
|
190
|
+
isAnimating,
|
|
190
191
|
})(tui, theme);
|
|
191
192
|
},
|
|
192
193
|
{ placement: "belowEditor" },
|
|
@@ -210,23 +211,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
210
211
|
const component = {
|
|
211
212
|
render(width: number): string[] {
|
|
212
213
|
if (Number.isFinite(width) && width > 0) observedWidth = width;
|
|
213
|
-
const cu = ctx.getContextUsage();
|
|
214
|
-
const branch = footerData.getGitBranch();
|
|
215
|
-
const extensionStatuses = collectExtensionStatuses(footerData.getExtensionStatuses());
|
|
216
|
-
const glyphs = resolveGlyphs(config.icons.mode);
|
|
217
|
-
const now = Date.now();
|
|
218
|
-
const bashBgStatus = renderBashBgSummary(
|
|
219
|
-
bashBg.snapshot(),
|
|
220
|
-
width,
|
|
221
|
-
theme,
|
|
222
|
-
FOOTER_UTILS,
|
|
223
|
-
{
|
|
224
|
-
glyphs,
|
|
225
|
-
spin: spinFrame(glyphs, now, isAnimating()),
|
|
226
|
-
now,
|
|
227
|
-
},
|
|
228
|
-
)[0];
|
|
229
|
-
return renderFooter({
|
|
214
|
+
const cu = ctx.getContextUsage();
|
|
215
|
+
const branch = footerData.getGitBranch();
|
|
216
|
+
const extensionStatuses = collectExtensionStatuses(footerData.getExtensionStatuses());
|
|
217
|
+
const glyphs = resolveGlyphs(config.icons.mode);
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
const bashBgStatus = renderBashBgSummary(
|
|
220
|
+
bashBg.snapshot(),
|
|
221
|
+
width,
|
|
222
|
+
theme,
|
|
223
|
+
FOOTER_UTILS,
|
|
224
|
+
{
|
|
225
|
+
glyphs,
|
|
226
|
+
spin: spinFrame(glyphs, now, isAnimating()),
|
|
227
|
+
now,
|
|
228
|
+
},
|
|
229
|
+
)[0];
|
|
230
|
+
return renderFooter({
|
|
230
231
|
width,
|
|
231
232
|
model: ctx.model?.id ?? "no-model",
|
|
232
233
|
provider: ctx.model?.provider,
|
|
@@ -234,13 +235,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
234
235
|
cwd: formatCwd(ctx.sessionManager.getCwd()),
|
|
235
236
|
ctxPct: cu?.percent ?? 0,
|
|
236
237
|
ctxTokens: cu?.tokens ?? 0,
|
|
237
|
-
ctxWindow: cu?.contextWindow ?? ctx.model?.contextWindow ?? 0,
|
|
238
|
-
totals: getUsageTotals(ctx.sessionManager.getEntries()),
|
|
239
|
-
git: branch ?? undefined,
|
|
240
|
-
bashBgStatus,
|
|
241
|
-
workflowStatus: extensionStatuses.find((status) => status.key === WORKFLOW_STATUS_KEY)?.text,
|
|
242
|
-
extensionStatuses: extensionStatuses.filter((status) => status.key !== WORKFLOW_STATUS_KEY),
|
|
243
|
-
glyphs,
|
|
238
|
+
ctxWindow: cu?.contextWindow ?? ctx.model?.contextWindow ?? 0,
|
|
239
|
+
totals: getUsageTotals(ctx.sessionManager.getEntries()),
|
|
240
|
+
git: branch ?? undefined,
|
|
241
|
+
bashBgStatus,
|
|
242
|
+
workflowStatus: extensionStatuses.find((status) => status.key === WORKFLOW_STATUS_KEY)?.text,
|
|
243
|
+
extensionStatuses: extensionStatuses.filter((status) => status.key !== WORKFLOW_STATUS_KEY),
|
|
244
|
+
glyphs,
|
|
244
245
|
theme,
|
|
245
246
|
utils: FOOTER_UTILS,
|
|
246
247
|
});
|
|
@@ -316,8 +317,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
316
317
|
// through /settings since — including an automatic "light/dark" pair, which
|
|
317
318
|
// cockpit cannot represent and would silently flatten to a single theme.
|
|
318
319
|
todos.hydrateFromEntries(ctx.sessionManager.getEntries());
|
|
319
|
-
|
|
320
|
-
|
|
320
|
+
if (config.enabled) {
|
|
321
|
+
publishUiOwnership();
|
|
322
|
+
applyUi(ctx);
|
|
323
|
+
} else {
|
|
324
|
+
applyUi(ctx);
|
|
325
|
+
publishUiOwnership();
|
|
326
|
+
}
|
|
321
327
|
pi.events.emit(BASH_BG_QUERY_EVENT, undefined);
|
|
322
328
|
req();
|
|
323
329
|
});
|
|
@@ -327,6 +333,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
327
333
|
pi.events.emit(COCKPIT_UI_OWNERSHIP_EVENT, {
|
|
328
334
|
todo: false,
|
|
329
335
|
agents: false,
|
|
336
|
+
footer: false,
|
|
330
337
|
todoExpanded: config.todoExpanded,
|
|
331
338
|
});
|
|
332
339
|
lastCtx = undefined;
|
|
@@ -520,10 +527,16 @@ export default function (pi: ExtensionAPI): void {
|
|
|
520
527
|
? { kind: "saved" }
|
|
521
528
|
: { kind: "failed", message: result.error ?? "unknown error" };
|
|
522
529
|
if (wasEnabled !== config.enabled) {
|
|
523
|
-
if (config.enabled)
|
|
524
|
-
|
|
530
|
+
if (config.enabled) {
|
|
531
|
+
publishUiOwnership();
|
|
532
|
+
applyUi(ctx);
|
|
533
|
+
} else {
|
|
534
|
+
uninstallUi(ctx);
|
|
535
|
+
publishUiOwnership();
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
publishUiOwnership();
|
|
525
539
|
}
|
|
526
|
-
publishUiOwnership();
|
|
527
540
|
req();
|
|
528
541
|
};
|
|
529
542
|
const ui = {
|
package/src/render.ts
CHANGED
|
@@ -106,6 +106,16 @@ export function formatDuration(ms: number): string {
|
|
|
106
106
|
return `${h}h ${m}m ${s}s`;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
export function formatAgentMetric(value: number): string {
|
|
110
|
+
if (value < 100) return String(value);
|
|
111
|
+
if (value < 999_950) return `${trimMetric(value / 1_000)}k`;
|
|
112
|
+
return `${trimMetric(value / 1_000_000)}m`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function trimMetric(value: number): string {
|
|
116
|
+
return value.toFixed(1).replace(/\.0$/, "");
|
|
117
|
+
}
|
|
118
|
+
|
|
109
119
|
// Role is identity, status is state — they must not share a palette. Reusing
|
|
110
120
|
// success/warning/error for roles meant a green row could mean "executor" or
|
|
111
121
|
// "done", and every theme inherits that ambiguity. Roles therefore draw from the
|
|
@@ -236,7 +246,12 @@ export function renderAgents(
|
|
|
236
246
|
if (r.activeTool) segs.push({ text: theme.fg("accent", `tool ${r.activeTool}`), priority: 50, minWidth: 8 });
|
|
237
247
|
if (r.tail) segs.push({ text: theme.fg("dim", r.tail), priority: 40, minWidth: 8 });
|
|
238
248
|
if (r.toolCount !== undefined) segs.push({ text: theme.fg("muted", `${r.toolCount} tools`), priority: 20, clippable: false });
|
|
239
|
-
if (r.
|
|
249
|
+
if (r.inputTokens !== undefined || r.outputTokens !== undefined) {
|
|
250
|
+
const usage = `in ${formatAgentMetric(r.inputTokens ?? 0)} · out ${formatAgentMetric(r.outputTokens ?? 0)}`;
|
|
251
|
+
segs.push({ text: theme.fg("muted", usage), priority: 10, clippable: false });
|
|
252
|
+
} else if (r.tokens !== undefined) {
|
|
253
|
+
segs.push({ text: theme.fg("muted", `${formatAgentMetric(r.tokens)} tok`), priority: 10, clippable: false });
|
|
254
|
+
}
|
|
240
255
|
return fitLineByPriority(segs, width, utils, " ", g.ellipsis);
|
|
241
256
|
});
|
|
242
257
|
if (hidden > 0) {
|
|
@@ -247,6 +262,12 @@ export function renderAgents(
|
|
|
247
262
|
|
|
248
263
|
const TODO_MAX_VISIBLE = 8;
|
|
249
264
|
|
|
265
|
+
const TODO_STATUS_RANK: Record<string, number> = { in_progress: 0, blocked: 1, pending: 2, completed: 3 };
|
|
266
|
+
|
|
267
|
+
function todoStatusRank(status: string): number {
|
|
268
|
+
return TODO_STATUS_RANK[status] ?? 4;
|
|
269
|
+
}
|
|
270
|
+
|
|
250
271
|
// Tasks keep their creation order (the numeric id assigned at allocateTaskId).
|
|
251
272
|
// Non-numeric ids (workflow mirrors) sort after numeric ones, then lexicographically.
|
|
252
273
|
function todoIdOrder(id: string): number {
|
|
@@ -361,7 +382,11 @@ export function renderTodos(
|
|
|
361
382
|
// Creation order drives both the next-task pointer and the expanded rows, so
|
|
362
383
|
// compute it once up front. The pointer is the first task the user can act on:
|
|
363
384
|
// not done, not blocked, and free of unresolved dependencies.
|
|
364
|
-
const ordered = [...items].sort((a, b) =>
|
|
385
|
+
const ordered = [...items].sort((a, b) =>
|
|
386
|
+
todoStatusRank(a.status) - todoStatusRank(b.status)
|
|
387
|
+
|| todoIdOrder(a.id) - todoIdOrder(b.id)
|
|
388
|
+
|| (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
|
389
|
+
);
|
|
365
390
|
const next = ordered.find((it) => it.status !== "completed" && it.status !== "blocked" && it.blockedBy.length === 0);
|
|
366
391
|
if (next) {
|
|
367
392
|
const np = todoPaint(next.status, g, spin);
|