@dyyz1993/pi-coding-agent 0.74.47 → 0.74.48
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/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +9 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/session-manager.d.ts +28 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +89 -10
- package/dist/core/session-manager.js.map +1 -1
- package/dist/extensions/ask-tools/index.ts +45 -0
- package/dist/extensions/auto-session-title/index.ts +2 -0
- package/dist/extensions/compaction-manager/index.ts +68 -7
- package/dist/extensions/coordinator/index.ts +39 -0
- package/dist/extensions/hooks-engine/index.ts +3 -0
- package/dist/extensions/lsp/lsp/client/smart-file-tracker.ts +302 -0
- package/dist/extensions/lsp/lsp/utils/project-scanner.ts +101 -12
- package/dist/extensions/output-guard/index.ts +39 -0
- package/dist/extensions/preview/index.ts +23 -0
- package/dist/extensions/subagent-v2/extract-parent-todos.test.ts +146 -0
- package/dist/extensions/subagent-v2/index.ts +372 -15
- package/dist/extensions/todo-ext/index.ts +55 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +6 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,26 +5,339 @@ import type { Message } from "@dyyz1993/pi-ai";
|
|
|
5
5
|
import { StringEnum } from "@dyyz1993/pi-ai";
|
|
6
6
|
import {
|
|
7
7
|
type AgentScope,
|
|
8
|
+
type ChannelContract,
|
|
8
9
|
type ExtensionAPI,
|
|
9
10
|
RpcClient,
|
|
10
11
|
createTypedChannel,
|
|
11
12
|
discoverAgents,
|
|
13
|
+
getMarkdownTheme,
|
|
14
|
+
withFileMutationQueue,
|
|
12
15
|
} from "@dyyz1993/pi-coding-agent";
|
|
13
|
-
import {
|
|
16
|
+
import type { Theme, ThemeColor } from "@dyyz1993/pi-coding-agent";
|
|
17
|
+
import { type Component, Container, Markdown, Spacer, Text } from "@dyyz1993/pi-tui";
|
|
14
18
|
import { Type } from "typebox";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
|
|
20
|
+
// ── Inlined from subagent-shared/contract.ts ──
|
|
21
|
+
|
|
22
|
+
const SUBAGENT_CHANNEL_NAME = "subagent";
|
|
23
|
+
|
|
24
|
+
interface SubagentEventPayload {
|
|
25
|
+
event: unknown;
|
|
26
|
+
sessionId: string;
|
|
27
|
+
taskId?: string;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface SubagentStartPayload {
|
|
32
|
+
event: {
|
|
33
|
+
type: "subagent_start";
|
|
34
|
+
toolCallId: string;
|
|
35
|
+
description: string;
|
|
36
|
+
instruction: string;
|
|
37
|
+
};
|
|
38
|
+
sessionId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SubagentChannelContract extends ChannelContract {
|
|
42
|
+
methods?: Record<string, never>;
|
|
43
|
+
events: {
|
|
44
|
+
event: SubagentEventPayload;
|
|
45
|
+
subagent_start: SubagentStartPayload;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Inlined from subagent-shared/types.ts ──
|
|
50
|
+
|
|
51
|
+
interface UsageStats {
|
|
52
|
+
input: number;
|
|
53
|
+
output: number;
|
|
54
|
+
cacheRead: number;
|
|
55
|
+
cacheWrite: number;
|
|
56
|
+
cost: number;
|
|
57
|
+
contextTokens: number;
|
|
58
|
+
turns: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface SingleResult {
|
|
62
|
+
agent: string;
|
|
63
|
+
agentSource: "user" | "project" | "unknown" | "builtin" | "plugin" | "flag" | "policy";
|
|
64
|
+
task: string;
|
|
65
|
+
exitCode: number;
|
|
66
|
+
messages: Message[];
|
|
67
|
+
stderr: string;
|
|
68
|
+
usage: UsageStats;
|
|
69
|
+
model?: string;
|
|
70
|
+
stopReason?: string;
|
|
71
|
+
errorMessage?: string;
|
|
72
|
+
step?: number;
|
|
73
|
+
sessionPath?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type DisplayItem =
|
|
77
|
+
| { type: "text"; text: string }
|
|
78
|
+
| { type: "toolCall"; name: string; args: Record<string, unknown> };
|
|
79
|
+
|
|
80
|
+
interface SubagentDetailsBase {
|
|
81
|
+
agentScope: string;
|
|
82
|
+
projectAgentsDir: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Inlined from subagent-shared/utils.ts ──
|
|
86
|
+
|
|
87
|
+
function formatTokens(count: number): string {
|
|
88
|
+
if (count < 1000) return count.toString();
|
|
89
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
90
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
91
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatUsageStats(
|
|
95
|
+
usage: {
|
|
96
|
+
input: number;
|
|
97
|
+
output: number;
|
|
98
|
+
cacheRead: number;
|
|
99
|
+
cacheWrite: number;
|
|
100
|
+
cost: number;
|
|
101
|
+
contextTokens?: number;
|
|
102
|
+
turns?: number;
|
|
103
|
+
},
|
|
104
|
+
model?: string,
|
|
105
|
+
): string {
|
|
106
|
+
const parts: string[] = [];
|
|
107
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
108
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
109
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
110
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
111
|
+
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
112
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
113
|
+
if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
114
|
+
if (model) parts.push(model);
|
|
115
|
+
return parts.join(" ");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getFinalOutput(messages: Message[]): string {
|
|
119
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
120
|
+
const msg = messages[i];
|
|
121
|
+
if (msg.role === "assistant") {
|
|
122
|
+
for (const part of msg.content) {
|
|
123
|
+
if (part.type === "text") return part.text;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
131
|
+
const items: DisplayItem[] = [];
|
|
132
|
+
for (const msg of messages) {
|
|
133
|
+
if (msg.role === "assistant") {
|
|
134
|
+
for (const part of msg.content) {
|
|
135
|
+
if (part.type === "text") items.push({ type: "text", text: part.text });
|
|
136
|
+
else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return items;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function writePromptToTempFile(
|
|
144
|
+
agentName: string,
|
|
145
|
+
prompt: string,
|
|
146
|
+
tmpPrefix = "pi-subagent-",
|
|
147
|
+
): Promise<{ dir: string; filePath: string }> {
|
|
148
|
+
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), tmpPrefix));
|
|
149
|
+
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
150
|
+
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
151
|
+
await withFileMutationQueue(filePath, async () => {
|
|
152
|
+
await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
|
|
153
|
+
});
|
|
154
|
+
return { dir: tmpDir, filePath };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function cleanupTempFiles(filePath: string | null, dir: string | null, logPrefix = "subagent"): void {
|
|
158
|
+
if (filePath)
|
|
159
|
+
try {
|
|
160
|
+
fs.unlinkSync(filePath);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.debug(`[${logPrefix}] temp file cleanup failed:`, err instanceof Error ? err.message : err);
|
|
163
|
+
}
|
|
164
|
+
if (dir)
|
|
165
|
+
try {
|
|
166
|
+
fs.rmdirSync(dir);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.debug(`[${logPrefix}] temp dir cleanup failed:`, err instanceof Error ? err.message : err);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function makeUsage(): UsageStats {
|
|
173
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function accumulateUsage(result: { usage: UsageStats; model?: string; stopReason?: string; errorMessage?: string }, msg: Message): void {
|
|
177
|
+
if (msg.role !== "assistant") return;
|
|
178
|
+
result.usage.turns++;
|
|
179
|
+
const usage = msg.usage;
|
|
180
|
+
if (usage) {
|
|
181
|
+
result.usage.input += usage.input || 0;
|
|
182
|
+
result.usage.output += usage.output || 0;
|
|
183
|
+
result.usage.cacheRead += usage.cacheRead || 0;
|
|
184
|
+
result.usage.cacheWrite += usage.cacheWrite || 0;
|
|
185
|
+
result.usage.cost += usage.cost?.total || 0;
|
|
186
|
+
result.usage.contextTokens = usage.totalTokens || 0;
|
|
187
|
+
}
|
|
188
|
+
if (!result.model && msg.model) result.model = msg.model;
|
|
189
|
+
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
190
|
+
if (msg.errorMessage) result.errorMessage = msg.errorMessage;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Inlined from subagent-shared/render.ts ──
|
|
194
|
+
|
|
195
|
+
const COLLAPSED_ITEM_COUNT = 10;
|
|
196
|
+
|
|
197
|
+
function formatToolCall(
|
|
198
|
+
toolName: string,
|
|
199
|
+
args: Record<string, unknown>,
|
|
200
|
+
themeFg: (color: ThemeColor, text: string) => string,
|
|
201
|
+
): string {
|
|
202
|
+
const shortenPath = (p: string) => {
|
|
203
|
+
const home = os.homedir();
|
|
204
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
switch (toolName) {
|
|
208
|
+
case "bash": {
|
|
209
|
+
const command = (args.command as string) || "...";
|
|
210
|
+
const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
|
|
211
|
+
return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
|
|
212
|
+
}
|
|
213
|
+
case "read": {
|
|
214
|
+
const rawPath = (args.file_path || args.path || "...") as string;
|
|
215
|
+
const filePath = shortenPath(rawPath);
|
|
216
|
+
const offset = args.offset as number | undefined;
|
|
217
|
+
const limit = args.limit as number | undefined;
|
|
218
|
+
let text = themeFg("accent", filePath);
|
|
219
|
+
if (offset !== undefined || limit !== undefined) {
|
|
220
|
+
const startLine = offset ?? 1;
|
|
221
|
+
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
222
|
+
text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
223
|
+
}
|
|
224
|
+
return themeFg("muted", "read ") + text;
|
|
225
|
+
}
|
|
226
|
+
case "write": {
|
|
227
|
+
const rawPath = (args.file_path || args.path || "...") as string;
|
|
228
|
+
const filePath = shortenPath(rawPath);
|
|
229
|
+
const content = (args.content || "") as string;
|
|
230
|
+
const lines = content.split("\n").length;
|
|
231
|
+
let text = themeFg("muted", "write ") + themeFg("accent", filePath);
|
|
232
|
+
if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
|
|
233
|
+
return text;
|
|
234
|
+
}
|
|
235
|
+
case "edit": {
|
|
236
|
+
const rawPath = (args.file_path || args.path || "...") as string;
|
|
237
|
+
return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
|
|
238
|
+
}
|
|
239
|
+
case "ls": {
|
|
240
|
+
const rawPath = (args.path || ".") as string;
|
|
241
|
+
return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath));
|
|
242
|
+
}
|
|
243
|
+
case "find": {
|
|
244
|
+
const pattern = (args.pattern || "*") as string;
|
|
245
|
+
const rawPath = (args.path || ".") as string;
|
|
246
|
+
return themeFg("muted", "find ") + themeFg("accent", pattern) + themeFg("dim", ` in ${shortenPath(rawPath)}`);
|
|
247
|
+
}
|
|
248
|
+
case "grep": {
|
|
249
|
+
const pattern = (args.pattern || "") as string;
|
|
250
|
+
const rawPath = (args.path || ".") as string;
|
|
251
|
+
return (
|
|
252
|
+
themeFg("muted", "grep ") +
|
|
253
|
+
themeFg("accent", `/${pattern}/`) +
|
|
254
|
+
themeFg("dim", ` in ${shortenPath(rawPath)}`)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
default: {
|
|
258
|
+
const argsStr = JSON.stringify(args);
|
|
259
|
+
const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr;
|
|
260
|
+
return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function renderDisplayItemsInner(
|
|
266
|
+
items: DisplayItem[],
|
|
267
|
+
expanded: boolean,
|
|
268
|
+
theme: Theme,
|
|
269
|
+
limit?: number,
|
|
270
|
+
): string {
|
|
271
|
+
const fg = (c: string, t: string) => theme.fg(c as ThemeColor, t);
|
|
272
|
+
const toShow = limit ? items.slice(-limit) : items;
|
|
273
|
+
const skipped = limit && items.length > limit ? items.length - limit : 0;
|
|
274
|
+
let text = "";
|
|
275
|
+
if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
|
|
276
|
+
for (const item of toShow) {
|
|
277
|
+
if (item.type === "text") {
|
|
278
|
+
const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
|
|
279
|
+
text += `${theme.fg("toolOutput", preview)}\n`;
|
|
280
|
+
} else {
|
|
281
|
+
text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, fg)}\n`;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return text.trimEnd();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function renderSingleResult(r: SingleResult, expanded: boolean, theme: Theme): Component {
|
|
288
|
+
const mdTheme = getMarkdownTheme();
|
|
289
|
+
const fg = (c: string, t: string) => theme.fg(c as ThemeColor, t);
|
|
290
|
+
const isError =
|
|
291
|
+
r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted" || r.stopReason === "timeout";
|
|
292
|
+
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
293
|
+
const displayItems = getDisplayItems(r.messages);
|
|
294
|
+
const finalOutput = getFinalOutput(r.messages);
|
|
295
|
+
|
|
296
|
+
if (expanded) {
|
|
297
|
+
const container = new Container();
|
|
298
|
+
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
299
|
+
if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
300
|
+
container.addChild(new Text(header, 0, 0));
|
|
301
|
+
if (isError && r.errorMessage) container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
302
|
+
container.addChild(new Spacer(1));
|
|
303
|
+
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
|
|
304
|
+
container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
|
|
305
|
+
container.addChild(new Spacer(1));
|
|
306
|
+
container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
|
|
307
|
+
if (displayItems.length === 0 && !finalOutput) {
|
|
308
|
+
container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
|
|
309
|
+
} else {
|
|
310
|
+
for (const item of displayItems) {
|
|
311
|
+
if (item.type === "toolCall")
|
|
312
|
+
container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, fg), 0, 0));
|
|
313
|
+
}
|
|
314
|
+
if (finalOutput) {
|
|
315
|
+
container.addChild(new Spacer(1));
|
|
316
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const usageStr = formatUsageStats(r.usage, r.model);
|
|
320
|
+
if (usageStr) {
|
|
321
|
+
container.addChild(new Spacer(1));
|
|
322
|
+
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
323
|
+
}
|
|
324
|
+
return container;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
328
|
+
if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
329
|
+
if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
330
|
+
else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
|
|
331
|
+
else {
|
|
332
|
+
text += `\n${renderDisplayItemsInner(displayItems, expanded, theme, COLLAPSED_ITEM_COUNT)}`;
|
|
333
|
+
if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
334
|
+
}
|
|
335
|
+
const usageStr = formatUsageStats(r.usage, r.model);
|
|
336
|
+
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
|
|
337
|
+
return new Text(text, 0, 0);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── subagent-v2 logic ──
|
|
28
341
|
|
|
29
342
|
const STEER_GRACE_MS = 30_000;
|
|
30
343
|
|
|
@@ -167,6 +480,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
167
480
|
const timeoutMs = Math.max((params.timeout ?? 300) * 1000, STEER_GRACE_MS + 10_000);
|
|
168
481
|
const background = params.background ?? false;
|
|
169
482
|
|
|
483
|
+
// Extract parent todo list from session history (for read-only reference)
|
|
484
|
+
const parentTodos = extractParentTodos(ctx.sessionManager.getBranch());
|
|
485
|
+
|
|
170
486
|
const details: SubagentDetails = {
|
|
171
487
|
agentScope,
|
|
172
488
|
projectAgentsDir: discovery.projectAgentsDir,
|
|
@@ -241,10 +557,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
241
557
|
}
|
|
242
558
|
};
|
|
243
559
|
|
|
244
|
-
|
|
560
|
+
// Pass subagent role and parent todos to child process via env vars
|
|
561
|
+
const env: Record<string, string> = { PI_SUBAGENT: "true" };
|
|
562
|
+
if (parentTodos.length > 0) {
|
|
563
|
+
env.PI_PARENT_TODOS = JSON.stringify(parentTodos);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const client = new RpcClient({
|
|
245
567
|
cwd: params.cwd ?? ctx.cwd,
|
|
246
568
|
provider: ctx.model?.provider || undefined,
|
|
247
569
|
model: agent.model,
|
|
570
|
+
env,
|
|
248
571
|
args: extraArgs,
|
|
249
572
|
});
|
|
250
573
|
|
|
@@ -613,3 +936,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
613
936
|
},
|
|
614
937
|
});
|
|
615
938
|
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Extract the parent session's todo list from session history entries.
|
|
942
|
+
* Scans custom "todo" entries and tool result messages for the latest list.
|
|
943
|
+
* Returns active (not deleted, not done) todos for read-only reference.
|
|
944
|
+
*/
|
|
945
|
+
export function extractParentTodos(branch: unknown[]): Array<{ id: number; text: string; priority?: string; done: boolean }> {
|
|
946
|
+
let todos: Array<{ id: number; text: string; done: boolean; deleted?: boolean; priority?: string }> = [];
|
|
947
|
+
let nextId = 1;
|
|
948
|
+
|
|
949
|
+
for (const entry of branch) {
|
|
950
|
+
const e = entry as Record<string, unknown>;
|
|
951
|
+
if (e.type === "custom" && (e as Record<string, unknown>).customType === "todo") {
|
|
952
|
+
const data = (e as Record<string, unknown>).data as Record<string, unknown> | undefined;
|
|
953
|
+
if (data?.todos) {
|
|
954
|
+
todos = data.todos as typeof todos;
|
|
955
|
+
nextId = (data.nextId as number) ?? nextId;
|
|
956
|
+
}
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (e.type !== "message") continue;
|
|
960
|
+
const msg = (e as Record<string, unknown>).message as Record<string, unknown> | undefined;
|
|
961
|
+
if (!msg || msg.role !== "toolResult" || (msg as Record<string, unknown>).toolName !== "todo") continue;
|
|
962
|
+
const details = (msg as Record<string, unknown>).details as Record<string, unknown> | undefined;
|
|
963
|
+
if (details?.todos) {
|
|
964
|
+
todos = details.todos as typeof todos;
|
|
965
|
+
nextId = (details.nextId as number) ?? nextId;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return todos
|
|
970
|
+
.filter((t) => !t.deleted && !t.done)
|
|
971
|
+
.map((t) => ({ id: t.id, text: t.text, priority: t.priority, done: t.done }));
|
|
972
|
+
}
|
|
@@ -79,6 +79,39 @@ function updateWidget(ctx: ExtensionContext | undefined, todos: Todo[]): void {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
export default function (pi: ExtensionAPI) {
|
|
82
|
+
// ── Sub-agent mode: inject parent's todos as read-only, no todo tool ──
|
|
83
|
+
if (process.env.PI_SUBAGENT === "true") {
|
|
84
|
+
let parentTodos: TodoItem[] = [];
|
|
85
|
+
try {
|
|
86
|
+
const raw = process.env.PI_PARENT_TODOS;
|
|
87
|
+
if (raw) parentTodos = JSON.parse(raw) as TodoItem[];
|
|
88
|
+
} catch {
|
|
89
|
+
// ignore parse errors
|
|
90
|
+
}
|
|
91
|
+
const active = parentTodos.filter((t) => !t.deleted && !t.done);
|
|
92
|
+
if (active.length > 0) {
|
|
93
|
+
const lines = active.map((t) => {
|
|
94
|
+
const pri = t.priority === "high" ? " [!]" : t.priority === "low" ? " [?]" : "";
|
|
95
|
+
return ` #${t.id}${pri}: ${t.text}`;
|
|
96
|
+
});
|
|
97
|
+
const header = `[Parent session's tasks — read-only]\nThese are the parent session's active tasks for reference. Do not modify them.\n${lines.join("\n")}`;
|
|
98
|
+
pi.on("context", (_event, _ctx) => {
|
|
99
|
+
return {
|
|
100
|
+
messages: [
|
|
101
|
+
...(_event as any).messages,
|
|
102
|
+
{
|
|
103
|
+
role: "user" as const,
|
|
104
|
+
content: [{ type: "text" as const, text: header }],
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return; // Skip all tool/command/channel registration
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Normal mode ──
|
|
82
115
|
let todos: Todo[] = [];
|
|
83
116
|
let nextId = 1;
|
|
84
117
|
let channel: ServerChannel<TodoChannelContract> | null = null;
|
|
@@ -403,6 +436,28 @@ IMPORTANT: For creating a plan with multiple steps, use a SINGLE add call with n
|
|
|
403
436
|
},
|
|
404
437
|
});
|
|
405
438
|
|
|
439
|
+
// Inject active todo list into context so the LLM is aware of ongoing tasks
|
|
440
|
+
pi.on("context", (_event, _ctx) => {
|
|
441
|
+
const active = todos.filter((t) => !t.deleted && !t.done);
|
|
442
|
+
if (active.length === 0) return;
|
|
443
|
+
|
|
444
|
+
const lines = active.map((t) => {
|
|
445
|
+
const pri = t.priority === "high" ? " [!]" : t.priority === "low" ? " [?]" : "";
|
|
446
|
+
return ` #${t.id}${pri}: ${t.text}`;
|
|
447
|
+
});
|
|
448
|
+
const text = `[Todo list — ${active.length} active task(s)]\n${lines.join("\n")}`;
|
|
449
|
+
return {
|
|
450
|
+
messages: [
|
|
451
|
+
...(_event as any).messages,
|
|
452
|
+
{
|
|
453
|
+
role: "user" as const,
|
|
454
|
+
content: [{ type: "text" as const, text }],
|
|
455
|
+
timestamp: Date.now(),
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
|
|
406
461
|
pi.registerCommand("todos", {
|
|
407
462
|
description: "Show all todos on the current branch",
|
|
408
463
|
handler: async (_args, ctx) => {
|