@heyhuynhgiabuu/pi-task 0.1.5 → 0.1.6
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 +52 -4
- package/README.md +16 -11
- package/dist/conversation.d.ts +68 -21
- package/dist/conversation.js +186 -71
- package/dist/helpers.d.ts +6 -6
- package/dist/helpers.js +30 -6
- package/dist/index.js +300 -203
- package/dist/session-text.d.ts +2 -2
- package/dist/session-text.js +28 -2
- package/dist/subagent/buildArgv.d.ts +1 -0
- package/dist/subagent/buildArgv.js +1 -1
- package/dist/subagent/waitCompletion.d.ts +1 -0
- package/dist/subagent/waitCompletion.js +27 -20
- package/dist/task-widget.d.ts +21 -0
- package/dist/task-widget.js +122 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,19 +14,20 @@
|
|
|
14
14
|
* P1: Foreground mode (background:false, inline subprocess), pane death
|
|
15
15
|
* detection, 30-minute timeout.
|
|
16
16
|
*/
|
|
17
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
18
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
17
|
import { execFileSync } from "node:child_process";
|
|
20
18
|
import { randomUUID } from "node:crypto";
|
|
19
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { mkdir } from "node:fs/promises";
|
|
21
21
|
import { dirname, join } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { Type } from "@sinclair/typebox";
|
|
24
|
-
import { getArtifactsDir, normalizeConversationId, readConversationMetadata, readConversationRegistry, renderConversationSessions, taskArtifactName, taskIdFromArtifactName, writeConversationArtifacts, writeConversationRegistry, } from "./conversation.js";
|
|
25
24
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
25
|
+
import { buildAgentToolSelection } from "./agent-tools.js";
|
|
26
|
+
import { normalizeConversationId, parseMetadataFromBody, readTaskBlock, readTaskSessionsRegistry, renderConversationSessions, writeConversationArtifacts, writeTaskSessionsRegistry, } from "./conversation.js";
|
|
26
27
|
import { TASK_BACKGROUND_DEFAULT, TASK_RESULT_XML_INSTRUCTIONS, TASK_TOOL_DESCRIPTION, buildTmuxSplitWindowArgs, chooseTmuxSplitDirection, formatBackgroundReceipt, buildPiArgs, parseResultXml, formatMs, shellQuote, discoverAgents, formatAgentList, countToolUses, readRecentToolCalls, } from "./helpers.js";
|
|
27
28
|
import { runSdkSubagent } from "./subagent/runSdk.js";
|
|
28
29
|
import { checkTaskCompletion, waitForTaskCompletion as waitForSessionTaskCompletion, } from "./subagent/waitCompletion.js";
|
|
29
|
-
import {
|
|
30
|
+
import { renderTaskWidget, TASK_WIDGET_RENDER_MS, } from "./task-widget.js";
|
|
30
31
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
31
32
|
const BUNDLED_AGENT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "agents");
|
|
32
33
|
const BACKGROUND_CHECK_MS = 10_000; // poll every 10 sec
|
|
@@ -46,6 +47,87 @@ function writeRegistry(piDir, entries) {
|
|
|
46
47
|
const path = join(piDir, "task-registry.json");
|
|
47
48
|
writeFileSync(path, JSON.stringify(entries, null, 2), "utf-8");
|
|
48
49
|
}
|
|
50
|
+
function readTaskSessionHistory(piDir) {
|
|
51
|
+
const path = join(piDir, "task-session-history.json");
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function writeTaskSessionHistory(piDir, entries) {
|
|
60
|
+
const path = join(piDir, "task-session-history.json");
|
|
61
|
+
writeFileSync(path, JSON.stringify(entries, null, 2), "utf-8");
|
|
62
|
+
}
|
|
63
|
+
function upsertTaskSessionHistory(piDir, entry) {
|
|
64
|
+
const entries = readTaskSessionHistory(piDir);
|
|
65
|
+
const index = entries.findIndex((existing) => existing.id === entry.id);
|
|
66
|
+
if (index >= 0) {
|
|
67
|
+
entries[index] = { ...entries[index], ...entry };
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
entries.push(entry);
|
|
71
|
+
}
|
|
72
|
+
writeTaskSessionHistory(piDir, entries);
|
|
73
|
+
}
|
|
74
|
+
function findTaskSessionHistory(piDir, idOrSessionName) {
|
|
75
|
+
return readTaskSessionHistory(piDir).find((entry) => entry.id === idOrSessionName || entry.sessionName === idOrSessionName);
|
|
76
|
+
}
|
|
77
|
+
function findJsonlSessionByName(piDir, sessionName, agentType) {
|
|
78
|
+
const artifactsDir = join(piDir, "artifacts");
|
|
79
|
+
const sessionDir = join(artifactsDir, "sessions");
|
|
80
|
+
try {
|
|
81
|
+
if (!existsSync(sessionDir))
|
|
82
|
+
return undefined;
|
|
83
|
+
const files = readdirSync(sessionDir)
|
|
84
|
+
.filter((file) => file.endsWith(".jsonl"))
|
|
85
|
+
.sort()
|
|
86
|
+
.reverse();
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
const content = readFileSync(join(sessionDir, file), "utf-8");
|
|
89
|
+
let startedAt = Date.now();
|
|
90
|
+
for (const rawLine of content.split("\n")) {
|
|
91
|
+
const line = rawLine.trim();
|
|
92
|
+
if (!line)
|
|
93
|
+
continue;
|
|
94
|
+
try {
|
|
95
|
+
const entry = JSON.parse(line);
|
|
96
|
+
if (entry.type === "session" && entry.timestamp) {
|
|
97
|
+
const parsed = Date.parse(entry.timestamp);
|
|
98
|
+
if (Number.isFinite(parsed))
|
|
99
|
+
startedAt = parsed;
|
|
100
|
+
}
|
|
101
|
+
if (entry.type === "session_info") {
|
|
102
|
+
const name = entry.name ?? entry.session_info?.name;
|
|
103
|
+
if (name === sessionName) {
|
|
104
|
+
return {
|
|
105
|
+
id: sessionName,
|
|
106
|
+
agentType,
|
|
107
|
+
description: `Resumed session ${sessionName}`,
|
|
108
|
+
sessionName,
|
|
109
|
+
sessionRef: join(sessionDir, file),
|
|
110
|
+
startedAt,
|
|
111
|
+
piDir,
|
|
112
|
+
dir: artifactsDir,
|
|
113
|
+
status: "done",
|
|
114
|
+
background: false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Skip malformed lines
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
49
131
|
// ─── Tmux Helpers ────────────────────────────────────────────────────────────
|
|
50
132
|
function tmuxCmd(args) {
|
|
51
133
|
return execFileSync("tmux", args, {
|
|
@@ -129,6 +211,22 @@ function completeTask(pi, id, task, content, phase, piDir) {
|
|
|
129
211
|
killAgentPane(task.paneId, task.originalPane);
|
|
130
212
|
const parsed = parseResultXml(content);
|
|
131
213
|
const durationMs = Date.now() - task.startedAt;
|
|
214
|
+
const completedSessionRef = findJsonlSessionByName(piDir, task.sessionName, task.agentType)?.sessionRef;
|
|
215
|
+
upsertTaskSessionHistory(piDir, {
|
|
216
|
+
id,
|
|
217
|
+
agentType: task.agentType,
|
|
218
|
+
description: task.description,
|
|
219
|
+
sessionName: task.sessionName,
|
|
220
|
+
startedAt: task.startedAt,
|
|
221
|
+
paneId: task.paneId,
|
|
222
|
+
piDir,
|
|
223
|
+
dir: task.dir,
|
|
224
|
+
conversationId: task.conversationId,
|
|
225
|
+
sessionRef: completedSessionRef,
|
|
226
|
+
status: phase,
|
|
227
|
+
completedAt: Date.now(),
|
|
228
|
+
background: true,
|
|
229
|
+
});
|
|
132
230
|
// Send completion notification
|
|
133
231
|
pi.sendMessage({
|
|
134
232
|
customType: "task-complete",
|
|
@@ -214,62 +312,27 @@ export default function (pi) {
|
|
|
214
312
|
]) {
|
|
215
313
|
const sessionDir = join(task.dir, "sessions");
|
|
216
314
|
// Single walk: counts + recent tool-call history with status
|
|
217
|
-
const { toolUses, turns, recent } = readRecentToolCalls(sessionDir, 12);
|
|
315
|
+
const { toolUses, turns, recent } = readRecentToolCalls(sessionDir, 12, task.sessionName);
|
|
218
316
|
task.toolUses = toolUses;
|
|
219
317
|
task.turns = turns;
|
|
220
318
|
task.recentCalls = recent;
|
|
221
319
|
}
|
|
222
320
|
}, COUNT_POLL_MS);
|
|
223
|
-
/**
|
|
224
|
-
* Render a streaming view of one active subagent. Layout per task:
|
|
225
|
-
*
|
|
226
|
-
* ⠋ Scout — SDK docs • 1m 0s 11 toolcalls (themed: accent + dim)
|
|
227
|
-
* ├─ ✓ websearch Model Context Protocol 2026 (green/success)
|
|
228
|
-
* ├─ ✓ codesearch MCP reference server typescript
|
|
229
|
-
* ├─ ✗ bash curl -sL "https://api.github.com..." (red/error)
|
|
230
|
-
* └─ ⠹ read /Users/.../scout.md (yellow/warning, animates)
|
|
231
|
-
*
|
|
232
|
-
* The header caret and in-progress tool marks share the same spinner
|
|
233
|
-
* frame set (rotates every WIDGET_RENDER_MS based on wall-clock time,
|
|
234
|
-
* so the animation cadence is stable regardless of TUI render rate).
|
|
235
|
-
*/
|
|
236
321
|
// Theme reference is captured at setWidget time so renderWidget can use it.
|
|
237
|
-
// We don't import the Theme type because it's not exported; structural typing
|
|
238
|
-
// via `any` here is safe — the c() helper only calls `theme(color, text)`.
|
|
239
322
|
let widgetTheme = null;
|
|
240
|
-
// 8-frame Braille spinner. 80ms cadence = 12.5 FPS, which is the human
|
|
241
|
-
// perception threshold for "smooth motion" (below ~10 FPS the brain
|
|
242
|
-
// sees discrete steps; above ~12 FPS it reads as continuous rotation).
|
|
243
|
-
// Full rotation: 8 × 80ms = 640ms. Used for both per-tool in-progress
|
|
244
|
-
// marks AND the header caret (the "agent is active" indicator).
|
|
245
|
-
const WIDGET_SPINNER_FRAMES = [
|
|
246
|
-
"\u280B",
|
|
247
|
-
"\u2819",
|
|
248
|
-
"\u2838",
|
|
249
|
-
"\u2834",
|
|
250
|
-
"\u2826",
|
|
251
|
-
"\u2827",
|
|
252
|
-
"\u2807",
|
|
253
|
-
"\u280F",
|
|
254
|
-
];
|
|
255
|
-
const WIDGET_CARET_FRAMES = WIDGET_SPINNER_FRAMES;
|
|
256
|
-
const WIDGET_RENDER_MS = 80;
|
|
257
|
-
const WIDGET_MAX_TOOL_LINES = 12;
|
|
258
|
-
const WIDGET_MAX_WIDTH = 120;
|
|
259
|
-
const TREE_MIDDLE = "\u251C\u2500"; // ├─
|
|
260
|
-
const TREE_LAST = "\u2514\u2500"; // └─
|
|
261
|
-
function c(color, text) {
|
|
262
|
-
// widgetTheme is a Theme object with a .fg(color, text) method,
|
|
263
|
-
// not a callable. Calling it as a function throws "widgetTheme is not
|
|
264
|
-
// a function" which the outer try/catch in renderWidget swallows.
|
|
265
|
-
return widgetTheme ? widgetTheme.fg(color, text) : text;
|
|
266
|
-
}
|
|
267
323
|
function renderWidget(width) {
|
|
268
324
|
// Defensive: never let a render exception kill the TUI. If anything
|
|
269
325
|
// throws (theme lookup miss, malformed session JSONL, etc.), fall
|
|
270
326
|
// back to a minimal single-line summary so the TUI stays alive.
|
|
271
327
|
try {
|
|
272
|
-
return
|
|
328
|
+
return renderTaskWidget({
|
|
329
|
+
foregroundTasks: foregroundTasks.entries(),
|
|
330
|
+
backgroundTasks: backgroundTasks.entries(),
|
|
331
|
+
foregroundCount: foregroundTasks.size,
|
|
332
|
+
backgroundCount: backgroundTasks.size,
|
|
333
|
+
width,
|
|
334
|
+
theme: widgetTheme,
|
|
335
|
+
});
|
|
273
336
|
}
|
|
274
337
|
catch (err) {
|
|
275
338
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -281,7 +344,7 @@ export default function (pi) {
|
|
|
281
344
|
return [];
|
|
282
345
|
const [, task] = active[0];
|
|
283
346
|
return [
|
|
284
|
-
truncateToWidth(`${task.agentType} \u2022 ${formatMs(Date.now() - task.startedAt)} (render error: ${msg})`, Math.min(width,
|
|
347
|
+
truncateToWidth(`${task.agentType} \u2022 ${formatMs(Date.now() - task.startedAt)} (render error: ${msg})`, Math.min(width, 120)),
|
|
285
348
|
];
|
|
286
349
|
}
|
|
287
350
|
}
|
|
@@ -291,7 +354,7 @@ export default function (pi) {
|
|
|
291
354
|
widgetCtx = targetCtx;
|
|
292
355
|
targetCtx.ui.setWidget("task", (tui, theme) => {
|
|
293
356
|
widgetTheme = theme ?? null;
|
|
294
|
-
widgetTimer = setInterval(() => tui.requestRender(),
|
|
357
|
+
widgetTimer = setInterval(() => tui.requestRender(), TASK_WIDGET_RENDER_MS);
|
|
295
358
|
// Don't keep the process alive just for the widget refresh.
|
|
296
359
|
widgetTimer.unref?.();
|
|
297
360
|
return {
|
|
@@ -313,62 +376,6 @@ export default function (pi) {
|
|
|
313
376
|
widgetCtx = null;
|
|
314
377
|
}
|
|
315
378
|
}
|
|
316
|
-
function renderWidgetInner(width) {
|
|
317
|
-
const active = [
|
|
318
|
-
...Array.from(foregroundTasks.entries()),
|
|
319
|
-
...Array.from(backgroundTasks.entries()),
|
|
320
|
-
];
|
|
321
|
-
if (active.length === 0)
|
|
322
|
-
return [];
|
|
323
|
-
const now = Date.now();
|
|
324
|
-
const maxWidth = Math.min(width, WIDGET_MAX_WIDTH);
|
|
325
|
-
const tick = Math.floor(now / WIDGET_RENDER_MS);
|
|
326
|
-
const spinner = WIDGET_SPINNER_FRAMES[tick % WIDGET_SPINNER_FRAMES.length];
|
|
327
|
-
const caret = WIDGET_CARET_FRAMES[tick % WIDGET_CARET_FRAMES.length];
|
|
328
|
-
const lines = [];
|
|
329
|
-
for (const [, task] of active) {
|
|
330
|
-
const agentName = task.agentType.charAt(0).toUpperCase() + task.agentType.slice(1);
|
|
331
|
-
const elapsed = formatMs(now - task.startedAt);
|
|
332
|
-
const total = task.toolUses > 0 ? ` ${task.turns || task.toolUses} toolcalls` : "";
|
|
333
|
-
const description = task.description ? ` — ${task.description}` : "";
|
|
334
|
-
// Header: ▼ <Agent> — <description> • 1m 0s 11 toolcalls
|
|
335
|
-
const header = c("accent", caret) +
|
|
336
|
-
" " +
|
|
337
|
-
c("toolTitle", agentName) +
|
|
338
|
-
c("dim", `${description} \u2022 ${elapsed}${total}`);
|
|
339
|
-
lines.push(truncateToWidth(header, maxWidth));
|
|
340
|
-
const recent = task.recentCalls ?? [];
|
|
341
|
-
if (recent.length > 0) {
|
|
342
|
-
const slice = recent.slice(-WIDGET_MAX_TOOL_LINES);
|
|
343
|
-
slice.forEach((tc, idx) => {
|
|
344
|
-
const isLast = idx === slice.length - 1;
|
|
345
|
-
const connector = isLast ? TREE_LAST : TREE_MIDDLE;
|
|
346
|
-
const isInProgress = tc.status === "in_progress";
|
|
347
|
-
const markChar = isInProgress
|
|
348
|
-
? spinner
|
|
349
|
-
: tc.status === "error"
|
|
350
|
-
? "\u2717"
|
|
351
|
-
: "\u2713";
|
|
352
|
-
const markColor = isInProgress
|
|
353
|
-
? "warning"
|
|
354
|
-
: tc.status === "error"
|
|
355
|
-
? "error"
|
|
356
|
-
: "success";
|
|
357
|
-
const detailStr = tc.detail ? ` ${tc.detail}` : "";
|
|
358
|
-
const line = " " +
|
|
359
|
-
c("dim", connector) +
|
|
360
|
-
" " +
|
|
361
|
-
c(markColor, markChar) +
|
|
362
|
-
" " +
|
|
363
|
-
c("text", tc.name) +
|
|
364
|
-
c("dim", detailStr);
|
|
365
|
-
lines.push(truncateToWidth(line, maxWidth));
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
lines.push("");
|
|
369
|
-
}
|
|
370
|
-
return lines;
|
|
371
|
-
}
|
|
372
379
|
// ── Polling loop (background task completion, pane death, timeout) ──────
|
|
373
380
|
const checkInterval = setInterval(async () => {
|
|
374
381
|
if (backgroundTasks.size === 0) {
|
|
@@ -381,24 +388,27 @@ export default function (pi) {
|
|
|
381
388
|
const task = backgroundTasks.get(id);
|
|
382
389
|
if (!task)
|
|
383
390
|
continue;
|
|
384
|
-
backgroundTasks.delete(id); // Remove atomically
|
|
385
391
|
// ── Check timeout ────────────────────────────────────────────
|
|
386
392
|
if (now - task.startedAt > TASK_TIMEOUT_MS) {
|
|
387
393
|
killAgentPane(task.paneId, task.originalPane);
|
|
394
|
+
backgroundTasks.delete(id);
|
|
395
|
+
clearTaskWidgetIfIdle();
|
|
388
396
|
completeTask(pi, id, task, "Task timed out after 30 minutes", "timeout", piDir);
|
|
389
397
|
continue;
|
|
390
398
|
}
|
|
391
399
|
const snapshot = await checkTaskCompletion({
|
|
392
400
|
resultPath: join(task.dir, "RESULT.md"),
|
|
393
|
-
sessionDir: task.dir,
|
|
401
|
+
sessionDir: join(task.dir, "sessions"),
|
|
394
402
|
sessionName: task.sessionName,
|
|
395
403
|
paneId: task.paneId,
|
|
404
|
+
sinceMs: task.startedAt,
|
|
396
405
|
});
|
|
397
406
|
if (snapshot.status === "running") {
|
|
398
|
-
backgroundTasks.set(id, task);
|
|
399
407
|
continue;
|
|
400
408
|
}
|
|
401
409
|
const phase = snapshot.status === "completed" ? "done" : "failed";
|
|
410
|
+
backgroundTasks.delete(id);
|
|
411
|
+
clearTaskWidgetIfIdle();
|
|
402
412
|
completeTask(pi, id, task, snapshot.content, phase, piDir);
|
|
403
413
|
}
|
|
404
414
|
}, BACKGROUND_CHECK_MS);
|
|
@@ -424,34 +434,36 @@ export default function (pi) {
|
|
|
424
434
|
const confidence = d.confidence || "";
|
|
425
435
|
const durationMs = d.duration_ms || 0;
|
|
426
436
|
const toolUses = d.tool_uses || 0;
|
|
427
|
-
|
|
428
|
-
let line = theme.fg("accent", agentType);
|
|
437
|
+
let line = " " + theme.fg("accent", agentType);
|
|
429
438
|
if (desc)
|
|
430
439
|
line += theme.fg("dim", ` - ${desc}`);
|
|
431
|
-
const useStr = toolUses > 0 ? `${
|
|
440
|
+
const useStr = toolUses > 0 ? `${toolUses} toolcalls` : "";
|
|
432
441
|
const durStr = durationMs >= 1000 ? formatMs(durationMs) : "";
|
|
433
442
|
const statsParts = [useStr, durStr].filter(Boolean);
|
|
434
|
-
|
|
435
|
-
line += "\n" + theme.fg("dim", statsParts.join(" • "));
|
|
436
|
-
}
|
|
443
|
+
const statsText = statsParts.join(" • ");
|
|
437
444
|
const confStr = confidence ? confidence.toUpperCase() : "";
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
line += "\n"
|
|
445
|
+
const confColor = confidence === "high"
|
|
446
|
+
? "success"
|
|
447
|
+
: confidence === "low"
|
|
448
|
+
? "error"
|
|
449
|
+
: "accent";
|
|
450
|
+
if (statsText || confStr) {
|
|
451
|
+
line += "\n ";
|
|
452
|
+
if (confStr)
|
|
453
|
+
line += theme.fg(confColor, `[${confStr}]`);
|
|
454
|
+
if (statsText)
|
|
455
|
+
line += (confStr ? " " : "") + theme.fg("dim", statsText);
|
|
445
456
|
}
|
|
446
457
|
if (expanded) {
|
|
447
458
|
if (summary)
|
|
448
|
-
line += "\n" + theme.fg("muted", summary);
|
|
459
|
+
line += "\n " + theme.fg("muted", summary);
|
|
449
460
|
if (findings)
|
|
450
|
-
line += "\n" + theme.fg("dim", findings);
|
|
461
|
+
line += "\n " + theme.fg("dim", findings);
|
|
451
462
|
}
|
|
452
463
|
if (!line.trim())
|
|
453
464
|
return undefined;
|
|
454
|
-
|
|
465
|
+
const subtleBg = (text) => `\x1b[48;2;30;28;44m${text}\x1b[0m`;
|
|
466
|
+
return new Text(line, 0, 1, subtleBg);
|
|
455
467
|
});
|
|
456
468
|
// ── Tool Registration ──────────────────────────────────────────────────
|
|
457
469
|
pi.registerTool({
|
|
@@ -515,14 +527,11 @@ export default function (pi) {
|
|
|
515
527
|
}
|
|
516
528
|
// ── Resolve task identity: new, task resume, or conversation resume ──
|
|
517
529
|
const conversationId = normalizeConversationId(params.conversation_id);
|
|
518
|
-
const
|
|
519
|
-
?
|
|
530
|
+
const taskSessionsRegistry = conversationId
|
|
531
|
+
? readTaskSessionsRegistry(piDir)
|
|
520
532
|
: {};
|
|
521
|
-
const
|
|
522
|
-
?
|
|
523
|
-
: undefined;
|
|
524
|
-
const registeredTaskId = registeredArtifact
|
|
525
|
-
? taskIdFromArtifactName(registeredArtifact)
|
|
533
|
+
const registeredTaskId = conversationId
|
|
534
|
+
? taskSessionsRegistry[conversationId]?.task_id
|
|
526
535
|
: undefined;
|
|
527
536
|
if (params.task_id &&
|
|
528
537
|
registeredTaskId &&
|
|
@@ -531,7 +540,7 @@ export default function (pi) {
|
|
|
531
540
|
content: [
|
|
532
541
|
{
|
|
533
542
|
type: "text",
|
|
534
|
-
text: `conversation_id "${conversationId}" maps to ${
|
|
543
|
+
text: `conversation_id "${conversationId}" maps to ${registeredTaskId}, not ${params.task_id}. Omit task_id or use the mapped task id.`,
|
|
535
544
|
},
|
|
536
545
|
],
|
|
537
546
|
details: {
|
|
@@ -543,37 +552,39 @@ export default function (pi) {
|
|
|
543
552
|
}
|
|
544
553
|
let id;
|
|
545
554
|
let sessionName;
|
|
546
|
-
let artifactDir;
|
|
547
555
|
let resultPath;
|
|
548
556
|
let resume = false;
|
|
557
|
+
let resumeSessionRef;
|
|
558
|
+
const artifactsDir = join(piDir, "artifacts");
|
|
549
559
|
if (registeredTaskId) {
|
|
550
560
|
id = registeredTaskId;
|
|
551
|
-
sessionName =
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
if (!existsSync(artifactDir)) {
|
|
561
|
+
sessionName = conversationId ?? `task-${id}`;
|
|
562
|
+
resultPath = join(artifactsDir, `RESULT-${id}.md`);
|
|
563
|
+
if (!existsSync(resultPath)) {
|
|
555
564
|
return {
|
|
556
565
|
content: [
|
|
557
566
|
{
|
|
558
567
|
type: "text",
|
|
559
|
-
text: `conversation_id "${conversationId}"
|
|
568
|
+
text: `conversation_id "${conversationId}" has no prior result file at ${resultPath}. Cannot resume.`,
|
|
560
569
|
},
|
|
561
570
|
],
|
|
562
571
|
details: {
|
|
563
572
|
phase: "failed",
|
|
564
|
-
error: "Conversation
|
|
573
|
+
error: "Conversation result missing",
|
|
565
574
|
conversation_id: conversationId,
|
|
566
575
|
},
|
|
567
576
|
isError: true,
|
|
568
577
|
};
|
|
569
578
|
}
|
|
570
|
-
const
|
|
571
|
-
|
|
579
|
+
const block = readTaskBlock(piDir, id);
|
|
580
|
+
const previousMetadata = parseMetadataFromBody(block?.body);
|
|
581
|
+
const metadataAgent = previousMetadata?.agent_type;
|
|
582
|
+
if (metadataAgent && metadataAgent !== agent.name) {
|
|
572
583
|
return {
|
|
573
584
|
content: [
|
|
574
585
|
{
|
|
575
586
|
type: "text",
|
|
576
|
-
text: `conversation_id "${conversationId}" belongs to agent "${
|
|
587
|
+
text: `conversation_id "${conversationId}" belongs to agent "${metadataAgent}", not "${agent.name}". Use the original agent_type or start a different conversation_id.`,
|
|
577
588
|
},
|
|
578
589
|
],
|
|
579
590
|
details: {
|
|
@@ -590,7 +601,7 @@ export default function (pi) {
|
|
|
590
601
|
entry?.paneId &&
|
|
591
602
|
paneExists(entry.paneId)) {
|
|
592
603
|
const bgtask = {
|
|
593
|
-
dir:
|
|
604
|
+
dir: artifactsDir,
|
|
594
605
|
agentType: entry.agentType,
|
|
595
606
|
sessionName,
|
|
596
607
|
paneId: entry.paneId,
|
|
@@ -607,7 +618,7 @@ export default function (pi) {
|
|
|
607
618
|
content: [
|
|
608
619
|
{
|
|
609
620
|
type: "text",
|
|
610
|
-
text: `Resumed conversation "${conversationId}" via ${
|
|
621
|
+
text: `Resumed conversation "${conversationId}" via ${sessionName}. The subagent is running in background and will notify on completion.`,
|
|
611
622
|
},
|
|
612
623
|
],
|
|
613
624
|
details: {
|
|
@@ -622,15 +633,31 @@ export default function (pi) {
|
|
|
622
633
|
}
|
|
623
634
|
}
|
|
624
635
|
else if (params.task_id) {
|
|
625
|
-
// Look up
|
|
636
|
+
// Look up active tasks first, then durable completed-session history.
|
|
626
637
|
const entries = readRegistry(piDir);
|
|
627
|
-
|
|
638
|
+
let entry = entries.find((e) => e.id === params.task_id || e.sessionName === params.task_id) ??
|
|
639
|
+
findTaskSessionHistory(piDir, params.task_id) ??
|
|
640
|
+
findJsonlSessionByName(piDir, params.task_id, agent.name);
|
|
641
|
+
// Older history entries were written before we stored the
|
|
642
|
+
// actual JSONL path needed by `pi --session`. Repair them by
|
|
643
|
+
// resolving the display session name to a session file.
|
|
644
|
+
if (entry && !entry.sessionRef) {
|
|
645
|
+
const discovered = findJsonlSessionByName(piDir, entry.sessionName, entry.agentType);
|
|
646
|
+
if (discovered?.sessionRef) {
|
|
647
|
+
entry = { ...entry, sessionRef: discovered.sessionRef };
|
|
648
|
+
upsertTaskSessionHistory(piDir, {
|
|
649
|
+
...entry,
|
|
650
|
+
status: "done",
|
|
651
|
+
background: false,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
628
655
|
if (!entry) {
|
|
629
656
|
return {
|
|
630
657
|
content: [
|
|
631
658
|
{
|
|
632
659
|
type: "text",
|
|
633
|
-
text: `Unknown task_id: "${params.task_id}". No task with that ID
|
|
660
|
+
text: `Unknown task_id: "${params.task_id}". No active or completed task session with that ID/session name was found.`,
|
|
634
661
|
},
|
|
635
662
|
],
|
|
636
663
|
details: {
|
|
@@ -655,23 +682,24 @@ export default function (pi) {
|
|
|
655
682
|
isError: true,
|
|
656
683
|
};
|
|
657
684
|
}
|
|
658
|
-
// Resume: reuse existing
|
|
685
|
+
// Resume: reuse the existing session name; runtime files are
|
|
686
|
+
// flat in artifactsDir, no per-task subdir.
|
|
659
687
|
id = entry.id;
|
|
660
688
|
sessionName = entry.sessionName;
|
|
661
|
-
|
|
662
|
-
resultPath = join(artifactDir, "RESULT.md");
|
|
689
|
+
resultPath = join(artifactsDir, `RESULT-${id}.md`);
|
|
663
690
|
resume = true;
|
|
691
|
+
resumeSessionRef = entry.sessionRef;
|
|
664
692
|
// If background and pane still alive, reattach to tracker
|
|
665
693
|
if (params.background !== false &&
|
|
666
694
|
entry.paneId &&
|
|
667
695
|
paneExists(entry.paneId)) {
|
|
668
696
|
const bgtask = {
|
|
669
|
-
dir:
|
|
670
|
-
agentType:
|
|
697
|
+
dir: artifactsDir,
|
|
698
|
+
agentType: entry.agentType,
|
|
671
699
|
sessionName,
|
|
672
700
|
paneId: entry.paneId,
|
|
673
701
|
originalPane: null,
|
|
674
|
-
description: params.description || entry.
|
|
702
|
+
description: params.description || entry.description,
|
|
675
703
|
startedAt: entry.startedAt,
|
|
676
704
|
toolUses: 0,
|
|
677
705
|
turns: 0,
|
|
@@ -688,21 +716,34 @@ export default function (pi) {
|
|
|
688
716
|
],
|
|
689
717
|
details: {
|
|
690
718
|
task_id: id,
|
|
691
|
-
agent_type:
|
|
692
|
-
description: params.description,
|
|
719
|
+
agent_type: entry.agentType,
|
|
720
|
+
description: params.description || entry.description,
|
|
693
721
|
conversation_id: entry.conversationId ?? conversationId,
|
|
694
722
|
tmux_session: sessionName,
|
|
695
723
|
background: true,
|
|
696
724
|
},
|
|
697
725
|
};
|
|
698
726
|
}
|
|
727
|
+
if (!resumeSessionRef) {
|
|
728
|
+
return {
|
|
729
|
+
content: [
|
|
730
|
+
{
|
|
731
|
+
type: "text",
|
|
732
|
+
text: `Task "${params.task_id}" was found, but its session JSONL file could not be resolved. Cannot resume without a --session file path.`,
|
|
733
|
+
},
|
|
734
|
+
],
|
|
735
|
+
details: {
|
|
736
|
+
phase: "failed",
|
|
737
|
+
error: "Task session file missing",
|
|
738
|
+
},
|
|
739
|
+
isError: true,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
699
742
|
}
|
|
700
743
|
else {
|
|
701
744
|
id = `${Date.now().toString(36)}-${randomUUID().slice(0, 4)}`;
|
|
702
|
-
sessionName =
|
|
703
|
-
|
|
704
|
-
await mkdir(artifactDir, { recursive: true });
|
|
705
|
-
resultPath = join(artifactDir, "RESULT.md");
|
|
745
|
+
sessionName = conversationId ?? `task-${id}`;
|
|
746
|
+
resultPath = join(artifactsDir, `RESULT-${id}.md`);
|
|
706
747
|
}
|
|
707
748
|
if (conversationId && !hasTmux()) {
|
|
708
749
|
return {
|
|
@@ -721,25 +762,19 @@ export default function (pi) {
|
|
|
721
762
|
};
|
|
722
763
|
}
|
|
723
764
|
if (conversationId) {
|
|
724
|
-
await mkdir(
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
agentType: agent.name,
|
|
732
|
-
sessionDir: join(artifactDir, "sessions"),
|
|
733
|
-
sessionName,
|
|
734
|
-
prompt: params.prompt,
|
|
735
|
-
});
|
|
765
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
766
|
+
const taskSessionsRegistry = readTaskSessionsRegistry(piDir);
|
|
767
|
+
taskSessionsRegistry[conversationId] = {
|
|
768
|
+
task_id: id,
|
|
769
|
+
session_file: `${artifactsDir}/${id}`,
|
|
770
|
+
};
|
|
771
|
+
writeTaskSessionsRegistry(piDir, taskSessionsRegistry);
|
|
736
772
|
}
|
|
737
773
|
const descText = params.description || "";
|
|
738
774
|
const isBackground = params.background ?? TASK_BACKGROUND_DEFAULT;
|
|
739
775
|
// default true
|
|
740
|
-
// ──
|
|
741
|
-
const
|
|
742
|
-
const contextContent = [
|
|
776
|
+
// ── Build the prompt (instructions are inlined; no CONTEXT.md file) ─
|
|
777
|
+
const promptContent = [
|
|
743
778
|
`# Task: ${descText}`,
|
|
744
779
|
"",
|
|
745
780
|
`## Agent`,
|
|
@@ -752,26 +787,18 @@ export default function (pi) {
|
|
|
752
787
|
ctx.cwd,
|
|
753
788
|
"",
|
|
754
789
|
`## Output`,
|
|
755
|
-
|
|
790
|
+
"Your final assistant message is the result. End with a clear summary of what you did and any findings. No file write is required.",
|
|
756
791
|
"",
|
|
757
|
-
"Use this format:",
|
|
792
|
+
"Use this format for the summary:",
|
|
758
793
|
"",
|
|
759
794
|
"```",
|
|
760
795
|
TASK_RESULT_XML_INSTRUCTIONS,
|
|
761
796
|
"```",
|
|
762
797
|
].join("\n");
|
|
763
|
-
|
|
764
|
-
const promptContent = [
|
|
765
|
-
`Read ${contextPath} for your task.`,
|
|
766
|
-
`Write your findings/output to ${resultPath}`,
|
|
767
|
-
"",
|
|
768
|
-
"Format:",
|
|
769
|
-
TASK_RESULT_XML_INSTRUCTIONS,
|
|
770
|
-
].join("\n");
|
|
771
|
-
const sessionDir = join(artifactDir, "sessions");
|
|
798
|
+
const sessionDir = join(artifactsDir, "sessions");
|
|
772
799
|
await mkdir(sessionDir, { recursive: true });
|
|
773
800
|
// ─── Build and run the sub-agent pi process ──────────────────────────
|
|
774
|
-
const piArgs = buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames);
|
|
801
|
+
const piArgs = buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames, resumeSessionRef);
|
|
775
802
|
const envPrefix = `PI_TASK_TOOL_DISABLED=1`;
|
|
776
803
|
const toolSelection = buildAgentToolSelection({
|
|
777
804
|
tools: agent.tools,
|
|
@@ -792,7 +819,7 @@ export default function (pi) {
|
|
|
792
819
|
const foregroundTask = isBackground
|
|
793
820
|
? undefined
|
|
794
821
|
: {
|
|
795
|
-
dir:
|
|
822
|
+
dir: artifactsDir,
|
|
796
823
|
agentType: agent.name,
|
|
797
824
|
sessionName,
|
|
798
825
|
originalPane: null,
|
|
@@ -811,7 +838,7 @@ export default function (pi) {
|
|
|
811
838
|
if (!hasTmux()) {
|
|
812
839
|
if (isBackground) {
|
|
813
840
|
const bgtask = {
|
|
814
|
-
dir:
|
|
841
|
+
dir: artifactsDir,
|
|
815
842
|
agentType: agent.name,
|
|
816
843
|
sessionName,
|
|
817
844
|
originalPane: null,
|
|
@@ -830,18 +857,22 @@ export default function (pi) {
|
|
|
830
857
|
sessionName,
|
|
831
858
|
startedAt: bgtask.startedAt,
|
|
832
859
|
piDir,
|
|
833
|
-
dir:
|
|
860
|
+
dir: artifactsDir,
|
|
834
861
|
conversationId,
|
|
835
862
|
};
|
|
836
863
|
const entries = readRegistry(piDir);
|
|
837
864
|
entries.push(entry);
|
|
838
865
|
writeRegistry(piDir, entries);
|
|
866
|
+
upsertTaskSessionHistory(piDir, {
|
|
867
|
+
...entry,
|
|
868
|
+
status: "running",
|
|
869
|
+
background: true,
|
|
870
|
+
});
|
|
839
871
|
pi.appendEntry("task-registry", entry);
|
|
840
872
|
ensureTaskWidget(ctx);
|
|
841
873
|
void runSdkFallback()
|
|
842
874
|
.then(async ({ output }) => {
|
|
843
875
|
const finalOutput = output || "SDK subagent completed without assistant text.";
|
|
844
|
-
await writeFile(resultPath, finalOutput, "utf-8");
|
|
845
876
|
backgroundTasks.delete(id);
|
|
846
877
|
clearTaskWidgetIfIdle();
|
|
847
878
|
completeTask(pi, id, bgtask, finalOutput, "done", piDir);
|
|
@@ -871,7 +902,17 @@ export default function (pi) {
|
|
|
871
902
|
try {
|
|
872
903
|
const { output, sessionPath } = await runSdkFallback();
|
|
873
904
|
const finalOutput = output || "SDK subagent completed without assistant text.";
|
|
874
|
-
|
|
905
|
+
if (conversationId) {
|
|
906
|
+
writeConversationArtifacts({
|
|
907
|
+
piDir,
|
|
908
|
+
taskId: id,
|
|
909
|
+
conversationId,
|
|
910
|
+
agentType: agent.name,
|
|
911
|
+
sessionFile: sessionPath ?? "unknown",
|
|
912
|
+
prompt: params.prompt,
|
|
913
|
+
result: finalOutput,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
875
916
|
return {
|
|
876
917
|
content: [{ type: "text", text: finalOutput }],
|
|
877
918
|
details: {
|
|
@@ -930,14 +971,29 @@ export default function (pi) {
|
|
|
930
971
|
}
|
|
931
972
|
// ── FOREGROUND MODE: block until result, return directly ────────────
|
|
932
973
|
if (!isBackground) {
|
|
933
|
-
const startedAt = Date.now();
|
|
974
|
+
const startedAt = foregroundTask?.startedAt ?? Date.now();
|
|
975
|
+
upsertTaskSessionHistory(piDir, {
|
|
976
|
+
id,
|
|
977
|
+
agentType: agent.name,
|
|
978
|
+
description: descText,
|
|
979
|
+
sessionName,
|
|
980
|
+
startedAt,
|
|
981
|
+
paneId,
|
|
982
|
+
piDir,
|
|
983
|
+
dir: artifactsDir,
|
|
984
|
+
conversationId,
|
|
985
|
+
status: "running",
|
|
986
|
+
background: false,
|
|
987
|
+
});
|
|
934
988
|
const completion = await waitForSessionTaskCompletion({
|
|
935
989
|
resultPath,
|
|
936
990
|
sessionDir,
|
|
937
991
|
sessionName,
|
|
938
992
|
paneId,
|
|
939
993
|
signal,
|
|
940
|
-
timeoutMs:
|
|
994
|
+
timeoutMs: TASK_TIMEOUT_MS,
|
|
995
|
+
pollMs: 1000,
|
|
996
|
+
sinceMs: startedAt,
|
|
941
997
|
});
|
|
942
998
|
const content = completion.content;
|
|
943
999
|
const phase = completion.status === "completed"
|
|
@@ -945,19 +1001,46 @@ export default function (pi) {
|
|
|
945
1001
|
: completion.status === "cancelled"
|
|
946
1002
|
? "cancelled"
|
|
947
1003
|
: "failed";
|
|
1004
|
+
const completedSessionRef = findJsonlSessionByName(piDir, sessionName, agent.name)?.sessionRef;
|
|
1005
|
+
upsertTaskSessionHistory(piDir, {
|
|
1006
|
+
id,
|
|
1007
|
+
agentType: agent.name,
|
|
1008
|
+
description: descText,
|
|
1009
|
+
sessionName,
|
|
1010
|
+
startedAt,
|
|
1011
|
+
paneId,
|
|
1012
|
+
piDir,
|
|
1013
|
+
dir: artifactsDir,
|
|
1014
|
+
conversationId,
|
|
1015
|
+
sessionRef: completedSessionRef,
|
|
1016
|
+
status: phase,
|
|
1017
|
+
completedAt: Date.now(),
|
|
1018
|
+
background: false,
|
|
1019
|
+
});
|
|
948
1020
|
killAgentPane(paneId, originalPane);
|
|
949
1021
|
foregroundTasks.delete(id);
|
|
950
1022
|
clearTaskWidgetIfIdle();
|
|
1023
|
+
if (conversationId) {
|
|
1024
|
+
writeConversationArtifacts({
|
|
1025
|
+
piDir,
|
|
1026
|
+
taskId: id,
|
|
1027
|
+
conversationId,
|
|
1028
|
+
agentType: agent.name,
|
|
1029
|
+
sessionFile: `${sessionDir}/${sessionName}`,
|
|
1030
|
+
prompt: params.prompt,
|
|
1031
|
+
result: content,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
951
1034
|
const parsed = parseResultXml(content);
|
|
952
1035
|
const durationMs = Date.now() - startedAt;
|
|
953
|
-
const { toolUses, turns } = countToolUses(sessionDir);
|
|
1036
|
+
const { toolUses, turns } = countToolUses(sessionDir, sessionName);
|
|
954
1037
|
return {
|
|
955
1038
|
content: [
|
|
956
1039
|
{
|
|
957
1040
|
type: "text",
|
|
958
1041
|
text: [
|
|
959
1042
|
`${parsed.status || "done"}: ${parsed.summary || content.slice(0, 300)}`,
|
|
960
|
-
toolUses > 0 ? `\n${
|
|
1043
|
+
toolUses > 0 ? `\n${toolUses} toolcalls` : "",
|
|
961
1044
|
durationMs >= 1000 ? `\n${formatMs(durationMs)}` : "",
|
|
962
1045
|
]
|
|
963
1046
|
.filter(Boolean)
|
|
@@ -984,7 +1067,7 @@ export default function (pi) {
|
|
|
984
1067
|
}
|
|
985
1068
|
// ── BACKGROUND MODE (default): add to tracker, return immediately ─────
|
|
986
1069
|
const bgtask = {
|
|
987
|
-
dir:
|
|
1070
|
+
dir: artifactsDir,
|
|
988
1071
|
agentType: agent.name,
|
|
989
1072
|
sessionName,
|
|
990
1073
|
paneId,
|
|
@@ -1003,16 +1086,21 @@ export default function (pi) {
|
|
|
1003
1086
|
agentType: agent.name,
|
|
1004
1087
|
description: descText,
|
|
1005
1088
|
sessionName,
|
|
1006
|
-
startedAt:
|
|
1089
|
+
startedAt: bgtask.startedAt,
|
|
1007
1090
|
paneId,
|
|
1008
1091
|
piDir,
|
|
1009
|
-
dir:
|
|
1092
|
+
dir: artifactsDir,
|
|
1010
1093
|
conversationId,
|
|
1011
1094
|
};
|
|
1012
1095
|
// Write to JSON registry for on-load restore
|
|
1013
1096
|
const entries = readRegistry(piDir);
|
|
1014
1097
|
entries.push(entry);
|
|
1015
1098
|
writeRegistry(piDir, entries);
|
|
1099
|
+
upsertTaskSessionHistory(piDir, {
|
|
1100
|
+
...entry,
|
|
1101
|
+
status: "running",
|
|
1102
|
+
background: true,
|
|
1103
|
+
});
|
|
1016
1104
|
// Also persist to session store via appendEntry (audit trail)
|
|
1017
1105
|
pi.appendEntry("task-registry", entry);
|
|
1018
1106
|
// ── Abort signal handling ──────────────────────────────────────────
|
|
@@ -1043,7 +1131,7 @@ export default function (pi) {
|
|
|
1043
1131
|
taskId: id,
|
|
1044
1132
|
agentType: agent.name,
|
|
1045
1133
|
tmuxSession: sessionName,
|
|
1046
|
-
artifactDir,
|
|
1134
|
+
artifactDir: artifactsDir,
|
|
1047
1135
|
}),
|
|
1048
1136
|
},
|
|
1049
1137
|
],
|
|
@@ -1070,7 +1158,17 @@ export default function (pi) {
|
|
|
1070
1158
|
if (!d)
|
|
1071
1159
|
return new Text("", 0, 0);
|
|
1072
1160
|
if (d.background) {
|
|
1073
|
-
|
|
1161
|
+
const toolUses = d.tool_uses || 0;
|
|
1162
|
+
const durationMs = d.duration_ms || 0;
|
|
1163
|
+
const confidence = d.confidence || "";
|
|
1164
|
+
const useStr = toolUses > 0 ? `${toolUses} toolcalls` : "";
|
|
1165
|
+
const durStr = durationMs >= 1000 ? formatMs(durationMs) : "";
|
|
1166
|
+
const statsParts = [useStr, durStr].filter(Boolean);
|
|
1167
|
+
const statsText = statsParts.join(" \u2022 ");
|
|
1168
|
+
const confStr = confidence ? `[${confidence.toUpperCase()}]` : "";
|
|
1169
|
+
const statsLine = [confStr, statsText].filter(Boolean).join(" ");
|
|
1170
|
+
const subtleBg = (text) => `\x1b[48;2;30;28;44m${text}\x1b[0m`;
|
|
1171
|
+
return new Text(statsLine ? " " + theme.fg("dim", statsLine.trim()) : "", 0, 0, subtleBg);
|
|
1074
1172
|
}
|
|
1075
1173
|
if (d.phase === "timeout" ||
|
|
1076
1174
|
d.phase === "aborted" ||
|
|
@@ -1085,8 +1183,7 @@ export default function (pi) {
|
|
|
1085
1183
|
d.status === "failed";
|
|
1086
1184
|
const durationMs = d.duration_ms || 0;
|
|
1087
1185
|
const toolUses = d.tool_uses || 0;
|
|
1088
|
-
const
|
|
1089
|
-
const useStr = toolUses > 0 ? `${turns || toolUses} toolcalls` : "";
|
|
1186
|
+
const useStr = toolUses > 0 ? `${toolUses} toolcalls` : "";
|
|
1090
1187
|
const durStr = durationMs >= 1000 ? formatMs(durationMs) : "";
|
|
1091
1188
|
const statsParts = [useStr, durStr].filter(Boolean);
|
|
1092
1189
|
const statsStr = statsParts.length
|