@heyhuynhgiabuu/pi-task 0.1.6 → 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 +67 -3
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +4 -0
- package/dist/conversation.d.ts +8 -0
- package/dist/conversation.js +96 -1
- package/dist/helpers.d.ts +2 -2
- package/dist/helpers.js +4 -9
- package/dist/index.d.ts +6 -23
- package/dist/index.js +91 -589
- package/dist/lifecycle/completion.d.ts +3 -0
- package/dist/lifecycle/completion.js +50 -0
- package/dist/lifecycle/index.d.ts +5 -0
- package/dist/lifecycle/index.js +5 -0
- package/dist/lifecycle/polling.d.ts +16 -0
- package/dist/lifecycle/polling.js +61 -0
- package/dist/lifecycle/restore.d.ts +2 -0
- package/dist/lifecycle/restore.js +34 -0
- package/dist/lifecycle/toolStats.d.ts +2 -0
- package/dist/lifecycle/toolStats.js +17 -0
- package/dist/lifecycle/widget.d.ts +8 -0
- package/dist/lifecycle/widget.js +75 -0
- package/dist/session-text.d.ts +9 -0
- package/dist/session-text.js +50 -0
- package/dist/subagent/runSdk.js +50 -26
- package/dist/subagent/tmux.d.ts +12 -9
- package/dist/subagent/tmux.js +107 -44
- package/dist/subagent/waitCompletion.d.ts +4 -5
- package/dist/subagent/waitCompletion.js +27 -43
- package/dist/tool/index.d.ts +5 -0
- package/dist/tool/index.js +5 -0
- package/dist/tool/prompt.d.ts +8 -0
- package/dist/tool/prompt.js +17 -0
- package/dist/tool/renderCall.d.ts +3 -0
- package/dist/tool/renderCall.js +12 -0
- package/dist/tool/renderResult.d.ts +8 -0
- package/dist/tool/renderResult.js +51 -0
- package/dist/tool/schema.d.ts +8 -0
- package/dist/tool/schema.js +24 -0
- package/dist/tool/taskComplete.d.ts +8 -0
- package/dist/tool/taskComplete.js +65 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,470 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Task Tool — Delegate complex work to specialist agents.
|
|
3
3
|
*
|
|
4
|
-
* Spawns pi CLI in a tmux split pane (
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Spawns pi CLI in a tmux split pane (foreground) or background.
|
|
5
|
+
* Completion is detected from the subagent's final assistant message
|
|
6
|
+
* in the persistent session JSONL (stopReason gating). The final message
|
|
7
|
+
* is the authoritative result; no RESULT.md is used.
|
|
7
8
|
*
|
|
8
9
|
* Three agent sources:
|
|
9
10
|
* - .pi/agents/*.md project-local agents
|
|
10
11
|
* - ~/.pi/agent/agents/*.md user-global agents (fallback)
|
|
11
12
|
*
|
|
12
13
|
* P0: Persistent task registry (appendEntry + JSON), --session resume,
|
|
13
|
-
* sendMessage completion notification.
|
|
14
|
-
* P1: Foreground mode (background:false
|
|
15
|
-
* detection, 30-minute timeout.
|
|
14
|
+
* sendMessage completion notification, Ctrl+O expand/collapse.
|
|
15
|
+
* P1: Foreground mode (background:false), pane death detection, timeout.
|
|
16
16
|
*/
|
|
17
|
-
import { execFileSync } from "node:child_process";
|
|
18
17
|
import { randomUUID } from "node:crypto";
|
|
19
|
-
import { existsSync
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
20
19
|
import { mkdir } from "node:fs/promises";
|
|
21
20
|
import { dirname, join } from "node:path";
|
|
22
21
|
import { fileURLToPath } from "node:url";
|
|
23
|
-
import { Type } from "@sinclair/typebox";
|
|
24
|
-
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
25
22
|
import { buildAgentToolSelection } from "./agent-tools.js";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
23
|
+
import { BACKGROUND_CHECK_MS, COUNT_POLL_MS, MAX_POLL_ERRORS, TASK_TIMEOUT_MS, } from "./constants.js";
|
|
24
|
+
import { findJsonlSessionByName, normalizeConversationId, parseMetadataFromBody, readTaskBlock, findTaskSessionHistory, readRegistry, readTaskSessionsRegistry, renderConversationSessions, upsertTaskSessionHistory, writeConversationArtifacts, writeRegistry, writeTaskSessionsRegistry, } from "./conversation.js";
|
|
25
|
+
import { TASK_BACKGROUND_DEFAULT, TASK_TOOL_DESCRIPTION, buildPiArgs, countToolUses, discoverAgents, formatAgentList, formatBackgroundReceipt, parseResultXml, shellQuote, } from "./helpers.js";
|
|
26
|
+
import { completeTask, createTaskWidgetController, restoreActiveBackgroundTasks, startBackgroundPolling, startToolStatsPolling, } from "./lifecycle/index.js";
|
|
28
27
|
import { runSdkSubagent } from "./subagent/runSdk.js";
|
|
29
28
|
import { checkTaskCompletion, waitForTaskCompletion as waitForSessionTaskCompletion, } from "./subagent/waitCompletion.js";
|
|
30
|
-
import {
|
|
29
|
+
import { hasTmux, killAgentPane, paneExists, setPaneRemainOnExit, splitWindowPane, wrapWithPaneExitWatcher, } from "./subagent/tmux.js";
|
|
30
|
+
import { buildTaskPrompt, createTaskCompleteRenderer, renderCall, renderResult, taskParametersSchema, } from "./tool/index.js";
|
|
31
31
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
32
32
|
const BUNDLED_AGENT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "agents");
|
|
33
|
-
const BACKGROUND_CHECK_MS = 10_000; // poll every 10 sec
|
|
34
|
-
const COUNT_POLL_MS = 3_000; // update toolcall counts every 3 sec
|
|
35
|
-
const TASK_TIMEOUT_MS = 30 * 60 * 1_000; // 30 minutes
|
|
36
33
|
// Conversation helpers live in ./conversation.js.
|
|
37
|
-
function readRegistry(piDir) {
|
|
38
|
-
const path = join(piDir, "task-registry.json");
|
|
39
|
-
try {
|
|
40
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function writeRegistry(piDir, entries) {
|
|
47
|
-
const path = join(piDir, "task-registry.json");
|
|
48
|
-
writeFileSync(path, JSON.stringify(entries, null, 2), "utf-8");
|
|
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
|
-
}
|
|
131
|
-
// ─── Tmux Helpers ────────────────────────────────────────────────────────────
|
|
132
|
-
function tmuxCmd(args) {
|
|
133
|
-
return execFileSync("tmux", args, {
|
|
134
|
-
encoding: "utf-8",
|
|
135
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
136
|
-
}).trim();
|
|
137
|
-
}
|
|
138
|
-
function hasTmux() {
|
|
139
|
-
try {
|
|
140
|
-
execFileSync("tmux", ["-V"], { stdio: "ignore" });
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
function paneExists(paneId) {
|
|
148
|
-
try {
|
|
149
|
-
return tmuxCmd(["list-panes", "-a", "-F", "#{pane_id}"])
|
|
150
|
-
.split("\n")
|
|
151
|
-
.includes(paneId);
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
function getCurrentPaneId() {
|
|
158
|
-
try {
|
|
159
|
-
return tmuxCmd(["display-message", "-p", "#{pane_id}"]);
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
function getCurrentPaneSize(targetPane) {
|
|
166
|
-
try {
|
|
167
|
-
const args = ["display-message", "-p", "#{pane_width} #{pane_height}"];
|
|
168
|
-
if (targetPane)
|
|
169
|
-
args.splice(1, 0, "-t", targetPane);
|
|
170
|
-
const raw = tmuxCmd(args);
|
|
171
|
-
const [widthRaw, heightRaw] = raw.trim().split(/\s+/, 2);
|
|
172
|
-
const width = Number(widthRaw);
|
|
173
|
-
const height = Number(heightRaw);
|
|
174
|
-
if (!Number.isFinite(width) || !Number.isFinite(height))
|
|
175
|
-
return null;
|
|
176
|
-
return { width, height };
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
function splitWindowPane(cwd, command) {
|
|
183
|
-
const originalPane = getCurrentPaneId();
|
|
184
|
-
const paneSize = getCurrentPaneSize(originalPane);
|
|
185
|
-
const direction = chooseTmuxSplitDirection(paneSize?.width ?? 0, paneSize?.height ?? 0);
|
|
186
|
-
const paneId = tmuxCmd(buildTmuxSplitWindowArgs(cwd, command, direction, originalPane));
|
|
187
|
-
return { paneId, originalPane };
|
|
188
|
-
}
|
|
189
|
-
function killAgentPane(paneId, originalPane) {
|
|
190
|
-
if (paneId) {
|
|
191
|
-
try {
|
|
192
|
-
if (paneExists(paneId))
|
|
193
|
-
tmuxCmd(["kill-pane", "-t", paneId]);
|
|
194
|
-
}
|
|
195
|
-
catch {
|
|
196
|
-
/* ignore */
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
if (originalPane) {
|
|
200
|
-
try {
|
|
201
|
-
tmuxCmd(["select-pane", "-t", originalPane]);
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
/* ignore */
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
// ─── Process a completed task (sendMessage + registry cleanup) ──────────────
|
|
209
|
-
function completeTask(pi, id, task, content, phase, piDir) {
|
|
210
|
-
// Kill the tmux pane if still alive
|
|
211
|
-
killAgentPane(task.paneId, task.originalPane);
|
|
212
|
-
const parsed = parseResultXml(content);
|
|
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
|
-
});
|
|
230
|
-
// Send completion notification
|
|
231
|
-
pi.sendMessage({
|
|
232
|
-
customType: "task-complete",
|
|
233
|
-
content: `Background task ${id} (${task.agentType}) ${phase}.\n\nResult:\n${content}`,
|
|
234
|
-
display: true,
|
|
235
|
-
details: {
|
|
236
|
-
task_id: id,
|
|
237
|
-
agent_type: task.agentType,
|
|
238
|
-
description: task.description,
|
|
239
|
-
phase,
|
|
240
|
-
status: phase,
|
|
241
|
-
result: content,
|
|
242
|
-
summary: parsed.summary,
|
|
243
|
-
findings: parsed.findings,
|
|
244
|
-
confidence: parsed.confidence,
|
|
245
|
-
duration_ms: durationMs,
|
|
246
|
-
tool_uses: task.toolUses,
|
|
247
|
-
turn_count: task.turns,
|
|
248
|
-
},
|
|
249
|
-
}, {
|
|
250
|
-
triggerTurn: true,
|
|
251
|
-
deliverAs: "followUp",
|
|
252
|
-
});
|
|
253
|
-
// Remove from registry
|
|
254
|
-
const entries = readRegistry(piDir).filter((e) => e.id !== id);
|
|
255
|
-
writeRegistry(piDir, entries);
|
|
256
|
-
}
|
|
257
34
|
// ─── Extension Entry Point ──────────────────────────────────────────────────
|
|
258
35
|
export default function (pi) {
|
|
259
36
|
// Prevent recursive loading
|
|
260
37
|
if (process.env.PI_TASK_TOOL_DISABLED === "1")
|
|
261
38
|
return;
|
|
262
39
|
// ── Background task tracker ────────────────────────────────────────────
|
|
40
|
+
const { piDir } = discoverAgents(process.cwd(), BUNDLED_AGENT_DIR);
|
|
263
41
|
const backgroundTasks = new Map();
|
|
264
42
|
const foregroundTasks = new Map();
|
|
265
|
-
|
|
43
|
+
const taskWidget = createTaskWidgetController(foregroundTasks, backgroundTasks);
|
|
44
|
+
const { ensureTaskWidget, clearTaskWidgetIfIdle } = taskWidget;
|
|
266
45
|
// ── Restore active tasks from registry on load ──────────────────────────
|
|
267
|
-
|
|
268
|
-
const registry = readRegistry(piDir);
|
|
269
|
-
const staleIds = [];
|
|
270
|
-
for (const entry of registry) {
|
|
271
|
-
// Only restore if artifact dir still exists
|
|
272
|
-
if (!existsSync(entry.dir)) {
|
|
273
|
-
staleIds.push(entry.id);
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
// Check if tmux pane is still alive
|
|
277
|
-
const paneAlive = entry.paneId ? paneExists(entry.paneId) : false;
|
|
278
|
-
if (!paneAlive) {
|
|
279
|
-
staleIds.push(entry.id);
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
const bgtask = {
|
|
283
|
-
dir: entry.dir,
|
|
284
|
-
agentType: entry.agentType,
|
|
285
|
-
sessionName: entry.sessionName,
|
|
286
|
-
paneId: entry.paneId,
|
|
287
|
-
originalPane: null,
|
|
288
|
-
description: entry.description,
|
|
289
|
-
startedAt: entry.startedAt,
|
|
290
|
-
toolUses: 0,
|
|
291
|
-
turns: 0,
|
|
292
|
-
conversationId: entry.conversationId,
|
|
293
|
-
recentCalls: [],
|
|
294
|
-
};
|
|
295
|
-
backgroundTasks.set(entry.id, bgtask);
|
|
296
|
-
}
|
|
297
|
-
if (staleIds.length) {
|
|
298
|
-
writeRegistry(piDir, registry.filter((e) => !staleIds.includes(e.id)));
|
|
299
|
-
}
|
|
46
|
+
restoreActiveBackgroundTasks(piDir, backgroundTasks);
|
|
300
47
|
// ── Widget / timer setup ───────────────────────────────────────────────
|
|
301
|
-
|
|
302
|
-
function stopWidget() {
|
|
303
|
-
if (widgetTimer) {
|
|
304
|
-
clearInterval(widgetTimer);
|
|
305
|
-
widgetTimer = null;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
const countInterval = setInterval(() => {
|
|
309
|
-
for (const task of [
|
|
310
|
-
...foregroundTasks.values(),
|
|
311
|
-
...backgroundTasks.values(),
|
|
312
|
-
]) {
|
|
313
|
-
const sessionDir = join(task.dir, "sessions");
|
|
314
|
-
// Single walk: counts + recent tool-call history with status
|
|
315
|
-
const { toolUses, turns, recent } = readRecentToolCalls(sessionDir, 12, task.sessionName);
|
|
316
|
-
task.toolUses = toolUses;
|
|
317
|
-
task.turns = turns;
|
|
318
|
-
task.recentCalls = recent;
|
|
319
|
-
}
|
|
320
|
-
}, COUNT_POLL_MS);
|
|
321
|
-
// Theme reference is captured at setWidget time so renderWidget can use it.
|
|
322
|
-
let widgetTheme = null;
|
|
323
|
-
function renderWidget(width) {
|
|
324
|
-
// Defensive: never let a render exception kill the TUI. If anything
|
|
325
|
-
// throws (theme lookup miss, malformed session JSONL, etc.), fall
|
|
326
|
-
// back to a minimal single-line summary so the TUI stays alive.
|
|
327
|
-
try {
|
|
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
|
-
});
|
|
336
|
-
}
|
|
337
|
-
catch (err) {
|
|
338
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
339
|
-
const active = [
|
|
340
|
-
...Array.from(foregroundTasks.entries()),
|
|
341
|
-
...Array.from(backgroundTasks.entries()),
|
|
342
|
-
];
|
|
343
|
-
if (active.length === 0)
|
|
344
|
-
return [];
|
|
345
|
-
const [, task] = active[0];
|
|
346
|
-
return [
|
|
347
|
-
truncateToWidth(`${task.agentType} \u2022 ${formatMs(Date.now() - task.startedAt)} (render error: ${msg})`, Math.min(width, 120)),
|
|
348
|
-
];
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
function ensureTaskWidget(targetCtx) {
|
|
352
|
-
if (widgetCtx || targetCtx.mode !== "tui")
|
|
353
|
-
return;
|
|
354
|
-
widgetCtx = targetCtx;
|
|
355
|
-
targetCtx.ui.setWidget("task", (tui, theme) => {
|
|
356
|
-
widgetTheme = theme ?? null;
|
|
357
|
-
widgetTimer = setInterval(() => tui.requestRender(), TASK_WIDGET_RENDER_MS);
|
|
358
|
-
// Don't keep the process alive just for the widget refresh.
|
|
359
|
-
widgetTimer.unref?.();
|
|
360
|
-
return {
|
|
361
|
-
render: (width) => renderWidget(width),
|
|
362
|
-
invalidate: () => { },
|
|
363
|
-
dispose: () => {
|
|
364
|
-
widgetTheme = null;
|
|
365
|
-
stopWidget();
|
|
366
|
-
},
|
|
367
|
-
};
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
function clearTaskWidgetIfIdle() {
|
|
371
|
-
if (foregroundTasks.size > 0 || backgroundTasks.size > 0)
|
|
372
|
-
return;
|
|
373
|
-
stopWidget();
|
|
374
|
-
if (widgetCtx) {
|
|
375
|
-
widgetCtx.ui.setWidget("task", undefined);
|
|
376
|
-
widgetCtx = null;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
48
|
+
const countInterval = startToolStatsPolling(foregroundTasks, backgroundTasks, COUNT_POLL_MS);
|
|
379
49
|
// ── Polling loop (background task completion, pane death, timeout) ──────
|
|
380
|
-
const checkInterval =
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
killAgentPane(task.paneId, task.originalPane);
|
|
394
|
-
backgroundTasks.delete(id);
|
|
395
|
-
clearTaskWidgetIfIdle();
|
|
396
|
-
completeTask(pi, id, task, "Task timed out after 30 minutes", "timeout", piDir);
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
const snapshot = await checkTaskCompletion({
|
|
400
|
-
resultPath: join(task.dir, "RESULT.md"),
|
|
401
|
-
sessionDir: join(task.dir, "sessions"),
|
|
402
|
-
sessionName: task.sessionName,
|
|
403
|
-
paneId: task.paneId,
|
|
404
|
-
sinceMs: task.startedAt,
|
|
405
|
-
});
|
|
406
|
-
if (snapshot.status === "running") {
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
const phase = snapshot.status === "completed" ? "done" : "failed";
|
|
410
|
-
backgroundTasks.delete(id);
|
|
411
|
-
clearTaskWidgetIfIdle();
|
|
412
|
-
completeTask(pi, id, task, snapshot.content, phase, piDir);
|
|
413
|
-
}
|
|
50
|
+
const checkInterval = startBackgroundPolling({
|
|
51
|
+
backgroundTasks,
|
|
52
|
+
checkTaskCompletion,
|
|
53
|
+
killAgentPane: (paneId, originalPane) => {
|
|
54
|
+
if (paneId)
|
|
55
|
+
killAgentPane(paneId, originalPane);
|
|
56
|
+
},
|
|
57
|
+
clearTaskWidgetIfIdle,
|
|
58
|
+
completeTask,
|
|
59
|
+
TASK_TIMEOUT_MS,
|
|
60
|
+
MAX_POLL_ERRORS,
|
|
61
|
+
piDir,
|
|
62
|
+
pi,
|
|
414
63
|
}, BACKGROUND_CHECK_MS);
|
|
415
64
|
// ── Cleanup on shutdown ────────────────────────────────────────────────
|
|
416
65
|
pi.on("session_shutdown", () => {
|
|
417
66
|
clearInterval(checkInterval);
|
|
418
67
|
clearInterval(countInterval);
|
|
419
|
-
|
|
420
|
-
if (widgetCtx) {
|
|
421
|
-
widgetCtx.ui.setWidget("task", undefined);
|
|
422
|
-
widgetCtx = null;
|
|
423
|
-
}
|
|
68
|
+
taskWidget.dispose();
|
|
424
69
|
});
|
|
425
70
|
// ── Custom notification renderer ───────────────────────────────────────
|
|
426
|
-
pi.registerMessageRenderer?.("task-complete", (
|
|
427
|
-
const d = message.details;
|
|
428
|
-
if (!d)
|
|
429
|
-
return undefined;
|
|
430
|
-
const agentType = d.agent_type || "";
|
|
431
|
-
const desc = d.description || "";
|
|
432
|
-
const summary = d.summary || "";
|
|
433
|
-
const findings = d.findings || "";
|
|
434
|
-
const confidence = d.confidence || "";
|
|
435
|
-
const durationMs = d.duration_ms || 0;
|
|
436
|
-
const toolUses = d.tool_uses || 0;
|
|
437
|
-
let line = " " + theme.fg("accent", agentType);
|
|
438
|
-
if (desc)
|
|
439
|
-
line += theme.fg("dim", ` - ${desc}`);
|
|
440
|
-
const useStr = toolUses > 0 ? `${toolUses} toolcalls` : "";
|
|
441
|
-
const durStr = durationMs >= 1000 ? formatMs(durationMs) : "";
|
|
442
|
-
const statsParts = [useStr, durStr].filter(Boolean);
|
|
443
|
-
const statsText = statsParts.join(" • ");
|
|
444
|
-
const confStr = confidence ? confidence.toUpperCase() : "";
|
|
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);
|
|
456
|
-
}
|
|
457
|
-
if (expanded) {
|
|
458
|
-
if (summary)
|
|
459
|
-
line += "\n " + theme.fg("muted", summary);
|
|
460
|
-
if (findings)
|
|
461
|
-
line += "\n " + theme.fg("dim", findings);
|
|
462
|
-
}
|
|
463
|
-
if (!line.trim())
|
|
464
|
-
return undefined;
|
|
465
|
-
const subtleBg = (text) => `\x1b[48;2;30;28;44m${text}\x1b[0m`;
|
|
466
|
-
return new Text(line, 0, 1, subtleBg);
|
|
467
|
-
});
|
|
71
|
+
pi.registerMessageRenderer?.("task-complete", createTaskCompleteRenderer());
|
|
468
72
|
// ── Tool Registration ──────────────────────────────────────────────────
|
|
469
73
|
pi.registerTool({
|
|
470
74
|
name: "task",
|
|
@@ -481,27 +85,7 @@ export default function (pi) {
|
|
|
481
85
|
"After delegated work completes, read changed files, review diff, verify scope, and run relevant checks",
|
|
482
86
|
"Send the user a concise summary of the result since the agent's output is not user-visible",
|
|
483
87
|
],
|
|
484
|
-
parameters:
|
|
485
|
-
agent_type: Type.String({
|
|
486
|
-
description: "The type of specialist agent to use for this task",
|
|
487
|
-
}),
|
|
488
|
-
prompt: Type.String({
|
|
489
|
-
description: "The complete task for the agent to perform. Be detailed and self-contained.",
|
|
490
|
-
}),
|
|
491
|
-
description: Type.String({
|
|
492
|
-
description: "A short (3-5 word) summary of the task",
|
|
493
|
-
}),
|
|
494
|
-
task_id: Type.Optional(Type.String({
|
|
495
|
-
description: "Resume an existing background task by id instead of starting a new task.",
|
|
496
|
-
})),
|
|
497
|
-
conversation_id: Type.Optional(Type.String({
|
|
498
|
-
description: "Durable specialist conversation id. Reuses .pi/artifacts/task-<id>/sessions when called again.",
|
|
499
|
-
})),
|
|
500
|
-
background: Type.Optional(Type.Boolean({
|
|
501
|
-
description: "Run in background (async). You will be notified when it completes. DO NOT sleep, poll, ask the task for status, or duplicate its work while it runs in background.",
|
|
502
|
-
default: true,
|
|
503
|
-
})),
|
|
504
|
-
}),
|
|
88
|
+
parameters: taskParametersSchema(),
|
|
505
89
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
506
90
|
const { agents, piDir } = discoverAgents(ctx.cwd, BUNDLED_AGENT_DIR);
|
|
507
91
|
const parentToolNames = pi
|
|
@@ -552,30 +136,12 @@ export default function (pi) {
|
|
|
552
136
|
}
|
|
553
137
|
let id;
|
|
554
138
|
let sessionName;
|
|
555
|
-
let resultPath;
|
|
556
139
|
let resume = false;
|
|
557
140
|
let resumeSessionRef;
|
|
558
141
|
const artifactsDir = join(piDir, "artifacts");
|
|
559
142
|
if (registeredTaskId) {
|
|
560
143
|
id = registeredTaskId;
|
|
561
144
|
sessionName = conversationId ?? `task-${id}`;
|
|
562
|
-
resultPath = join(artifactsDir, `RESULT-${id}.md`);
|
|
563
|
-
if (!existsSync(resultPath)) {
|
|
564
|
-
return {
|
|
565
|
-
content: [
|
|
566
|
-
{
|
|
567
|
-
type: "text",
|
|
568
|
-
text: `conversation_id "${conversationId}" has no prior result file at ${resultPath}. Cannot resume.`,
|
|
569
|
-
},
|
|
570
|
-
],
|
|
571
|
-
details: {
|
|
572
|
-
phase: "failed",
|
|
573
|
-
error: "Conversation result missing",
|
|
574
|
-
conversation_id: conversationId,
|
|
575
|
-
},
|
|
576
|
-
isError: true,
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
145
|
const block = readTaskBlock(piDir, id);
|
|
580
146
|
const previousMetadata = parseMetadataFromBody(block?.body);
|
|
581
147
|
const metadataAgent = previousMetadata?.agent_type;
|
|
@@ -686,7 +252,6 @@ export default function (pi) {
|
|
|
686
252
|
// flat in artifactsDir, no per-task subdir.
|
|
687
253
|
id = entry.id;
|
|
688
254
|
sessionName = entry.sessionName;
|
|
689
|
-
resultPath = join(artifactsDir, `RESULT-${id}.md`);
|
|
690
255
|
resume = true;
|
|
691
256
|
resumeSessionRef = entry.sessionRef;
|
|
692
257
|
// If background and pane still alive, reattach to tracker
|
|
@@ -743,7 +308,6 @@ export default function (pi) {
|
|
|
743
308
|
else {
|
|
744
309
|
id = `${Date.now().toString(36)}-${randomUUID().slice(0, 4)}`;
|
|
745
310
|
sessionName = conversationId ?? `task-${id}`;
|
|
746
|
-
resultPath = join(artifactsDir, `RESULT-${id}.md`);
|
|
747
311
|
}
|
|
748
312
|
if (conversationId && !hasTmux()) {
|
|
749
313
|
return {
|
|
@@ -774,32 +338,24 @@ export default function (pi) {
|
|
|
774
338
|
const isBackground = params.background ?? TASK_BACKGROUND_DEFAULT;
|
|
775
339
|
// default true
|
|
776
340
|
// ── Build the prompt (instructions are inlined; no CONTEXT.md file) ─
|
|
777
|
-
const promptContent =
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
"",
|
|
786
|
-
`## Working Directory`,
|
|
787
|
-
ctx.cwd,
|
|
788
|
-
"",
|
|
789
|
-
`## Output`,
|
|
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.",
|
|
791
|
-
"",
|
|
792
|
-
"Use this format for the summary:",
|
|
793
|
-
"",
|
|
794
|
-
"```",
|
|
795
|
-
TASK_RESULT_XML_INSTRUCTIONS,
|
|
796
|
-
"```",
|
|
797
|
-
].join("\n");
|
|
798
|
-
const sessionDir = join(artifactsDir, "sessions");
|
|
341
|
+
const promptContent = buildTaskPrompt({
|
|
342
|
+
description: descText,
|
|
343
|
+
agentName: agent.name,
|
|
344
|
+
agentSource: agent.source,
|
|
345
|
+
prompt: params.prompt,
|
|
346
|
+
cwd: ctx.cwd,
|
|
347
|
+
});
|
|
348
|
+
const sessionDir = join(artifactsDir, "sessions", id);
|
|
799
349
|
await mkdir(sessionDir, { recursive: true });
|
|
800
350
|
// ─── Build and run the sub-agent pi process ──────────────────────────
|
|
801
351
|
const piArgs = buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames, resumeSessionRef);
|
|
802
352
|
const envPrefix = `PI_TASK_TOOL_DISABLED=1`;
|
|
353
|
+
const forceTmuxBackend = process.env.PI_TASK_BACKEND === "tmux" ||
|
|
354
|
+
process.env.PI_TASK_USE_TMUX_BACKEND === "1";
|
|
355
|
+
const forceSdkBackend = process.env.PI_TASK_BACKEND === "sdk" ||
|
|
356
|
+
process.env.PI_TASK_USE_SDK_BACKEND === "1";
|
|
357
|
+
const tmuxAvailable = hasTmux();
|
|
358
|
+
const useSdkBackend = forceSdkBackend || (!forceTmuxBackend && !tmuxAvailable);
|
|
803
359
|
const toolSelection = buildAgentToolSelection({
|
|
804
360
|
tools: agent.tools,
|
|
805
361
|
disallowedTools: agent.disallowedTools,
|
|
@@ -834,8 +390,10 @@ export default function (pi) {
|
|
|
834
390
|
foregroundTasks.set(id, foregroundTask);
|
|
835
391
|
ensureTaskWidget(ctx);
|
|
836
392
|
}
|
|
837
|
-
// Prefer tmux
|
|
838
|
-
|
|
393
|
+
// Prefer tmux when the parent Pi is running inside tmux so users can watch
|
|
394
|
+
// the subagent's interactive Pi TUI. Fall back to the SDK only when tmux is
|
|
395
|
+
// unavailable, or when explicitly forced with PI_TASK_BACKEND=sdk.
|
|
396
|
+
if (useSdkBackend) {
|
|
839
397
|
if (isBackground) {
|
|
840
398
|
const bgtask = {
|
|
841
399
|
dir: artifactsDir,
|
|
@@ -887,14 +445,12 @@ export default function (pi) {
|
|
|
887
445
|
content: [
|
|
888
446
|
{
|
|
889
447
|
type: "text",
|
|
890
|
-
text: `Task ${id} started with SDK backend
|
|
448
|
+
text: `Task ${id} started with SDK backend.`,
|
|
891
449
|
},
|
|
892
450
|
],
|
|
893
451
|
details: {
|
|
894
452
|
task_id: id,
|
|
895
453
|
background: true,
|
|
896
|
-
backend: "sdk",
|
|
897
|
-
result_path: resultPath,
|
|
898
454
|
conversation_id: conversationId,
|
|
899
455
|
},
|
|
900
456
|
};
|
|
@@ -919,7 +475,6 @@ export default function (pi) {
|
|
|
919
475
|
phase: "done",
|
|
920
476
|
backend: "sdk",
|
|
921
477
|
session_path: sessionPath,
|
|
922
|
-
result_path: resultPath,
|
|
923
478
|
conversation_id: conversationId,
|
|
924
479
|
},
|
|
925
480
|
};
|
|
@@ -944,12 +499,15 @@ export default function (pi) {
|
|
|
944
499
|
}
|
|
945
500
|
}
|
|
946
501
|
const shellCommand = `${envPrefix} pi ${piArgs.map((a) => shellQuote(a)).join(" ")}`;
|
|
502
|
+
const sessionFile = join(sessionDir, sessionName + ".jsonl");
|
|
503
|
+
const tmuxCommand = wrapWithPaneExitWatcher(sessionFile, `cd ${shellQuote(ctx.cwd)} && ${shellCommand}`);
|
|
947
504
|
let paneId;
|
|
948
505
|
let originalPane;
|
|
949
506
|
try {
|
|
950
|
-
const splitResult = splitWindowPane(ctx.cwd,
|
|
507
|
+
const splitResult = splitWindowPane(ctx.cwd, tmuxCommand);
|
|
951
508
|
paneId = splitResult.paneId;
|
|
952
509
|
originalPane = splitResult.originalPane;
|
|
510
|
+
setPaneRemainOnExit(paneId, true);
|
|
953
511
|
if (foregroundTask) {
|
|
954
512
|
foregroundTask.paneId = paneId;
|
|
955
513
|
foregroundTask.originalPane = originalPane;
|
|
@@ -985,8 +543,31 @@ export default function (pi) {
|
|
|
985
543
|
status: "running",
|
|
986
544
|
background: false,
|
|
987
545
|
});
|
|
546
|
+
// Poll tool-call progress while waiting for completion
|
|
547
|
+
let lastToolCalls = -1;
|
|
548
|
+
const onAbort = () => clearInterval(toolProgressInterval);
|
|
549
|
+
const toolProgressInterval = setInterval(() => {
|
|
550
|
+
try {
|
|
551
|
+
const stats = countToolUses(sessionDir, sessionName);
|
|
552
|
+
if (stats.toolUses > 0 && stats.toolUses !== lastToolCalls) {
|
|
553
|
+
lastToolCalls = stats.toolUses;
|
|
554
|
+
_onUpdate?.({
|
|
555
|
+
content: [
|
|
556
|
+
{
|
|
557
|
+
type: "text",
|
|
558
|
+
text: `${stats.toolUses} tool call${stats.toolUses !== 1 ? "s" : ""}`,
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
details: { toolCalls: stats.toolUses },
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
// session file may not exist yet
|
|
567
|
+
}
|
|
568
|
+
}, COUNT_POLL_MS);
|
|
569
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
988
570
|
const completion = await waitForSessionTaskCompletion({
|
|
989
|
-
resultPath,
|
|
990
571
|
sessionDir,
|
|
991
572
|
sessionName,
|
|
992
573
|
paneId,
|
|
@@ -995,6 +576,8 @@ export default function (pi) {
|
|
|
995
576
|
pollMs: 1000,
|
|
996
577
|
sinceMs: startedAt,
|
|
997
578
|
});
|
|
579
|
+
clearInterval(toolProgressInterval);
|
|
580
|
+
signal?.removeEventListener("abort", onAbort);
|
|
998
581
|
const content = completion.content;
|
|
999
582
|
const phase = completion.status === "completed"
|
|
1000
583
|
? "done"
|
|
@@ -1017,7 +600,9 @@ export default function (pi) {
|
|
|
1017
600
|
completedAt: Date.now(),
|
|
1018
601
|
background: false,
|
|
1019
602
|
});
|
|
1020
|
-
|
|
603
|
+
if (phase === "done") {
|
|
604
|
+
killAgentPane(paneId, originalPane);
|
|
605
|
+
}
|
|
1021
606
|
foregroundTasks.delete(id);
|
|
1022
607
|
clearTaskWidgetIfIdle();
|
|
1023
608
|
if (conversationId) {
|
|
@@ -1038,13 +623,7 @@ export default function (pi) {
|
|
|
1038
623
|
content: [
|
|
1039
624
|
{
|
|
1040
625
|
type: "text",
|
|
1041
|
-
text:
|
|
1042
|
-
`${parsed.status || "done"}: ${parsed.summary || content.slice(0, 300)}`,
|
|
1043
|
-
toolUses > 0 ? `\n${toolUses} toolcalls` : "",
|
|
1044
|
-
durationMs >= 1000 ? `\n${formatMs(durationMs)}` : "",
|
|
1045
|
-
]
|
|
1046
|
-
.filter(Boolean)
|
|
1047
|
-
.join(""),
|
|
626
|
+
text: parsed.summary || content.trim(),
|
|
1048
627
|
},
|
|
1049
628
|
],
|
|
1050
629
|
details: {
|
|
@@ -1052,7 +631,7 @@ export default function (pi) {
|
|
|
1052
631
|
agent_type: agent.name,
|
|
1053
632
|
description: descText,
|
|
1054
633
|
phase,
|
|
1055
|
-
status:
|
|
634
|
+
status: "done",
|
|
1056
635
|
summary: parsed.summary || "",
|
|
1057
636
|
findings: parsed.findings || "",
|
|
1058
637
|
evidence: parsed.evidence || "",
|
|
@@ -1112,13 +691,7 @@ export default function (pi) {
|
|
|
1112
691
|
// Clean registry
|
|
1113
692
|
const remaining = readRegistry(piDir).filter((e) => e.id !== id);
|
|
1114
693
|
writeRegistry(piDir, remaining);
|
|
1115
|
-
|
|
1116
|
-
stopWidget();
|
|
1117
|
-
if (widgetCtx) {
|
|
1118
|
-
widgetCtx.ui.setWidget("task", undefined);
|
|
1119
|
-
widgetCtx = null;
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
694
|
+
clearTaskWidgetIfIdle();
|
|
1122
695
|
}, { once: true });
|
|
1123
696
|
}
|
|
1124
697
|
// ── Sticky widget ──────────────────────────────────────────────────
|
|
@@ -1144,79 +717,8 @@ export default function (pi) {
|
|
|
1144
717
|
},
|
|
1145
718
|
};
|
|
1146
719
|
},
|
|
1147
|
-
renderCall
|
|
1148
|
-
|
|
1149
|
-
const desc = args.description || "";
|
|
1150
|
-
let text = theme.fg("toolTitle", "");
|
|
1151
|
-
text += theme.fg("accent", agentName);
|
|
1152
|
-
if (desc)
|
|
1153
|
-
text += theme.fg("dim", ` - ${desc}`);
|
|
1154
|
-
return new Text(text, 0, 0);
|
|
1155
|
-
},
|
|
1156
|
-
renderResult(result, { expanded }, theme, _context) {
|
|
1157
|
-
const d = result.details;
|
|
1158
|
-
if (!d)
|
|
1159
|
-
return new Text("", 0, 0);
|
|
1160
|
-
if (d.background) {
|
|
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);
|
|
1172
|
-
}
|
|
1173
|
-
if (d.phase === "timeout" ||
|
|
1174
|
-
d.phase === "aborted" ||
|
|
1175
|
-
d.phase === "failed") {
|
|
1176
|
-
const line = theme.fg("error", "x") + " " + theme.fg("dim", `[${d.phase}]`);
|
|
1177
|
-
return new Text(line, 0, 0);
|
|
1178
|
-
}
|
|
1179
|
-
const isError = d.status === "failure" ||
|
|
1180
|
-
d.status === "blocked" ||
|
|
1181
|
-
d.status === "unknown" ||
|
|
1182
|
-
d.status === "timeout" ||
|
|
1183
|
-
d.status === "failed";
|
|
1184
|
-
const durationMs = d.duration_ms || 0;
|
|
1185
|
-
const toolUses = d.tool_uses || 0;
|
|
1186
|
-
const useStr = toolUses > 0 ? `${toolUses} toolcalls` : "";
|
|
1187
|
-
const durStr = durationMs >= 1000 ? formatMs(durationMs) : "";
|
|
1188
|
-
const statsParts = [useStr, durStr].filter(Boolean);
|
|
1189
|
-
const statsStr = statsParts.length
|
|
1190
|
-
? " " + theme.fg("dim", statsParts.join(" • "))
|
|
1191
|
-
: "";
|
|
1192
|
-
const icon = isError ? theme.fg("error", "x") : theme.fg("success", "✓");
|
|
1193
|
-
const statusLabel = d.status && d.status !== "done" ? d.status : "done";
|
|
1194
|
-
let line = icon +
|
|
1195
|
-
" " +
|
|
1196
|
-
theme.fg(isError ? "error" : "success", statusLabel) +
|
|
1197
|
-
statsStr;
|
|
1198
|
-
if (expanded) {
|
|
1199
|
-
const s = d.summary || "";
|
|
1200
|
-
const f = d.findings || "";
|
|
1201
|
-
const e = d.evidence || "";
|
|
1202
|
-
if (s)
|
|
1203
|
-
line += "\n" + theme.fg("muted", s);
|
|
1204
|
-
if (f)
|
|
1205
|
-
line += "\n" + theme.fg("dim", f);
|
|
1206
|
-
if (e)
|
|
1207
|
-
line += "\n" + theme.fg("muted", "Evidence: ") + theme.fg("dim", e);
|
|
1208
|
-
}
|
|
1209
|
-
else {
|
|
1210
|
-
const preview = (d.summary || "").slice(0, 80);
|
|
1211
|
-
if (preview)
|
|
1212
|
-
line += "\n" + theme.fg("dim", ` ⎿ ${preview}`);
|
|
1213
|
-
else
|
|
1214
|
-
line +=
|
|
1215
|
-
"\n" +
|
|
1216
|
-
theme.fg("dim", ` ⎿ ${isError ? d.status || "error" : "Done"}`);
|
|
1217
|
-
}
|
|
1218
|
-
return new Text(line, 0, 0);
|
|
1219
|
-
},
|
|
720
|
+
renderCall,
|
|
721
|
+
renderResult,
|
|
1220
722
|
});
|
|
1221
723
|
pi.registerCommand("task-sessions", {
|
|
1222
724
|
description: "List durable pi-task conversations",
|