pi-shit 0.1.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/README.md +44 -0
- package/extensions/README.md +23 -0
- package/extensions/deep-review/README.md +56 -0
- package/extensions/deep-review/index.test.ts +97 -0
- package/extensions/deep-review/index.ts +1541 -0
- package/extensions/pi-notify/LICENSE +21 -0
- package/extensions/pi-notify/README.md +84 -0
- package/extensions/pi-notify/index.ts +75 -0
- package/extensions/pi-notify/package.json +28 -0
- package/extensions/plan-mode/README.md +69 -0
- package/extensions/plan-mode/index.ts +345 -0
- package/extensions/plan-mode/utils.test.ts +261 -0
- package/extensions/plan-mode/utils.ts +168 -0
- package/package.json +35 -0
- package/skills/README.md +70 -0
- package/skills/brave-search/SKILL.md +83 -0
- package/skills/brave-search/content.js +86 -0
- package/skills/brave-search/package-lock.json +623 -0
- package/skills/brave-search/package.json +14 -0
- package/skills/brave-search/search.js +199 -0
- package/skills/code-review/SKILL.md +97 -0
- package/skills/code-simplifier/SKILL.md +55 -0
- package/skills/context-packer/SKILL.md +77 -0
- package/skills/context-packer/prepare-context.sh +490 -0
- package/skills/image-compress/SKILL.md +53 -0
- package/skills/image-compress/compress.sh +172 -0
- package/skills/markdown-converter/SKILL.md +71 -0
- package/skills/multi-review/SKILL.md +143 -0
- package/skills/package.json +26 -0
- package/skills/pr-context-packer/SKILL.md +76 -0
- package/skills/pr-context-packer/prepare-pr-context.sh +941 -0
- package/skills/session-analyzer/IDEAS.md +42 -0
- package/skills/session-analyzer/SKILL.md +81 -0
- package/skills/session-analyzer/analyze.js +460 -0
- package/skills/session-analyzer/package-lock.json +3943 -0
- package/skills/session-analyzer/package.json +7 -0
- package/skills/video-compress/SKILL.md +43 -0
- package/skills/video-compress/compress.sh +107 -0
- package/skills/youtube-transcript/SKILL.md +59 -0
- package/skills/youtube-transcript/transcript.sh +46 -0
- package/themes/rose-pine-dawn.json +102 -0
- package/themes/rose-pine.json +102 -0
|
@@ -0,0 +1,1541 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { access, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { calculateCost, getModel, type Model, type Usage } from "@mariozechner/pi-ai";
|
|
7
|
+
import { getMarkdownTheme, type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import { Markdown } from "@mariozechner/pi-tui";
|
|
9
|
+
|
|
10
|
+
type ReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
11
|
+
type TextVerbosity = "low" | "medium" | "high";
|
|
12
|
+
type ReasoningSummary = "auto" | "detailed" | null;
|
|
13
|
+
|
|
14
|
+
type DeepReviewOptions = {
|
|
15
|
+
query: string;
|
|
16
|
+
projectDir: string;
|
|
17
|
+
baseRef?: string;
|
|
18
|
+
model: string;
|
|
19
|
+
effort: ReasoningEffort;
|
|
20
|
+
verbosity: TextVerbosity;
|
|
21
|
+
summary: ReasoningSummary;
|
|
22
|
+
organization?: string;
|
|
23
|
+
projectId?: string;
|
|
24
|
+
debug: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ParseResult =
|
|
28
|
+
| {
|
|
29
|
+
ok: true;
|
|
30
|
+
options: DeepReviewOptions;
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
ok: false;
|
|
34
|
+
message: string;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
ok: false;
|
|
38
|
+
help: true;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type ActiveRun = {
|
|
42
|
+
controller: AbortController;
|
|
43
|
+
child?: ChildProcess;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type ContextPackResult = {
|
|
47
|
+
stdout: string;
|
|
48
|
+
stderr: string;
|
|
49
|
+
exitCode: number;
|
|
50
|
+
durationMs: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type ResponsesResult = {
|
|
54
|
+
responseId?: string;
|
|
55
|
+
answer: string;
|
|
56
|
+
thinking: string;
|
|
57
|
+
usage: {
|
|
58
|
+
inputTokens: number;
|
|
59
|
+
outputTokens: number;
|
|
60
|
+
cachedTokens: number;
|
|
61
|
+
totalTokens: number;
|
|
62
|
+
estimatedCostUsd?: number;
|
|
63
|
+
};
|
|
64
|
+
durationMs: number;
|
|
65
|
+
debugEvents: string[];
|
|
66
|
+
debugPayload?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type ClipboardResult = {
|
|
70
|
+
copied: boolean;
|
|
71
|
+
method?: string;
|
|
72
|
+
error?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type OutputArtifacts = {
|
|
76
|
+
directory: string;
|
|
77
|
+
answerPath: string;
|
|
78
|
+
thinkingPath: string;
|
|
79
|
+
reportPath: string;
|
|
80
|
+
metadataPath: string;
|
|
81
|
+
clipboard: ClipboardResult;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type LiveState = {
|
|
85
|
+
startedAt: number;
|
|
86
|
+
phase: "context-pack" | "responses";
|
|
87
|
+
thinking: string;
|
|
88
|
+
answer: string;
|
|
89
|
+
responsesEventCount: number;
|
|
90
|
+
lastResponsesEventType?: string;
|
|
91
|
+
lastRenderAt: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const HELP_TEXT = `# /deep-review
|
|
95
|
+
|
|
96
|
+
Run PR context packing via a nested \`pi -p\` skill call, then send a direct OpenAI Responses API request.
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
|
|
100
|
+
/deep-review <query> [options]
|
|
101
|
+
|
|
102
|
+
A query is required, either as positional text or via \`--query\`.
|
|
103
|
+
|
|
104
|
+
## Options
|
|
105
|
+
|
|
106
|
+
- \`--query <text>\` Review request text (alternative to positional query; cannot combine both)
|
|
107
|
+
- \`--project <path>\` Project dir for context packing (default: current cwd)
|
|
108
|
+
- \`--base <ref>\` Base ref for context pack diff (default: context-packer auto-detect)
|
|
109
|
+
- \`--model <id>\` Responses model (default: \`gpt-5.2\`)
|
|
110
|
+
- \`--effort <level>\` \`minimal|low|medium|high|xhigh\` (default: \`xhigh\`)
|
|
111
|
+
- \`--verbosity <level>\` \`low|medium|high\` (default: \`medium\`)
|
|
112
|
+
- \`--summary <mode>\` \`auto|detailed|null\` (default: \`auto\`)
|
|
113
|
+
- \`--no-summary\` Shortcut for \`--summary null\`
|
|
114
|
+
- \`--org <id>\` Override \`openai-organization\` header
|
|
115
|
+
- \`--project-id <id>\` Override \`OpenAI-Project\` header
|
|
116
|
+
- \`--debug\` Save payload + stream events to /tmp for parity debugging
|
|
117
|
+
- \`--help\` Show this help
|
|
118
|
+
|
|
119
|
+
## Stop command
|
|
120
|
+
|
|
121
|
+
- \`/deep-review-stop\` stops an in-flight run.
|
|
122
|
+
|
|
123
|
+
## Requirement
|
|
124
|
+
|
|
125
|
+
- Bundled skill file must exist at \`skills/pr-context-packer/SKILL.md\` (via pi-shit package layout).
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
const ANSI_REGEX = new RegExp(String.raw`\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007]*(?:\u0007|\u001b\\)`, "g");
|
|
129
|
+
const WIDGET_TICK_MS = 250;
|
|
130
|
+
const SPINNER_FRAME_MS = 100;
|
|
131
|
+
const MARKDOWN_THEME = getMarkdownTheme();
|
|
132
|
+
|
|
133
|
+
function stripAnsi(value: string): string {
|
|
134
|
+
return value.replace(ANSI_REGEX, "");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function stripMarkdownFenceLines(value: string): string {
|
|
138
|
+
return value.replace(/^[ \t]*```[^\r\n]*\r?\n?/gm, "").replace(/```+/g, "");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function truncate(value: string, maxChars: number): { text: string; truncated: boolean } {
|
|
142
|
+
if (value.length <= maxChars) {
|
|
143
|
+
return { text: value, truncated: false };
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
text: `${value.slice(0, maxChars)}\n\n...[truncated ${value.length - maxChars} chars]`,
|
|
147
|
+
truncated: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function formatDuration(ms: number): string {
|
|
152
|
+
if (ms < 1000) {
|
|
153
|
+
return `${ms}ms`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const totalSeconds = ms / 1000;
|
|
157
|
+
if (totalSeconds < 60) {
|
|
158
|
+
return `${totalSeconds.toFixed(1)}s`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
162
|
+
const seconds = Math.floor(totalSeconds % 60);
|
|
163
|
+
return `${minutes}m ${seconds}s`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function describeResponsesEvent(eventType: string): string {
|
|
167
|
+
switch (eventType) {
|
|
168
|
+
case "response.created":
|
|
169
|
+
return "request accepted";
|
|
170
|
+
case "response.in_progress":
|
|
171
|
+
return "model reasoning";
|
|
172
|
+
case "response.output_item.added":
|
|
173
|
+
return "new output block";
|
|
174
|
+
case "response.reasoning_summary_part.added":
|
|
175
|
+
return "thinking started";
|
|
176
|
+
case "response.reasoning_summary_text.delta":
|
|
177
|
+
return "thinking update";
|
|
178
|
+
case "response.output_text.delta":
|
|
179
|
+
return "answer update";
|
|
180
|
+
case "response.output_text.done":
|
|
181
|
+
return "answer block finished";
|
|
182
|
+
case "response.content_part.added":
|
|
183
|
+
return "content part added";
|
|
184
|
+
case "response.completed":
|
|
185
|
+
return "response complete";
|
|
186
|
+
case "error":
|
|
187
|
+
return "stream error";
|
|
188
|
+
default:
|
|
189
|
+
return eventType.startsWith("response.")
|
|
190
|
+
? eventType.slice("response.".length).replace(/[._]/g, " ")
|
|
191
|
+
: eventType.replace(/[._]/g, " ");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function runClipboardProgram(command: string, args: string[], text: string): Promise<void> {
|
|
196
|
+
await new Promise<void>((resolve, reject) => {
|
|
197
|
+
const child = spawn(command, args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
198
|
+
let stderr = "";
|
|
199
|
+
|
|
200
|
+
child.on("error", (error) => {
|
|
201
|
+
reject(error);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
child.stderr.on("data", (chunk) => {
|
|
205
|
+
stderr += chunk.toString();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
child.on("close", (code) => {
|
|
209
|
+
if (code === 0) {
|
|
210
|
+
resolve();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
reject(new Error(stderr.trim() || `${command} exited with code ${code ?? "unknown"}`));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
child.stdin.on("error", () => {
|
|
218
|
+
// Ignore EPIPE and similar shutdown errors.
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
child.stdin.end(text);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function copyTextToClipboard(text: string): Promise<ClipboardResult> {
|
|
226
|
+
const payload = text.trim().length > 0 ? text : "(no output text returned)";
|
|
227
|
+
|
|
228
|
+
const candidates: Array<{ command: string; args: string[]; method: string }> =
|
|
229
|
+
process.platform === "darwin"
|
|
230
|
+
? [{ command: "pbcopy", args: [], method: "pbcopy" }]
|
|
231
|
+
: process.platform === "win32"
|
|
232
|
+
? [{ command: "clip", args: [], method: "clip" }]
|
|
233
|
+
: [
|
|
234
|
+
{ command: "wl-copy", args: [], method: "wl-copy" },
|
|
235
|
+
{ command: "xclip", args: ["-selection", "clipboard"], method: "xclip" },
|
|
236
|
+
{ command: "xsel", args: ["--clipboard", "--input"], method: "xsel" },
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
const errors: string[] = [];
|
|
240
|
+
|
|
241
|
+
for (const candidate of candidates) {
|
|
242
|
+
try {
|
|
243
|
+
await runClipboardProgram(candidate.command, candidate.args, payload);
|
|
244
|
+
return { copied: true, method: candidate.method };
|
|
245
|
+
} catch (error) {
|
|
246
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
247
|
+
errors.push(`${candidate.method}: ${message}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
copied: false,
|
|
253
|
+
error: errors.length > 0 ? errors.join(" | ") : "No clipboard command available",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function writeOutputArtifacts(
|
|
258
|
+
options: DeepReviewOptions,
|
|
259
|
+
packPath: string,
|
|
260
|
+
responses: ResponsesResult,
|
|
261
|
+
totalDurationMs: number,
|
|
262
|
+
reportContent: string,
|
|
263
|
+
debugDir?: string,
|
|
264
|
+
): Promise<OutputArtifacts> {
|
|
265
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "deep-review-output-"));
|
|
266
|
+
const answerPath = path.join(directory, "answer.txt");
|
|
267
|
+
const thinkingPath = path.join(directory, "thinking.txt");
|
|
268
|
+
const reportPath = path.join(directory, "report.md");
|
|
269
|
+
const metadataPath = path.join(directory, "metadata.json");
|
|
270
|
+
|
|
271
|
+
const answerText = responses.answer.trim().length > 0 ? responses.answer : "(no output text returned)";
|
|
272
|
+
const thinkingText =
|
|
273
|
+
responses.thinking.trim().length > 0 ? responses.thinking : "(no reasoning summary text returned)";
|
|
274
|
+
|
|
275
|
+
await writeFile(answerPath, `${answerText}\n`, "utf8");
|
|
276
|
+
await writeFile(thinkingPath, `${thinkingText}\n`, "utf8");
|
|
277
|
+
await writeFile(reportPath, `${reportContent}\n`, "utf8");
|
|
278
|
+
|
|
279
|
+
const metadata = {
|
|
280
|
+
createdAt: new Date().toISOString(),
|
|
281
|
+
query: options.query,
|
|
282
|
+
model: options.model,
|
|
283
|
+
effort: options.effort,
|
|
284
|
+
summary: options.summary,
|
|
285
|
+
verbosity: options.verbosity,
|
|
286
|
+
contextPackPath: packPath,
|
|
287
|
+
totalDurationMs,
|
|
288
|
+
usage: responses.usage,
|
|
289
|
+
responseId: responses.responseId,
|
|
290
|
+
debugDir,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
await writeFile(metadataPath, `${JSON.stringify(metadata, null, 4)}\n`, "utf8");
|
|
294
|
+
|
|
295
|
+
const clipboard = await copyTextToClipboard(answerText);
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
directory,
|
|
299
|
+
answerPath,
|
|
300
|
+
thinkingPath,
|
|
301
|
+
reportPath,
|
|
302
|
+
metadataPath,
|
|
303
|
+
clipboard,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function quoteForPrompt(value: string): string {
|
|
308
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
|
|
309
|
+
return value;
|
|
310
|
+
}
|
|
311
|
+
return JSON.stringify(value);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function splitArgs(input: string, platform = process.platform): string[] {
|
|
315
|
+
const tokens: string[] = [];
|
|
316
|
+
let current = "";
|
|
317
|
+
let quote: '"' | "'" | null = null;
|
|
318
|
+
let escaping = false;
|
|
319
|
+
|
|
320
|
+
for (const char of input) {
|
|
321
|
+
if (escaping) {
|
|
322
|
+
current += char;
|
|
323
|
+
escaping = false;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (char === "\\") {
|
|
328
|
+
const shouldEscape = platform !== "win32" || quote !== null;
|
|
329
|
+
if (shouldEscape) {
|
|
330
|
+
escaping = true;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
current += char;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (quote) {
|
|
339
|
+
if (char === quote) {
|
|
340
|
+
quote = null;
|
|
341
|
+
} else {
|
|
342
|
+
current += char;
|
|
343
|
+
}
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (char === '"' || char === "'") {
|
|
348
|
+
quote = char;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (/\s/.test(char)) {
|
|
353
|
+
if (current.length > 0) {
|
|
354
|
+
tokens.push(current);
|
|
355
|
+
current = "";
|
|
356
|
+
}
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
current += char;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (escaping) {
|
|
364
|
+
current += "\\";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (current.length > 0) {
|
|
368
|
+
tokens.push(current);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return tokens;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function normalizeSummary(value: string): ReasoningSummary | undefined {
|
|
375
|
+
const lowered = value.toLowerCase();
|
|
376
|
+
if (lowered === "auto") return "auto";
|
|
377
|
+
if (lowered === "detailed") return "detailed";
|
|
378
|
+
if (lowered === "null" || lowered === "none" || lowered === "off") return null;
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function parseOptions(rawArgs: string, cwd: string): ParseResult {
|
|
383
|
+
const tokens = splitArgs(rawArgs);
|
|
384
|
+
|
|
385
|
+
const options: DeepReviewOptions = {
|
|
386
|
+
query: "",
|
|
387
|
+
projectDir: cwd,
|
|
388
|
+
model: "gpt-5.2",
|
|
389
|
+
effort: "xhigh",
|
|
390
|
+
verbosity: "medium",
|
|
391
|
+
summary: "auto",
|
|
392
|
+
debug: false,
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const positional: string[] = [];
|
|
396
|
+
|
|
397
|
+
const takeValue = (index: number): string | null => {
|
|
398
|
+
const value = tokens[index + 1];
|
|
399
|
+
if (!value || value.startsWith("--")) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
return value;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
406
|
+
const token = tokens[i];
|
|
407
|
+
|
|
408
|
+
if (token === "--help" || token === "-h") {
|
|
409
|
+
return { ok: false, help: true };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!token.startsWith("--")) {
|
|
413
|
+
positional.push(token);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
switch (token) {
|
|
418
|
+
case "--project": {
|
|
419
|
+
const value = takeValue(i);
|
|
420
|
+
if (!value) return { ok: false, message: `${token} requires a value` };
|
|
421
|
+
options.projectDir = value;
|
|
422
|
+
i++;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case "--base": {
|
|
426
|
+
const value = takeValue(i);
|
|
427
|
+
if (!value) return { ok: false, message: `${token} requires a value` };
|
|
428
|
+
options.baseRef = value;
|
|
429
|
+
i++;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
case "--model": {
|
|
433
|
+
const value = takeValue(i);
|
|
434
|
+
if (!value) return { ok: false, message: `${token} requires a value` };
|
|
435
|
+
options.model = value;
|
|
436
|
+
i++;
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
case "--effort": {
|
|
440
|
+
const value = takeValue(i);
|
|
441
|
+
if (!value) return { ok: false, message: `${token} requires a value` };
|
|
442
|
+
if (!["minimal", "low", "medium", "high", "xhigh"].includes(value)) {
|
|
443
|
+
return { ok: false, message: `Invalid effort: ${value}` };
|
|
444
|
+
}
|
|
445
|
+
options.effort = value as ReasoningEffort;
|
|
446
|
+
i++;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case "--verbosity": {
|
|
450
|
+
const value = takeValue(i);
|
|
451
|
+
if (!value) return { ok: false, message: `${token} requires a value` };
|
|
452
|
+
if (!["low", "medium", "high"].includes(value)) {
|
|
453
|
+
return { ok: false, message: `Invalid verbosity: ${value}` };
|
|
454
|
+
}
|
|
455
|
+
options.verbosity = value as TextVerbosity;
|
|
456
|
+
i++;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
case "--summary": {
|
|
460
|
+
const value = takeValue(i);
|
|
461
|
+
if (!value) return { ok: false, message: `${token} requires a value` };
|
|
462
|
+
const normalized = normalizeSummary(value);
|
|
463
|
+
if (normalized === undefined) {
|
|
464
|
+
return { ok: false, message: `Invalid summary mode: ${value}` };
|
|
465
|
+
}
|
|
466
|
+
options.summary = normalized;
|
|
467
|
+
i++;
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
case "--no-summary": {
|
|
471
|
+
options.summary = null;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
case "--org": {
|
|
475
|
+
const value = takeValue(i);
|
|
476
|
+
if (!value) return { ok: false, message: `${token} requires a value` };
|
|
477
|
+
options.organization = value;
|
|
478
|
+
i++;
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
case "--project-id": {
|
|
482
|
+
const value = takeValue(i);
|
|
483
|
+
if (!value) return { ok: false, message: `${token} requires a value` };
|
|
484
|
+
options.projectId = value;
|
|
485
|
+
i++;
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
case "--query": {
|
|
489
|
+
const value = takeValue(i);
|
|
490
|
+
if (!value) return { ok: false, message: `${token} requires a value` };
|
|
491
|
+
options.query = value;
|
|
492
|
+
i++;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
case "--debug": {
|
|
496
|
+
options.debug = true;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
default:
|
|
500
|
+
return { ok: false, message: `Unknown option: ${token}` };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (positional.length > 0) {
|
|
505
|
+
if (options.query) {
|
|
506
|
+
return {
|
|
507
|
+
ok: false,
|
|
508
|
+
message: "Query provided both positionally and via --query; choose one.",
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
options.query = positional.join(" ").trim();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
options.query = options.query.trim();
|
|
516
|
+
if (!options.query) {
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
message: 'Query is required. Use /deep-review "..." or pass --query "...".',
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
options.projectDir = path.resolve(cwd, options.projectDir);
|
|
524
|
+
|
|
525
|
+
return { ok: true, options };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function pathExists(value: string): Promise<boolean> {
|
|
529
|
+
try {
|
|
530
|
+
await access(value);
|
|
531
|
+
return true;
|
|
532
|
+
} catch {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function resolveBundledContextPackerSkillPath(): Promise<string> {
|
|
538
|
+
const extensionDir = path.dirname(fileURLToPath(import.meta.url));
|
|
539
|
+
const bundledPath = path.resolve(extensionDir, "..", "..", "skills", "pr-context-packer", "SKILL.md");
|
|
540
|
+
|
|
541
|
+
if (!(await pathExists(bundledPath))) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`Bundled skill not found at ${bundledPath}. Install/run deep-review from the pi-shit package layout (extensions + skills).`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return bundledPath;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function buildContextPackSkillPrompt(options: DeepReviewOptions): string {
|
|
551
|
+
const args = [quoteForPrompt(options.projectDir), "--no-clipboard"];
|
|
552
|
+
|
|
553
|
+
if (options.baseRef) {
|
|
554
|
+
args.push("--base", quoteForPrompt(options.baseRef));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const skillInvocation = `/skill:pr-context-packer ${args.join(" ")}`;
|
|
558
|
+
|
|
559
|
+
return [
|
|
560
|
+
skillInvocation,
|
|
561
|
+
"",
|
|
562
|
+
"Run the context pack workflow now.",
|
|
563
|
+
"Return only the script terminal output in plain text, with no commentary.",
|
|
564
|
+
].join("\n");
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function isLikelyContextPackPath(value: string): boolean {
|
|
568
|
+
return value.endsWith(".txt") && (value.includes("pr-context") || value.includes("context-packer"));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function rankContextPackCandidate(value: string): number {
|
|
572
|
+
if (value.endsWith("/pr-context.txt") || value.endsWith("\\pr-context.txt")) {
|
|
573
|
+
return 0;
|
|
574
|
+
}
|
|
575
|
+
if (value.includes("/tmp/context-packer/") || value.toLowerCase().includes("\\temp\\context-packer\\")) {
|
|
576
|
+
return 1;
|
|
577
|
+
}
|
|
578
|
+
if (value.includes("pr-context")) {
|
|
579
|
+
return 2;
|
|
580
|
+
}
|
|
581
|
+
return 3;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export async function extractContextPackPath(output: string): Promise<string | undefined> {
|
|
585
|
+
const cleaned = stripAnsi(output);
|
|
586
|
+
const candidates: string[] = [];
|
|
587
|
+
|
|
588
|
+
const explicitOutputLine = cleaned
|
|
589
|
+
.split(/\r?\n/)
|
|
590
|
+
.map((line) => line.trim())
|
|
591
|
+
.find((line) => /^(?:📄\s*)?Output:\s+/i.test(line));
|
|
592
|
+
|
|
593
|
+
if (explicitOutputLine) {
|
|
594
|
+
const explicitPath = explicitOutputLine
|
|
595
|
+
.replace(/^(?:📄\s*)?Output:\s+/i, "")
|
|
596
|
+
.trim()
|
|
597
|
+
.replace(/^['"]|['"]$/g, "")
|
|
598
|
+
.replace(/[),.:;]+$/g, "");
|
|
599
|
+
|
|
600
|
+
if (isLikelyContextPackPath(explicitPath)) {
|
|
601
|
+
candidates.push(explicitPath);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const posixMatches = cleaned.match(/\/[\w./-]+\.txt/g) ?? [];
|
|
606
|
+
const winMatches = cleaned.match(/[A-Za-z]:\\[^\s"'<>|?*]+\.txt/g) ?? [];
|
|
607
|
+
|
|
608
|
+
for (const match of [...posixMatches, ...winMatches]) {
|
|
609
|
+
const candidate = match.replace(/[),.:;]+$/g, "");
|
|
610
|
+
if (isLikelyContextPackPath(candidate)) {
|
|
611
|
+
candidates.push(candidate);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const uniqueSortedCandidates = [...new Set(candidates)].sort(
|
|
616
|
+
(left, right) => rankContextPackCandidate(left) - rankContextPackCandidate(right),
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
for (const candidate of uniqueSortedCandidates) {
|
|
620
|
+
if (await pathExists(candidate)) {
|
|
621
|
+
return candidate;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function renderLiveWidget(ctx: ExtensionCommandContext, state: LiveState, force = false): void {
|
|
629
|
+
if (!ctx.hasUI) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
if (!force && now - state.lastRenderAt < 120) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
state.lastRenderAt = now;
|
|
639
|
+
|
|
640
|
+
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
641
|
+
const spinner = spinnerFrames[Math.floor(now / SPINNER_FRAME_MS) % spinnerFrames.length];
|
|
642
|
+
|
|
643
|
+
const lines: string[] = [];
|
|
644
|
+
lines.push(ctx.ui.theme.fg("accent", `${spinner} deep-review · ${state.phase}`));
|
|
645
|
+
lines.push(ctx.ui.theme.fg("dim", `elapsed ${formatDuration(now - state.startedAt)} · /deep-review-stop to stop`));
|
|
646
|
+
|
|
647
|
+
if (state.phase === "context-pack") {
|
|
648
|
+
lines.push(ctx.ui.theme.fg("muted", "building context pack…"));
|
|
649
|
+
} else {
|
|
650
|
+
const streamSummary = state.lastResponsesEventType
|
|
651
|
+
? `${state.responsesEventCount.toLocaleString()} events · ${describeResponsesEvent(state.lastResponsesEventType)}`
|
|
652
|
+
: `${state.responsesEventCount.toLocaleString()} events · waiting for first event`;
|
|
653
|
+
|
|
654
|
+
lines.push(ctx.ui.theme.fg("dim", `stream: ${streamSummary}`));
|
|
655
|
+
|
|
656
|
+
if (state.answer.trim().length > 0) {
|
|
657
|
+
lines.push(ctx.ui.theme.fg("muted", "answer streaming… full markdown answer posts at completion"));
|
|
658
|
+
} else {
|
|
659
|
+
lines.push(ctx.ui.theme.fg("muted", "reasoning in progress… waiting for answer tokens"));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
lines.push(ctx.ui.theme.fg("dim", "handoff files are written after completion"));
|
|
664
|
+
|
|
665
|
+
ctx.ui.setWidget("deep-review-live", lines);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function runContextPackViaPi(
|
|
669
|
+
options: DeepReviewOptions,
|
|
670
|
+
cwd: string,
|
|
671
|
+
skillPath: string,
|
|
672
|
+
activeRun: ActiveRun,
|
|
673
|
+
): Promise<ContextPackResult> {
|
|
674
|
+
const commandPrompt = buildContextPackSkillPrompt(options);
|
|
675
|
+
const args = [
|
|
676
|
+
"-p",
|
|
677
|
+
"--no-session",
|
|
678
|
+
"--no-extensions",
|
|
679
|
+
"--no-skills",
|
|
680
|
+
"--skill",
|
|
681
|
+
skillPath,
|
|
682
|
+
"--thinking",
|
|
683
|
+
"off",
|
|
684
|
+
commandPrompt,
|
|
685
|
+
];
|
|
686
|
+
|
|
687
|
+
return await new Promise<ContextPackResult>((resolve, reject) => {
|
|
688
|
+
const startedAt = Date.now();
|
|
689
|
+
let stdout = "";
|
|
690
|
+
let stderr = "";
|
|
691
|
+
|
|
692
|
+
const child = spawn("pi", args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
693
|
+
activeRun.child = child;
|
|
694
|
+
|
|
695
|
+
let exited = false;
|
|
696
|
+
let forceKillTimer: ReturnType<typeof setTimeout> | undefined;
|
|
697
|
+
|
|
698
|
+
const clearForceKillTimer = () => {
|
|
699
|
+
if (forceKillTimer) {
|
|
700
|
+
clearTimeout(forceKillTimer);
|
|
701
|
+
forceKillTimer = undefined;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const killChild = () => {
|
|
706
|
+
if (exited) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
child.kill("SIGTERM");
|
|
712
|
+
} catch {
|
|
713
|
+
// ignore kill errors if process already exited
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
clearForceKillTimer();
|
|
717
|
+
forceKillTimer = setTimeout(() => {
|
|
718
|
+
if (exited) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
try {
|
|
723
|
+
child.kill("SIGKILL");
|
|
724
|
+
} catch {
|
|
725
|
+
// ignore kill errors if process already exited
|
|
726
|
+
}
|
|
727
|
+
}, 4000);
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const onAbort = () => {
|
|
731
|
+
killChild();
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
if (activeRun.controller.signal.aborted) {
|
|
735
|
+
killChild();
|
|
736
|
+
} else {
|
|
737
|
+
activeRun.controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
child.stdout.on("data", (data) => {
|
|
741
|
+
stdout += data.toString();
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
child.stderr.on("data", (data) => {
|
|
745
|
+
stderr += data.toString();
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
child.on("error", (error) => {
|
|
749
|
+
exited = true;
|
|
750
|
+
clearForceKillTimer();
|
|
751
|
+
activeRun.controller.signal.removeEventListener("abort", onAbort);
|
|
752
|
+
activeRun.child = undefined;
|
|
753
|
+
reject(error);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
child.on("close", (code) => {
|
|
757
|
+
exited = true;
|
|
758
|
+
clearForceKillTimer();
|
|
759
|
+
activeRun.controller.signal.removeEventListener("abort", onAbort);
|
|
760
|
+
activeRun.child = undefined;
|
|
761
|
+
resolve({
|
|
762
|
+
stdout,
|
|
763
|
+
stderr,
|
|
764
|
+
exitCode: code ?? 1,
|
|
765
|
+
durationMs: Date.now() - startedAt,
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
type SseEvent = {
|
|
772
|
+
type?: string;
|
|
773
|
+
[key: string]: unknown;
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
function findSseBoundary(buffer: string): { index: number; length: number } | null {
|
|
777
|
+
const rnIndex = buffer.indexOf("\r\n\r\n");
|
|
778
|
+
const nIndex = buffer.indexOf("\n\n");
|
|
779
|
+
|
|
780
|
+
if (rnIndex === -1 && nIndex === -1) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (rnIndex === -1) {
|
|
785
|
+
return { index: nIndex, length: 2 };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (nIndex === -1) {
|
|
789
|
+
return { index: rnIndex, length: 4 };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return rnIndex < nIndex ? { index: rnIndex, length: 4 } : { index: nIndex, length: 2 };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export async function* parseSseStream(body: ReadableStream<Uint8Array>): AsyncGenerator<SseEvent, void, void> {
|
|
796
|
+
const reader = body.getReader();
|
|
797
|
+
const decoder = new TextDecoder();
|
|
798
|
+
let buffer = "";
|
|
799
|
+
|
|
800
|
+
while (true) {
|
|
801
|
+
const { done, value } = await reader.read();
|
|
802
|
+
if (done) {
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
buffer += decoder.decode(value, { stream: true });
|
|
807
|
+
|
|
808
|
+
while (true) {
|
|
809
|
+
const boundary = findSseBoundary(buffer);
|
|
810
|
+
if (!boundary) {
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const rawEvent = buffer.slice(0, boundary.index);
|
|
815
|
+
buffer = buffer.slice(boundary.index + boundary.length);
|
|
816
|
+
|
|
817
|
+
const lines = rawEvent.split(/\r?\n/);
|
|
818
|
+
const dataLines: string[] = [];
|
|
819
|
+
|
|
820
|
+
for (const line of lines) {
|
|
821
|
+
if (line.startsWith("data:")) {
|
|
822
|
+
dataLines.push(line.slice(5).trimStart());
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (dataLines.length === 0) {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const data = dataLines.join("\n");
|
|
831
|
+
if (data === "[DONE]") {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
try {
|
|
836
|
+
const parsed = JSON.parse(data) as SseEvent;
|
|
837
|
+
yield parsed;
|
|
838
|
+
} catch {
|
|
839
|
+
// Ignore malformed SSE chunks
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function resolveBearerToken(
|
|
846
|
+
ctx: ExtensionCommandContext,
|
|
847
|
+
modelId: string,
|
|
848
|
+
): Promise<{ token: string; source: string }> {
|
|
849
|
+
const fromEnv = process.env.OPENAI_API_KEY?.trim();
|
|
850
|
+
if (fromEnv) {
|
|
851
|
+
return { token: fromEnv, source: "OPENAI_API_KEY" };
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const modelCandidates = [
|
|
855
|
+
getModel("openai", modelId as never),
|
|
856
|
+
getModel("openai-codex", modelId as never),
|
|
857
|
+
getModel("openai", "gpt-5" as never),
|
|
858
|
+
getModel("openai", "gpt-4.1" as never),
|
|
859
|
+
].filter(Boolean);
|
|
860
|
+
|
|
861
|
+
for (const candidate of modelCandidates) {
|
|
862
|
+
const fromAuth = await ctx.modelRegistry.getApiKey(candidate as unknown as Model<any>);
|
|
863
|
+
if (fromAuth) {
|
|
864
|
+
return { token: fromAuth, source: "auth.json/openai" };
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const sessionLike = process.env.OPENAI_SESSION_TOKEN?.trim() ?? process.env.OPENAI_BEARER_TOKEN?.trim();
|
|
869
|
+
if (sessionLike) {
|
|
870
|
+
return { token: sessionLike, source: "OPENAI_SESSION_TOKEN/OPENAI_BEARER_TOKEN" };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
throw new Error("No OpenAI token found. Set OPENAI_API_KEY (recommended) or OPENAI_SESSION_TOKEN.");
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function extractCompletedAnswer(responseObject: any): string {
|
|
877
|
+
const output = Array.isArray(responseObject?.output) ? responseObject.output : [];
|
|
878
|
+
const parts: string[] = [];
|
|
879
|
+
|
|
880
|
+
for (const item of output) {
|
|
881
|
+
if (item?.type !== "message" || !Array.isArray(item?.content)) {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
for (const content of item.content) {
|
|
886
|
+
if (content?.type === "output_text" && typeof content.text === "string") {
|
|
887
|
+
parts.push(content.text);
|
|
888
|
+
}
|
|
889
|
+
if (content?.type === "refusal" && typeof content.refusal === "string") {
|
|
890
|
+
parts.push(content.refusal);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return parts.join("\n").trim();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async function streamResponses(
|
|
899
|
+
options: DeepReviewOptions,
|
|
900
|
+
contextText: string,
|
|
901
|
+
ctx: ExtensionCommandContext,
|
|
902
|
+
signal: AbortSignal,
|
|
903
|
+
onThinkingDelta: (delta: string) => void,
|
|
904
|
+
onAnswerDelta: (delta: string) => void,
|
|
905
|
+
onEvent: (eventType: string) => void,
|
|
906
|
+
): Promise<ResponsesResult> {
|
|
907
|
+
const startedAt = Date.now();
|
|
908
|
+
const { token, source } = await resolveBearerToken(ctx, options.model);
|
|
909
|
+
|
|
910
|
+
const organization = options.organization ?? process.env.OPENAI_ORGANIZATION ?? process.env.OPENAI_ORG_ID;
|
|
911
|
+
const projectId = options.projectId ?? process.env.OPENAI_PROJECT ?? process.env.OPENAI_PROJECT_ID;
|
|
912
|
+
|
|
913
|
+
const payload: Record<string, unknown> = {
|
|
914
|
+
model: options.model,
|
|
915
|
+
input: [
|
|
916
|
+
{
|
|
917
|
+
role: "user",
|
|
918
|
+
content: [{ type: "input_text", text: contextText }],
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
role: "user",
|
|
922
|
+
content: [{ type: "input_text", text: options.query }],
|
|
923
|
+
},
|
|
924
|
+
],
|
|
925
|
+
tools: [],
|
|
926
|
+
text: {
|
|
927
|
+
format: { type: "text" },
|
|
928
|
+
verbosity: options.verbosity,
|
|
929
|
+
},
|
|
930
|
+
reasoning: {
|
|
931
|
+
effort: options.effort,
|
|
932
|
+
summary: options.summary,
|
|
933
|
+
},
|
|
934
|
+
stream: true,
|
|
935
|
+
store: false,
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
const headers: Record<string, string> = {
|
|
939
|
+
Authorization: `Bearer ${token}`,
|
|
940
|
+
"Content-Type": "application/json",
|
|
941
|
+
Accept: "text/event-stream",
|
|
942
|
+
"openai-beta": "responses=v1",
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
if (organization) {
|
|
946
|
+
headers["openai-organization"] = organization;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (projectId) {
|
|
950
|
+
headers["OpenAI-Project"] = projectId;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const response = await fetch("https://api.openai.com/v1/responses", {
|
|
954
|
+
method: "POST",
|
|
955
|
+
headers,
|
|
956
|
+
body: JSON.stringify(payload),
|
|
957
|
+
signal,
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
if (!response.ok || !response.body) {
|
|
961
|
+
const body = await response.text();
|
|
962
|
+
throw new Error(`Responses API failed (${response.status}): ${body}`);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (signal.aborted) {
|
|
966
|
+
throw new Error("Request was aborted");
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const debugEvents: string[] = [];
|
|
970
|
+
let thinking = "";
|
|
971
|
+
let answer = "";
|
|
972
|
+
let completedResponse: any;
|
|
973
|
+
|
|
974
|
+
for await (const event of parseSseStream(response.body)) {
|
|
975
|
+
if (signal.aborted) {
|
|
976
|
+
throw new Error("Request was aborted");
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (options.debug) {
|
|
980
|
+
debugEvents.push(JSON.stringify(event));
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const eventType = typeof event.type === "string" ? event.type : "(unknown)";
|
|
984
|
+
onEvent(eventType);
|
|
985
|
+
|
|
986
|
+
if (event.type === "response.reasoning_summary_text.delta") {
|
|
987
|
+
const delta = typeof event.delta === "string" ? event.delta : "";
|
|
988
|
+
if (delta) {
|
|
989
|
+
thinking += delta;
|
|
990
|
+
onThinkingDelta(delta);
|
|
991
|
+
}
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (event.type === "response.output_text.delta") {
|
|
996
|
+
const delta = typeof event.delta === "string" ? event.delta : "";
|
|
997
|
+
if (delta) {
|
|
998
|
+
answer += delta;
|
|
999
|
+
onAnswerDelta(delta);
|
|
1000
|
+
}
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (event.type === "response.completed") {
|
|
1005
|
+
completedResponse = event.response;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (event.type === "error") {
|
|
1010
|
+
const message = typeof event.message === "string" ? event.message : JSON.stringify(event);
|
|
1011
|
+
throw new Error(`Responses stream error: ${message}`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (!answer.trim() && completedResponse) {
|
|
1016
|
+
answer = extractCompletedAnswer(completedResponse);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const usagePayload = completedResponse?.usage ?? {};
|
|
1020
|
+
const inputTokens = Number(usagePayload.input_tokens ?? 0);
|
|
1021
|
+
const outputTokens = Number(usagePayload.output_tokens ?? 0);
|
|
1022
|
+
const cachedTokens = Number(usagePayload.input_tokens_details?.cached_tokens ?? 0);
|
|
1023
|
+
const totalTokens = Number(usagePayload.total_tokens ?? inputTokens + outputTokens);
|
|
1024
|
+
|
|
1025
|
+
let estimatedCostUsd: number | undefined;
|
|
1026
|
+
const billingModel =
|
|
1027
|
+
(getModel("openai", options.model as never) as Model<any> | undefined) ??
|
|
1028
|
+
(getModel("openai-codex", options.model as never) as Model<any> | undefined);
|
|
1029
|
+
|
|
1030
|
+
if (billingModel) {
|
|
1031
|
+
const usage: Usage = {
|
|
1032
|
+
input: Math.max(0, inputTokens - cachedTokens),
|
|
1033
|
+
output: outputTokens,
|
|
1034
|
+
cacheRead: cachedTokens,
|
|
1035
|
+
cacheWrite: 0,
|
|
1036
|
+
totalTokens,
|
|
1037
|
+
cost: {
|
|
1038
|
+
input: 0,
|
|
1039
|
+
output: 0,
|
|
1040
|
+
cacheRead: 0,
|
|
1041
|
+
cacheWrite: 0,
|
|
1042
|
+
total: 0,
|
|
1043
|
+
},
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
calculateCost(billingModel, usage);
|
|
1047
|
+
estimatedCostUsd = usage.cost.total;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const responseId = typeof completedResponse?.id === "string" ? completedResponse.id : undefined;
|
|
1051
|
+
|
|
1052
|
+
let debugPayload: string | undefined;
|
|
1053
|
+
|
|
1054
|
+
if (options.debug) {
|
|
1055
|
+
debugPayload = JSON.stringify(payload, null, 4);
|
|
1056
|
+
|
|
1057
|
+
debugEvents.unshift(
|
|
1058
|
+
JSON.stringify({
|
|
1059
|
+
type: "request_meta",
|
|
1060
|
+
tokenSource: source,
|
|
1061
|
+
hasOrganizationHeader: !!organization,
|
|
1062
|
+
hasProjectHeader: !!projectId,
|
|
1063
|
+
payloadMeta: {
|
|
1064
|
+
model: options.model,
|
|
1065
|
+
effort: options.effort,
|
|
1066
|
+
summary: options.summary,
|
|
1067
|
+
verbosity: options.verbosity,
|
|
1068
|
+
contextChars: contextText.length,
|
|
1069
|
+
queryChars: options.query.length,
|
|
1070
|
+
},
|
|
1071
|
+
}),
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return {
|
|
1076
|
+
responseId,
|
|
1077
|
+
answer,
|
|
1078
|
+
thinking,
|
|
1079
|
+
usage: {
|
|
1080
|
+
inputTokens,
|
|
1081
|
+
outputTokens,
|
|
1082
|
+
cachedTokens,
|
|
1083
|
+
totalTokens,
|
|
1084
|
+
estimatedCostUsd,
|
|
1085
|
+
},
|
|
1086
|
+
durationMs: Date.now() - startedAt,
|
|
1087
|
+
debugEvents,
|
|
1088
|
+
debugPayload,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function summarizeContextPackMessage(packResult: ContextPackResult, packPath: string): string {
|
|
1093
|
+
const cleanedStdout = stripMarkdownFenceLines(stripAnsi(packResult.stdout)).trim();
|
|
1094
|
+
const cleanedStderr = stripMarkdownFenceLines(stripAnsi(packResult.stderr)).trim();
|
|
1095
|
+
const maxChars = 12000;
|
|
1096
|
+
|
|
1097
|
+
const { text: truncatedStdout, truncated } = truncate(cleanedStdout, maxChars);
|
|
1098
|
+
|
|
1099
|
+
const lines = [
|
|
1100
|
+
"## Deep review · context pack stage",
|
|
1101
|
+
"",
|
|
1102
|
+
`- Duration: ${formatDuration(packResult.durationMs)}`,
|
|
1103
|
+
`- Pack path: \`${packPath}\``,
|
|
1104
|
+
"",
|
|
1105
|
+
"### pi -p output",
|
|
1106
|
+
"",
|
|
1107
|
+
truncatedStdout || "(no stdout)",
|
|
1108
|
+
];
|
|
1109
|
+
|
|
1110
|
+
if (truncated) {
|
|
1111
|
+
lines.push("", "_stdout truncated for chat display_", "");
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (cleanedStderr) {
|
|
1115
|
+
lines.push("", "### stderr", "", cleanedStderr);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
return lines.join("\n");
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export function normalizeSectionLikeBoldMarkdown(markdown: string): string {
|
|
1122
|
+
if (!markdown.trim()) {
|
|
1123
|
+
return markdown;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const inputLines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
1127
|
+
const out: string[] = [];
|
|
1128
|
+
let inFence = false;
|
|
1129
|
+
|
|
1130
|
+
const pushHeading = (headingText: string) => {
|
|
1131
|
+
const heading = headingText.trim().replace(/[::]\s*$/, "");
|
|
1132
|
+
if (!heading) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (out.length > 0 && out[out.length - 1] !== "") {
|
|
1137
|
+
out.push("");
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
out.push(`### ${heading}`);
|
|
1141
|
+
out.push("");
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
for (const line of inputLines) {
|
|
1145
|
+
if (/^[ \t]*```/.test(line)) {
|
|
1146
|
+
inFence = !inFence;
|
|
1147
|
+
out.push(line);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (inFence) {
|
|
1152
|
+
out.push(line);
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const standaloneBold = line.match(/^[ \t]*\*\*([^*\n]{2,100})\*\*[ \t]*$/);
|
|
1157
|
+
if (standaloneBold) {
|
|
1158
|
+
pushHeading(standaloneBold[1]);
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const boldLabelWithBody = line.match(/^[ \t]*\*\*([^*\n:]{2,80}):\*\*[ \t]*(.+)$/);
|
|
1163
|
+
if (boldLabelWithBody) {
|
|
1164
|
+
pushHeading(boldLabelWithBody[1]);
|
|
1165
|
+
out.push(boldLabelWithBody[2].trim());
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
out.push(line);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const collapsed: string[] = [];
|
|
1173
|
+
for (const line of out) {
|
|
1174
|
+
if (line === "" && collapsed[collapsed.length - 1] === "") {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
collapsed.push(line);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return collapsed.join("\n").trim();
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function summarizeFinalMessage(
|
|
1184
|
+
options: DeepReviewOptions,
|
|
1185
|
+
packPath: string,
|
|
1186
|
+
packDurationMs: number,
|
|
1187
|
+
responses: ResponsesResult,
|
|
1188
|
+
totalDurationMs: number,
|
|
1189
|
+
debugDir?: string,
|
|
1190
|
+
artifacts?: OutputArtifacts,
|
|
1191
|
+
): string {
|
|
1192
|
+
const costLine =
|
|
1193
|
+
responses.usage.estimatedCostUsd !== undefined
|
|
1194
|
+
? `$${responses.usage.estimatedCostUsd.toFixed(6)} (estimated)`
|
|
1195
|
+
: "n/a (model price metadata unavailable)";
|
|
1196
|
+
const normalizedAnswer = normalizeSectionLikeBoldMarkdown(responses.answer || "");
|
|
1197
|
+
|
|
1198
|
+
const lines = [
|
|
1199
|
+
`# Deep review (${options.model})`,
|
|
1200
|
+
"",
|
|
1201
|
+
`- Query: ${options.query}`,
|
|
1202
|
+
`- Context pack: \`${packPath}\``,
|
|
1203
|
+
`- Context pack time: ${formatDuration(packDurationMs)}`,
|
|
1204
|
+
`- Responses time: ${formatDuration(responses.durationMs)}`,
|
|
1205
|
+
`- Total time: ${formatDuration(totalDurationMs)}`,
|
|
1206
|
+
responses.responseId ? `- Response ID: \`${responses.responseId}\`` : undefined,
|
|
1207
|
+
"",
|
|
1208
|
+
"## Usage",
|
|
1209
|
+
"",
|
|
1210
|
+
`- Input tokens: ${responses.usage.inputTokens.toLocaleString()}`,
|
|
1211
|
+
`- Output tokens: ${responses.usage.outputTokens.toLocaleString()}`,
|
|
1212
|
+
`- Cached tokens: ${responses.usage.cachedTokens.toLocaleString()}`,
|
|
1213
|
+
`- Total tokens: ${responses.usage.totalTokens.toLocaleString()}`,
|
|
1214
|
+
`- Cost: ${costLine}`,
|
|
1215
|
+
"",
|
|
1216
|
+
"## Final response",
|
|
1217
|
+
"",
|
|
1218
|
+
normalizedAnswer || "(no output text returned)",
|
|
1219
|
+
debugDir ? "" : undefined,
|
|
1220
|
+
debugDir ? `Debug artifacts: \`${debugDir}\`` : undefined,
|
|
1221
|
+
].filter((line): line is string => line !== undefined);
|
|
1222
|
+
|
|
1223
|
+
if (artifacts) {
|
|
1224
|
+
lines.push(
|
|
1225
|
+
"",
|
|
1226
|
+
"## Handoff",
|
|
1227
|
+
"",
|
|
1228
|
+
`- Output directory: \`${artifacts.directory}\``,
|
|
1229
|
+
`- Answer file: \`${artifacts.answerPath}\``,
|
|
1230
|
+
`- Thinking file: \`${artifacts.thinkingPath}\``,
|
|
1231
|
+
`- Report file: \`${artifacts.reportPath}\``,
|
|
1232
|
+
`- Metadata file: \`${artifacts.metadataPath}\``,
|
|
1233
|
+
artifacts.clipboard.copied
|
|
1234
|
+
? `- Clipboard: copied answer via ${artifacts.clipboard.method ?? "clipboard tool"}`
|
|
1235
|
+
: `- Clipboard: not copied (${artifacts.clipboard.error ?? "no clipboard backend"})`,
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return lines.join("\n");
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
export default function deepReviewExtension(pi: ExtensionAPI): void {
|
|
1243
|
+
let activeRun: ActiveRun | null = null;
|
|
1244
|
+
|
|
1245
|
+
const markdownTypes = ["deep-review-help", "deep-review-context-pack", "deep-review-result", "deep-review-error"];
|
|
1246
|
+
|
|
1247
|
+
for (const customType of markdownTypes) {
|
|
1248
|
+
pi.registerMessageRenderer(customType, (message) => {
|
|
1249
|
+
const content =
|
|
1250
|
+
typeof message.content === "string"
|
|
1251
|
+
? message.content
|
|
1252
|
+
: message.content
|
|
1253
|
+
.map((block) => {
|
|
1254
|
+
if (block.type === "text") {
|
|
1255
|
+
return block.text;
|
|
1256
|
+
}
|
|
1257
|
+
return "[non-text content omitted]";
|
|
1258
|
+
})
|
|
1259
|
+
.join("\n");
|
|
1260
|
+
|
|
1261
|
+
return new Markdown(content, 0, 0, MARKDOWN_THEME);
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
pi.registerCommand("deep-review-stop", {
|
|
1266
|
+
description: "Stop an in-flight /deep-review run",
|
|
1267
|
+
handler: async (_args, ctx) => {
|
|
1268
|
+
if (!activeRun) {
|
|
1269
|
+
if (ctx.hasUI) {
|
|
1270
|
+
ctx.ui.notify("No deep-review run in progress", "info");
|
|
1271
|
+
}
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
activeRun.controller.abort();
|
|
1276
|
+
|
|
1277
|
+
if (activeRun.child) {
|
|
1278
|
+
activeRun.child.kill("SIGTERM");
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
pi.registerCommand("deep-review", {
|
|
1284
|
+
description: "Run pr-context-packer via pi -p, then stream OpenAI Responses in real time",
|
|
1285
|
+
handler: async (rawArgs, ctx) => {
|
|
1286
|
+
if (activeRun) {
|
|
1287
|
+
if (ctx.hasUI) {
|
|
1288
|
+
ctx.ui.notify("deep-review already running. Use /deep-review-stop first.", "warning");
|
|
1289
|
+
}
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const parsed = parseOptions(rawArgs, ctx.cwd);
|
|
1294
|
+
if (!parsed.ok && "help" in parsed) {
|
|
1295
|
+
pi.sendMessage({ customType: "deep-review-help", content: HELP_TEXT, display: true });
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (!parsed.ok) {
|
|
1300
|
+
const content = `deep-review argument error: ${parsed.message}\n\nUse /deep-review --help for usage.`;
|
|
1301
|
+
pi.sendMessage({ customType: "deep-review-error", content, display: true });
|
|
1302
|
+
if (ctx.hasUI) {
|
|
1303
|
+
ctx.ui.notify(parsed.message, "error");
|
|
1304
|
+
}
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const options = parsed.options;
|
|
1309
|
+
|
|
1310
|
+
let skillPath: string;
|
|
1311
|
+
try {
|
|
1312
|
+
skillPath = await resolveBundledContextPackerSkillPath();
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1315
|
+
pi.sendMessage({ customType: "deep-review-error", content: message, display: true });
|
|
1316
|
+
if (ctx.hasUI) {
|
|
1317
|
+
ctx.ui.notify(message, "error");
|
|
1318
|
+
}
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const active: ActiveRun = {
|
|
1323
|
+
controller: new AbortController(),
|
|
1324
|
+
};
|
|
1325
|
+
activeRun = active;
|
|
1326
|
+
|
|
1327
|
+
void (async () => {
|
|
1328
|
+
const startedAt = Date.now();
|
|
1329
|
+
|
|
1330
|
+
const live: LiveState = {
|
|
1331
|
+
startedAt,
|
|
1332
|
+
phase: "context-pack",
|
|
1333
|
+
thinking: "",
|
|
1334
|
+
answer: "",
|
|
1335
|
+
responsesEventCount: 0,
|
|
1336
|
+
lastResponsesEventType: undefined,
|
|
1337
|
+
lastRenderAt: 0,
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
if (ctx.hasUI) {
|
|
1341
|
+
ctx.ui.setStatus("deep-review", ctx.ui.theme.fg("accent", "deep-review: context pack"));
|
|
1342
|
+
ctx.ui.setWorkingMessage("deep-review running...");
|
|
1343
|
+
renderLiveWidget(ctx, live, true);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const renderTicker = ctx.hasUI
|
|
1347
|
+
? setInterval(() => {
|
|
1348
|
+
renderLiveWidget(ctx, live, true);
|
|
1349
|
+
}, WIDGET_TICK_MS)
|
|
1350
|
+
: undefined;
|
|
1351
|
+
|
|
1352
|
+
let debugDir: string | undefined;
|
|
1353
|
+
|
|
1354
|
+
try {
|
|
1355
|
+
const packResult = await runContextPackViaPi(options, ctx.cwd, skillPath, active);
|
|
1356
|
+
|
|
1357
|
+
const cleanedPackOutput = stripMarkdownFenceLines(stripAnsi(packResult.stdout));
|
|
1358
|
+
const cleanedPackStderr = stripMarkdownFenceLines(stripAnsi(packResult.stderr));
|
|
1359
|
+
const packPath = await extractContextPackPath(`${cleanedPackOutput}\n${cleanedPackStderr}`);
|
|
1360
|
+
|
|
1361
|
+
if (packResult.exitCode !== 0) {
|
|
1362
|
+
if (active.controller.signal.aborted) {
|
|
1363
|
+
throw new Error("Request was aborted");
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const stdoutSnippet = truncate(cleanedPackOutput, 8000).text.trim();
|
|
1367
|
+
const stderrSnippet = truncate(cleanedPackStderr, 8000).text.trim();
|
|
1368
|
+
const contentLines = ["deep-review failed."];
|
|
1369
|
+
|
|
1370
|
+
if (stderrSnippet) {
|
|
1371
|
+
contentLines.push("", "### stderr", "", "```text", stderrSnippet, "```");
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
if (stdoutSnippet) {
|
|
1375
|
+
contentLines.push("", "### stdout", "", "```text", stdoutSnippet, "```");
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
pi.sendMessage({
|
|
1379
|
+
customType: "deep-review-error",
|
|
1380
|
+
content: contentLines.join("\n"),
|
|
1381
|
+
display: true,
|
|
1382
|
+
});
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (!packPath) {
|
|
1387
|
+
const content = [
|
|
1388
|
+
"deep-review could not find the generated pack path in pi -p output.",
|
|
1389
|
+
"",
|
|
1390
|
+
"```text",
|
|
1391
|
+
truncate(cleanedPackOutput, 12000).text,
|
|
1392
|
+
"```",
|
|
1393
|
+
].join("\n");
|
|
1394
|
+
pi.sendMessage({ customType: "deep-review-error", content, display: true });
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
pi.sendMessage({
|
|
1399
|
+
customType: "deep-review-context-pack",
|
|
1400
|
+
content: summarizeContextPackMessage(packResult, packPath),
|
|
1401
|
+
display: true,
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
const contextText = await readFile(packPath, "utf8");
|
|
1405
|
+
|
|
1406
|
+
live.phase = "responses";
|
|
1407
|
+
live.responsesEventCount = 0;
|
|
1408
|
+
live.lastResponsesEventType = undefined;
|
|
1409
|
+
|
|
1410
|
+
if (ctx.hasUI) {
|
|
1411
|
+
ctx.ui.setStatus("deep-review", ctx.ui.theme.fg("accent", "deep-review: responses stream"));
|
|
1412
|
+
renderLiveWidget(ctx, live, true);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
const responses = await streamResponses(
|
|
1416
|
+
options,
|
|
1417
|
+
contextText,
|
|
1418
|
+
ctx,
|
|
1419
|
+
active.controller.signal,
|
|
1420
|
+
(delta) => {
|
|
1421
|
+
live.thinking += delta;
|
|
1422
|
+
renderLiveWidget(ctx, live);
|
|
1423
|
+
},
|
|
1424
|
+
(delta) => {
|
|
1425
|
+
live.answer += delta;
|
|
1426
|
+
renderLiveWidget(ctx, live);
|
|
1427
|
+
},
|
|
1428
|
+
(eventType) => {
|
|
1429
|
+
live.responsesEventCount += 1;
|
|
1430
|
+
live.lastResponsesEventType = eventType;
|
|
1431
|
+
renderLiveWidget(ctx, live);
|
|
1432
|
+
},
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
if (options.debug) {
|
|
1436
|
+
debugDir = await mkdtemp(path.join(os.tmpdir(), "deep-review-"));
|
|
1437
|
+
await writeFile(path.join(debugDir, "context-pack-output.txt"), cleanedPackOutput, "utf8");
|
|
1438
|
+
await writeFile(
|
|
1439
|
+
path.join(debugDir, "responses-events.jsonl"),
|
|
1440
|
+
`${responses.debugEvents.join("\n")}\n`,
|
|
1441
|
+
"utf8",
|
|
1442
|
+
);
|
|
1443
|
+
|
|
1444
|
+
if (responses.debugPayload) {
|
|
1445
|
+
await writeFile(
|
|
1446
|
+
path.join(debugDir, "responses-request.json"),
|
|
1447
|
+
`${responses.debugPayload}\n`,
|
|
1448
|
+
"utf8",
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const totalDurationMs = Date.now() - startedAt;
|
|
1454
|
+
const preliminaryContent = summarizeFinalMessage(
|
|
1455
|
+
options,
|
|
1456
|
+
packPath,
|
|
1457
|
+
packResult.durationMs,
|
|
1458
|
+
responses,
|
|
1459
|
+
totalDurationMs,
|
|
1460
|
+
debugDir,
|
|
1461
|
+
);
|
|
1462
|
+
|
|
1463
|
+
let artifacts: OutputArtifacts | undefined;
|
|
1464
|
+
|
|
1465
|
+
try {
|
|
1466
|
+
artifacts = await writeOutputArtifacts(
|
|
1467
|
+
options,
|
|
1468
|
+
packPath,
|
|
1469
|
+
responses,
|
|
1470
|
+
totalDurationMs,
|
|
1471
|
+
preliminaryContent,
|
|
1472
|
+
debugDir,
|
|
1473
|
+
);
|
|
1474
|
+
} catch (artifactError) {
|
|
1475
|
+
const artifactMessage =
|
|
1476
|
+
artifactError instanceof Error ? artifactError.message : String(artifactError);
|
|
1477
|
+
|
|
1478
|
+
if (ctx.hasUI) {
|
|
1479
|
+
ctx.ui.notify(`Could not write deep-review artifacts: ${artifactMessage}`, "warning");
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const finalContent = summarizeFinalMessage(
|
|
1484
|
+
options,
|
|
1485
|
+
packPath,
|
|
1486
|
+
packResult.durationMs,
|
|
1487
|
+
responses,
|
|
1488
|
+
totalDurationMs,
|
|
1489
|
+
debugDir,
|
|
1490
|
+
artifacts,
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
if (artifacts) {
|
|
1494
|
+
await writeFile(artifacts.reportPath, `${finalContent}\n`, "utf8");
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
pi.sendMessage({ customType: "deep-review-result", content: finalContent, display: true });
|
|
1498
|
+
|
|
1499
|
+
if (ctx.hasUI) {
|
|
1500
|
+
if (artifacts?.clipboard.copied) {
|
|
1501
|
+
ctx.ui.notify("deep-review complete · answer copied to clipboard", "info");
|
|
1502
|
+
} else {
|
|
1503
|
+
ctx.ui.notify("deep-review complete", "info");
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
const stopped = active.controller.signal.aborted;
|
|
1508
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1509
|
+
|
|
1510
|
+
if (stopped) {
|
|
1511
|
+
pi.sendMessage({
|
|
1512
|
+
customType: "deep-review-result",
|
|
1513
|
+
content: "deep-review stopped.",
|
|
1514
|
+
display: true,
|
|
1515
|
+
});
|
|
1516
|
+
} else {
|
|
1517
|
+
const content = `deep-review failed: ${message}\n\nUse /deep-review --help for options.`;
|
|
1518
|
+
pi.sendMessage({ customType: "deep-review-error", content, display: true });
|
|
1519
|
+
if (ctx.hasUI) {
|
|
1520
|
+
ctx.ui.notify(message, "error");
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
} finally {
|
|
1524
|
+
if (renderTicker) {
|
|
1525
|
+
clearInterval(renderTicker);
|
|
1526
|
+
}
|
|
1527
|
+
if (ctx.hasUI) {
|
|
1528
|
+
ctx.ui.setStatus("deep-review", undefined);
|
|
1529
|
+
ctx.ui.setWidget("deep-review-live", undefined);
|
|
1530
|
+
ctx.ui.setWorkingMessage();
|
|
1531
|
+
}
|
|
1532
|
+
activeRun = null;
|
|
1533
|
+
}
|
|
1534
|
+
})();
|
|
1535
|
+
|
|
1536
|
+
if (ctx.hasUI) {
|
|
1537
|
+
ctx.ui.notify("deep-review started · use /deep-review-stop to stop", "info");
|
|
1538
|
+
}
|
|
1539
|
+
},
|
|
1540
|
+
});
|
|
1541
|
+
}
|