pi-crew 0.6.3 → 0.6.4
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 +34 -0
- package/README.md +12 -1
- package/docs/ui-optimization-plan.md +447 -0
- package/package.json +1 -1
- package/src/extension/register.ts +19 -1
- package/src/extension/registration/brief-tool-overrides.ts +400 -0
- package/src/extension/registration/commands.ts +25 -0
- package/src/extension/registration/subagent-tools.ts +8 -3
- package/src/extension/registration/team-tool.ts +18 -11
- package/src/extension/team-tool/run.ts +4 -3
- package/src/extension/team-tool-types.ts +2 -0
- package/src/state/session-state-map.ts +51 -0
- package/src/ui/powerbar-publisher.ts +1 -1
- package/src/ui/status-colors.ts +5 -1
- package/src/ui/theme-adapter.ts +80 -1
- package/src/ui/tool-progress-formatter.ts +9 -5
- package/src/ui/tool-render.ts +4 -0
- package/src/ui/tool-renderers/brief-mode.ts +207 -0
- package/src/ui/tool-renderers/index.ts +627 -0
- package/src/ui/widget/index.ts +224 -0
- package/src/ui/widget/widget-formatters.ts +148 -0
- package/src/ui/widget/widget-model.ts +90 -0
- package/src/ui/widget/widget-renderer.ts +130 -0
- package/src/ui/widget/widget-types.ts +37 -0
- package/src/utils/guards.ts +110 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brief tool overrides — re-registers built-in Pi tools with custom rendering.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by oh-my-pi's pi-brief extension. Wraps each built-in tool
|
|
5
|
+
* (read, bash, edit, write, find, grep, ls) keeping the original execute
|
|
6
|
+
* but replacing renderCall/renderResult with themed, brief-aware versions.
|
|
7
|
+
*
|
|
8
|
+
* Brief mode shows CONTEXTUAL one-liners that preserve WHAT was done:
|
|
9
|
+
* read ~/file.ts:1-50 → 142 lines · 2.3s
|
|
10
|
+
* bash $ npm test → done · 12.5s
|
|
11
|
+
* edit ~/file.ts → +3 -1 · 0.8s
|
|
12
|
+
* write ~/file.ts (42 lines) → ✓ · 0.2s
|
|
13
|
+
* find *.ts in ~/src → 5 files · 0.5s
|
|
14
|
+
* grep /pattern/ in ~/src → 3 matches · 0.3s
|
|
15
|
+
* ls ~/src → 8 entries · 0.1s
|
|
16
|
+
*
|
|
17
|
+
* Timing: ctx.state.startedAt is set in renderCall, read in renderResult.
|
|
18
|
+
* NOTE: Pi's executionStarted is a boolean flag (NOT a timestamp).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import {
|
|
24
|
+
createBashTool,
|
|
25
|
+
createEditTool,
|
|
26
|
+
createFindTool,
|
|
27
|
+
createGrepTool,
|
|
28
|
+
createLsTool,
|
|
29
|
+
createReadTool,
|
|
30
|
+
createWriteTool,
|
|
31
|
+
} from "@earendil-works/pi-coding-agent";
|
|
32
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
33
|
+
import { isBrief } from "../../ui/tool-renderers/brief-mode.ts";
|
|
34
|
+
|
|
35
|
+
// ── Path shortening ────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const HOME = homedir();
|
|
38
|
+
function shortenPath(p: string): string {
|
|
39
|
+
if (!p) return "";
|
|
40
|
+
if (p.startsWith(HOME)) return `~${p.slice(HOME.length)}`;
|
|
41
|
+
return p;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
interface TextResult {
|
|
47
|
+
content: Array<{ type: string; text?: string }>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface Theme {
|
|
51
|
+
fg: (slot: string, text: string) => string;
|
|
52
|
+
bold: (text: string) => string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface RenderCtx {
|
|
56
|
+
args?: Record<string, unknown>;
|
|
57
|
+
state?: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function fullText(result: TextResult): string | undefined {
|
|
61
|
+
const c = result.content.find((x): x is { type: "text"; text: string } => x.type === "text");
|
|
62
|
+
return c?.text;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fullRender(result: TextResult, theme: Theme): Text {
|
|
66
|
+
const text = fullText(result);
|
|
67
|
+
if (!text) return new Text("", 0, 0);
|
|
68
|
+
const lines = text
|
|
69
|
+
.trim()
|
|
70
|
+
.split("\n")
|
|
71
|
+
.map((line) => theme.fg("toolOutput", line))
|
|
72
|
+
.join("\n");
|
|
73
|
+
return new Text(`\n${lines}`, 0, 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Record start time in ctx.state during renderCall (called once per tool invocation). */
|
|
77
|
+
function noteStarted(ctx: RenderCtx | undefined): void {
|
|
78
|
+
if (ctx?.state && !ctx.state.briefStartedAt) {
|
|
79
|
+
ctx.state.briefStartedAt = Date.now();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Compute elapsed from ctx.state.briefStartedAt. Returns " · 1.2s" or "". */
|
|
84
|
+
function elapsed(ctx: RenderCtx | undefined): string {
|
|
85
|
+
const startedAt = ctx?.state?.briefStartedAt as number | undefined;
|
|
86
|
+
if (!startedAt || typeof startedAt !== "number") return "";
|
|
87
|
+
const ms = Date.now() - startedAt;
|
|
88
|
+
if (ms < 1000) return "";
|
|
89
|
+
if (ms < 60_000) return ` · ${(ms / 1000).toFixed(1)}s`;
|
|
90
|
+
const m = Math.floor(ms / 60_000), s = Math.floor((ms % 60_000) / 1000);
|
|
91
|
+
return ` · ${m}m${s}s`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Truncate to maxLen with ellipsis (visual). */
|
|
95
|
+
function trunc(text: string, maxLen: number): string {
|
|
96
|
+
if (text.length <= maxLen) return text;
|
|
97
|
+
return text.slice(0, maxLen - 1) + "…";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Flatten multiline command to single line, truncated. */
|
|
101
|
+
function flatCmd(cmd: string, maxLen: number): string {
|
|
102
|
+
return trunc(cmd.replace(/\s+/g, " ").trim(), maxLen);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Tool registration ──────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
export function registerBriefToolOverrides(pi: ExtensionAPI, cwd: string): void {
|
|
108
|
+
const tools = {
|
|
109
|
+
read: createReadTool(cwd),
|
|
110
|
+
bash: createBashTool(cwd),
|
|
111
|
+
edit: createEditTool(cwd),
|
|
112
|
+
write: createWriteTool(cwd),
|
|
113
|
+
find: createFindTool(cwd),
|
|
114
|
+
grep: createGrepTool(cwd),
|
|
115
|
+
ls: createLsTool(cwd),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// ─── Read ───
|
|
119
|
+
pi.registerTool({
|
|
120
|
+
name: "read",
|
|
121
|
+
label: "read",
|
|
122
|
+
description: tools.read.description,
|
|
123
|
+
parameters: tools.read.parameters,
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
125
|
+
async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
|
|
126
|
+
return tools.read.execute(toolCallId, params, signal, onUpdate);
|
|
127
|
+
},
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
+
renderCall(args: any, theme: any, ctx: any): any {
|
|
130
|
+
noteStarted(ctx);
|
|
131
|
+
const p = shortenPath(args.path || "");
|
|
132
|
+
const pathDisplay = p ? theme.fg("accent", p) : theme.fg("toolOutput", "...");
|
|
133
|
+
let text = `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`;
|
|
134
|
+
if (args.offset !== undefined || args.limit !== undefined) {
|
|
135
|
+
const startLine = args.offset ?? 1;
|
|
136
|
+
const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
|
|
137
|
+
text += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
138
|
+
}
|
|
139
|
+
return new Text(text, 0, 0);
|
|
140
|
+
},
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
142
|
+
renderResult(result: any, options: any, theme: any, ctx: any): any {
|
|
143
|
+
if (!isBrief() || options.expanded) return fullRender(result, theme);
|
|
144
|
+
const args = ctx?.args ?? {};
|
|
145
|
+
const p = shortenPath(args.path || "?");
|
|
146
|
+
let range = "";
|
|
147
|
+
if (args.offset || args.limit) {
|
|
148
|
+
const s = args.offset ?? 1;
|
|
149
|
+
const e = args.limit ? s + args.limit - 1 : "";
|
|
150
|
+
range = `:${s}${e ? `-${e}` : ""}`;
|
|
151
|
+
}
|
|
152
|
+
const text = fullText(result);
|
|
153
|
+
const count = text ? text.trim().split("\n").filter(Boolean).length : 0;
|
|
154
|
+
const time = elapsed(ctx);
|
|
155
|
+
const label = count === 0 ? "empty" : `${count} lines`;
|
|
156
|
+
return new Text(
|
|
157
|
+
`${theme.fg("toolTitle", "read")} ${theme.fg("accent", p)}${theme.fg("warning", range)} ${theme.fg("dim", "→")} ${theme.fg("muted", label)}${theme.fg("dim", time)}`,
|
|
158
|
+
0, 0,
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ─── Bash ───
|
|
164
|
+
pi.registerTool({
|
|
165
|
+
name: "bash",
|
|
166
|
+
label: "bash",
|
|
167
|
+
description: tools.bash.description,
|
|
168
|
+
parameters: tools.bash.parameters,
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
170
|
+
async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
|
|
171
|
+
return tools.bash.execute(toolCallId, params, signal, onUpdate);
|
|
172
|
+
},
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
174
|
+
renderCall(args: any, theme: any, ctx: any): any {
|
|
175
|
+
noteStarted(ctx);
|
|
176
|
+
const command = args.command || "...";
|
|
177
|
+
const timeout = args.timeout as number | undefined;
|
|
178
|
+
const timeoutSuffix = timeout ? theme.fg("muted", ` (${timeout}s)`) : "";
|
|
179
|
+
return new Text(theme.fg("toolTitle", theme.bold(`$ ${command}`)) + timeoutSuffix, 0, 0);
|
|
180
|
+
},
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
182
|
+
renderResult(result: any, options: any, theme: any, ctx: any): any {
|
|
183
|
+
if (!isBrief() || options.expanded) return fullRender(result, theme);
|
|
184
|
+
const args = ctx?.args ?? {};
|
|
185
|
+
const cmd = flatCmd(String(args.command || "?"), 50);
|
|
186
|
+
const text = fullText(result);
|
|
187
|
+
const time = elapsed(ctx);
|
|
188
|
+
let label: string;
|
|
189
|
+
let color: string;
|
|
190
|
+
if (!text || !text.trim()) {
|
|
191
|
+
label = "done";
|
|
192
|
+
color = "muted";
|
|
193
|
+
} else {
|
|
194
|
+
const lines = text.trim().split("\n");
|
|
195
|
+
if (lines.length === 1) {
|
|
196
|
+
label = trunc(lines[0]!, 40);
|
|
197
|
+
color = "muted";
|
|
198
|
+
} else {
|
|
199
|
+
label = `${lines.length} lines`;
|
|
200
|
+
color = "muted";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return new Text(
|
|
204
|
+
`${theme.fg("toolTitle", "$")} ${theme.fg("accent", cmd)} ${theme.fg("dim", "→")} ${theme.fg(color, label)}${theme.fg("dim", time)}`,
|
|
205
|
+
0, 0,
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ─── Edit ───
|
|
211
|
+
pi.registerTool({
|
|
212
|
+
name: "edit",
|
|
213
|
+
label: "edit",
|
|
214
|
+
description: tools.edit.description,
|
|
215
|
+
parameters: tools.edit.parameters,
|
|
216
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
217
|
+
async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
|
|
218
|
+
return tools.edit.execute(toolCallId, params, signal, onUpdate);
|
|
219
|
+
},
|
|
220
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
221
|
+
renderCall(args: any, theme: any, ctx: any): any {
|
|
222
|
+
noteStarted(ctx);
|
|
223
|
+
const p = shortenPath(args.path || "");
|
|
224
|
+
const pathDisplay = p ? theme.fg("accent", p) : theme.fg("toolOutput", "...");
|
|
225
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`, 0, 0);
|
|
226
|
+
},
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
228
|
+
renderResult(result: any, options: any, theme: any, ctx: any): any {
|
|
229
|
+
if (!isBrief() || options.expanded) return fullRender(result, theme);
|
|
230
|
+
const args = ctx?.args ?? {};
|
|
231
|
+
const p = shortenPath(args.path || "?");
|
|
232
|
+
const text = fullText(result);
|
|
233
|
+
const time = elapsed(ctx);
|
|
234
|
+
if (text && (text.includes("Error") || text.includes("error"))) {
|
|
235
|
+
return new Text(
|
|
236
|
+
`${theme.fg("toolTitle", "edit")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("error", "failed")}${theme.fg("dim", time)}`,
|
|
237
|
+
0, 0,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const added = text ? (text.match(/^\+ /gm) ?? []).length : 0;
|
|
241
|
+
const removed = text ? (text.match(/^- /gm) ?? []).length : 0;
|
|
242
|
+
if (added === 0 && removed === 0) {
|
|
243
|
+
return new Text(
|
|
244
|
+
`${theme.fg("toolTitle", "edit")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("success", "edited")}${theme.fg("dim", time)}`,
|
|
245
|
+
0, 0,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return new Text(
|
|
249
|
+
`${theme.fg("toolTitle", "edit")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("toolDiffAdded", `+${added} `)}${theme.fg("toolDiffRemoved", `-${removed}`)}${theme.fg("dim", time)}`,
|
|
250
|
+
0, 0,
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ─── Write ───
|
|
256
|
+
pi.registerTool({
|
|
257
|
+
name: "write",
|
|
258
|
+
label: "write",
|
|
259
|
+
description: tools.write.description,
|
|
260
|
+
parameters: tools.write.parameters,
|
|
261
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
262
|
+
async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
|
|
263
|
+
return tools.write.execute(toolCallId, params, signal, onUpdate);
|
|
264
|
+
},
|
|
265
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
266
|
+
renderCall(args: any, theme: any, ctx: any): any {
|
|
267
|
+
noteStarted(ctx);
|
|
268
|
+
const p = shortenPath(args.path || "");
|
|
269
|
+
const pathDisplay = p ? theme.fg("accent", p) : theme.fg("toolOutput", "...");
|
|
270
|
+
const lineCount = args.content ? String(args.content).split("\n").length : 0;
|
|
271
|
+
const lineInfo = lineCount > 0 ? theme.fg("muted", ` (${lineCount} lines)`) : "";
|
|
272
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("write"))} ${pathDisplay}${lineInfo}`, 0, 0);
|
|
273
|
+
},
|
|
274
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
275
|
+
renderResult(result: any, options: any, theme: any, ctx: any): any {
|
|
276
|
+
if (!isBrief() || options.expanded) return fullRender(result, theme);
|
|
277
|
+
const args = ctx?.args ?? {};
|
|
278
|
+
const p = shortenPath(args.path || "?");
|
|
279
|
+
const text = fullText(result);
|
|
280
|
+
const time = elapsed(ctx);
|
|
281
|
+
if (text) {
|
|
282
|
+
return new Text(
|
|
283
|
+
`${theme.fg("toolTitle", "write")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("error", trunc(text.trim().split("\n")[0] ?? "", 30))}${theme.fg("dim", time)}`,
|
|
284
|
+
0, 0,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return new Text(
|
|
288
|
+
`${theme.fg("toolTitle", "write")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("success", "✓")}${theme.fg("dim", time)}`,
|
|
289
|
+
0, 0,
|
|
290
|
+
);
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ─── Find ───
|
|
295
|
+
pi.registerTool({
|
|
296
|
+
name: "find",
|
|
297
|
+
label: "find",
|
|
298
|
+
description: tools.find.description,
|
|
299
|
+
parameters: tools.find.parameters,
|
|
300
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
301
|
+
async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
|
|
302
|
+
return tools.find.execute(toolCallId, params, signal, onUpdate);
|
|
303
|
+
},
|
|
304
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
305
|
+
renderCall(args: any, theme: any, ctx: any): any {
|
|
306
|
+
noteStarted(ctx);
|
|
307
|
+
const pattern = args.pattern || "";
|
|
308
|
+
const p = shortenPath(args.path || ".");
|
|
309
|
+
let text = `${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}`;
|
|
310
|
+
text += theme.fg("toolOutput", ` in ${p}`);
|
|
311
|
+
return new Text(text, 0, 0);
|
|
312
|
+
},
|
|
313
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
314
|
+
renderResult(result: any, options: any, theme: any, ctx: any): any {
|
|
315
|
+
if (!isBrief() || options.expanded) return fullRender(result, theme);
|
|
316
|
+
const args = ctx?.args ?? {};
|
|
317
|
+
const pattern = trunc(String(args.pattern || "*"), 20);
|
|
318
|
+
const p = shortenPath(args.path || ".");
|
|
319
|
+
const text = fullText(result);
|
|
320
|
+
const count = text ? text.trim().split("\n").filter(Boolean).length : 0;
|
|
321
|
+
const time = elapsed(ctx);
|
|
322
|
+
const label = count === 0 ? "none" : `${count} files`;
|
|
323
|
+
const color = count === 0 ? "dim" : "muted";
|
|
324
|
+
return new Text(
|
|
325
|
+
`${theme.fg("toolTitle", "find")} ${theme.fg("accent", pattern)} ${theme.fg("dim", `in ${p} →`)} ${theme.fg(color, label)}${theme.fg("dim", time)}`,
|
|
326
|
+
0, 0,
|
|
327
|
+
);
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// ─── Grep ───
|
|
332
|
+
pi.registerTool({
|
|
333
|
+
name: "grep",
|
|
334
|
+
label: "grep",
|
|
335
|
+
description: tools.grep.description,
|
|
336
|
+
parameters: tools.grep.parameters,
|
|
337
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
338
|
+
async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
|
|
339
|
+
return tools.grep.execute(toolCallId, params, signal, onUpdate);
|
|
340
|
+
},
|
|
341
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
342
|
+
renderCall(args: any, theme: any, ctx: any): any {
|
|
343
|
+
noteStarted(ctx);
|
|
344
|
+
const pattern = args.pattern || "";
|
|
345
|
+
const p = shortenPath(args.path || ".");
|
|
346
|
+
let text = `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", `/${pattern}/`)}`;
|
|
347
|
+
text += theme.fg("toolOutput", ` in ${p}`);
|
|
348
|
+
return new Text(text, 0, 0);
|
|
349
|
+
},
|
|
350
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
351
|
+
renderResult(result: any, options: any, theme: any, ctx: any): any {
|
|
352
|
+
if (!isBrief() || options.expanded) return fullRender(result, theme);
|
|
353
|
+
const args = ctx?.args ?? {};
|
|
354
|
+
const pattern = trunc(String(args.pattern || "?"), 20);
|
|
355
|
+
const p = shortenPath(args.path || ".");
|
|
356
|
+
const text = fullText(result);
|
|
357
|
+
const count = text ? text.trim().split("\n").filter(Boolean).length : 0;
|
|
358
|
+
const time = elapsed(ctx);
|
|
359
|
+
const label = count === 0 ? "none" : `${count} matches`;
|
|
360
|
+
const color = count === 0 ? "dim" : "muted";
|
|
361
|
+
return new Text(
|
|
362
|
+
`${theme.fg("toolTitle", "grep")} ${theme.fg("accent", `/${pattern}/`)} ${theme.fg("dim", `in ${p} →`)} ${theme.fg(color, label)}${theme.fg("dim", time)}`,
|
|
363
|
+
0, 0,
|
|
364
|
+
);
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ─── Ls ───
|
|
369
|
+
pi.registerTool({
|
|
370
|
+
name: "ls",
|
|
371
|
+
label: "ls",
|
|
372
|
+
description: tools.ls.description,
|
|
373
|
+
parameters: tools.ls.parameters,
|
|
374
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
375
|
+
async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
|
|
376
|
+
return tools.ls.execute(toolCallId, params, signal, onUpdate);
|
|
377
|
+
},
|
|
378
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
379
|
+
renderCall(args: any, theme: any, ctx: any): any {
|
|
380
|
+
noteStarted(ctx);
|
|
381
|
+
const p = shortenPath(args.path || ".");
|
|
382
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", p)}`, 0, 0);
|
|
383
|
+
},
|
|
384
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
385
|
+
renderResult(result: any, options: any, theme: any, ctx: any): any {
|
|
386
|
+
if (!isBrief() || options.expanded) return fullRender(result, theme);
|
|
387
|
+
const args = ctx?.args ?? {};
|
|
388
|
+
const p = shortenPath(args.path || ".");
|
|
389
|
+
const text = fullText(result);
|
|
390
|
+
const count = text ? text.trim().split("\n").filter(Boolean).length : 0;
|
|
391
|
+
const time = elapsed(ctx);
|
|
392
|
+
const label = count === 0 ? "empty" : `${count} entries`;
|
|
393
|
+
const color = count === 0 ? "dim" : "muted";
|
|
394
|
+
return new Text(
|
|
395
|
+
`${theme.fg("toolTitle", "ls")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg(color, label)}${theme.fg("dim", time)}`,
|
|
396
|
+
0, 0,
|
|
397
|
+
);
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
}
|
|
@@ -571,6 +571,31 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
571
571
|
pi.registerCommand("team-help", { description: "Show pi-crew command help", handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
572
572
|
await notifyCommandResult(ctx, piTeamsHelp());
|
|
573
573
|
} });
|
|
574
|
+
|
|
575
|
+
// Brief mode command — toggle compact tool output display
|
|
576
|
+
pi.registerCommand("crew-brief", {
|
|
577
|
+
description: "Toggle brief tool output mode: on | off | status",
|
|
578
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
579
|
+
const { isBrief, setBrief, BRIEF_ENTRY_TYPE, makeBriefEntry } = await import("../../ui/tool-renderers/brief-mode.ts");
|
|
580
|
+
const trimmed = args.trim();
|
|
581
|
+
|
|
582
|
+
if (trimmed === "on") {
|
|
583
|
+
setBrief(true);
|
|
584
|
+
pi.appendEntry(BRIEF_ENTRY_TYPE, { enabled: true });
|
|
585
|
+
ctx.ui.notify("Brief mode: on — tool output will show compact summaries", "info");
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (trimmed === "off") {
|
|
589
|
+
setBrief(false);
|
|
590
|
+
pi.appendEntry(BRIEF_ENTRY_TYPE, { enabled: false });
|
|
591
|
+
ctx.ui.notify("Brief mode: off — full tool output restored", "info");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
// status (default)
|
|
595
|
+
ctx.ui.notify(`Brief mode: ${isBrief() ? "on" : "off"}`, "info");
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
|
|
574
599
|
time("register.commands");
|
|
575
600
|
printTimings();
|
|
576
601
|
}
|
|
@@ -22,7 +22,8 @@ import { t } from "../../i18n.ts";
|
|
|
22
22
|
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
23
23
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
24
24
|
import { formatCompactToolProgress } from "../../ui/tool-progress-formatter.ts";
|
|
25
|
-
import {
|
|
25
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
26
|
+
import { agentToolRenderer } from "../../ui/tool-renderers/index.ts";
|
|
26
27
|
|
|
27
28
|
const TOOL_PROGRESS_TICK_MS = 1000;
|
|
28
29
|
|
|
@@ -98,11 +99,15 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
98
99
|
},
|
|
99
100
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
101
|
renderCall(args: any, theme: any, context: any): any {
|
|
101
|
-
return
|
|
102
|
+
return agentToolRenderer.renderCall(args, theme, context);
|
|
102
103
|
},
|
|
103
104
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
105
|
renderResult(result: any, options: any, theme: any, context: any): any {
|
|
105
|
-
|
|
106
|
+
try {
|
|
107
|
+
return agentToolRenderer.renderResult(result, options, theme, context);
|
|
108
|
+
} catch (e: any) {
|
|
109
|
+
return new Text('agent-err: ' + (e?.message ?? 'unknown'), 0, 0);
|
|
110
|
+
}
|
|
106
111
|
},
|
|
107
112
|
};
|
|
108
113
|
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { loadConfig } from "../../config/config.ts";
|
|
4
4
|
import { TeamToolParams, type TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
5
|
-
import type { CrewWidgetState } from "../../ui/
|
|
6
|
-
import { updateCrewWidget } from "../../ui/
|
|
5
|
+
import type { CrewWidgetState } from "../../ui/widget/index.ts";
|
|
6
|
+
import { updateCrewWidget } from "../../ui/widget/index.ts";
|
|
7
7
|
import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
|
|
8
8
|
import type { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
9
9
|
import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
10
10
|
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
11
11
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
13
|
+
import { teamToolRenderer, statusIcon } from "../../ui/tool-renderers/index.ts";
|
|
13
14
|
// Team tool handler — lazy-loaded because team-tool.ts imports many modules
|
|
14
15
|
import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
|
|
15
16
|
let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
|
|
@@ -48,7 +49,7 @@ export function resolveCwdOverride(baseCwd: string, override: string | undefined
|
|
|
48
49
|
if (!override) return { ok: true, cwd: baseCwd };
|
|
49
50
|
try {
|
|
50
51
|
const resolved = resolveRealContainedPath(baseCwd, override);
|
|
51
|
-
const stat =
|
|
52
|
+
const stat = statSync(resolved);
|
|
52
53
|
if (!stat.isDirectory()) return { ok: false, error: `cwd override is not a directory: ${resolved}` };
|
|
53
54
|
return { ok: true, cwd: resolved };
|
|
54
55
|
} catch (error) {
|
|
@@ -107,11 +108,15 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
|
|
|
107
108
|
},
|
|
108
109
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
110
|
renderCall(args: any, theme: any, context: any): any {
|
|
110
|
-
return
|
|
111
|
+
return teamToolRenderer.renderCall(args, theme, context);
|
|
111
112
|
},
|
|
112
113
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
114
|
renderResult(result: any, options: any, theme: any, context: any): any {
|
|
114
|
-
|
|
115
|
+
try {
|
|
116
|
+
return teamToolRenderer.renderResult(result, options, theme, context);
|
|
117
|
+
} catch {
|
|
118
|
+
return new Text(statusIcon("completed", theme) + " done", 0, 0);
|
|
119
|
+
}
|
|
115
120
|
},
|
|
116
121
|
};
|
|
117
122
|
pi.registerTool(tool);
|
|
@@ -133,13 +138,15 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
|
|
|
133
138
|
try {
|
|
134
139
|
if (!cwd || !runId) {
|
|
135
140
|
const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
136
|
-
|
|
141
|
+
const msg = `team status=starting elapsed=${elapsed}s`;
|
|
142
|
+
onUpdate({ content: [{ type: "text", text: msg }] });
|
|
137
143
|
return;
|
|
138
144
|
}
|
|
139
|
-
const loaded = loadRunManifestById(cwd, runId);
|
|
145
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
140
146
|
if (!loaded) {
|
|
141
147
|
const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
142
|
-
|
|
148
|
+
const msg = `team run=${runId} elapsed=${elapsed}s (manifest pending)`;
|
|
149
|
+
onUpdate({ content: [{ type: "text", text: msg }] });
|
|
143
150
|
return;
|
|
144
151
|
}
|
|
145
152
|
let agents;
|
|
@@ -162,7 +169,7 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
|
|
|
162
169
|
const timer = setInterval(tick, TEAM_TOOL_PROGRESS_TICK_MS);
|
|
163
170
|
if (typeof timer.unref === "function") timer.unref();
|
|
164
171
|
return {
|
|
165
|
-
attach: (boundCwd, boundRunId) => { cwd = boundCwd; runId = boundRunId; tick(); },
|
|
172
|
+
attach: (boundCwd: string, boundRunId: string) => { cwd = boundCwd; runId = boundRunId; tick(); },
|
|
166
173
|
stop: () => clearInterval(timer),
|
|
167
174
|
};
|
|
168
175
|
}
|
|
@@ -254,6 +254,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
254
254
|
const asyncManifest = { ...effectiveManifest, async: { pid: spawned.pid, logPath: spawned.logPath, spawnedAt: new Date().toISOString() } };
|
|
255
255
|
atomicWriteJson(paths.manifestPath, asyncManifest);
|
|
256
256
|
void appendEventAsync(effectiveManifest.eventsPath, { type: "async.spawned", runId: effectiveManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
|
|
257
|
+
ctx.onRunStarted?.(effectiveManifest.runId);
|
|
257
258
|
scheduleBackgroundEarlyExitGuard(resolvedCtx.cwd, effectiveManifest.runId, spawned.pid, spawned.logPath);
|
|
258
259
|
// Wait for the async run to complete and return actual results.
|
|
259
260
|
try {
|
|
@@ -341,7 +342,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
341
342
|
}
|
|
342
343
|
|
|
343
344
|
const runFailed = completed.manifest.status === "failed" || completed.manifest.status === "blocked";
|
|
344
|
-
return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot }, runFailed);
|
|
345
|
+
return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot, metrics }, runFailed);
|
|
345
346
|
} catch (waitError: unknown) {
|
|
346
347
|
const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
|
|
347
348
|
return result(
|
|
@@ -474,7 +475,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
474
475
|
}
|
|
475
476
|
|
|
476
477
|
const runFailed = completed.manifest.status === "failed" || completed.manifest.status === "blocked";
|
|
477
|
-
return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot }, runFailed);
|
|
478
|
+
return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot, metrics }, runFailed);
|
|
478
479
|
} catch (waitError: unknown) {
|
|
479
480
|
const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
|
|
480
481
|
return result(
|
|
@@ -514,5 +515,5 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
514
515
|
? "Experimental live-session worker execution was enabled."
|
|
515
516
|
: "Safe scaffold mode: child Pi workers were not launched because runtime.mode=scaffold or executeWorkers=false was configured.",
|
|
516
517
|
].join("\n");
|
|
517
|
-
return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
518
|
+
return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot, metrics: collectRunMetrics(resolvedCtx.cwd, executed.manifest.runId) }, executed.manifest.status === "failed");
|
|
518
519
|
}
|
|
@@ -10,6 +10,8 @@ export interface TeamToolDetails {
|
|
|
10
10
|
resumedIds?: string[];
|
|
11
11
|
retriedTaskIds?: string[];
|
|
12
12
|
mailboxIds?: string[];
|
|
13
|
+
/** Run metrics for compact display in TUI tool result rendering. */
|
|
14
|
+
metrics?: { taskCount?: number; completedCount?: number; totalTokens?: number; totalCost?: number; durationMs?: number; consistencyScore?: number };
|
|
13
15
|
/** Structured data for programmatic consumption (e.g. TUI widgets). */
|
|
14
16
|
data?: Record<string, unknown>;
|
|
15
17
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped state container.
|
|
3
|
+
*
|
|
4
|
+
* Generic Map keyed by session ID. Used by widget, dashboard, and
|
|
5
|
+
* live-agent-manager to store per-session state without polluting globals.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by @ayulab/pi-checkpoint SessionStateMap.
|
|
8
|
+
*/
|
|
9
|
+
export class SessionStateMap<T> {
|
|
10
|
+
private readonly map = new Map<string, T>();
|
|
11
|
+
|
|
12
|
+
getOrUndefined(sessionId: string): T | undefined {
|
|
13
|
+
return this.map.get(sessionId);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get(sessionId: string): T | undefined {
|
|
17
|
+
return this.map.get(sessionId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
set(sessionId: string, value: T): void {
|
|
21
|
+
this.map.set(sessionId, value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
has(sessionId: string): boolean {
|
|
25
|
+
return this.map.has(sessionId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
delete(sessionId: string): boolean {
|
|
29
|
+
return this.map.delete(sessionId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
clear(): void {
|
|
33
|
+
this.map.clear();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get size(): number {
|
|
37
|
+
return this.map.size;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
entries(): IterableIterator<[string, T]> {
|
|
41
|
+
return this.map.entries();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
values(): IterableIterator<T> {
|
|
45
|
+
return this.map.values();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
keys(): IterableIterator<string> {
|
|
49
|
+
return this.map.keys();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -10,7 +10,7 @@ import { listLiveAgents } from "../runtime/live-agent-manager.ts";
|
|
|
10
10
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
11
11
|
import type { ManifestCache } from "../runtime/manifest-cache.ts";
|
|
12
12
|
import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
|
|
13
|
-
import { notificationBadge } from "./
|
|
13
|
+
import { notificationBadge } from "./widget/widget-formatters.ts";
|
|
14
14
|
import { RenderCoalescer } from "./render-coalescer.ts";
|
|
15
15
|
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
16
16
|
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
package/src/ui/status-colors.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
|
|
2
2
|
|
|
3
|
-
export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped" | "blocked" | (string & {});
|
|
3
|
+
export type RunStatus = "queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "stopped" | "blocked" | "stale" | "needs_attention" | (string & {});
|
|
4
4
|
|
|
5
5
|
export function colorForStatus(status: RunStatus): CrewThemeColor {
|
|
6
6
|
switch (status) {
|
|
@@ -17,6 +17,8 @@ export function colorForStatus(status: RunStatus): CrewThemeColor {
|
|
|
17
17
|
case "blocked":
|
|
18
18
|
case "stopped":
|
|
19
19
|
return "warning";
|
|
20
|
+
case "needs_attention":
|
|
21
|
+
return "warning";
|
|
20
22
|
case "queued":
|
|
21
23
|
default:
|
|
22
24
|
return "dim";
|
|
@@ -42,6 +44,8 @@ export function iconForStatus(status: RunStatus, options?: { runningGlyph?: stri
|
|
|
42
44
|
return "◦";
|
|
43
45
|
case "blocked":
|
|
44
46
|
return "⏸";
|
|
47
|
+
case "needs_attention":
|
|
48
|
+
return "⚠";
|
|
45
49
|
default:
|
|
46
50
|
return "·";
|
|
47
51
|
}
|