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,14 +1,7 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
2
|
import * as fs from "node:fs";
|
|
4
|
-
import * as path from "node:path";
|
|
5
3
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
CURRENT_SESSION_VERSION,
|
|
8
|
-
SessionManager,
|
|
9
|
-
highlightCode,
|
|
10
|
-
type SessionHeader,
|
|
11
|
-
} from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { SessionManager, highlightCode, type SessionHeader } from "@earendil-works/pi-coding-agent";
|
|
12
5
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
13
6
|
|
|
14
7
|
const TERMINAL_FLAG = "branch-term";
|
|
@@ -46,7 +39,7 @@ const TMUX_LAYOUT_CONFIG: Record<
|
|
|
46
39
|
};
|
|
47
40
|
|
|
48
41
|
function getStringFlag(pi: ExtensionAPI, flagName: string): string | undefined {
|
|
49
|
-
const value = pi.getFlag(
|
|
42
|
+
const value = pi.getFlag(flagName);
|
|
50
43
|
if (typeof value !== "string") return undefined;
|
|
51
44
|
const trimmed = value.trim();
|
|
52
45
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
@@ -55,7 +48,7 @@ function getStringFlag(pi: ExtensionAPI, flagName: string): string | undefined {
|
|
|
55
48
|
function parseTmuxLayout(value: string | undefined): TmuxLayout | undefined {
|
|
56
49
|
if (!value) return undefined;
|
|
57
50
|
const normalized = value.trim().toLowerCase();
|
|
58
|
-
if (normalized
|
|
51
|
+
if (Object.hasOwn(TMUX_LAYOUT_CONFIG, normalized)) {
|
|
59
52
|
return normalized as TmuxLayout;
|
|
60
53
|
}
|
|
61
54
|
return undefined;
|
|
@@ -103,73 +96,23 @@ function getBranchArgumentCompletions(
|
|
|
103
96
|
return matches;
|
|
104
97
|
}
|
|
105
98
|
|
|
106
|
-
function renderTerminalCommand(template: string
|
|
107
|
-
|
|
108
|
-
command =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (command.includes("{session}")) {
|
|
116
|
-
command = command.split("{session}").join(sessionFile);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (template.includes("{command}") || template.includes("{session}")) {
|
|
120
|
-
return command;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return `${command} ${sessionFile}`;
|
|
99
|
+
function renderTerminalCommand(template: string): string {
|
|
100
|
+
// Paths stay data even when they contain shell syntax or placeholder-like text.
|
|
101
|
+
const command = template.replace(
|
|
102
|
+
/\{(cwd|session|command)\}/g,
|
|
103
|
+
(_match, name: string) => `"$TAU_BRANCH_${name.toUpperCase()}"`,
|
|
104
|
+
);
|
|
105
|
+
return /\{(command|session)\}/.test(template) ? command : `${command} "$TAU_BRANCH_SESSION"`;
|
|
124
106
|
}
|
|
125
107
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
child.unref();
|
|
129
|
-
if (onError) child.on("error", onError);
|
|
130
|
-
}
|
|
108
|
+
// Shared outside the factory so detached children cannot retain a session through this listener.
|
|
109
|
+
function ignoreDetachedLauncherError(): void {}
|
|
131
110
|
|
|
132
111
|
function shellQuote(value: string): string {
|
|
133
112
|
if (value.length === 0) return "''";
|
|
134
113
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
135
114
|
}
|
|
136
115
|
|
|
137
|
-
function escapeForDoubleQuotes(value: string): string {
|
|
138
|
-
return value.replace(/(["\\$`!])/g, "\\$1");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function shellQuoteCompact(value: string): string {
|
|
142
|
-
const home = process.env.HOME;
|
|
143
|
-
if (!home) return shellQuote(value);
|
|
144
|
-
|
|
145
|
-
if (value === home) return "$HOME";
|
|
146
|
-
|
|
147
|
-
if (value.startsWith(`${home}/`)) {
|
|
148
|
-
const suffix = value.slice(home.length + 1);
|
|
149
|
-
return `"$HOME/${escapeForDoubleQuotes(suffix)}"`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return shellQuote(value);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function parseSessionIdFromFile(sessionFile: string): string | undefined {
|
|
156
|
-
const fileName = path.basename(sessionFile, ".jsonl");
|
|
157
|
-
const separatorIndex = fileName.lastIndexOf("_");
|
|
158
|
-
if (separatorIndex <= 0) return undefined;
|
|
159
|
-
|
|
160
|
-
const candidate = fileName.slice(separatorIndex + 1);
|
|
161
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(candidate)
|
|
162
|
-
? candidate
|
|
163
|
-
: undefined;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function formatResumeSessionArgument(sessionFile: string): string {
|
|
167
|
-
const sessionId = parseSessionIdFromFile(sessionFile);
|
|
168
|
-
if (sessionId) return sessionId;
|
|
169
|
-
|
|
170
|
-
return shellQuoteCompact(sessionFile);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
116
|
function runClipboardCommand(command: string, args: string[], text: string): boolean {
|
|
174
117
|
try {
|
|
175
118
|
const result = spawnSync(command, args, {
|
|
@@ -241,26 +184,32 @@ function hasValidSessionFile(sessionFile: string): boolean {
|
|
|
241
184
|
}
|
|
242
185
|
}
|
|
243
186
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
187
|
+
/** Write a standalone handoff snapshot when Pi defers persistence; discard the manager afterward. */
|
|
188
|
+
function persistFork(manager: SessionManager): string {
|
|
189
|
+
const sessionFile = manager.getSessionFile();
|
|
190
|
+
const header = manager.getHeader();
|
|
191
|
+
if (!sessionFile || !header) throw new Error("Failed to create branched session");
|
|
192
|
+
if (!fs.existsSync(sessionFile)) {
|
|
193
|
+
const contents =
|
|
194
|
+
[header, ...manager.getEntries()].map((entry) => JSON.stringify(entry)).join("\n") + "\n";
|
|
195
|
+
const fd = fs.openSync(sessionFile, "wx");
|
|
196
|
+
try {
|
|
197
|
+
fs.writeFileSync(fd, contents);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
fs.rmSync(sessionFile, { force: true });
|
|
200
|
+
throw error;
|
|
201
|
+
} finally {
|
|
202
|
+
fs.closeSync(fd);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
260
205
|
return sessionFile;
|
|
261
206
|
}
|
|
262
207
|
|
|
263
208
|
export default function (pi: ExtensionAPI) {
|
|
209
|
+
const launchObservers = new Set<() => void>();
|
|
210
|
+
pi.on("session_start", releaseLaunchObservers);
|
|
211
|
+
pi.on("session_shutdown", releaseLaunchObservers);
|
|
212
|
+
|
|
264
213
|
pi.registerEntryRenderer<CommandMessageDetails>(BRANCH_ENTRY_TYPE, (entry, _options, theme) => {
|
|
265
214
|
const details = entry.data;
|
|
266
215
|
if (!details || typeof details.intro !== "string" || typeof details.command !== "string") {
|
|
@@ -287,7 +236,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
287
236
|
|
|
288
237
|
pi.registerFlag(TERMINAL_FLAG, {
|
|
289
238
|
description:
|
|
290
|
-
"
|
|
239
|
+
"Bash launcher template. Unquoted {cwd}, {session}, and {command} become single arguments; {command} is a command string for e.g. bash -c.",
|
|
291
240
|
type: "string",
|
|
292
241
|
});
|
|
293
242
|
|
|
@@ -326,38 +275,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
326
275
|
|
|
327
276
|
let forkFile: string;
|
|
328
277
|
if (leafId && hasValidSessionFile(sessionFile)) {
|
|
329
|
-
const forkManager = SessionManager.open(
|
|
330
|
-
|
|
331
|
-
|
|
278
|
+
const forkManager = SessionManager.open(
|
|
279
|
+
sessionFile,
|
|
280
|
+
ctx.sessionManager.getSessionDir(),
|
|
281
|
+
ctx.cwd,
|
|
282
|
+
);
|
|
283
|
+
if (!forkManager.createBranchedSession(leafId)) {
|
|
332
284
|
throw new Error("Failed to create branched session");
|
|
333
285
|
}
|
|
334
|
-
forkFile =
|
|
286
|
+
forkFile = persistFork(forkManager);
|
|
335
287
|
} else {
|
|
336
|
-
if (hasAssistantReply) {
|
|
288
|
+
if (leafId && hasAssistantReply) {
|
|
337
289
|
throw new Error(`Current session file is missing or invalid: ${sessionFile}`);
|
|
338
290
|
}
|
|
339
291
|
|
|
340
|
-
const message =
|
|
292
|
+
const message = leafId
|
|
293
|
+
? "Current session has no persisted history yet. Opening a fresh session."
|
|
294
|
+
: "Selected conversation is empty. Opening a fresh session.";
|
|
341
295
|
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
342
296
|
else console.log(message);
|
|
343
297
|
|
|
344
|
-
forkFile =
|
|
298
|
+
forkFile = persistFork(
|
|
299
|
+
SessionManager.create(ctx.cwd, ctx.sessionManager.getSessionDir(), {
|
|
300
|
+
parentSession: sessionFile,
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
345
303
|
}
|
|
346
304
|
|
|
347
|
-
const resumeCommand = `cd ${
|
|
305
|
+
const resumeCommand = `cd ${shellQuote(ctx.cwd)} && pi --session ${shellQuote(forkFile)}`;
|
|
348
306
|
|
|
349
307
|
const terminalFlag = getStringFlag(pi, TERMINAL_FLAG);
|
|
350
308
|
if (terminalFlag) {
|
|
351
|
-
|
|
352
|
-
spawnDetached("bash", ["-lc", command], (error) => {
|
|
353
|
-
if (ctx.hasUI) {
|
|
354
|
-
ctx.ui.notify(`Terminal command failed: ${error.message}`, "error");
|
|
355
|
-
} else {
|
|
356
|
-
console.error(`Terminal command failed: ${error.message}`);
|
|
357
|
-
}
|
|
358
|
-
showCommandMessage(pi, ctx, MANUAL_RESUME_INTRO, resumeCommand);
|
|
359
|
-
});
|
|
360
|
-
if (ctx.hasUI) ctx.ui.notify("Opened fork in new terminal", "info");
|
|
309
|
+
launchTerminal(terminalFlag, forkFile, resumeCommand, ctx);
|
|
361
310
|
return;
|
|
362
311
|
}
|
|
363
312
|
|
|
@@ -397,4 +346,64 @@ export default function (pi: ExtensionAPI) {
|
|
|
397
346
|
showCommandMessage(pi, ctx, MANUAL_RESUME_INTRO, resumeCommand);
|
|
398
347
|
},
|
|
399
348
|
});
|
|
349
|
+
|
|
350
|
+
function launchTerminal(
|
|
351
|
+
template: string,
|
|
352
|
+
forkFile: string,
|
|
353
|
+
resumeCommand: string,
|
|
354
|
+
ctx: ExtensionCommandContext,
|
|
355
|
+
) {
|
|
356
|
+
const reportFailure = (message: string) => {
|
|
357
|
+
try {
|
|
358
|
+
if (ctx.hasUI) ctx.ui.notify(`Terminal command failed: ${message}`, "error");
|
|
359
|
+
else console.error(`Terminal command failed: ${message}`);
|
|
360
|
+
showCommandMessage(pi, ctx, MANUAL_RESUME_INTRO, resumeCommand);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.error(
|
|
363
|
+
`Could not show terminal recovery instructions: ${error instanceof Error ? error.message : String(error)}`,
|
|
364
|
+
);
|
|
365
|
+
console.error(`${MANUAL_RESUME_INTRO}:\n${resumeCommand}`);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
try {
|
|
369
|
+
const child = spawn("bash", ["-c", renderTerminalCommand(template)], {
|
|
370
|
+
cwd: ctx.cwd,
|
|
371
|
+
env: {
|
|
372
|
+
...process.env,
|
|
373
|
+
TAU_BRANCH_CWD: ctx.cwd,
|
|
374
|
+
TAU_BRANCH_SESSION: forkFile,
|
|
375
|
+
TAU_BRANCH_COMMAND: `pi --session ${shellQuote(forkFile)}`,
|
|
376
|
+
},
|
|
377
|
+
detached: true,
|
|
378
|
+
stdio: "ignore",
|
|
379
|
+
});
|
|
380
|
+
const stopObserving = () => {
|
|
381
|
+
child.removeListener("error", onError);
|
|
382
|
+
child.removeListener("close", onClose);
|
|
383
|
+
launchObservers.delete(stopObserving);
|
|
384
|
+
};
|
|
385
|
+
const onError = (error: Error) => {
|
|
386
|
+
stopObserving();
|
|
387
|
+
reportFailure(error.message);
|
|
388
|
+
};
|
|
389
|
+
const onClose = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
390
|
+
stopObserving();
|
|
391
|
+
if (code !== 0) reportFailure(signal ? `terminated by ${signal}` : `exit code ${code}`);
|
|
392
|
+
};
|
|
393
|
+
// A detached, user-owned launcher may still emit an error after Pi stops observing it.
|
|
394
|
+
child.on("error", ignoreDetachedLauncherError);
|
|
395
|
+
child.once("error", onError);
|
|
396
|
+
child.once("close", onClose);
|
|
397
|
+
launchObservers.add(stopObserving);
|
|
398
|
+
child.unref();
|
|
399
|
+
} catch (error) {
|
|
400
|
+
reportFailure(error instanceof Error ? error.message : String(error));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (ctx.hasUI) ctx.ui.notify("Terminal launch requested", "info");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function releaseLaunchObservers() {
|
|
407
|
+
for (const stopObserving of launchObservers) stopObserving();
|
|
408
|
+
}
|
|
400
409
|
}
|
package/extensions/btw.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
|
|
32
32
|
const STATUS_KEY = "0-btw";
|
|
33
33
|
const RESULT_MARKDOWN_THEME = getMarkdownTheme();
|
|
34
|
+
const MIN_RESULT_WIDTH = 50;
|
|
34
35
|
const STATUS_SPINNER_INTERVAL_MS = 80;
|
|
35
36
|
const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
36
37
|
|
|
@@ -380,6 +381,7 @@ function extractText(content: AssistantMessage["content"]): string {
|
|
|
380
381
|
class BtwResultComponent implements Component {
|
|
381
382
|
private readonly markdown: Markdown;
|
|
382
383
|
private scrollOffset = 0;
|
|
384
|
+
private renderWidth?: number;
|
|
383
385
|
private cachedBodyWidth?: number;
|
|
384
386
|
private cachedBodyLines?: string[];
|
|
385
387
|
|
|
@@ -409,8 +411,11 @@ class BtwResultComponent implements Component {
|
|
|
409
411
|
return;
|
|
410
412
|
}
|
|
411
413
|
|
|
414
|
+
const width = this.renderWidth ?? this.tui.terminal.columns;
|
|
415
|
+
if (width < MIN_RESULT_WIDTH) return;
|
|
416
|
+
|
|
412
417
|
const bodyHeight = this.getBodyHeight();
|
|
413
|
-
const boxWidth = this.getBoxWidth(
|
|
418
|
+
const boxWidth = this.getBoxWidth(width);
|
|
414
419
|
const bodyLines = this.getBodyLines(this.getContentWidth(boxWidth));
|
|
415
420
|
const maxScroll = Math.max(0, bodyLines.length - bodyHeight);
|
|
416
421
|
|
|
@@ -434,6 +439,16 @@ class BtwResultComponent implements Component {
|
|
|
434
439
|
}
|
|
435
440
|
|
|
436
441
|
render(width: number): string[] {
|
|
442
|
+
this.renderWidth = width;
|
|
443
|
+
if (width < MIN_RESULT_WIDTH) {
|
|
444
|
+
return [
|
|
445
|
+
truncateToWidth(
|
|
446
|
+
this.theme.fg("muted", `Resize to ${MIN_RESULT_WIDTH}+ columns to view answer.`),
|
|
447
|
+
width,
|
|
448
|
+
),
|
|
449
|
+
];
|
|
450
|
+
}
|
|
451
|
+
|
|
437
452
|
const boxWidth = this.getBoxWidth(width);
|
|
438
453
|
const contentWidth = this.getContentWidth(boxWidth);
|
|
439
454
|
const questionLines = wrapTextWithAnsi(
|
|
@@ -486,7 +501,7 @@ class BtwResultComponent implements Component {
|
|
|
486
501
|
}
|
|
487
502
|
|
|
488
503
|
private getBoxWidth(width: number): number {
|
|
489
|
-
return Math.max(
|
|
504
|
+
return Math.max(MIN_RESULT_WIDTH, Math.min(width - 2, 140));
|
|
490
505
|
}
|
|
491
506
|
|
|
492
507
|
private getContentWidth(boxWidth: number): number {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Caffeinate
|
|
2
|
+
|
|
3
|
+
`caffeinate` prevents system sleep during agent runs, including retries, automatic compaction, and queued follow-ups. It releases the inhibitor when the agent settles or the session closes or reloads. Display sleep settings remain unchanged.
|
|
4
|
+
|
|
5
|
+
macOS uses `caffeinate -i`; Linux uses `systemd-inhibit --what=sleep` and requires an accessible, authorized logind service. Linux's sleep lock can also block manual suspend; lid-close and forced-sleep behavior remain OS-policy decisions. If inhibition is unavailable, Pi warns once per load, on the first failed attempt, and continues normally. There are no commands or settings; disable the extension through Pi's package configuration if unwanted.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/** Keep the system awake during agent runs without inhibiting display sleep. */
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { platform } from "node:os";
|
|
5
|
+
import { stripVTControlCharacters } from "node:util";
|
|
6
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
interface InhibitCommand {
|
|
9
|
+
command: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
waitForStdin: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function (pi: ExtensionAPI) {
|
|
15
|
+
let inhibitor: ReturnType<typeof startInhibitor> | undefined;
|
|
16
|
+
let releasing = Promise.resolve();
|
|
17
|
+
let shutdown = false;
|
|
18
|
+
let warned = false;
|
|
19
|
+
|
|
20
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
21
|
+
await releasing;
|
|
22
|
+
if (shutdown || inhibitor) return;
|
|
23
|
+
const command = inhibitCommand();
|
|
24
|
+
if (!command) return;
|
|
25
|
+
try {
|
|
26
|
+
inhibitor = startInhibitor(command, ctx.cwd, (reason) => warn(ctx, reason));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
warn(ctx, error instanceof Error ? error.message : String(error));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
33
|
+
if (!shutdown && ctx.isIdle()) await release();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
pi.on("session_shutdown", async () => {
|
|
37
|
+
shutdown = true;
|
|
38
|
+
await release();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
async function release() {
|
|
42
|
+
if (inhibitor) {
|
|
43
|
+
releasing = inhibitor.stop();
|
|
44
|
+
inhibitor = undefined;
|
|
45
|
+
}
|
|
46
|
+
await releasing;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function warn(ctx: ExtensionContext, reason: string) {
|
|
50
|
+
if (shutdown || warned) return;
|
|
51
|
+
warned = true;
|
|
52
|
+
const message = `Caffeinate: sleep prevention unavailable. ${stripVTControlCharacters(reason)}`;
|
|
53
|
+
try {
|
|
54
|
+
if (ctx.hasUI) {
|
|
55
|
+
ctx.ui.notify(message, "warning");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// A missing UI must not turn an optional inhibitor failure into an uncaught error.
|
|
60
|
+
}
|
|
61
|
+
console.warn(message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function inhibitCommand(): InhibitCommand | undefined {
|
|
66
|
+
switch (platform()) {
|
|
67
|
+
case "darwin":
|
|
68
|
+
return {
|
|
69
|
+
command: "/usr/bin/caffeinate",
|
|
70
|
+
args: ["-i", "-w", String(process.pid)],
|
|
71
|
+
waitForStdin: false,
|
|
72
|
+
};
|
|
73
|
+
case "linux":
|
|
74
|
+
return {
|
|
75
|
+
command: "systemd-inhibit",
|
|
76
|
+
args: [
|
|
77
|
+
"--what=sleep",
|
|
78
|
+
"--mode=block",
|
|
79
|
+
"--who=Pi",
|
|
80
|
+
"--why=Pi agent is running",
|
|
81
|
+
"--",
|
|
82
|
+
"cat",
|
|
83
|
+
],
|
|
84
|
+
waitForStdin: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Own the inhibitor through close; a parent PID watch or stdin EOF also releases it if Pi exits. */
|
|
90
|
+
function startInhibitor(command: InhibitCommand, cwd: string, onFailure: (reason: string) => void) {
|
|
91
|
+
const child = spawn(command.command, command.args, {
|
|
92
|
+
cwd,
|
|
93
|
+
detached: true,
|
|
94
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
95
|
+
});
|
|
96
|
+
let stderr = "";
|
|
97
|
+
let error: string | undefined;
|
|
98
|
+
let stopped = false;
|
|
99
|
+
let closed = false;
|
|
100
|
+
let escalation: NodeJS.Timeout | undefined;
|
|
101
|
+
child.stderr.setEncoding("utf8");
|
|
102
|
+
child.stderr.on("data", (chunk: string) => {
|
|
103
|
+
stderr = (stderr + chunk).slice(0, 2048);
|
|
104
|
+
});
|
|
105
|
+
child.on("error", (cause: Error) => {
|
|
106
|
+
error = cause.message;
|
|
107
|
+
});
|
|
108
|
+
child.stdin.on("error", (cause: Error) => {
|
|
109
|
+
error = cause.message;
|
|
110
|
+
});
|
|
111
|
+
const completion = new Promise<void>((resolve) => {
|
|
112
|
+
child.once("close", (code, signal) => {
|
|
113
|
+
closed = true;
|
|
114
|
+
clearTimeout(escalation);
|
|
115
|
+
if (!stopped) {
|
|
116
|
+
onFailure(
|
|
117
|
+
error || stderr.trim() || `${command.command} exited unexpectedly (${signal ?? code}).`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
resolve();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
stop() {
|
|
126
|
+
if (!stopped && !closed) {
|
|
127
|
+
stopped = true;
|
|
128
|
+
child.stdin.end();
|
|
129
|
+
if (!command.waitForStdin) sendSignal("SIGTERM");
|
|
130
|
+
escalation = setTimeout(() => sendSignal("SIGKILL"), 500);
|
|
131
|
+
}
|
|
132
|
+
return completion;
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
function sendSignal(signal: "SIGTERM" | "SIGKILL") {
|
|
137
|
+
if (closed || child.pid === undefined) return;
|
|
138
|
+
try {
|
|
139
|
+
process.kill(-child.pid, signal);
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
if ((cause as NodeJS.ErrnoException).code !== "ESRCH") child.kill(signal);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|