tau-coding-agent 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -12
- package/extensions/answer.ts +129 -79
- package/extensions/branch-term/README.md +7 -0
- package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
- package/extensions/btw.ts +17 -2
- 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 +109 -191
- package/extensions/loop.ts +149 -145
- package/extensions/memory.ts +138 -86
- 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 +41 -38
- package/extensions/review/review.ts +161 -166
- package/extensions/review/runner.ts +139 -147
- package/extensions/review/runtime.ts +173 -120
- 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 +132 -3338
- 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/index.ts +249 -356
- 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 +33 -39
- 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 +3 -3
- package/skills/oracle/scripts/oracle +10 -1
- 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,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import {
|
|
2
3
|
isBashToolResult,
|
|
3
4
|
type ExtensionAPI,
|
|
@@ -6,10 +7,11 @@ import {
|
|
|
6
7
|
|
|
7
8
|
const STATUS_KEY = "1-git-pr-status";
|
|
8
9
|
|
|
9
|
-
const REFRESH_DEBOUNCE_MS = 250;
|
|
10
10
|
const PR_LOOKUP_TIMEOUT_MS = 5_000;
|
|
11
11
|
const BRANCH_CHECK_TIMEOUT_MS = 1_000;
|
|
12
12
|
const USER_BASH_SETTLE_DELAY_MS = 2_000;
|
|
13
|
+
const PROCESS_KILL_GRACE_MS = 500;
|
|
14
|
+
const PROCESS_PIPE_GRACE_MS = 100;
|
|
13
15
|
|
|
14
16
|
const GIT_BRANCH_CHANGE_COMMAND_PATTERN = /\bgit\s+(checkout|switch)\b/;
|
|
15
17
|
const GH_BRANCH_CHANGE_COMMAND_PATTERN = /\bgh\s+pr\s+checkout\b/;
|
|
@@ -28,6 +30,182 @@ type PullRequestLookupResult =
|
|
|
28
30
|
| { kind: "none" }
|
|
29
31
|
| { kind: "error" };
|
|
30
32
|
|
|
33
|
+
export default function gitPrStatusExtension(pi: ExtensionAPI) {
|
|
34
|
+
let ctx: ExtensionContext | undefined;
|
|
35
|
+
let currentBranch: string | null | undefined;
|
|
36
|
+
let pullRequest: PullRequestStatus | undefined;
|
|
37
|
+
let generation = 0;
|
|
38
|
+
let refreshTimer: NodeJS.Timeout | undefined;
|
|
39
|
+
let branchCheckTimer: NodeJS.Timeout | undefined;
|
|
40
|
+
let refreshInFlight: Promise<void> | undefined;
|
|
41
|
+
let branchCheckInFlight: Promise<void> | undefined;
|
|
42
|
+
let refreshQueued = false;
|
|
43
|
+
let branchCheckQueued = false;
|
|
44
|
+
let refreshController: AbortController | undefined;
|
|
45
|
+
let branchCheckController: AbortController | undefined;
|
|
46
|
+
|
|
47
|
+
pi.on("session_start", async (_event, nextCtx) => {
|
|
48
|
+
await reset(nextCtx);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
pi.on("user_bash", (event, nextCtx) => {
|
|
52
|
+
if (!ctx) return;
|
|
53
|
+
ctx = nextCtx;
|
|
54
|
+
|
|
55
|
+
if (isBranchChangeCommand(event.command)) {
|
|
56
|
+
scheduleBranchCheck(USER_BASH_SETTLE_DELAY_MS);
|
|
57
|
+
} else if (isPullRequestStateCommand(event.command)) {
|
|
58
|
+
scheduleRefresh(USER_BASH_SETTLE_DELAY_MS);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
pi.on("tool_result", async (event, nextCtx) => {
|
|
63
|
+
if (!ctx) return;
|
|
64
|
+
ctx = nextCtx;
|
|
65
|
+
|
|
66
|
+
// PowerShell is intentionally not observed because Tau does not officially support Windows.
|
|
67
|
+
if (!isBashToolResult(event)) return;
|
|
68
|
+
if (typeof event.input.command !== "string") return;
|
|
69
|
+
|
|
70
|
+
if (isBranchChangeCommand(event.input.command)) {
|
|
71
|
+
scheduleBranchCheck(0);
|
|
72
|
+
} else if (isPullRequestStateCommand(event.input.command)) {
|
|
73
|
+
scheduleRefresh(0);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
pi.on("turn_end", async (_event, nextCtx) => {
|
|
78
|
+
if (!ctx) return;
|
|
79
|
+
ctx = nextCtx;
|
|
80
|
+
scheduleBranchCheck(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
pi.on("session_shutdown", async () => {
|
|
84
|
+
await reset();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
async function reset(nextCtx?: ExtensionContext): Promise<void> {
|
|
88
|
+
const resetGeneration = ++generation;
|
|
89
|
+
clearTimeout(refreshTimer);
|
|
90
|
+
clearTimeout(branchCheckTimer);
|
|
91
|
+
refreshTimer = undefined;
|
|
92
|
+
branchCheckTimer = undefined;
|
|
93
|
+
refreshQueued = false;
|
|
94
|
+
branchCheckQueued = false;
|
|
95
|
+
const pending = Promise.all([refreshInFlight, branchCheckInFlight]);
|
|
96
|
+
refreshController?.abort();
|
|
97
|
+
branchCheckController?.abort();
|
|
98
|
+
const previousCtx = ctx;
|
|
99
|
+
ctx = undefined;
|
|
100
|
+
currentBranch = undefined;
|
|
101
|
+
pullRequest = undefined;
|
|
102
|
+
try {
|
|
103
|
+
previousCtx?.ui.setStatus(STATUS_KEY, undefined);
|
|
104
|
+
} finally {
|
|
105
|
+
await pending;
|
|
106
|
+
}
|
|
107
|
+
if (generation === resetGeneration && nextCtx?.hasUI) {
|
|
108
|
+
ctx = nextCtx;
|
|
109
|
+
scheduleBranchCheck(0);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function scheduleBranchCheck(delay: number): void {
|
|
114
|
+
if (!ctx?.hasUI) return;
|
|
115
|
+
clearTimeout(branchCheckTimer);
|
|
116
|
+
const cwd = ctx.cwd;
|
|
117
|
+
const branchGeneration = generation;
|
|
118
|
+
branchCheckTimer = setTimeout(() => {
|
|
119
|
+
branchCheckTimer = undefined;
|
|
120
|
+
checkForBranchChange(cwd, branchGeneration);
|
|
121
|
+
}, delay);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function checkForBranchChange(cwd: string, branchGeneration: number): void {
|
|
125
|
+
if (branchCheckInFlight) {
|
|
126
|
+
branchCheckQueued = true;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
branchCheckController = controller;
|
|
131
|
+
branchCheckInFlight = (async () => {
|
|
132
|
+
try {
|
|
133
|
+
const nextBranch = await loadCurrentBranch(cwd, controller.signal);
|
|
134
|
+
if (generation !== branchGeneration || controller.signal.aborted) return;
|
|
135
|
+
if (nextBranch === currentBranch) return;
|
|
136
|
+
|
|
137
|
+
currentBranch = nextBranch;
|
|
138
|
+
refreshController?.abort();
|
|
139
|
+
pullRequest = undefined;
|
|
140
|
+
applyStatus();
|
|
141
|
+
scheduleRefresh(0);
|
|
142
|
+
} catch {
|
|
143
|
+
// A failed branch check is not evidence that the previous PR disappeared.
|
|
144
|
+
} finally {
|
|
145
|
+
branchCheckInFlight = undefined;
|
|
146
|
+
branchCheckController = undefined;
|
|
147
|
+
const queued = branchCheckQueued;
|
|
148
|
+
branchCheckQueued = false;
|
|
149
|
+
if (queued && generation === branchGeneration) scheduleBranchCheck(0);
|
|
150
|
+
}
|
|
151
|
+
})();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function scheduleRefresh(delay: number): void {
|
|
155
|
+
if (!ctx?.hasUI) return;
|
|
156
|
+
if (currentBranch === undefined) {
|
|
157
|
+
scheduleBranchCheck(delay);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
clearTimeout(refreshTimer);
|
|
161
|
+
const cwd = ctx.cwd;
|
|
162
|
+
const refreshGeneration = generation;
|
|
163
|
+
refreshTimer = setTimeout(() => {
|
|
164
|
+
refreshTimer = undefined;
|
|
165
|
+
refresh(cwd, refreshGeneration);
|
|
166
|
+
}, delay);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function refresh(cwd: string, refreshGeneration: number): void {
|
|
170
|
+
if (refreshInFlight) {
|
|
171
|
+
refreshQueued = true;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
refreshController = controller;
|
|
176
|
+
refreshInFlight = (async () => {
|
|
177
|
+
try {
|
|
178
|
+
const result = await loadPullRequestStatus(cwd, controller.signal);
|
|
179
|
+
if (generation !== refreshGeneration || controller.signal.aborted) return;
|
|
180
|
+
|
|
181
|
+
if (result.kind === "found") {
|
|
182
|
+
pullRequest = result.pullRequest;
|
|
183
|
+
applyStatus();
|
|
184
|
+
} else if (result.kind === "none") {
|
|
185
|
+
pullRequest = undefined;
|
|
186
|
+
applyStatus();
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
// Cancellation and transport failures preserve the current branch's last known status.
|
|
190
|
+
} finally {
|
|
191
|
+
refreshInFlight = undefined;
|
|
192
|
+
refreshController = undefined;
|
|
193
|
+
const queued = refreshQueued;
|
|
194
|
+
refreshQueued = false;
|
|
195
|
+
if (queued && generation === refreshGeneration) scheduleRefresh(0);
|
|
196
|
+
}
|
|
197
|
+
})();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function applyStatus(): void {
|
|
201
|
+
if (!ctx?.hasUI) return;
|
|
202
|
+
ctx.ui.setStatus(
|
|
203
|
+
STATUS_KEY,
|
|
204
|
+
pullRequest ? formatPullRequestStatus(ctx, pullRequest) : undefined,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
31
209
|
function isBranchChangeCommand(command: string): boolean {
|
|
32
210
|
return (
|
|
33
211
|
GIT_BRANCH_CHANGE_COMMAND_PATTERN.test(command) ||
|
|
@@ -39,248 +217,136 @@ function isPullRequestStateCommand(command: string): boolean {
|
|
|
39
217
|
return GH_PR_STATE_COMMAND_PATTERN.test(command);
|
|
40
218
|
}
|
|
41
219
|
|
|
42
|
-
function
|
|
43
|
-
|
|
220
|
+
function formatPullRequestStatus(ctx: ExtensionContext, status: PullRequestStatus): string {
|
|
221
|
+
const suffix = status.state === "OPEN" ? "" : ` (${status.state.toLowerCase()})`;
|
|
222
|
+
return ctx.ui.theme.fg("dim", `#${status.number}${suffix}`);
|
|
44
223
|
}
|
|
45
224
|
|
|
46
|
-
async function loadCurrentBranch(
|
|
47
|
-
const result = await
|
|
225
|
+
async function loadCurrentBranch(cwd: string, signal: AbortSignal): Promise<string | null> {
|
|
226
|
+
const result = await statusCommand(
|
|
227
|
+
"git",
|
|
228
|
+
["branch", "--show-current"],
|
|
48
229
|
cwd,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return branch || undefined;
|
|
230
|
+
signal,
|
|
231
|
+
BRANCH_CHECK_TIMEOUT_MS,
|
|
232
|
+
);
|
|
233
|
+
if (result.code !== 0) throw new Error(result.stderr.trim() || "Could not read Git branch");
|
|
234
|
+
return result.stdout.trim() || null;
|
|
55
235
|
}
|
|
56
236
|
|
|
57
237
|
async function loadPullRequestStatus(
|
|
58
|
-
pi: ExtensionAPI,
|
|
59
238
|
cwd: string,
|
|
60
|
-
signal
|
|
239
|
+
signal: AbortSignal,
|
|
61
240
|
): Promise<PullRequestLookupResult> {
|
|
62
|
-
const result = await
|
|
241
|
+
const result = await statusCommand(
|
|
242
|
+
"gh",
|
|
243
|
+
["pr", "view", "--json", "number,url,state"],
|
|
63
244
|
cwd,
|
|
64
245
|
signal,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (result.killed) return { kind: "error" };
|
|
246
|
+
PR_LOOKUP_TIMEOUT_MS,
|
|
247
|
+
);
|
|
69
248
|
if (result.code !== 0) {
|
|
70
|
-
return
|
|
249
|
+
return result.stderr.toLowerCase().includes("no pull requests found for branch")
|
|
250
|
+
? { kind: "none" }
|
|
251
|
+
: { kind: "error" };
|
|
71
252
|
}
|
|
72
253
|
|
|
73
|
-
const output = result.stdout.trim();
|
|
74
|
-
if (!output) return { kind: "error" };
|
|
75
|
-
|
|
76
254
|
try {
|
|
77
|
-
const parsed = JSON.parse(
|
|
255
|
+
const parsed = JSON.parse(result.stdout) as {
|
|
78
256
|
number?: unknown;
|
|
79
257
|
state?: unknown;
|
|
80
258
|
url?: unknown;
|
|
81
259
|
};
|
|
82
|
-
|
|
83
260
|
if (parsed.state !== "OPEN" && parsed.state !== "CLOSED" && parsed.state !== "MERGED")
|
|
84
261
|
return { kind: "error" };
|
|
85
262
|
if (typeof parsed.number !== "number" || !Number.isInteger(parsed.number) || parsed.number <= 0)
|
|
86
263
|
return { kind: "error" };
|
|
87
|
-
if (typeof parsed.url !== "string") return { kind: "error" };
|
|
88
|
-
|
|
89
|
-
const url = parsed.url.trim();
|
|
90
|
-
if (!url) return { kind: "error" };
|
|
264
|
+
if (typeof parsed.url !== "string" || !parsed.url.trim()) return { kind: "error" };
|
|
91
265
|
|
|
92
266
|
return {
|
|
93
267
|
kind: "found",
|
|
94
|
-
pullRequest: {
|
|
95
|
-
number: parsed.number,
|
|
96
|
-
url,
|
|
97
|
-
state: parsed.state,
|
|
98
|
-
},
|
|
268
|
+
pullRequest: { number: parsed.number, url: parsed.url.trim(), state: parsed.state },
|
|
99
269
|
};
|
|
100
270
|
} catch {
|
|
101
271
|
return { kind: "error" };
|
|
102
272
|
}
|
|
103
273
|
}
|
|
104
274
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}, delay);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function primeCurrentBranch(cwd: string, branchGeneration: number): Promise<void> {
|
|
168
|
-
const branch = await loadCurrentBranch(pi, cwd);
|
|
169
|
-
if (generation !== branchGeneration) return;
|
|
170
|
-
currentBranch = branch;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function scheduleBranchCheck(delay = 0): void {
|
|
174
|
-
const activeCtx = ctx;
|
|
175
|
-
if (!activeCtx?.hasUI) return;
|
|
176
|
-
|
|
177
|
-
if (branchCheckTimer) clearTimeout(branchCheckTimer);
|
|
178
|
-
|
|
179
|
-
const branchGeneration = generation;
|
|
180
|
-
const cwd = activeCtx.cwd;
|
|
181
|
-
branchCheckTimer = setTimeout(() => {
|
|
182
|
-
branchCheckTimer = undefined;
|
|
183
|
-
void checkForBranchChange(cwd, branchGeneration);
|
|
184
|
-
}, delay);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async function checkForBranchChange(cwd: string, branchGeneration: number): Promise<void> {
|
|
188
|
-
const nextBranch = await loadCurrentBranch(pi, cwd);
|
|
189
|
-
if (generation !== branchGeneration) return;
|
|
190
|
-
if (nextBranch === currentBranch) return;
|
|
191
|
-
|
|
192
|
-
currentBranch = nextBranch;
|
|
193
|
-
pullRequest = undefined;
|
|
194
|
-
applyStatus();
|
|
195
|
-
scheduleRefresh(0);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async function refresh(cwd: string, refreshGeneration: number): Promise<void> {
|
|
199
|
-
if (refreshInFlight) {
|
|
200
|
-
refreshQueued = true;
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const abortController = new AbortController();
|
|
205
|
-
activeRefreshAbortController = abortController;
|
|
206
|
-
|
|
207
|
-
const promise = (async () => {
|
|
208
|
-
const result = await loadPullRequestStatus(pi, cwd, abortController.signal);
|
|
209
|
-
if (generation !== refreshGeneration) return;
|
|
210
|
-
|
|
211
|
-
if (result.kind === "found") {
|
|
212
|
-
pullRequest = result.pullRequest;
|
|
213
|
-
applyStatus();
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (result.kind === "none") {
|
|
218
|
-
pullRequest = undefined;
|
|
219
|
-
applyStatus();
|
|
275
|
+
/** Own the command and its pipes through close, including cancellation and timeout escalation. */
|
|
276
|
+
async function statusCommand(
|
|
277
|
+
command: string,
|
|
278
|
+
args: string[],
|
|
279
|
+
cwd: string,
|
|
280
|
+
signal: AbortSignal,
|
|
281
|
+
timeout: number,
|
|
282
|
+
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
283
|
+
signal.throwIfAborted();
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
const child = spawn(command, args, { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
286
|
+
let stdout = "";
|
|
287
|
+
let stderr = "";
|
|
288
|
+
let processError: Error | undefined;
|
|
289
|
+
let cancelled = false;
|
|
290
|
+
let escalated = false;
|
|
291
|
+
let killTimer: NodeJS.Timeout | undefined;
|
|
292
|
+
let pipeTimer: NodeJS.Timeout | undefined;
|
|
293
|
+
const releaseOrphanedPipes = () => {
|
|
294
|
+
if (!escalated || pipeTimer || (child.exitCode === null && child.signalCode === null)) return;
|
|
295
|
+
pipeTimer = setTimeout(() => {
|
|
296
|
+
child.stdout?.destroy();
|
|
297
|
+
child.stderr?.destroy();
|
|
298
|
+
}, PROCESS_PIPE_GRACE_MS);
|
|
299
|
+
};
|
|
300
|
+
const onAbort = () => {
|
|
301
|
+
if (cancelled) return;
|
|
302
|
+
cancelled = true;
|
|
303
|
+
killProcessGroup(child, "SIGTERM");
|
|
304
|
+
killTimer = setTimeout(() => {
|
|
305
|
+
killProcessGroup(child, "SIGKILL");
|
|
306
|
+
escalated = true;
|
|
307
|
+
releaseOrphanedPipes();
|
|
308
|
+
}, PROCESS_KILL_GRACE_MS);
|
|
309
|
+
};
|
|
310
|
+
const timeoutTimer = setTimeout(onAbort, timeout);
|
|
311
|
+
child.on("error", (error) => {
|
|
312
|
+
processError = error;
|
|
313
|
+
});
|
|
314
|
+
child.stdout?.setEncoding("utf8").on("data", (chunk: string) => {
|
|
315
|
+
stdout += chunk;
|
|
316
|
+
});
|
|
317
|
+
child.stderr?.setEncoding("utf8").on("data", (chunk: string) => {
|
|
318
|
+
stderr += chunk;
|
|
319
|
+
});
|
|
320
|
+
child.once("exit", releaseOrphanedPipes);
|
|
321
|
+
child.once("close", (code) => {
|
|
322
|
+
clearTimeout(timeoutTimer);
|
|
323
|
+
clearTimeout(killTimer);
|
|
324
|
+
clearTimeout(pipeTimer);
|
|
325
|
+
child.removeListener("exit", releaseOrphanedPipes);
|
|
326
|
+
signal.removeEventListener("abort", onAbort);
|
|
327
|
+
if (cancelled) {
|
|
328
|
+
killProcessGroup(child, "SIGKILL");
|
|
329
|
+
reject(signal.reason ?? new Error(`${command} timed out`));
|
|
330
|
+
} else if (processError) {
|
|
331
|
+
reject(processError);
|
|
332
|
+
} else {
|
|
333
|
+
resolve({ stdout, stderr, code: code ?? -1 });
|
|
220
334
|
}
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
|
|
335
|
+
});
|
|
336
|
+
if (signal.aborted) onAbort();
|
|
337
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
338
|
+
});
|
|
339
|
+
}
|
|
224
340
|
|
|
341
|
+
function killProcessGroup(child: ReturnType<typeof spawn>, signal: NodeJS.Signals): void {
|
|
342
|
+
if (!child.pid) return;
|
|
343
|
+
try {
|
|
344
|
+
process.kill(-child.pid, signal);
|
|
345
|
+
} catch {
|
|
225
346
|
try {
|
|
226
|
-
|
|
347
|
+
child.kill(signal);
|
|
227
348
|
} catch {
|
|
228
|
-
|
|
229
|
-
} finally {
|
|
230
|
-
if (activeRefreshAbortController === abortController) {
|
|
231
|
-
activeRefreshAbortController = undefined;
|
|
232
|
-
}
|
|
233
|
-
if (refreshInFlight === promise) {
|
|
234
|
-
refreshInFlight = null;
|
|
235
|
-
if (refreshQueued) {
|
|
236
|
-
refreshQueued = false;
|
|
237
|
-
scheduleRefresh(0);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
349
|
+
// Process likely already exited.
|
|
240
350
|
}
|
|
241
351
|
}
|
|
242
|
-
|
|
243
|
-
pi.on("session_start", async (_event, nextCtx) => {
|
|
244
|
-
reset(nextCtx);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
pi.on("user_bash", (event, nextCtx) => {
|
|
248
|
-
ctx = nextCtx;
|
|
249
|
-
|
|
250
|
-
if (isBranchChangeCommand(event.command)) {
|
|
251
|
-
scheduleBranchCheck(USER_BASH_SETTLE_DELAY_MS);
|
|
252
|
-
return undefined;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (isPullRequestStateCommand(event.command)) {
|
|
256
|
-
scheduleRefresh(USER_BASH_SETTLE_DELAY_MS);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return undefined;
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
pi.on("tool_result", async (event, nextCtx) => {
|
|
263
|
-
ctx = nextCtx;
|
|
264
|
-
|
|
265
|
-
if (!isBashToolResult(event)) return;
|
|
266
|
-
if (typeof event.input.command !== "string") return;
|
|
267
|
-
|
|
268
|
-
if (isBranchChangeCommand(event.input.command)) {
|
|
269
|
-
scheduleBranchCheck(0);
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (isPullRequestStateCommand(event.input.command)) {
|
|
274
|
-
scheduleRefresh(0);
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
pi.on("turn_end", async (_event, nextCtx) => {
|
|
279
|
-
ctx = nextCtx;
|
|
280
|
-
scheduleBranchCheck(0);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
pi.on("session_shutdown", async () => {
|
|
284
|
-
reset(undefined);
|
|
285
|
-
});
|
|
286
352
|
}
|