tau-coding-agent 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -12
- package/extensions/answer.ts +129 -73
- package/extensions/branch-term/README.md +7 -0
- package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
- package/extensions/btw.ts +90 -4
- package/extensions/caffeinate/README.md +5 -0
- package/extensions/caffeinate/index.ts +144 -0
- package/extensions/fast.ts +292 -0
- package/extensions/ghostty.ts +214 -211
- package/extensions/git-diff-stats.ts +124 -84
- package/extensions/git-pr-status.ts +274 -208
- package/extensions/insights.ts +138 -183
- package/extensions/loop.ts +150 -135
- package/extensions/memory.ts +172 -97
- package/extensions/notify.ts +14 -26
- package/extensions/openai-verbosity.ts +108 -42
- package/extensions/review/fix.ts +15 -6
- package/extensions/review/git.ts +93 -103
- package/extensions/review/index.ts +60 -62
- package/extensions/review/interrupt.ts +117 -0
- package/extensions/review/message-queue.ts +45 -12
- package/extensions/review/models.ts +79 -3
- package/extensions/review/prompts.ts +43 -40
- package/extensions/review/review.ts +161 -166
- package/extensions/review/runner.ts +139 -147
- package/extensions/review/runtime.ts +173 -120
- package/extensions/review/submit-review-tool.ts +1 -5
- package/extensions/review/submit-triage-tool.ts +1 -0
- package/extensions/review/triage.ts +41 -44
- package/extensions/sandbox/bash.ts +775 -0
- package/extensions/sandbox/command.ts +605 -0
- package/extensions/sandbox/config.ts +764 -0
- package/extensions/sandbox/index.ts +138 -2850
- package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
- package/extensions/sandbox/permissions/dialog.ts +118 -0
- package/extensions/sandbox/permissions/filesystem.ts +559 -0
- package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
- package/extensions/sandbox/permissions/network.ts +164 -0
- package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
- package/extensions/sandbox/runtime.ts +616 -0
- package/extensions/stash.ts +28 -15
- package/extensions/subagent/README.md +73 -0
- package/extensions/subagent/index.ts +822 -0
- package/extensions/subagent/interrupt.ts +117 -0
- package/extensions/subagent/permissions.ts +101 -0
- package/extensions/subagent/rpc.ts +177 -0
- package/extensions/tool-display-mode.ts +267 -64
- package/extensions/usage/anthropic.ts +5 -6
- package/extensions/usage/github-copilot.ts +30 -9
- package/extensions/usage/index.ts +250 -357
- package/extensions/usage/openai-codex.ts +2 -2
- package/extensions/usage/openrouter.ts +10 -2
- package/extensions/websearch/README.md +12 -47
- package/extensions/websearch/config.ts +5 -2
- package/extensions/websearch/index.ts +94 -109
- package/extensions/websearch/output.ts +51 -0
- package/extensions/websearch/providers/anthropic.pi.ts +27 -44
- package/extensions/websearch/providers/gemini.browser.ts +68 -77
- package/extensions/websearch/providers/gemini.pi.ts +17 -17
- package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
- package/extensions/websearch/providers/pi-model.shared.ts +44 -32
- package/extensions/websearch/providers/shared.ts +87 -2
- package/extensions/websearch/types.ts +0 -3
- package/extensions/worktree.ts +132 -172
- package/package.json +10 -5
- package/skills/browser-tools/SKILL.md +29 -234
- package/skills/browser-tools/references/cookies.md +36 -0
- package/skills/browser-tools/references/interaction.md +90 -0
- package/skills/browser-tools/references/logging.md +34 -0
- package/skills/git-clean-history/SKILL.md +6 -6
- package/skills/git-commit/SKILL.md +5 -3
- package/skills/github-pull-request/SKILL.md +60 -0
- package/skills/github-pull-request/references/create.md +40 -0
- package/skills/github-pull-request/references/stewardship.md +60 -0
- package/skills/oracle/SKILL.md +4 -4
- package/skills/oracle/scripts/oracle +64 -51
- package/skills/sentry/SKILL.md +15 -185
- package/skills/sentry/references/events.md +79 -0
- package/skills/sentry/references/issues.md +62 -0
- package/skills/sentry/references/logs.md +46 -0
- package/skills/update-changelog/SKILL.md +27 -121
- package/skills/web-design/SKILL.md +16 -105
- package/themes/tau-dark.json +4 -0
- package/extensions/openai-fast.ts +0 -229
- package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
- package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
@@ -20,9 +20,9 @@ export type TaskErrorKind =
|
|
|
20
20
|
| "unsupported_reasoning"
|
|
21
21
|
| "other";
|
|
22
22
|
|
|
23
|
-
export type
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
export type ReviewCommandContext = {
|
|
24
|
+
cwd: string;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
export type PiTaskStatus =
|
|
@@ -47,7 +47,7 @@ export type PiTaskOptions = {
|
|
|
47
47
|
prompt: string;
|
|
48
48
|
cwd: string;
|
|
49
49
|
timeoutMs: number;
|
|
50
|
-
|
|
50
|
+
signal: AbortSignal;
|
|
51
51
|
submitTool?: string;
|
|
52
52
|
};
|
|
53
53
|
|
|
@@ -347,12 +347,12 @@ export async function runPiSubmitToolTask(options: PiSubmitToolTaskOptions): Pro
|
|
|
347
347
|
if (
|
|
348
348
|
firstResult.status !== "ok" ||
|
|
349
349
|
firstResult.submittedPayloads.length > 0 ||
|
|
350
|
-
options.
|
|
350
|
+
options.signal.aborted
|
|
351
351
|
) {
|
|
352
352
|
return firstResult;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
return runPiOneShotTask({
|
|
355
|
+
return await runPiOneShotTask({
|
|
356
356
|
...options,
|
|
357
357
|
args,
|
|
358
358
|
prompt: SUBMIT_TOOL_RETRY_PROMPT.replaceAll("{SUBMIT_TOOL}", options.submitTool),
|
|
@@ -367,156 +367,148 @@ export async function runPiOneShotTask({
|
|
|
367
367
|
prompt,
|
|
368
368
|
cwd,
|
|
369
369
|
timeoutMs,
|
|
370
|
-
|
|
370
|
+
signal,
|
|
371
371
|
submitTool,
|
|
372
372
|
}: PiTaskOptions): Promise<PiTaskResult> {
|
|
373
|
-
if (
|
|
374
|
-
return {
|
|
375
|
-
status: "cancelled",
|
|
376
|
-
assistantOutput: "",
|
|
377
|
-
stderr: "",
|
|
378
|
-
submittedPayloads: [],
|
|
379
|
-
};
|
|
373
|
+
if (signal.aborted) {
|
|
374
|
+
return { status: "cancelled", assistantOutput: "", stderr: "", submittedPayloads: [] };
|
|
380
375
|
}
|
|
381
376
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
timeoutId = undefined;
|
|
405
|
-
}
|
|
406
|
-
unregisterProcess?.();
|
|
407
|
-
resolve({
|
|
408
|
-
...result,
|
|
409
|
-
submittedPayloads,
|
|
410
|
-
});
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
const processLine = (line: string) => {
|
|
414
|
-
const trimmed = line.trim();
|
|
415
|
-
if (!trimmed) return;
|
|
416
|
-
try {
|
|
417
|
-
const event = JSON.parse(trimmed);
|
|
418
|
-
const submittedPayload = submitTool
|
|
419
|
-
? extractSubmitToolPayloadFromEvent(event, submitTool)
|
|
420
|
-
: null;
|
|
421
|
-
if (submittedPayload !== null) {
|
|
422
|
-
submittedPayloads.push(submittedPayload);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const message = extractAssistantMessageFromEvent(event);
|
|
426
|
-
if (message) {
|
|
427
|
-
const text = extractTextContent(message.content);
|
|
428
|
-
if (text) latestAssistantOutput = text;
|
|
429
|
-
latestAssistantError =
|
|
430
|
-
message.stopReason === "error" && typeof message.errorMessage === "string"
|
|
431
|
-
? message.errorMessage
|
|
432
|
-
: "";
|
|
433
|
-
}
|
|
434
|
-
} catch {
|
|
435
|
-
// Ignore non-JSON lines.
|
|
436
|
-
}
|
|
437
|
-
};
|
|
377
|
+
const child = spawnReviewProcess("pi", [...args, prompt], { cwd, signal });
|
|
378
|
+
let stdoutBuffer = "";
|
|
379
|
+
let latestAssistantOutput = "";
|
|
380
|
+
let latestAssistantError = "";
|
|
381
|
+
const submittedPayloads: unknown[] = [];
|
|
382
|
+
let stderr = "";
|
|
383
|
+
let timedOut = false;
|
|
384
|
+
const timeout = setTimeout(() => {
|
|
385
|
+
timedOut = true;
|
|
386
|
+
child.stop();
|
|
387
|
+
}, timeoutMs);
|
|
388
|
+
|
|
389
|
+
child.proc.stdout.on("data", (chunk: string) => {
|
|
390
|
+
stdoutBuffer += chunk;
|
|
391
|
+
const lines = stdoutBuffer.split("\n");
|
|
392
|
+
stdoutBuffer = lines.pop() ?? "";
|
|
393
|
+
for (const line of lines) processLine(line);
|
|
394
|
+
});
|
|
395
|
+
child.proc.stderr.on("data", (chunk: string) => {
|
|
396
|
+
stderr += chunk;
|
|
397
|
+
});
|
|
398
|
+
child.proc.stdin.end();
|
|
438
399
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
});
|
|
450
|
-
return;
|
|
400
|
+
try {
|
|
401
|
+
const { code, error } = await child.completion;
|
|
402
|
+
if (stdoutBuffer.trim()) processLine(stdoutBuffer);
|
|
403
|
+
const output = { assistantOutput: latestAssistantOutput, stderr, submittedPayloads };
|
|
404
|
+
if (signal.aborted) return { ...output, status: "cancelled" };
|
|
405
|
+
if (timedOut) return { ...output, status: "timeout" };
|
|
406
|
+
if (error) return { ...output, status: "spawn_error", error: error.message };
|
|
407
|
+
if (code !== 0) return { ...output, status: "non_zero_exit", exitCode: code ?? 1 };
|
|
408
|
+
if (latestAssistantError) {
|
|
409
|
+
return { ...output, status: "assistant_error", error: latestAssistantError, exitCode: 0 };
|
|
451
410
|
}
|
|
411
|
+
return { ...output, status: "ok", exitCode: 0 };
|
|
412
|
+
} finally {
|
|
413
|
+
clearTimeout(timeout);
|
|
414
|
+
}
|
|
452
415
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
for (const line of lines) {
|
|
471
|
-
processLine(line);
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
proc.stderr.on("data", (chunk) => {
|
|
476
|
-
stderr += chunk.toString();
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
proc.on("error", (error) => {
|
|
480
|
-
finish({
|
|
481
|
-
status: "spawn_error",
|
|
482
|
-
assistantOutput: latestAssistantOutput,
|
|
483
|
-
stderr,
|
|
484
|
-
error: error.message,
|
|
485
|
-
});
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
proc.on("close", (code) => {
|
|
489
|
-
if (stdoutBuffer.trim()) {
|
|
490
|
-
processLine(stdoutBuffer);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if ((code ?? 1) !== 0) {
|
|
494
|
-
finish({
|
|
495
|
-
status: "non_zero_exit",
|
|
496
|
-
assistantOutput: latestAssistantOutput,
|
|
497
|
-
stderr,
|
|
498
|
-
exitCode: code ?? 1,
|
|
499
|
-
});
|
|
500
|
-
return;
|
|
416
|
+
function processLine(line: string) {
|
|
417
|
+
const trimmed = line.trim();
|
|
418
|
+
if (!trimmed) return;
|
|
419
|
+
try {
|
|
420
|
+
const event = JSON.parse(trimmed);
|
|
421
|
+
const submittedPayload = submitTool
|
|
422
|
+
? extractSubmitToolPayloadFromEvent(event, submitTool)
|
|
423
|
+
: null;
|
|
424
|
+
if (submittedPayload !== null) submittedPayloads.push(submittedPayload);
|
|
425
|
+
const message = extractAssistantMessageFromEvent(event);
|
|
426
|
+
if (message) {
|
|
427
|
+
const text = extractTextContent(message.content);
|
|
428
|
+
if (text) latestAssistantOutput = text;
|
|
429
|
+
latestAssistantError =
|
|
430
|
+
message.stopReason === "error" && typeof message.errorMessage === "string"
|
|
431
|
+
? message.errorMessage
|
|
432
|
+
: "";
|
|
501
433
|
}
|
|
434
|
+
} catch {
|
|
435
|
+
// Ignore non-JSON lines.
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
502
439
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
440
|
+
export async function runReviewCommand(
|
|
441
|
+
context: ReviewCommandContext,
|
|
442
|
+
command: string,
|
|
443
|
+
args: string[],
|
|
444
|
+
input?: string,
|
|
445
|
+
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
446
|
+
const child = spawnReviewProcess(command, args, context);
|
|
447
|
+
let stdout = "";
|
|
448
|
+
let stderr = "";
|
|
449
|
+
child.proc.stdout.on("data", (chunk: string) => {
|
|
450
|
+
stdout += chunk;
|
|
451
|
+
});
|
|
452
|
+
child.proc.stderr.on("data", (chunk: string) => {
|
|
453
|
+
stderr += chunk;
|
|
454
|
+
});
|
|
455
|
+
child.proc.stdin.end(input);
|
|
456
|
+
const { code, error } = await child.completion;
|
|
457
|
+
context.signal?.throwIfAborted();
|
|
458
|
+
return { stdout, stderr: error?.message ?? stderr, code: error ? 1 : (code ?? 1) };
|
|
459
|
+
}
|
|
513
460
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
461
|
+
/** Join process and pipes after graceful cancellation, escalating only if they have not closed. */
|
|
462
|
+
function spawnReviewProcess(
|
|
463
|
+
command: string,
|
|
464
|
+
args: string[],
|
|
465
|
+
{ cwd, signal }: ReviewCommandContext,
|
|
466
|
+
) {
|
|
467
|
+
signal?.throwIfAborted();
|
|
468
|
+
const proc = spawn(command, args, {
|
|
469
|
+
cwd,
|
|
470
|
+
detached: true,
|
|
471
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
472
|
+
env: process.env,
|
|
473
|
+
});
|
|
474
|
+
let closed = false;
|
|
475
|
+
let stopping = false;
|
|
476
|
+
let error: Error | undefined;
|
|
477
|
+
let escalation: NodeJS.Timeout | undefined;
|
|
478
|
+
proc.stdout.setEncoding("utf8");
|
|
479
|
+
proc.stderr.setEncoding("utf8");
|
|
480
|
+
proc.on("error", (cause: Error) => {
|
|
481
|
+
error = cause;
|
|
482
|
+
});
|
|
483
|
+
proc.stdin.on("error", () => {
|
|
484
|
+
/* A cancelled child may close stdin before it drains. */
|
|
485
|
+
});
|
|
486
|
+
const completion = new Promise<{ code: number | null; error?: Error }>((resolve) => {
|
|
487
|
+
proc.once("close", (code) => {
|
|
488
|
+
closed = true;
|
|
489
|
+
clearTimeout(escalation);
|
|
490
|
+
signal?.removeEventListener("abort", stop);
|
|
491
|
+
resolve({ code, error });
|
|
520
492
|
});
|
|
521
493
|
});
|
|
494
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
495
|
+
if (signal?.aborted) stop();
|
|
496
|
+
return { proc, completion, stop };
|
|
497
|
+
|
|
498
|
+
function stop() {
|
|
499
|
+
if (closed || stopping) return;
|
|
500
|
+
stopping = true;
|
|
501
|
+
// Pi handles SIGTERM by cancelling its agent and cleaning up detached Bash tools.
|
|
502
|
+
sendSignal("SIGTERM");
|
|
503
|
+
escalation = setTimeout(() => sendSignal("SIGKILL"), 1_000);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function sendSignal(value: "SIGTERM" | "SIGKILL") {
|
|
507
|
+
if (closed || proc.pid === undefined) return;
|
|
508
|
+
try {
|
|
509
|
+
process.kill(-proc.pid, value);
|
|
510
|
+
} catch (cause) {
|
|
511
|
+
if ((cause as NodeJS.ErrnoException).code !== "ESRCH") proc.kill(value);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
522
514
|
}
|
|
@@ -3,9 +3,8 @@ import type {
|
|
|
3
3
|
ExtensionCommandContext,
|
|
4
4
|
ExtensionContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
|
|
6
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
|
+
import { createInterruptGuard } from "./interrupt.js";
|
|
9
8
|
import type { ReviewRunOutcome, ReviewRunSource } from "./schema.js";
|
|
10
9
|
|
|
11
10
|
export const REVIEW_CANCELLED_ERROR = "Review aborted";
|
|
@@ -13,23 +12,12 @@ export const REVIEW_CANCELLED_ERROR = "Review aborted";
|
|
|
13
12
|
export const REVIEW_EVENT_START = "review:start";
|
|
14
13
|
export const REVIEW_EVENT_END = "review:end";
|
|
15
14
|
|
|
16
|
-
export const
|
|
15
|
+
export const REVIEW_PROGRESS_WIDGET_KEY = "review-progress";
|
|
17
16
|
|
|
18
17
|
export const STATUS_SPINNER_INTERVAL_MS = 80;
|
|
19
18
|
|
|
20
19
|
export const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
21
20
|
|
|
22
|
-
export type ReviewExecutionControl = {
|
|
23
|
-
isCancelled: () => boolean;
|
|
24
|
-
registerProcess: (proc: ChildProcess) => () => void;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export type ManagedReviewRun = {
|
|
28
|
-
control: ReviewExecutionControl;
|
|
29
|
-
markSuccessful: () => void;
|
|
30
|
-
markCancelled: () => void;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
21
|
export type AgentEndMessage = {
|
|
34
22
|
role?: string;
|
|
35
23
|
stopReason?: string;
|
|
@@ -56,79 +44,125 @@ export type AgentRunTracker = FixPassAgentTracker & {
|
|
|
56
44
|
reset: () => void;
|
|
57
45
|
};
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
activeReviewRuns: new Set<string>(),
|
|
61
|
-
activeReviewCancels: new Map<string, () => void>(),
|
|
62
|
-
activePromptCount: 0,
|
|
63
|
-
};
|
|
47
|
+
export type ReviewRuntime = ReturnType<typeof createReviewRuntime>;
|
|
64
48
|
|
|
65
|
-
export
|
|
49
|
+
export function createReviewRuntime(
|
|
66
50
|
pi: ExtensionAPI,
|
|
67
|
-
ctx:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let cancelRequested = false;
|
|
74
|
-
let outcome: ReviewRunOutcome = "failed";
|
|
51
|
+
onInterrupt: (ctx: ExtensionContext) => void,
|
|
52
|
+
) {
|
|
53
|
+
let busy = false;
|
|
54
|
+
let closed = false;
|
|
55
|
+
let promptActive = false;
|
|
56
|
+
let active: { controller: AbortController; completion: Promise<void> } | undefined;
|
|
75
57
|
|
|
76
|
-
|
|
77
|
-
|
|
58
|
+
return {
|
|
59
|
+
get closed() {
|
|
60
|
+
return closed;
|
|
61
|
+
},
|
|
62
|
+
acquire(ctx: ExtensionContext, busyMessage: string): boolean {
|
|
63
|
+
if (closed) return false;
|
|
64
|
+
if (busy) {
|
|
65
|
+
notify(ctx, busyMessage, "warning");
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
busy = true;
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
71
|
+
release() {
|
|
72
|
+
busy = false;
|
|
73
|
+
},
|
|
74
|
+
setPromptActive(value: boolean) {
|
|
75
|
+
promptActive = value;
|
|
76
|
+
},
|
|
77
|
+
async run<T extends { ok: boolean }>(
|
|
78
|
+
ctx: ExtensionCommandContext,
|
|
79
|
+
source: ReviewRunSource,
|
|
80
|
+
run: (signal: AbortSignal) => Promise<T>,
|
|
81
|
+
): Promise<T | { ok: false; error: string }> {
|
|
82
|
+
if (closed) return { ok: false, error: REVIEW_CANCELLED_ERROR };
|
|
83
|
+
const sessionKey = getReviewSessionKey(ctx);
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
const { signal } = controller;
|
|
86
|
+
let resolveCompletion!: () => void;
|
|
87
|
+
const completion = new Promise<void>((resolve) => {
|
|
88
|
+
resolveCompletion = resolve;
|
|
89
|
+
});
|
|
90
|
+
active = { controller, completion };
|
|
91
|
+
let outcome: ReviewRunOutcome = "failed";
|
|
92
|
+
let started = false;
|
|
93
|
+
let interrupt: ReturnType<typeof createInterruptGuard> | undefined;
|
|
78
94
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
publishReviewActivity(pi, ctx, "review preparing");
|
|
96
|
+
if (ctx.mode === "tui") {
|
|
97
|
+
ctx.ui.setWidget(
|
|
98
|
+
REVIEW_PROGRESS_WIDGET_KEY,
|
|
99
|
+
(tui, theme) => {
|
|
100
|
+
interrupt = createInterruptGuard(
|
|
101
|
+
pi,
|
|
102
|
+
ctx,
|
|
103
|
+
tui,
|
|
104
|
+
() => !signal.aborted,
|
|
105
|
+
() => {
|
|
106
|
+
onInterrupt(ctx);
|
|
107
|
+
controller.abort();
|
|
108
|
+
},
|
|
109
|
+
() => promptActive,
|
|
110
|
+
);
|
|
111
|
+
return {
|
|
112
|
+
invalidate() {},
|
|
113
|
+
render: (width) => {
|
|
114
|
+
const handled = publishReviewActivity(pi, ctx, "review preparing");
|
|
115
|
+
return !ctx.ui.getToolsExpanded() && handled
|
|
116
|
+
? []
|
|
117
|
+
: [renderReviewProgressHeader("preparing", theme, width)];
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
{ placement: "aboveEditor" },
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
started = true;
|
|
125
|
+
pi.events.emit(REVIEW_EVENT_START, { sessionKey, source });
|
|
126
|
+
signal.throwIfAborted();
|
|
127
|
+
const result = await run(signal);
|
|
128
|
+
signal.throwIfAborted();
|
|
129
|
+
outcome = result.ok ? "success" : "failed";
|
|
130
|
+
return result;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (!signal.aborted) throw error;
|
|
133
|
+
outcome = "cancelled";
|
|
134
|
+
return { ok: false, error: REVIEW_CANCELLED_ERROR };
|
|
135
|
+
} finally {
|
|
136
|
+
try {
|
|
137
|
+
controller.abort();
|
|
138
|
+
await interrupt?.dispose();
|
|
139
|
+
if (ctx.mode === "tui") ctx.ui.setWidget(REVIEW_PROGRESS_WIDGET_KEY, undefined);
|
|
140
|
+
active = undefined;
|
|
141
|
+
if (started) pi.events.emit(REVIEW_EVENT_END, { sessionKey, source, outcome });
|
|
142
|
+
} finally {
|
|
143
|
+
publishReviewActivity(pi, ctx, undefined);
|
|
144
|
+
resolveCompletion();
|
|
145
|
+
}
|
|
82
146
|
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
cancelRequested = true;
|
|
90
|
-
cancelActiveProcesses();
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const control: ReviewExecutionControl = {
|
|
94
|
-
isCancelled: () => cancelRequested,
|
|
95
|
-
registerProcess: (proc) => {
|
|
96
|
-
activeProcesses.add(proc);
|
|
97
|
-
return () => {
|
|
98
|
-
activeProcesses.delete(proc);
|
|
99
|
-
};
|
|
147
|
+
},
|
|
148
|
+
async shutdown() {
|
|
149
|
+
closed = true;
|
|
150
|
+
const current = active;
|
|
151
|
+
current?.controller.abort();
|
|
152
|
+
await current?.completion;
|
|
100
153
|
},
|
|
101
154
|
};
|
|
155
|
+
}
|
|
102
156
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
: undefined;
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
return await run({
|
|
116
|
-
control,
|
|
117
|
-
markSuccessful: () => {
|
|
118
|
-
outcome = "success";
|
|
119
|
-
},
|
|
120
|
-
markCancelled: () => {
|
|
121
|
-
outcome = "cancelled";
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
} finally {
|
|
125
|
-
unsubscribeInterrupt?.();
|
|
126
|
-
cancelActiveProcesses();
|
|
127
|
-
if (runtimeState.activeReviewCancels.get(sessionKey) === requestCancellation) {
|
|
128
|
-
runtimeState.activeReviewCancels.delete(sessionKey);
|
|
129
|
-
}
|
|
130
|
-
pi.events.emit(REVIEW_EVENT_END, { sessionKey, source, outcome });
|
|
131
|
-
}
|
|
157
|
+
/** Join every sibling before propagating failure, so cancellation cannot leave background work behind. */
|
|
158
|
+
export async function joinAll<T extends readonly unknown[] | []>(
|
|
159
|
+
tasks: T,
|
|
160
|
+
): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }> {
|
|
161
|
+
const results = await Promise.allSettled(tasks);
|
|
162
|
+
return results.map((result) => {
|
|
163
|
+
if (result.status === "rejected") throw result.reason;
|
|
164
|
+
return result.value;
|
|
165
|
+
}) as { -readonly [P in keyof T]: Awaited<T[P]> };
|
|
132
166
|
}
|
|
133
167
|
|
|
134
168
|
export function notify(
|
|
@@ -144,7 +178,19 @@ export function getReviewSessionKey(ctx: ExtensionContext): string {
|
|
|
144
178
|
return ctx.sessionManager.getSessionFile() ?? `session:${ctx.sessionManager.getSessionId()}`;
|
|
145
179
|
}
|
|
146
180
|
|
|
181
|
+
export function publishReviewActivity(
|
|
182
|
+
pi: ExtensionAPI,
|
|
183
|
+
ctx: ExtensionContext,
|
|
184
|
+
text: string | undefined,
|
|
185
|
+
): boolean {
|
|
186
|
+
if (ctx.mode !== "tui") return false;
|
|
187
|
+
const request = { sessionKey: getReviewSessionKey(ctx), source: "review", text, handled: false };
|
|
188
|
+
pi.events.emit("tau:activity", request);
|
|
189
|
+
return request.handled === true;
|
|
190
|
+
}
|
|
191
|
+
|
|
147
192
|
export async function withSpinner<T>(
|
|
193
|
+
pi: ExtensionAPI,
|
|
148
194
|
ctx: ExtensionContext,
|
|
149
195
|
buildStatusText: () => string,
|
|
150
196
|
run: () => Promise<T>,
|
|
@@ -152,9 +198,39 @@ export async function withSpinner<T>(
|
|
|
152
198
|
if (!ctx.hasUI) return run();
|
|
153
199
|
|
|
154
200
|
let frame = 0;
|
|
201
|
+
let requestRender: (() => void) | undefined;
|
|
202
|
+
if (ctx.mode === "tui") {
|
|
203
|
+
ctx.ui.setWidget(
|
|
204
|
+
REVIEW_PROGRESS_WIDGET_KEY,
|
|
205
|
+
(tui, theme) => {
|
|
206
|
+
requestRender = () => tui.requestRender();
|
|
207
|
+
return {
|
|
208
|
+
invalidate() {},
|
|
209
|
+
render(width) {
|
|
210
|
+
const handled = publishReviewActivity(pi, ctx, `review ${buildStatusText()}`);
|
|
211
|
+
if (!ctx.ui.getToolsExpanded() && handled) return [];
|
|
212
|
+
const spinner = theme.fg("accent", STATUS_SPINNER_FRAMES[frame]!);
|
|
213
|
+
return [
|
|
214
|
+
renderReviewProgressHeader(
|
|
215
|
+
`${spinner} ${theme.fg("muted", buildStatusText())}`,
|
|
216
|
+
theme,
|
|
217
|
+
width,
|
|
218
|
+
),
|
|
219
|
+
];
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
{ placement: "aboveEditor" },
|
|
224
|
+
);
|
|
225
|
+
}
|
|
155
226
|
const render = () => {
|
|
156
|
-
|
|
157
|
-
|
|
227
|
+
if (ctx.mode === "tui") {
|
|
228
|
+
publishReviewActivity(pi, ctx, `review ${buildStatusText()}`);
|
|
229
|
+
requestRender?.();
|
|
230
|
+
} else
|
|
231
|
+
ctx.ui.setWidget(REVIEW_PROGRESS_WIDGET_KEY, [`Review · ${buildStatusText()}`], {
|
|
232
|
+
placement: "aboveEditor",
|
|
233
|
+
});
|
|
158
234
|
};
|
|
159
235
|
|
|
160
236
|
render();
|
|
@@ -167,23 +243,24 @@ export async function withSpinner<T>(
|
|
|
167
243
|
return await run();
|
|
168
244
|
} finally {
|
|
169
245
|
clearInterval(timer);
|
|
170
|
-
|
|
246
|
+
publishReviewActivity(pi, ctx, "review");
|
|
247
|
+
ctx.ui.setWidget(REVIEW_PROGRESS_WIDGET_KEY, undefined);
|
|
171
248
|
}
|
|
172
249
|
}
|
|
173
250
|
|
|
174
|
-
export function
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
251
|
+
export function renderReviewProgressHeader(
|
|
252
|
+
status: string,
|
|
253
|
+
theme: ExtensionContext["ui"]["theme"],
|
|
254
|
+
width: number,
|
|
255
|
+
hint = "",
|
|
256
|
+
): string {
|
|
257
|
+
const label = `Review · ${status}`;
|
|
258
|
+
const innerWidth = Math.max(0, width - 2);
|
|
259
|
+
const gap = innerWidth - visibleWidth(label) - visibleWidth(hint);
|
|
260
|
+
return truncateToWidth(
|
|
261
|
+
` ${theme.fg("muted", truncateToWidth(label, innerWidth))}${hint && gap >= 2 ? " ".repeat(gap) + theme.fg("dim", hint) : ""}`,
|
|
262
|
+
width,
|
|
263
|
+
);
|
|
187
264
|
}
|
|
188
265
|
|
|
189
266
|
export function createAgentRunTracker(): AgentRunTracker {
|
|
@@ -249,27 +326,3 @@ export function createAgentRunTracker(): AgentRunTracker {
|
|
|
249
326
|
},
|
|
250
327
|
};
|
|
251
328
|
}
|
|
252
|
-
|
|
253
|
-
export function recordPromptStart(): void {
|
|
254
|
-
runtimeState.activePromptCount += 1;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function recordPromptEnd(): void {
|
|
258
|
-
runtimeState.activePromptCount = Math.max(0, runtimeState.activePromptCount - 1);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function handleReviewSessionStart(ctx: ExtensionContext): void {
|
|
262
|
-
runtimeState.activePromptCount = 0;
|
|
263
|
-
const sessionKey = getReviewSessionKey(ctx);
|
|
264
|
-
for (const [key, cancel] of runtimeState.activeReviewCancels) {
|
|
265
|
-
if (key !== sessionKey) cancel();
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export function handleReviewSessionShutdown(ctx: ExtensionContext): void {
|
|
270
|
-
const sessionKey = getReviewSessionKey(ctx);
|
|
271
|
-
runtimeState.activeReviewCancels.get(sessionKey)?.();
|
|
272
|
-
runtimeState.activeReviewCancels.delete(sessionKey);
|
|
273
|
-
runtimeState.activeReviewRuns.delete(sessionKey);
|
|
274
|
-
runtimeState.activePromptCount = 0;
|
|
275
|
-
}
|