@dyyz1993/pi-coding-agent 0.74.46 → 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 +16 -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-memory/__tests__/extract-result.test.ts +42 -0
- package/dist/extensions/auto-memory/__tests__/prefetch-history.test.ts +136 -0
- package/dist/extensions/auto-memory/__tests__/prompts.test.ts +29 -0
- package/dist/extensions/auto-memory/__tests__/skip-rules.test.ts +366 -0
- package/dist/extensions/auto-memory/contract.d.ts +16 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -1
- package/dist/extensions/auto-memory/contract.js.map +1 -1
- package/dist/extensions/auto-memory/contract.ts +16 -0
- package/dist/extensions/auto-memory/index.ts +134 -13
- package/dist/extensions/auto-memory/prompts.ts +10 -0
- package/dist/extensions/auto-memory/skip-rules.ts +2 -0
- package/dist/extensions/auto-session-title/index.ts +2 -0
- package/dist/extensions/bash-ext/index.ts +855 -845
- package/dist/extensions/claude-hooks-compat/index.ts +12 -7
- package/dist/extensions/compaction-manager/index.ts +68 -7
- package/dist/extensions/coordinator/handler.test.ts +388 -123
- package/dist/extensions/coordinator/handler.ts +78 -12
- package/dist/extensions/coordinator/index.ts +306 -198
- package/dist/extensions/coordinator/types.d.ts +16 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -1
- package/dist/extensions/coordinator/types.js.map +1 -1
- package/dist/extensions/coordinator/types.ts +57 -49
- 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/index.ts +15 -9
- package/dist/extensions/lsp/lsp/lsp-clangd-e2e.test.ts +229 -0
- package/dist/extensions/lsp/lsp/utils/project-scanner.ts +101 -12
- package/dist/extensions/message-bridge/index.ts +14 -11
- package/dist/extensions/output-guard/index.ts +39 -0
- package/dist/extensions/preview/index.ts +23 -0
- package/dist/extensions/session-supervisor/index.ts +14 -8
- package/dist/extensions/subagent-v2/extract-parent-todos.test.ts +146 -0
- package/dist/extensions/subagent-v2/index.ts +430 -57
- package/dist/extensions/todo-ext/index.ts +62 -3
- 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 +10 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,8 +5,50 @@ import type { ResolvedLspServerConfig } from "../config/resolver.js";
|
|
|
5
5
|
|
|
6
6
|
export interface ProjectScanResult {
|
|
7
7
|
discoveredExtensions: Set<string>;
|
|
8
|
+
fileCount?: number;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
// Common source code file extensions to scan for
|
|
12
|
+
// This prevents scanning unnecessary files like .bak, .log, .bin, etc.
|
|
13
|
+
const COMMON_SOURCE_EXTENSIONS = new Set([
|
|
14
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
15
|
+
".vue", ".svelte", ".jsx",
|
|
16
|
+
".py", ".pyi",
|
|
17
|
+
".rs",
|
|
18
|
+
".go",
|
|
19
|
+
".java", ".kt", ".kts",
|
|
20
|
+
".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".hxx",
|
|
21
|
+
".cs", ".vb",
|
|
22
|
+
".php",
|
|
23
|
+
".rb",
|
|
24
|
+
".swift", ".m", ".mm",
|
|
25
|
+
".dart",
|
|
26
|
+
".lua",
|
|
27
|
+
".sh", ".bash", ".zsh",
|
|
28
|
+
".sql",
|
|
29
|
+
".graphql", ".gql",
|
|
30
|
+
".yaml", ".yml",
|
|
31
|
+
".toml",
|
|
32
|
+
".json",
|
|
33
|
+
".md",
|
|
34
|
+
".xml", ".html", ".htm", ".css", ".scss", ".less", ".sass",
|
|
35
|
+
".txt",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// Directories to exclude from scanning (in addition to .git, node_modules)
|
|
39
|
+
const EXCLUDED_DIRS = [
|
|
40
|
+
"node_modules", ".git", "target", "dist", "build", ".pi",
|
|
41
|
+
".next", ".nuxt", ".output", ".vercel",
|
|
42
|
+
"venv", "env", ".venv", "envs", ".envs", "__pycache__",
|
|
43
|
+
".vscode", ".idea",
|
|
44
|
+
"coverage", ".nyc_output",
|
|
45
|
+
".cache", "tmp", "temp",
|
|
46
|
+
".DS_Store", "Thumbs.db",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Maximum files to scan before stopping
|
|
50
|
+
const MAX_FILES_TO_SCAN = 5000;
|
|
51
|
+
|
|
10
52
|
/**
|
|
11
53
|
* Scan the project for file types present on disk.
|
|
12
54
|
* Uses `git ls-files` when available (fast, respects .gitignore),
|
|
@@ -14,38 +56,76 @@ export interface ProjectScanResult {
|
|
|
14
56
|
*/
|
|
15
57
|
export function scanProjectFileTypes(cwd: string): ProjectScanResult {
|
|
16
58
|
const extensions = new Set<string>();
|
|
59
|
+
let fileCount = 0;
|
|
17
60
|
|
|
18
61
|
// Strategy 1: git ls-files (fast, respects gitignore)
|
|
19
62
|
const gitFiles = tryGitLsFiles(cwd);
|
|
20
63
|
if (gitFiles.length > 0) {
|
|
21
64
|
for (const file of gitFiles) {
|
|
65
|
+
fileCount++;
|
|
66
|
+
if (fileCount > MAX_FILES_TO_SCAN) {
|
|
67
|
+
console.warn(`[lsp] Stopped scan after ${MAX_FILES_TO_SCAN} files (too many files)`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
22
71
|
const ext = extname(file).toLowerCase();
|
|
23
|
-
|
|
72
|
+
// Only collect common source code extensions
|
|
73
|
+
if (ext && COMMON_SOURCE_EXTENSIONS.has(ext)) {
|
|
24
74
|
extensions.add(ext);
|
|
25
75
|
}
|
|
26
76
|
}
|
|
27
|
-
|
|
77
|
+
console.log(`[lsp] Project scan found ${extensions.size} file types from ${fileCount} files (git mode)`);
|
|
78
|
+
return { discoveredExtensions: extensions, fileCount };
|
|
28
79
|
}
|
|
29
80
|
|
|
30
|
-
// Strategy 2: shallow find (maxdepth 3, skip
|
|
81
|
+
// Strategy 2: shallow find (maxdepth 3, skip many common dirs)
|
|
31
82
|
try {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
83
|
+
const excludeArgs = EXCLUDED_DIRS.map((dir) => `-not -path "*/${dir}/*"`).join(" ");
|
|
84
|
+
const command = `find . -maxdepth 3 -type f ${excludeArgs} 2>/dev/null | head -${MAX_FILES_TO_SCAN}`;
|
|
85
|
+
const output = execSync(command, {
|
|
86
|
+
cwd,
|
|
87
|
+
timeout: 3000,
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const lines = output.split("\n").filter(Boolean);
|
|
92
|
+
fileCount = 0;
|
|
93
|
+
|
|
94
|
+
for (const line of lines) {
|
|
37
95
|
const trimmed = line.trim();
|
|
38
96
|
if (!trimmed) continue;
|
|
97
|
+
|
|
98
|
+
fileCount++;
|
|
99
|
+
|
|
100
|
+
// Check memory usage periodically
|
|
101
|
+
if (fileCount % 1000 === 0) {
|
|
102
|
+
const memUsage = process.memoryUsage();
|
|
103
|
+
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
|
|
104
|
+
const heapTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024);
|
|
105
|
+
|
|
106
|
+
// If we're using >3GB of heap, stop scanning
|
|
107
|
+
if (heapUsedMB > 3000) {
|
|
108
|
+
console.warn(`[lsp] Stopping scan due to high memory usage (${heapUsedMB}MB heap used)`);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
39
113
|
const ext = extname(trimmed).toLowerCase();
|
|
40
|
-
|
|
114
|
+
// Only collect common source code extensions
|
|
115
|
+
if (ext && COMMON_SOURCE_EXTENSIONS.has(ext)) {
|
|
41
116
|
extensions.add(ext);
|
|
42
117
|
}
|
|
43
118
|
}
|
|
44
|
-
|
|
119
|
+
|
|
120
|
+
console.log(`[lsp] Project scan found ${extensions.size} file types from ${fileCount} files (find mode)`);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error instanceof Error) {
|
|
123
|
+
console.warn(`[lsp] Project scan failed: ${error.message}`);
|
|
124
|
+
}
|
|
45
125
|
// If scan fails, return empty — will fall back to starting all servers
|
|
46
126
|
}
|
|
47
127
|
|
|
48
|
-
return { discoveredExtensions: extensions };
|
|
128
|
+
return { discoveredExtensions: extensions, fileCount };
|
|
49
129
|
}
|
|
50
130
|
|
|
51
131
|
function tryGitLsFiles(cwd: string): string[] {
|
|
@@ -76,10 +156,17 @@ export function filterServersByProject(
|
|
|
76
156
|
servers: ResolvedLspServerConfig[],
|
|
77
157
|
scanResult: ProjectScanResult,
|
|
78
158
|
): ResolvedLspServerConfig[] {
|
|
79
|
-
const { discoveredExtensions } = scanResult;
|
|
159
|
+
const { discoveredExtensions, fileCount } = scanResult;
|
|
80
160
|
|
|
81
161
|
// Safe fallback: if scan found nothing, start everything
|
|
82
162
|
if (discoveredExtensions.size === 0) {
|
|
163
|
+
console.log(`[lsp] No file types discovered, starting all ${servers.length} servers`);
|
|
164
|
+
return servers;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// If we scanned very few files (<10), might be an empty project - start all servers
|
|
168
|
+
if (fileCount !== undefined && fileCount < 10) {
|
|
169
|
+
console.log(`[lsp] Only ${fileCount} files scanned, starting all ${servers.length} servers`);
|
|
83
170
|
return servers;
|
|
84
171
|
}
|
|
85
172
|
|
|
@@ -98,5 +185,7 @@ export function filterServersByProject(
|
|
|
98
185
|
}
|
|
99
186
|
}
|
|
100
187
|
|
|
188
|
+
console.log(`[lsp] Filtered to ${filtered.length}/${servers.length} servers based on project files`);
|
|
189
|
+
|
|
101
190
|
return filtered;
|
|
102
191
|
}
|
|
@@ -139,6 +139,7 @@ export default function messageBridgeExtension(pi: any) {
|
|
|
139
139
|
|
|
140
140
|
pi.on("ui", async (event: any, ctx: any) => {
|
|
141
141
|
if (event.method === "notify") {
|
|
142
|
+
if (event.message == null) return undefined;
|
|
142
143
|
pushAndWait(event.message, sessionId).catch((err) => console.debug("[message-bridge] notify push failed:", err instanceof Error ? err.message : err));
|
|
143
144
|
return undefined;
|
|
144
145
|
}
|
|
@@ -148,7 +149,7 @@ export default function messageBridgeExtension(pi: any) {
|
|
|
148
149
|
pushAndWait(question, sessionId)
|
|
149
150
|
.then((answer) => {
|
|
150
151
|
const confirmed = parseConfirmAnswer(answer);
|
|
151
|
-
ctx.respondUI(event.id, { action: "responded", confirmed });
|
|
152
|
+
try { ctx.respondUI(event.id, { action: "responded", confirmed }); } catch (e) { if (!/stale/i.test(e instanceof Error ? e.message : "")) throw e; }
|
|
152
153
|
})
|
|
153
154
|
.catch((err) => console.debug("[message-bridge] confirm push failed:", err instanceof Error ? err.message : err));
|
|
154
155
|
return undefined;
|
|
@@ -160,13 +161,15 @@ export default function messageBridgeExtension(pi: any) {
|
|
|
160
161
|
const question = buildSelectQuestion(event.title, options, multiple);
|
|
161
162
|
pushAndWait(question, sessionId)
|
|
162
163
|
.then((answer) => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
try {
|
|
165
|
+
if (multiple) {
|
|
166
|
+
const values = parseMultiSelectAnswer(answer, options);
|
|
167
|
+
ctx.respondUI(event.id, { action: "responded", value: values });
|
|
168
|
+
} else {
|
|
169
|
+
const value = parseSelectAnswer(answer);
|
|
170
|
+
ctx.respondUI(event.id, { action: "responded", value });
|
|
171
|
+
}
|
|
172
|
+
} catch (e) { if (!/stale/i.test(e instanceof Error ? e.message : "")) throw e; }
|
|
170
173
|
})
|
|
171
174
|
.catch((err) => console.debug("[message-bridge] select push failed:", err instanceof Error ? err.message : err));
|
|
172
175
|
return undefined;
|
|
@@ -178,7 +181,7 @@ export default function messageBridgeExtension(pi: any) {
|
|
|
178
181
|
: event.title;
|
|
179
182
|
pushAndWait(question, sessionId)
|
|
180
183
|
.then((answer) => {
|
|
181
|
-
ctx.respondUI(event.id, { action: "responded", value: answer });
|
|
184
|
+
try { ctx.respondUI(event.id, { action: "responded", value: answer }); } catch (e) { if (!/stale/i.test(e instanceof Error ? e.message : "")) throw e; }
|
|
182
185
|
})
|
|
183
186
|
.catch((err) => console.debug("[message-bridge] input push failed:", err instanceof Error ? err.message : err));
|
|
184
187
|
return undefined;
|
|
@@ -190,7 +193,7 @@ export default function messageBridgeExtension(pi: any) {
|
|
|
190
193
|
: event.title;
|
|
191
194
|
pushAndWait(question, sessionId)
|
|
192
195
|
.then((answer) => {
|
|
193
|
-
ctx.respondUI(event.id, { action: "responded", value: answer });
|
|
196
|
+
try { ctx.respondUI(event.id, { action: "responded", value: answer }); } catch (e) { if (!/stale/i.test(e instanceof Error ? e.message : "")) throw e; }
|
|
194
197
|
})
|
|
195
198
|
.catch((err) => console.debug("[message-bridge] editor push failed:", err instanceof Error ? err.message : err));
|
|
196
199
|
return undefined;
|
|
@@ -214,7 +217,7 @@ export default function messageBridgeExtension(pi: any) {
|
|
|
214
217
|
.then((id) => pullAnswer(id))
|
|
215
218
|
.then((answer) => {
|
|
216
219
|
if (answer?.trim()) {
|
|
217
|
-
pi.sendUserMessage(answer.trim());
|
|
220
|
+
try { pi.sendUserMessage(answer.trim()); } catch (e) { if (!/stale/i.test(e instanceof Error ? e.message : "")) throw e; }
|
|
218
221
|
}
|
|
219
222
|
})
|
|
220
223
|
.catch((err) => console.debug("[message-bridge] agent_end push failed:", err instanceof Error ? err.message : err));
|
|
@@ -217,6 +217,9 @@ function saveFullOutput(content: string, ctx: ExtensionContext): string | undefi
|
|
|
217
217
|
// ============================================================================
|
|
218
218
|
|
|
219
219
|
export default function outputGuard(pi: ExtensionAPI) {
|
|
220
|
+
pi.setName("output-guard");
|
|
221
|
+
let truncatedCount = 0;
|
|
222
|
+
let limitAdjustedCount = 0;
|
|
220
223
|
// ------------------------------------------------------------------
|
|
221
224
|
// 1. Global truncation fallback via tool_result hook
|
|
222
225
|
//
|
|
@@ -255,6 +258,7 @@ export default function outputGuard(pi: ExtensionAPI) {
|
|
|
255
258
|
|
|
256
259
|
// Truncate
|
|
257
260
|
const result = truncateOutput(fullText, config, ctx);
|
|
261
|
+
truncatedCount++;
|
|
258
262
|
|
|
259
263
|
let finalContent = result.content;
|
|
260
264
|
if (result.truncated) {
|
|
@@ -262,6 +266,21 @@ export default function outputGuard(pi: ExtensionAPI) {
|
|
|
262
266
|
finalContent = finalContent + "\n\n" + notice;
|
|
263
267
|
}
|
|
264
268
|
|
|
269
|
+
console.debug(
|
|
270
|
+
`[output-guard] truncated tool "${event.toolName}": ${result.totalLines} lines / ${result.totalBytes} bytes → ${result.outputLines} lines / ${result.outputBytes} bytes (truncatedBy: ${result.truncatedBy ?? "none"}, path: ${result.fullOutputPath ?? "N/A"})`,
|
|
271
|
+
);
|
|
272
|
+
pi.appendEntry("output_guard_truncate", {
|
|
273
|
+
toolName: event.toolName,
|
|
274
|
+
totalLines: result.totalLines,
|
|
275
|
+
totalBytes: result.totalBytes,
|
|
276
|
+
outputLines: result.outputLines,
|
|
277
|
+
outputBytes: result.outputBytes,
|
|
278
|
+
truncated: result.truncated,
|
|
279
|
+
truncatedBy: result.truncatedBy,
|
|
280
|
+
fullOutputPath: result.fullOutputPath,
|
|
281
|
+
truncatedCount,
|
|
282
|
+
});
|
|
283
|
+
|
|
265
284
|
return {
|
|
266
285
|
content: [{ type: "text" as const, text: finalContent }],
|
|
267
286
|
};
|
|
@@ -281,7 +300,9 @@ export default function outputGuard(pi: ExtensionAPI) {
|
|
|
281
300
|
if (event.toolName === "find") {
|
|
282
301
|
const input = event.input as { limit?: number };
|
|
283
302
|
if (input.limit === undefined || input.limit > config.findLimit) {
|
|
303
|
+
console.debug(`[output-guard] capped find limit: ${input.limit ?? "unlimited"} → ${config.findLimit}`);
|
|
284
304
|
input.limit = config.findLimit;
|
|
305
|
+
limitAdjustedCount++;
|
|
285
306
|
}
|
|
286
307
|
}
|
|
287
308
|
|
|
@@ -289,7 +310,9 @@ export default function outputGuard(pi: ExtensionAPI) {
|
|
|
289
310
|
if (event.toolName === "ls") {
|
|
290
311
|
const input = event.input as { limit?: number };
|
|
291
312
|
if (input.limit === undefined || input.limit > config.lsLimit) {
|
|
313
|
+
console.debug(`[output-guard] capped ls limit: ${input.limit ?? "unlimited"} → ${config.lsLimit}`);
|
|
292
314
|
input.limit = config.lsLimit;
|
|
315
|
+
limitAdjustedCount++;
|
|
293
316
|
}
|
|
294
317
|
}
|
|
295
318
|
});
|
|
@@ -335,11 +358,18 @@ export default function outputGuard(pi: ExtensionAPI) {
|
|
|
335
358
|
try {
|
|
336
359
|
const buffer = await fs.readFile(absolutePath);
|
|
337
360
|
|
|
361
|
+
console.debug(`[output-guard] pdf_read: ${args.path} (${buffer.length} bytes, pages: ${args.maxPages ?? "all"})`);
|
|
362
|
+
|
|
338
363
|
// Dynamic import of pdf-parse (optional dependency)
|
|
339
364
|
let pdfParse: typeof import("pdf-parse") | undefined;
|
|
340
365
|
try {
|
|
341
366
|
pdfParse = (await import("pdf-parse")).default;
|
|
342
367
|
} catch {
|
|
368
|
+
console.debug("[output-guard] pdf_read failed: pdf-parse not installed");
|
|
369
|
+
pi.appendEntry("output_guard_pdf_error", {
|
|
370
|
+
path: args.path,
|
|
371
|
+
error: "pdf-parse not installed",
|
|
372
|
+
});
|
|
343
373
|
return {
|
|
344
374
|
content: [
|
|
345
375
|
{
|
|
@@ -354,6 +384,15 @@ export default function outputGuard(pi: ExtensionAPI) {
|
|
|
354
384
|
const data = await pdfParse(buffer);
|
|
355
385
|
let text = data.text;
|
|
356
386
|
|
|
387
|
+
console.debug(`[output-guard] pdf_read success: ${args.path} (${data.numpages} pages, ${text.length} chars extracted)`);
|
|
388
|
+
pi.appendEntry("output_guard_pdf_read", {
|
|
389
|
+
path: args.path,
|
|
390
|
+
pages: data.numpages,
|
|
391
|
+
chars: text.length,
|
|
392
|
+
title: data.info?.Title ?? null,
|
|
393
|
+
author: data.info?.Author ?? null,
|
|
394
|
+
});
|
|
395
|
+
|
|
357
396
|
// Add metadata header
|
|
358
397
|
const header = [
|
|
359
398
|
`PDF: ${args.path}`,
|
|
@@ -101,6 +101,8 @@ const PreviewParams = Type.Object({
|
|
|
101
101
|
title: Type.Optional(Type.String({ description: "Optional display title for the card" })),
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
let previewId = 0;
|
|
105
|
+
|
|
104
106
|
export default function (pi: ExtensionAPI) {
|
|
105
107
|
pi.registerTool({
|
|
106
108
|
name: "preview",
|
|
@@ -118,8 +120,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
118
120
|
ctx?: ExtensionContext,
|
|
119
121
|
): Promise<AgentToolResult<PreviewDetails>> {
|
|
120
122
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
123
|
+
previewId++;
|
|
121
124
|
|
|
122
125
|
if (!params.source?.trim()) {
|
|
126
|
+
console.debug(`[preview] #${previewId} error: source is required`);
|
|
127
|
+
pi.appendEntry("preview", { id: previewId, status: "error", error: "source required" });
|
|
123
128
|
return {
|
|
124
129
|
content: [{ type: "text", text: "Error: source is required" }],
|
|
125
130
|
details: { source: "", resourceType: "text", status: "error", error: "source required" },
|
|
@@ -138,6 +143,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
138
143
|
const reachable = await checkReachable(parsed.hostname, port);
|
|
139
144
|
if (!reachable) {
|
|
140
145
|
const msg = `Preview 失败:${parsed.host} 未在局域网开放,服务可能只监听 127.0.0.1。请将服务绑定到 0.0.0.0 后重试。`;
|
|
146
|
+
console.debug(`[preview] #${previewId} error: local address "${parsed.host}:${parsed.port || 80}" not reachable`);
|
|
147
|
+
pi.appendEntry("preview", { id: previewId, source: params.source, status: "error", error: "local address not reachable", host: parsed.host, port: parseInt(parsed.port || "80", 10) });
|
|
141
148
|
return {
|
|
142
149
|
content: [{ type: "text", text: msg }],
|
|
143
150
|
details: {
|
|
@@ -156,6 +163,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
156
163
|
}
|
|
157
164
|
}
|
|
158
165
|
|
|
166
|
+
console.debug(`[preview] #${previewId} url: ${params.source}`);
|
|
167
|
+
pi.appendEntry("preview", { id: previewId, source: params.source, type: "url", status: "ok", title: params.title });
|
|
159
168
|
return {
|
|
160
169
|
content: [{ type: "text", text: `Preview: ${params.source} (url)` }],
|
|
161
170
|
details: {
|
|
@@ -169,6 +178,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
if (!absolutePath || !existsSync(absolutePath)) {
|
|
181
|
+
console.debug(`[preview] #${previewId} not_found: ${params.source}`);
|
|
182
|
+
pi.appendEntry("preview", { id: previewId, source: params.source, type: resourceType, status: "not_found" });
|
|
172
183
|
return {
|
|
173
184
|
content: [{ type: "text", text: `Preview: ${params.source} not found` }],
|
|
174
185
|
details: {
|
|
@@ -184,6 +195,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
184
195
|
|
|
185
196
|
const stat = statSync(absolutePath);
|
|
186
197
|
if (stat.isDirectory()) {
|
|
198
|
+
console.debug(`[preview] #${previewId} error: ${params.source} is a directory`);
|
|
199
|
+
pi.appendEntry("preview", { id: previewId, source: params.source, type: resourceType, status: "error", error: "is a directory" });
|
|
187
200
|
return {
|
|
188
201
|
content: [{ type: "text", text: `Preview: ${params.source} is a directory` }],
|
|
189
202
|
details: {
|
|
@@ -204,6 +217,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
204
217
|
? `${(stat.size / 1024).toFixed(1)}KB`
|
|
205
218
|
: `${stat.size}B`;
|
|
206
219
|
|
|
220
|
+
console.debug(`[preview] #${previewId} ok: ${params.source} (${resourceType}, ${sizeStr})`);
|
|
221
|
+
pi.appendEntry("preview", {
|
|
222
|
+
id: previewId,
|
|
223
|
+
source: params.source,
|
|
224
|
+
type: resourceType,
|
|
225
|
+
mimeType,
|
|
226
|
+
size: stat.size,
|
|
227
|
+
status: "ok",
|
|
228
|
+
title: params.title,
|
|
229
|
+
});
|
|
207
230
|
return {
|
|
208
231
|
content: [
|
|
209
232
|
{
|
|
@@ -369,14 +369,20 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
369
369
|
|
|
370
370
|
currentState = "continuing";
|
|
371
371
|
emitStatusChanged();
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
372
|
+
try {
|
|
373
|
+
pi.sendMessage(
|
|
374
|
+
{
|
|
375
|
+
customType: "supervisor_continue",
|
|
376
|
+
content: continueMessage,
|
|
377
|
+
display: true,
|
|
378
|
+
},
|
|
379
|
+
{ triggerTurn: true },
|
|
380
|
+
);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
383
|
+
if (/stale|abort/i.test(msg)) return;
|
|
384
|
+
throw err;
|
|
385
|
+
}
|
|
380
386
|
});
|
|
381
387
|
}
|
|
382
388
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for extractParentTodos — extracts parent session's todo list
|
|
3
|
+
* from session history entries for read-only reference in sub-agents.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { extractParentTodos } from "./index.js";
|
|
7
|
+
|
|
8
|
+
interface CustomEntry {
|
|
9
|
+
type: "custom";
|
|
10
|
+
customType: string;
|
|
11
|
+
data?: { todos: unknown[]; nextId?: number };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface MessageEntry {
|
|
15
|
+
type: "message";
|
|
16
|
+
message: {
|
|
17
|
+
role: string;
|
|
18
|
+
toolName?: string;
|
|
19
|
+
details?: { todos: unknown[]; nextId?: number };
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Entry = CustomEntry | MessageEntry;
|
|
24
|
+
|
|
25
|
+
function customEntry(customType: string, todos: unknown[], nextId = 1): CustomEntry {
|
|
26
|
+
return { type: "custom", customType, data: { todos, nextId } };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toolResultEntry(details: { todos: unknown[]; nextId?: number }): MessageEntry {
|
|
30
|
+
return {
|
|
31
|
+
type: "message",
|
|
32
|
+
message: { role: "toolResult", toolName: "todo", details },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function userMessageEntry(): MessageEntry {
|
|
37
|
+
return { type: "message", message: { role: "user" } };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function assistantMessageEntry(): MessageEntry {
|
|
41
|
+
return { type: "message", message: { role: "assistant" } };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("extractParentTodos", () => {
|
|
45
|
+
it("returns empty array when branch is empty", () => {
|
|
46
|
+
expect(extractParentTodos([])).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns empty array when branch has no todo entries", () => {
|
|
50
|
+
const branch: Entry[] = [userMessageEntry(), assistantMessageEntry()];
|
|
51
|
+
expect(extractParentTodos(branch)).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns active todos from custom entry", () => {
|
|
55
|
+
const todos = [
|
|
56
|
+
{ id: 1, text: "Fix login", done: false },
|
|
57
|
+
{ id: 2, text: "Add tests", done: false },
|
|
58
|
+
];
|
|
59
|
+
const branch: Entry[] = [customEntry("todo", todos)];
|
|
60
|
+
const result = extractParentTodos(branch);
|
|
61
|
+
expect(result).toHaveLength(2);
|
|
62
|
+
expect(result[0]).toEqual({ id: 1, text: "Fix login", priority: undefined, done: false });
|
|
63
|
+
expect(result[1]).toEqual({ id: 2, text: "Add tests", priority: undefined, done: false });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("filters out done todos", () => {
|
|
67
|
+
const todos = [
|
|
68
|
+
{ id: 1, text: "Done task", done: true },
|
|
69
|
+
{ id: 2, text: "Active task", done: false },
|
|
70
|
+
];
|
|
71
|
+
const branch: Entry[] = [customEntry("todo", todos)];
|
|
72
|
+
const result = extractParentTodos(branch);
|
|
73
|
+
expect(result).toHaveLength(1);
|
|
74
|
+
expect(result[0].id).toBe(2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("filters out deleted todos", () => {
|
|
78
|
+
const todos = [
|
|
79
|
+
{ id: 1, text: "Deleted task", done: false, deleted: true },
|
|
80
|
+
{ id: 2, text: "Active task", done: false },
|
|
81
|
+
];
|
|
82
|
+
const branch: Entry[] = [customEntry("todo", todos)];
|
|
83
|
+
const result = extractParentTodos(branch);
|
|
84
|
+
expect(result).toHaveLength(1);
|
|
85
|
+
expect(result[0].id).toBe(2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns the latest todo list from toolResult messages (overwrites previous)", () => {
|
|
89
|
+
const earlyTodos = [{ id: 1, text: "Old task", done: false }];
|
|
90
|
+
const latestTodos = [
|
|
91
|
+
{ id: 1, text: "Old task", done: true },
|
|
92
|
+
{ id: 2, text: "New task", done: false },
|
|
93
|
+
];
|
|
94
|
+
const branch: Entry[] = [customEntry("todo", earlyTodos), toolResultEntry({ todos: latestTodos, nextId: 3 })];
|
|
95
|
+
const result = extractParentTodos(branch);
|
|
96
|
+
expect(result).toHaveLength(1);
|
|
97
|
+
expect(result[0].id).toBe(2);
|
|
98
|
+
expect(result[0].text).toBe("New task");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("preserves priority field", () => {
|
|
102
|
+
const todos = [
|
|
103
|
+
{ id: 1, text: "High priority", done: false, priority: "high" },
|
|
104
|
+
{ id: 2, text: "Low priority", done: false, priority: "low" },
|
|
105
|
+
{ id: 3, text: "Medium priority", done: false },
|
|
106
|
+
];
|
|
107
|
+
const branch: Entry[] = [customEntry("todo", todos)];
|
|
108
|
+
const result = extractParentTodos(branch);
|
|
109
|
+
expect(result[0].priority).toBe("high");
|
|
110
|
+
expect(result[1].priority).toBe("low");
|
|
111
|
+
expect(result[2].priority).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("ignores non-todo entries interspersed", () => {
|
|
115
|
+
const todos = [{ id: 1, text: "The only task", done: false }];
|
|
116
|
+
const branch: Entry[] = [
|
|
117
|
+
userMessageEntry(),
|
|
118
|
+
assistantMessageEntry(),
|
|
119
|
+
customEntry("some_other_thing", [{ unrelated: true }]),
|
|
120
|
+
customEntry("todo", todos),
|
|
121
|
+
userMessageEntry(),
|
|
122
|
+
];
|
|
123
|
+
const result = extractParentTodos(branch);
|
|
124
|
+
expect(result).toHaveLength(1);
|
|
125
|
+
expect(result[0].text).toBe("The only task");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("handles toolResult with no matching details gracefully", () => {
|
|
129
|
+
const branch: Entry[] = [
|
|
130
|
+
{
|
|
131
|
+
type: "message",
|
|
132
|
+
message: { role: "toolResult", toolName: "bash", details: { exitCode: 0 } },
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
expect(extractParentTodos(branch)).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("returns empty array when all todos are done", () => {
|
|
139
|
+
const todos = [
|
|
140
|
+
{ id: 1, text: "Done A", done: true },
|
|
141
|
+
{ id: 2, text: "Done B", done: true, deleted: true },
|
|
142
|
+
];
|
|
143
|
+
const branch: Entry[] = [customEntry("todo", todos)];
|
|
144
|
+
expect(extractParentTodos(branch)).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
});
|