@prjct.app/pi-activity 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/CHANGELOG.md +20 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +27 -3
- package/docs/package.md +4 -2
- package/docs/releases.md +29 -0
- package/index.ts +370 -103
- package/package.json +4 -2
- package/src/components.ts +448 -0
- package/src/format.ts +272 -0
- package/src/types.ts +67 -0
package/index.ts
CHANGED
|
@@ -6,80 +6,179 @@ import {
|
|
|
6
6
|
createLsTool,
|
|
7
7
|
createReadTool,
|
|
8
8
|
createWriteTool,
|
|
9
|
+
getSettingsListTheme,
|
|
10
|
+
keyHint,
|
|
9
11
|
type ExtensionAPI,
|
|
10
12
|
type ExtensionContext,
|
|
11
13
|
} from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { Container, Text, TruncatedText, truncateToWidth } from "@earendil-works/pi-tui";
|
|
13
|
-
import { stripVTControlCharacters } from "node:util";
|
|
14
|
-
import { homedir } from "node:os";
|
|
15
14
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
15
|
+
import { Container, SettingsList, Text, type SettingItem } from "@earendil-works/pi-tui";
|
|
16
16
|
import type { TSchema } from "typebox";
|
|
17
|
+
import {
|
|
18
|
+
ActiveToolsWidget,
|
|
19
|
+
ActivityInspectorComponent,
|
|
20
|
+
ActivityRowComponent,
|
|
21
|
+
ActivitySummaryComponent,
|
|
22
|
+
renderExpandedToolResult,
|
|
23
|
+
} from "./src/components.ts";
|
|
24
|
+
import {
|
|
25
|
+
actionTarget,
|
|
26
|
+
activityCategory,
|
|
27
|
+
aggregateFileChanges,
|
|
28
|
+
applyToolResult,
|
|
29
|
+
finishRecord,
|
|
30
|
+
formatDuration,
|
|
31
|
+
resultText,
|
|
32
|
+
snapshotRecord,
|
|
33
|
+
TOOL_VERBS,
|
|
34
|
+
} from "./src/format.ts";
|
|
35
|
+
import type {
|
|
36
|
+
ActivityCategory,
|
|
37
|
+
ActivityDensity,
|
|
38
|
+
ActivityRecord,
|
|
39
|
+
ActivityRecordSnapshot,
|
|
40
|
+
ActivitySummaryData,
|
|
41
|
+
} from "./src/types.ts";
|
|
17
42
|
|
|
18
|
-
const
|
|
43
|
+
const DENSITIES: ActivityDensity[] = ["minimal", "balanced", "forensic"];
|
|
44
|
+
const HISTORY_LIMIT = 200;
|
|
19
45
|
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
: name === "grep" || name === "find" ? `${args.pattern ?? ""}${args.path ? ` · ${args.path}` : ""}`
|
|
23
|
-
: args.path;
|
|
24
|
-
if (typeof value !== "string" || !value) return ".";
|
|
25
|
-
const home = homedir();
|
|
26
|
-
const target = value.startsWith(`${home}/`) ? `~/${value.slice(home.length + 1)}` : value;
|
|
27
|
-
return stripVTControlCharacters(target).replace(/\r?\n/g, " ↵ ").replace(/\t/g, " ").replace(/[\x00-\x1f\x7f]/g, "");
|
|
46
|
+
function isDensity(value: unknown): value is ActivityDensity {
|
|
47
|
+
return typeof value === "string" && DENSITIES.includes(value as ActivityDensity);
|
|
28
48
|
}
|
|
29
49
|
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
.filter((item) => item.type === "text" && item.text)
|
|
33
|
-
.map((item) => item.text)
|
|
34
|
-
.join("\n")
|
|
35
|
-
.trim();
|
|
36
|
-
return text || "No additional details.";
|
|
50
|
+
function unique(values: string[]): string[] {
|
|
51
|
+
return [...new Set(values)];
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
export default function activityMode(pi: ExtensionAPI) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
let
|
|
44
|
-
let errorCount = 0;
|
|
45
|
-
const activeTools = new Map<string, string>();
|
|
55
|
+
let density: ActivityDensity = "balanced";
|
|
56
|
+
let runStartedAt: number | undefined;
|
|
57
|
+
let agentRunning = false;
|
|
58
|
+
let waitingForUser = false;
|
|
46
59
|
let lastWorkingMessage: string | undefined;
|
|
60
|
+
let history: Array<ActivityRecord | ActivityRecordSnapshot> = [];
|
|
61
|
+
let runRecords: ActivityRecord[] = [];
|
|
62
|
+
const recordsById = new Map<string, ActivityRecord>();
|
|
63
|
+
const activeRecords = new Map<string, ActivityRecord>();
|
|
47
64
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
const completedCount = () => runRecords.filter((record) => record.status !== "running").length;
|
|
66
|
+
|
|
67
|
+
function ensureRecord(
|
|
68
|
+
id: string,
|
|
69
|
+
name: string,
|
|
70
|
+
args: Record<string, unknown> = {},
|
|
71
|
+
startedAt = Date.now(),
|
|
72
|
+
): ActivityRecord {
|
|
73
|
+
const existing = recordsById.get(id);
|
|
74
|
+
if (existing) {
|
|
75
|
+
if (Object.keys(args).length) {
|
|
76
|
+
existing.args = args;
|
|
77
|
+
existing.target = actionTarget(name, args);
|
|
78
|
+
existing.category = activityCategory(name, args);
|
|
79
|
+
}
|
|
80
|
+
return existing;
|
|
81
|
+
}
|
|
82
|
+
const record: ActivityRecord = {
|
|
83
|
+
id,
|
|
84
|
+
name,
|
|
85
|
+
args,
|
|
86
|
+
target: actionTarget(name, args),
|
|
87
|
+
category: activityCategory(name, args),
|
|
88
|
+
status: "running",
|
|
89
|
+
startedAt,
|
|
90
|
+
};
|
|
91
|
+
recordsById.set(id, record);
|
|
92
|
+
runRecords.push(record);
|
|
93
|
+
runStartedAt ??= startedAt;
|
|
94
|
+
return record;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function reconstructSessionState(ctx: ExtensionContext): void {
|
|
98
|
+
density = "balanced";
|
|
99
|
+
history = [];
|
|
100
|
+
const byId = new Map<string, ActivityRecordSnapshot>();
|
|
101
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
102
|
+
if (entry.type !== "custom") continue;
|
|
103
|
+
if (entry.customType === "activity-settings") {
|
|
104
|
+
const savedDensity = (entry.data as { density?: unknown } | undefined)?.density;
|
|
105
|
+
if (isDensity(savedDensity)) density = savedDensity;
|
|
106
|
+
}
|
|
107
|
+
if (entry.customType !== "activity-summary") continue;
|
|
108
|
+
const data = entry.data as Partial<ActivitySummaryData> | undefined;
|
|
109
|
+
if (data?.version !== 2 || !Array.isArray(data.records)) continue;
|
|
110
|
+
for (const record of data.records) byId.set(record.id, record);
|
|
111
|
+
}
|
|
112
|
+
history = [...byId.values()].slice(-HISTORY_LIMIT);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function activeList(): ActivityRecord[] {
|
|
116
|
+
return [...activeRecords.values()];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setWorkingMessage(ctx: ExtensionContext, message: string | undefined): void {
|
|
54
120
|
if (message === lastWorkingMessage) return;
|
|
55
121
|
lastWorkingMessage = message;
|
|
56
122
|
ctx.ui.setWorkingMessage(message);
|
|
57
123
|
}
|
|
58
124
|
|
|
125
|
+
function refreshPresence(ctx: ExtensionContext, settled = false): void {
|
|
126
|
+
if (ctx.mode !== "tui") return;
|
|
127
|
+
const records = activeList();
|
|
128
|
+
if (settled) {
|
|
129
|
+
ctx.ui.setWidget("pi-activity", undefined);
|
|
130
|
+
setWorkingMessage(ctx, undefined);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (waitingForUser || records.length) {
|
|
135
|
+
ctx.ui.setWidget(
|
|
136
|
+
"pi-activity",
|
|
137
|
+
(_tui, theme) => new ActiveToolsWidget(activeList, completedCount, () => waitingForUser, theme),
|
|
138
|
+
{ placement: "aboveEditor" },
|
|
139
|
+
);
|
|
140
|
+
} else {
|
|
141
|
+
ctx.ui.setWidget("pi-activity", undefined);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (waitingForUser) {
|
|
145
|
+
setWorkingMessage(ctx, "Waiting for input…");
|
|
146
|
+
ctx.ui.setStatus("pi-activity", ctx.ui.theme.fg("warning", "! waiting for input"));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (records.length) {
|
|
150
|
+
const first = records[0]!;
|
|
151
|
+
const verb = TOOL_VERBS[first.name] ?? first.name.toUpperCase();
|
|
152
|
+
const message = records.length > 1
|
|
153
|
+
? `${records.length} active · ${verb} ${first.target}`
|
|
154
|
+
: `${verb} ${first.target}`;
|
|
155
|
+
setWorkingMessage(ctx, message);
|
|
156
|
+
ctx.ui.setStatus("pi-activity", ctx.ui.theme.fg("accent", `◆ ${records.length} active · ${completedCount()} done`));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (agentRunning) {
|
|
160
|
+
setWorkingMessage(ctx, "Thinking…");
|
|
161
|
+
ctx.ui.setStatus("pi-activity", ctx.ui.theme.fg("dim", "◆ thinking"));
|
|
162
|
+
} else {
|
|
163
|
+
setWorkingMessage(ctx, undefined);
|
|
164
|
+
ctx.ui.setStatus("pi-activity", ctx.ui.theme.fg("dim", `◇ ready · ${density}`));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
59
168
|
pi.registerEntryRenderer("activity-summary", (entry, options, theme) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
} | undefined;
|
|
66
|
-
const files = data?.modifiedFiles ?? [];
|
|
67
|
-
const failures = data?.failedActions ?? [];
|
|
68
|
-
const count = (n: number, label: string) => `${n} ${label}${n === 1 ? "" : "s"}`;
|
|
69
|
-
// Legacy summaries stored categories, not counts; do not invent action totals.
|
|
70
|
-
const total = data?.actionCount === undefined ? "previous run" : count(data.actionCount, "action");
|
|
71
|
-
const errors = data?.errorCount === undefined ? count(failures.length, "error category") : count(data.errorCount, "error");
|
|
72
|
-
const title = `Activity · ${total} · ${count(files.length, "file")} · ${errors}`;
|
|
73
|
-
if (!options.expanded) return new TruncatedText(theme.fg(failures.length ? "error" : "muted", title), 0, 0);
|
|
74
|
-
const lines = [title, ...files.map((path) => ` • ${actionTarget("read", { path })}`)];
|
|
75
|
-
if (failures.length) lines.push(`Errors: ${failures.join(" · ")}`);
|
|
76
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
169
|
+
return new ActivitySummaryComponent(
|
|
170
|
+
(entry.data ?? {}) as ActivitySummaryData,
|
|
171
|
+
options.expanded,
|
|
172
|
+
theme,
|
|
173
|
+
);
|
|
77
174
|
});
|
|
78
175
|
|
|
176
|
+
// Settings are persisted as invisible session entries and restored on resume/tree navigation.
|
|
177
|
+
pi.registerEntryRenderer("activity-settings", () => new Container());
|
|
178
|
+
|
|
79
179
|
function registerCompactTool<T extends TSchema, D>(createTool: (cwd: string) => AgentTool<T, D>) {
|
|
80
180
|
const initialTool = createTool(process.cwd());
|
|
81
181
|
const name = initialTool.name;
|
|
82
|
-
|
|
83
182
|
pi.registerTool({
|
|
84
183
|
...initialTool,
|
|
85
184
|
name,
|
|
@@ -88,22 +187,33 @@ export default function activityMode(pi: ExtensionAPI) {
|
|
|
88
187
|
return createTool(ctx.cwd).execute(toolCallId, params, signal, onUpdate);
|
|
89
188
|
},
|
|
90
189
|
renderCall(args, theme, context) {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
return new TruncatedText(title, 0, 0);
|
|
190
|
+
const id = context.toolCallId || `${name}:render`;
|
|
191
|
+
const record = ensureRecord(id, name, args as Record<string, unknown>);
|
|
192
|
+
if (context.isError && record.status === "running") record.status = "error";
|
|
193
|
+
const component = context.lastComponent instanceof ActivityRowComponent
|
|
194
|
+
? context.lastComponent
|
|
195
|
+
: new ActivityRowComponent(record, theme, () => density);
|
|
196
|
+
component.update(record, theme);
|
|
197
|
+
return component;
|
|
101
198
|
},
|
|
102
|
-
renderResult(result,
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
199
|
+
renderResult(result, options, theme, context) {
|
|
200
|
+
const id = context.toolCallId || `${name}:render`;
|
|
201
|
+
const record = ensureRecord(id, name, context.args as Record<string, unknown>);
|
|
202
|
+
if (!options.isPartial || context.isError) applyToolResult(record, result, context.isError);
|
|
203
|
+
else {
|
|
204
|
+
const preview = resultText(result);
|
|
205
|
+
if (preview) record.outputPreview = preview.slice(0, 2_000);
|
|
206
|
+
}
|
|
207
|
+
return renderExpandedToolResult(
|
|
208
|
+
name,
|
|
209
|
+
context.args as Record<string, unknown>,
|
|
210
|
+
result,
|
|
211
|
+
options.expanded,
|
|
212
|
+
context.isError,
|
|
213
|
+
context.showImages,
|
|
214
|
+
theme,
|
|
215
|
+
record,
|
|
216
|
+
);
|
|
107
217
|
},
|
|
108
218
|
});
|
|
109
219
|
}
|
|
@@ -116,68 +226,225 @@ export default function activityMode(pi: ExtensionAPI) {
|
|
|
116
226
|
registerCompactTool(createGrepTool);
|
|
117
227
|
registerCompactTool(createLsTool);
|
|
118
228
|
|
|
229
|
+
pi.registerCommand("activity", {
|
|
230
|
+
description: "Inspect recent tool activity with filters for changes, commands, and issues.",
|
|
231
|
+
handler: async (_args, ctx) => {
|
|
232
|
+
if (ctx.mode !== "tui") {
|
|
233
|
+
ctx.ui.notify("/activity requires interactive TUI mode.", "error");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const currentRecords = runRecords.filter((record) => !history.some((saved) => saved.id === record.id));
|
|
237
|
+
const records = [...history, ...currentRecords].slice(-HISTORY_LIMIT);
|
|
238
|
+
if (!records.length) {
|
|
239
|
+
ctx.ui.notify("No activity has been recorded in this session yet.", "info");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
await ctx.ui.custom<void>((tui, theme, keybindings, done) => {
|
|
243
|
+
return new ActivityInspectorComponent(records, theme, keybindings, () => done(), () => tui.requestRender());
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
pi.registerCommand("activity-settings", {
|
|
249
|
+
description: "Choose minimal, balanced, or forensic activity-row density.",
|
|
250
|
+
handler: async (args, ctx) => {
|
|
251
|
+
const requested = args.trim().toLowerCase();
|
|
252
|
+
if (requested) {
|
|
253
|
+
if (!isDensity(requested)) {
|
|
254
|
+
if (ctx.hasUI) ctx.ui.notify("Usage: /activity-settings [minimal|balanced|forensic]", "error");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
density = requested;
|
|
258
|
+
pi.appendEntry("activity-settings", { density });
|
|
259
|
+
if (ctx.mode === "tui") {
|
|
260
|
+
ctx.ui.setStatus("pi-activity", ctx.ui.theme.fg("dim", `Activity · ${density}`));
|
|
261
|
+
ctx.ui.notify(`Activity density set to ${density}.`, "info");
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (ctx.mode !== "tui") {
|
|
266
|
+
if (ctx.hasUI) ctx.ui.notify("Pass a density: /activity-settings [minimal|balanced|forensic]", "error");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const items: SettingItem[] = [{
|
|
270
|
+
id: "density",
|
|
271
|
+
label: "Activity density",
|
|
272
|
+
description: "Minimal hides metadata; balanced shows outcomes; forensic adds category and truncation evidence.",
|
|
273
|
+
currentValue: density,
|
|
274
|
+
values: DENSITIES,
|
|
275
|
+
}];
|
|
276
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
277
|
+
const container = new Container();
|
|
278
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Activity settings")), 1, 1));
|
|
279
|
+
const settings = new SettingsList(
|
|
280
|
+
items,
|
|
281
|
+
3,
|
|
282
|
+
getSettingsListTheme(),
|
|
283
|
+
(id, value) => {
|
|
284
|
+
if (id !== "density" || !isDensity(value)) return;
|
|
285
|
+
density = value;
|
|
286
|
+
settings.updateValue(id, value);
|
|
287
|
+
pi.appendEntry("activity-settings", { density });
|
|
288
|
+
ctx.ui.setStatus("pi-activity", theme.fg("dim", `Activity · ${density}`));
|
|
289
|
+
tui.requestRender();
|
|
290
|
+
},
|
|
291
|
+
() => done(),
|
|
292
|
+
);
|
|
293
|
+
container.addChild(settings);
|
|
294
|
+
container.addChild(new Text(theme.fg("dim", `${keyHint("tui.select.confirm", "change")} · ${keyHint("tui.select.cancel", "close")}`), 1, 1));
|
|
295
|
+
return {
|
|
296
|
+
render: (width) => container.render(width),
|
|
297
|
+
invalidate: () => container.invalidate(),
|
|
298
|
+
handleInput: (data) => {
|
|
299
|
+
settings.handleInput(data);
|
|
300
|
+
tui.requestRender();
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
119
307
|
pi.on("session_start", (_event, ctx) => {
|
|
120
|
-
|
|
308
|
+
runStartedAt = undefined;
|
|
309
|
+
agentRunning = false;
|
|
310
|
+
waitingForUser = false;
|
|
121
311
|
lastWorkingMessage = undefined;
|
|
312
|
+
runRecords = [];
|
|
313
|
+
recordsById.clear();
|
|
314
|
+
activeRecords.clear();
|
|
315
|
+
reconstructSessionState(ctx);
|
|
122
316
|
if (ctx.mode !== "tui") return;
|
|
123
317
|
ctx.ui.setToolsExpanded(false);
|
|
124
|
-
ctx.ui.
|
|
318
|
+
ctx.ui.setStatus("pi-activity", ctx.ui.theme.fg("dim", `◇ ready · ${density}`));
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
322
|
+
activeRecords.clear();
|
|
323
|
+
recordsById.clear();
|
|
324
|
+
runRecords = [];
|
|
325
|
+
runStartedAt = undefined;
|
|
326
|
+
agentRunning = false;
|
|
327
|
+
reconstructSessionState(ctx);
|
|
328
|
+
if (ctx.mode === "tui") {
|
|
329
|
+
ctx.ui.setWidget("pi-activity", undefined);
|
|
330
|
+
setWorkingMessage(ctx, undefined);
|
|
331
|
+
ctx.ui.setStatus("pi-activity", ctx.ui.theme.fg("dim", `◇ ready · ${density}`));
|
|
332
|
+
}
|
|
125
333
|
});
|
|
126
334
|
|
|
127
335
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
128
|
-
|
|
129
|
-
|
|
336
|
+
activeRecords.clear();
|
|
337
|
+
agentRunning = false;
|
|
338
|
+
waitingForUser = false;
|
|
130
339
|
if (ctx.mode !== "tui") return;
|
|
131
|
-
ctx.ui.
|
|
340
|
+
ctx.ui.setWidget("pi-activity", undefined);
|
|
341
|
+
ctx.ui.setStatus("pi-activity", undefined);
|
|
342
|
+
setWorkingMessage(ctx, undefined);
|
|
132
343
|
});
|
|
133
344
|
|
|
134
345
|
pi.on("agent_start", (_event, ctx) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
346
|
+
activeRecords.clear();
|
|
347
|
+
agentRunning = true;
|
|
348
|
+
waitingForUser = false;
|
|
349
|
+
runStartedAt ??= Date.now();
|
|
350
|
+
refreshPresence(ctx);
|
|
138
351
|
});
|
|
139
352
|
|
|
140
|
-
pi.on("
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
refreshWorkingMessage(ctx);
|
|
353
|
+
pi.on("ui_prompt_start", (_event, ctx) => {
|
|
354
|
+
waitingForUser = true;
|
|
355
|
+
refreshPresence(ctx);
|
|
144
356
|
});
|
|
145
357
|
|
|
146
|
-
pi.on("
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (event.isError) {
|
|
151
|
-
errorCount++;
|
|
152
|
-
failedActions.add(event.toolName);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
completedActions.add(event.toolName);
|
|
358
|
+
pi.on("ui_prompt_end", (_event, ctx) => {
|
|
359
|
+
waitingForUser = false;
|
|
360
|
+
refreshPresence(ctx);
|
|
361
|
+
});
|
|
156
362
|
|
|
363
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
364
|
+
agentRunning = true;
|
|
365
|
+
const record = ensureRecord(event.toolCallId, event.toolName, event.args ?? {}, Date.now());
|
|
366
|
+
record.status = "running";
|
|
367
|
+
record.startedAt = Date.now();
|
|
368
|
+
record.endedAt = undefined;
|
|
369
|
+
record.durationMs = undefined;
|
|
370
|
+
activeRecords.set(event.toolCallId, record);
|
|
371
|
+
refreshPresence(ctx);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
pi.on("tool_execution_update", (event) => {
|
|
375
|
+
const record = ensureRecord(event.toolCallId, event.toolName, event.args ?? {});
|
|
376
|
+
const preview = resultText(event.partialResult);
|
|
377
|
+
if (preview) record.outputPreview = preview.slice(0, 2_000);
|
|
157
378
|
});
|
|
158
379
|
|
|
159
380
|
pi.on("tool_result", (event) => {
|
|
160
|
-
|
|
161
|
-
|
|
381
|
+
const record = ensureRecord(event.toolCallId, event.toolName, event.input ?? {});
|
|
382
|
+
applyToolResult(record, { content: event.content, details: event.details }, event.isError);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
386
|
+
const record = ensureRecord(event.toolCallId, event.toolName);
|
|
387
|
+
if (event.result) {
|
|
388
|
+
finishRecord(record, Date.now(), event.result, event.isError);
|
|
389
|
+
} else {
|
|
390
|
+
record.endedAt = Date.now();
|
|
391
|
+
record.durationMs = Math.max(0, record.endedAt - record.startedAt);
|
|
392
|
+
if (record.status === "running") applyToolResult(record, { content: [] }, event.isError);
|
|
162
393
|
}
|
|
394
|
+
activeRecords.delete(event.toolCallId);
|
|
395
|
+
refreshPresence(ctx);
|
|
163
396
|
});
|
|
164
397
|
|
|
165
|
-
pi.on("turn_start", (_event, ctx) =>
|
|
398
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
399
|
+
agentRunning = true;
|
|
400
|
+
refreshPresence(ctx);
|
|
401
|
+
});
|
|
166
402
|
|
|
167
403
|
pi.on("agent_settled", (_event, ctx) => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
404
|
+
const settledAt = Date.now();
|
|
405
|
+
agentRunning = false;
|
|
406
|
+
for (const record of runRecords) {
|
|
407
|
+
if (record.status !== "running") continue;
|
|
408
|
+
record.status = "cancelled";
|
|
409
|
+
record.outcome = "interrupted";
|
|
410
|
+
record.endedAt = settledAt;
|
|
411
|
+
record.durationMs = Math.max(0, settledAt - record.startedAt);
|
|
412
|
+
}
|
|
413
|
+
activeRecords.clear();
|
|
414
|
+
waitingForUser = false;
|
|
415
|
+
refreshPresence(ctx, true);
|
|
416
|
+
|
|
417
|
+
const records = runRecords.map(snapshotRecord);
|
|
418
|
+
if (records.length && ctx.mode === "tui") {
|
|
419
|
+
const categoryCounts: Partial<Record<ActivityCategory, number>> = {};
|
|
420
|
+
for (const record of records) categoryCounts[record.category] = (categoryCounts[record.category] ?? 0) + 1;
|
|
421
|
+
const errors = records.filter((record) => record.status === "error");
|
|
422
|
+
const cancelled = records.filter((record) => record.status === "cancelled");
|
|
423
|
+
const data: ActivitySummaryData = {
|
|
424
|
+
version: 2,
|
|
425
|
+
startedAt: runStartedAt ?? records[0]!.startedAt,
|
|
426
|
+
durationMs: Math.max(0, settledAt - (runStartedAt ?? records[0]!.startedAt)),
|
|
427
|
+
actionCount: records.length,
|
|
428
|
+
errorCount: errors.length,
|
|
429
|
+
cancelledCount: cancelled.length,
|
|
430
|
+
truncatedCount: records.filter((record) => record.truncated).length,
|
|
431
|
+
modifiedFiles: aggregateFileChanges(records),
|
|
432
|
+
completedActions: unique(records.filter((record) => record.status === "success").map((record) => record.name)),
|
|
433
|
+
failedActions: unique([...errors, ...cancelled].map((record) => record.name)),
|
|
434
|
+
categoryCounts,
|
|
435
|
+
records,
|
|
436
|
+
};
|
|
437
|
+
pi.appendEntry("activity-summary", data);
|
|
438
|
+
history = [...history, ...runRecords].slice(-HISTORY_LIMIT);
|
|
439
|
+
const issueCount = data.errorCount + data.cancelledCount;
|
|
440
|
+
const status = issueCount
|
|
441
|
+
? ctx.ui.theme.fg("warning", `! ${data.actionCount} actions · ${issueCount} issues · ${formatDuration(data.durationMs)}`)
|
|
442
|
+
: ctx.ui.theme.fg("success", `✓ ${data.actionCount} actions · ${data.modifiedFiles.length} files · ${formatDuration(data.durationMs)}`);
|
|
443
|
+
ctx.ui.setStatus("pi-activity", status);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
runRecords = [];
|
|
447
|
+
recordsById.clear();
|
|
448
|
+
runStartedAt = undefined;
|
|
182
449
|
});
|
|
183
450
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prjct.app/pi-activity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Keep PI Agent tool output readable with compact, expandable activity rows and summaries of actions, errors, and changed files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -59,10 +59,12 @@
|
|
|
59
59
|
},
|
|
60
60
|
"files": [
|
|
61
61
|
"index.ts",
|
|
62
|
+
"src",
|
|
62
63
|
"README.md",
|
|
63
64
|
"LICENSE",
|
|
64
65
|
"CONTRIBUTING.md",
|
|
65
66
|
"CHANGELOG.md",
|
|
66
|
-
"docs/package.md"
|
|
67
|
+
"docs/package.md",
|
|
68
|
+
"docs/releases.md"
|
|
67
69
|
]
|
|
68
70
|
}
|