one-code-extension 0.3.0 → 0.4.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 +189 -94
- package/docs/guide/README.md +3 -2
- package/docs/guide/bring-your-claude-code-setup.md +7 -5
- package/docs/guide/configuration.md +7 -5
- package/docs/guide/differences-from-claude-code.md +25 -14
- package/docs/guide/doctor.md +5 -3
- package/docs/guide/hooks.md +20 -6
- package/docs/guide/installation.md +62 -12
- package/docs/guide/permissions-modes-and-auto-mode.md +46 -28
- package/docs/guide/providers-and-models.md +12 -10
- package/docs/guide/reference.md +9 -4
- package/docs/guide/sessions-and-context.md +23 -3
- package/docs/guide/skills-plugins-and-mcp.md +7 -5
- package/docs/guide/subagents-and-workflows.md +13 -11
- package/docs/guide/tasks-and-background-work.md +10 -8
- package/docs/guide/terminal-interface.md +7 -5
- package/docs/guide/tools.md +29 -5
- package/docs/guide/troubleshooting.md +26 -14
- package/docs/guide/windows.md +184 -0
- package/extensions/auto-mode/actions.ts +3 -1
- package/extensions/auto-mode/paths.ts +21 -23
- package/extensions/auto-mode/safety-floor.ts +44 -6
- package/extensions/auto-mode/shell-analysis.ts +25 -10
- package/extensions/auto-mode/transcript.ts +15 -5
- package/extensions/background/index.ts +83 -34
- package/extensions/background/monitor-batch.ts +7 -5
- package/extensions/background/wakeup.ts +18 -24
- package/extensions/bash/background.ts +19 -10
- package/extensions/bash/guards.ts +36 -16
- package/extensions/bash/index.ts +36 -166
- package/extensions/branding/index.ts +35 -4
- package/extensions/btw/index.ts +207 -0
- package/extensions/btw/panel.ts +179 -0
- package/extensions/btw/prompt.ts +45 -0
- package/extensions/clear/index.ts +15 -0
- package/extensions/compaction/fit.ts +9 -6
- package/extensions/doctor/build.ts +22 -2
- package/extensions/doctor/dependencies.ts +63 -31
- package/extensions/doctor/fix-prompt.ts +12 -4
- package/extensions/doctor/index.ts +1 -0
- package/extensions/doctor/models.ts +2 -1
- package/extensions/doctor/report.ts +2 -3
- package/extensions/doctor/update-lookup.ts +19 -11
- package/extensions/effort/index.ts +2 -1
- package/extensions/footer/footer-line.ts +19 -0
- package/extensions/footer/index.ts +25 -5
- package/extensions/hooks/executor.ts +84 -18
- package/extensions/hooks/index.ts +1 -0
- package/extensions/hooks/matcher.ts +23 -9
- package/extensions/hooks/settings.ts +15 -0
- package/extensions/lib/branch-restore.ts +12 -0
- package/extensions/lib/claude-context.ts +2 -3
- package/extensions/lib/claude-settings.ts +19 -0
- package/extensions/lib/command-launch.ts +71 -0
- package/extensions/lib/deferred.ts +173 -33
- package/extensions/lib/git.ts +16 -0
- package/extensions/lib/keys.ts +38 -0
- package/extensions/lib/local-command.ts +112 -0
- package/extensions/lib/model-facts.generated.json +263 -68
- package/extensions/lib/model-policy.ts +13 -0
- package/extensions/lib/model-tier.ts +16 -5
- package/extensions/lib/notifications.ts +547 -53
- package/extensions/lib/paths.ts +130 -2
- package/extensions/lib/pi-ai-estimate.ts +57 -26
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/process-tree.ts +131 -5
- package/extensions/lib/reminders.ts +80 -27
- package/extensions/lib/scratchpad.ts +11 -10
- package/extensions/lib/shell-spawn.ts +298 -0
- package/extensions/lib/shell-tool.ts +252 -0
- package/extensions/lib/shell-tools.ts +13 -0
- package/extensions/lib/side-call.ts +76 -0
- package/extensions/lib/tui-render.ts +62 -11
- package/extensions/lib/update-check.mjs +155 -0
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/which.ts +62 -0
- package/extensions/lib/worktree-channel.ts +18 -0
- package/extensions/lib/worktree-isolation.ts +2 -1
- package/extensions/lsp/client.ts +14 -3
- package/extensions/lsp/index.ts +12 -5
- package/extensions/lsp/install-hints.ts +33 -3
- package/extensions/lsp/plugin-servers.ts +2 -2
- package/extensions/lsp/servers.ts +29 -7
- package/extensions/mcp/index.ts +3 -2
- package/extensions/memory/entries.ts +9 -6
- package/extensions/memory/index.ts +3 -2
- package/extensions/memory/open-external.ts +4 -1
- package/extensions/model-default/index.ts +21 -2
- package/extensions/permissions/index.ts +57 -28
- package/extensions/permissions/matcher.ts +120 -33
- package/extensions/permissions/modes.ts +14 -6
- package/extensions/permissions/powershell-rules.ts +549 -0
- package/extensions/permissions/session-grant.ts +9 -9
- package/extensions/plugins/index.ts +3 -2
- package/extensions/plugins/install/install.ts +1 -1
- package/extensions/powershell/description.ts +139 -0
- package/extensions/powershell/guards.ts +91 -0
- package/extensions/powershell/index.ts +89 -0
- package/extensions/powershell/policy.ts +91 -0
- package/extensions/recap/index.ts +10 -24
- package/extensions/recap/prompt.ts +3 -3
- package/extensions/session-title/index.ts +120 -0
- package/extensions/session-title/title.ts +111 -0
- package/extensions/skill/index.ts +40 -18
- package/extensions/skill/invoke.ts +1 -0
- package/extensions/subagents/hand-back-review.ts +10 -12
- package/extensions/subagents/index.ts +221 -78
- package/extensions/subagents/model-select.ts +75 -7
- package/extensions/system-prompt/environment.ts +14 -1
- package/extensions/system-reminder/index.ts +9 -0
- package/extensions/tasks/index.ts +2 -1
- package/extensions/tool-search/index.ts +41 -20
- package/extensions/workflow/agent-session.ts +6 -4
- package/extensions/workflow/index.ts +28 -5
- package/extensions/worktree/guards.ts +5 -4
- package/extensions/worktree/index.ts +15 -2
- package/extensions/worktree/rewrite.ts +17 -0
- package/package.json +7 -4
- package/types/app.d.ts +18 -2
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* notification and task_output can never disagree.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { spawn } from "node:child_process";
|
|
16
15
|
import { createWriteStream } from "node:fs";
|
|
17
16
|
import type { BackgroundTask } from "../background/registry.ts";
|
|
18
17
|
import { whenAborted } from "../lib/abort.ts";
|
|
19
|
-
import { detachedSpawnOptions, KILL_GRACE_MS, stopProcessTree } from "../lib/process-tree.ts";
|
|
18
|
+
import { detachedSpawnOptions, KILL_GRACE_MS, stopProcessTree, waitForChildExit } from "../lib/process-tree.ts";
|
|
19
|
+
import { bashSpawnOrThrow, type ShellSpawn, spawnShellCommand } from "../lib/shell-spawn.ts";
|
|
20
20
|
|
|
21
21
|
export const STORED_OUTPUT_CAP = 200_000;
|
|
22
22
|
|
|
@@ -45,12 +45,18 @@ export interface StartBackgroundBashOptions {
|
|
|
45
45
|
/** Kill the process tree after this many seconds. */
|
|
46
46
|
timeoutSeconds?: number;
|
|
47
47
|
logPath?: string;
|
|
48
|
+
/**
|
|
49
|
+
* The interpreter to run `command` under. Default: the session's bash
|
|
50
|
+
* (lib/shell-spawn.ts — Git Bash on Windows, `CLAUDE_CODE_GIT_BASH_PATH`
|
|
51
|
+
* honoured); the powershell extension passes its own spec.
|
|
52
|
+
*/
|
|
53
|
+
shell?: ShellSpawn;
|
|
48
54
|
onFinished(task: BackgroundTask, summary: BashFinishSummary): void;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export function startBackgroundBash(options: StartBackgroundBashOptions): BackgroundTask {
|
|
52
58
|
// Own process group, so stop/timeout can signal the whole tree (lib/process-tree.ts).
|
|
53
|
-
const child =
|
|
59
|
+
const child = spawnShellCommand(options.shell ?? bashSpawnOrThrow(), options.command, {
|
|
54
60
|
cwd: options.cwd,
|
|
55
61
|
...detachedSpawnOptions(),
|
|
56
62
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -80,6 +86,7 @@ export function startBackgroundBash(options: StartBackgroundBashOptions): Backgr
|
|
|
80
86
|
|
|
81
87
|
const task: BackgroundTask = {
|
|
82
88
|
id: options.id,
|
|
89
|
+
// "bash" for a PowerShell run too: the kind is what the shell panel lists.
|
|
83
90
|
kind: "bash",
|
|
84
91
|
description: options.description,
|
|
85
92
|
command: options.command,
|
|
@@ -124,13 +131,15 @@ export function startBackgroundBash(options: StartBackgroundBashOptions): Backgr
|
|
|
124
131
|
else complete();
|
|
125
132
|
};
|
|
126
133
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
+
// Exit plus a short stdio grace, not `close`: a descendant the stop missed
|
|
135
|
+
// must not keep the task "running" for its whole life (lib/process-tree.ts).
|
|
136
|
+
waitForChildExit(child).then(
|
|
137
|
+
({ code, signal }) => end(stopRequested ? "stopped" : timedOut || code !== 0 ? "failed" : "completed", code, signal),
|
|
138
|
+
(error: Error) => {
|
|
139
|
+
stored = stored ? `${stored}\n${error.message}` : error.message;
|
|
140
|
+
end("failed", null, null);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
134
143
|
|
|
135
144
|
return task;
|
|
136
145
|
}
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
type Token,
|
|
35
35
|
} from "../auto-mode/shell-analysis.ts";
|
|
36
36
|
|
|
37
|
-
const clip = (text: string, max = 200): string => (text.length > max ? `${text.slice(0, max)}…` : text);
|
|
37
|
+
export const clip = (text: string, max = 200): string => (text.length > max ? `${text.slice(0, max)}…` : text);
|
|
38
38
|
|
|
39
39
|
export function bashGuardReason(command: string, opts: { background: boolean }): string | undefined {
|
|
40
40
|
// A heredoc/here-string body is data, but parseCommand tokenizes it as live
|
|
@@ -72,34 +72,54 @@ function sleepSeconds(args: Token[]): number | undefined {
|
|
|
72
72
|
// A segment's raw slice can keep half of the operator that ended the previous
|
|
73
73
|
// one (`a && b` → second raw is `& b`) — strip it for a clean echo.
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The wait guard's grammar-independent half, shared with the PowerShell
|
|
77
|
+
* guards: measure the run of LEADING sleep statements — `sleep 1 && sleep 1 &&
|
|
78
|
+
* …` is one wait spelled as a chain, so the pacing exemption applies only to a
|
|
79
|
+
* single provably-short sleep. Unprovable durations (`sleep "$DELAY"`) count
|
|
80
|
+
* as long: the model controls the literal. `sleepSeconds` returns undefined
|
|
81
|
+
* for a non-sleep statement.
|
|
82
|
+
*/
|
|
83
|
+
export function leadingSleepReason<T>(
|
|
84
|
+
statements: T[],
|
|
85
|
+
sleepSeconds: (statement: T) => number | undefined | null,
|
|
86
|
+
raw: (statement: T) => string,
|
|
87
|
+
opts: { sleepName: string; joiner: string; untilExample: string },
|
|
88
|
+
): string | undefined {
|
|
80
89
|
let leadSleeps = 0;
|
|
81
90
|
let total: number | undefined = 0;
|
|
82
|
-
for (const
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
91
|
+
for (const statement of statements) {
|
|
92
|
+
const seconds = sleepSeconds(statement);
|
|
93
|
+
if (seconds === null) break; // not a sleep
|
|
85
94
|
leadSleeps++;
|
|
86
|
-
const seconds = sleepSeconds(args);
|
|
87
95
|
total = total === undefined || seconds === undefined ? undefined : total + seconds;
|
|
88
96
|
}
|
|
89
97
|
if (leadSleeps === 0) return undefined;
|
|
90
98
|
if (leadSleeps === 1 && total !== undefined && total < 2) return undefined;
|
|
91
|
-
const rest =
|
|
92
|
-
const shown =
|
|
99
|
+
const rest = statements.slice(leadSleeps).map(raw).filter(Boolean).join("; ");
|
|
100
|
+
const shown = statements.slice(0, leadSleeps).map(raw).join(opts.joiner);
|
|
93
101
|
const echo = rest ? `\`${shown}\` followed by: ${clip(rest)}` : `standalone \`${shown}\``;
|
|
94
102
|
return (
|
|
95
|
-
`Blocked: ${echo}. A foreground
|
|
96
|
-
"To wait for a command you started, run it with run_in_background: true — its completion arrives as a
|
|
97
|
-
|
|
103
|
+
`Blocked: ${echo}. A foreground ${opts.sleepName} stalls the whole session while it runs. ` +
|
|
104
|
+
"To wait for a command you started, run it with run_in_background: true — its completion arrives as a task notification on its own, so you never need to poll. " +
|
|
105
|
+
`To wait for a condition, use the monitor tool with an until-loop (e.g. \`${opts.untilExample}\`) — monitor is deferred, load it with tool_search select:monitor. ` +
|
|
98
106
|
"If you genuinely need a delay (rate limiting, deliberate pacing), keep it under 2 seconds. " +
|
|
99
107
|
"Do not chain shorter sleeps to work around this block."
|
|
100
108
|
);
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
function waitReason(segments: Segment[]): string | undefined {
|
|
112
|
+
return leadingSleepReason(
|
|
113
|
+
segments,
|
|
114
|
+
(seg) => {
|
|
115
|
+
const { command: cmd, args } = resolvePayload(leadTokens(seg));
|
|
116
|
+
return cmd === "sleep" ? sleepSeconds(args) : null;
|
|
117
|
+
},
|
|
118
|
+
(seg) => seg.raw,
|
|
119
|
+
{ sleepName: "sleep", joiner: " && ", untilExample: "until <check>; do sleep 2; done" },
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
103
123
|
// ---------------------------------------------------------------------------
|
|
104
124
|
// Poll-loop guard
|
|
105
125
|
|
|
@@ -173,7 +193,7 @@ export function hasBackgroundAmp(command: string): boolean {
|
|
|
173
193
|
function orphanReason(command: string, segments: Segment[]): string | undefined {
|
|
174
194
|
const message = (via: string) =>
|
|
175
195
|
`Blocked: this command detaches a process with ${via}, leaving an orphan this session cannot manage — its output and exit status would be lost. ` +
|
|
176
|
-
"Run it with run_in_background: true instead: it returns a task id immediately, completion arrives as a
|
|
196
|
+
"Run it with run_in_background: true instead: it returns a task id immediately, completion arrives as a task notification, output stays readable with task_output, and it can be stopped with task_stop. " +
|
|
177
197
|
"If you need shell-level parallelism inside one command, end it with `wait` so the children are reaped.";
|
|
178
198
|
|
|
179
199
|
for (const seg of segments) {
|
package/extensions/bash/index.ts
CHANGED
|
@@ -6,39 +6,24 @@
|
|
|
6
6
|
* is how pi's official sandbox example does it too. The foreground path
|
|
7
7
|
* delegates to pi's real executor (`createBashToolDefinition`) so upstream
|
|
8
8
|
* bash behavior — timeout handling, truncation, PI_* env, spawn hooks — stays
|
|
9
|
-
* exactly pi's;
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
9
|
+
* exactly pi's; the shared body in lib/shell-tool.ts adds the background
|
|
10
|
+
* branch (spool to `<sessionDir>/bash/<taskId>/output.log`, the shared
|
|
11
|
+
* background registry, a steered completion notification), and this file only
|
|
12
|
+
* supplies what is bash's: pi's definition, the bash the session resolved
|
|
13
|
+
* (lib/shell-spawn.ts — Git Bash on Windows, `CLAUDE_CODE_GIT_BASH_PATH`
|
|
14
|
+
* honoured), the CC-shaped description and the bash guards. The permission
|
|
15
|
+
* gate and auto-mode classifier run before execute like any bash call — a
|
|
16
|
+
* background command is NOT auto-allowed, and the gate fires before anything
|
|
17
|
+
* detaches.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import {
|
|
21
|
-
import { join } from "node:path";
|
|
22
|
-
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
23
21
|
import { createBashToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
24
22
|
import { Type } from "typebox";
|
|
25
|
-
import { generateTaskId, TASK_REGISTER_CHANNEL } from "../background/registry.ts";
|
|
26
|
-
import { type BashFinishSummary, runBackgroundBashBlocking, startBackgroundBash, tailCap } from "./background.ts";
|
|
27
|
-
import { bashGuardReason } from "./guards.ts";
|
|
28
|
-
import { commandToEvaluate, trackOriginalCommands } from "../lib/original-command.ts";
|
|
29
|
-
import { createTaskNotifier, oneShotNote, sessionOutlivesTurn, systemNotification } from "../lib/notifications.ts";
|
|
30
|
-
import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
|
|
31
23
|
import { perCwd } from "../lib/per-cwd.ts";
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// plus only a short tail: a finished build used to push 30 KB (~8k tokens) into
|
|
36
|
-
// context unasked (review T7). The full tail stays behind task_output / the log.
|
|
37
|
-
const NOTIFY_OUTPUT_CAP = 2_000;
|
|
38
|
-
/** One-shot modes return the whole output in the result; past this it is persisted (file + preview), never cut. */
|
|
39
|
-
const ONE_SHOT_OUTPUT_CAP = 30_000;
|
|
40
|
-
/** Claude Code's Bash cap; the tool's `timeout` is milliseconds, as CC's is. */
|
|
41
|
-
const MAX_TIMEOUT_MS = 600_000;
|
|
24
|
+
import { bashSpawn } from "../lib/shell-spawn.ts";
|
|
25
|
+
import { registerShellTool } from "../lib/shell-tool.ts";
|
|
26
|
+
import { bashGuardReason } from "./guards.ts";
|
|
42
27
|
|
|
43
28
|
const BashParams = Type.Object({
|
|
44
29
|
command: Type.String({ description: "Bash command to execute" }),
|
|
@@ -46,7 +31,7 @@ const BashParams = Type.Object({
|
|
|
46
31
|
run_in_background: Type.Optional(
|
|
47
32
|
Type.Boolean({
|
|
48
33
|
description:
|
|
49
|
-
"Run detached and return a task id immediately instead of waiting. Completion arrives as a
|
|
34
|
+
"Run detached and return a task id immediately instead of waiting. Completion arrives as a task notification; inspect with task_output, stop with task_stop. Use this instead of nohup/'&' — those leave an unmanaged orphan process",
|
|
50
35
|
}),
|
|
51
36
|
),
|
|
52
37
|
description: Type.Optional(
|
|
@@ -55,11 +40,23 @@ const BashParams = Type.Object({
|
|
|
55
40
|
});
|
|
56
41
|
|
|
57
42
|
export default function bashExtension(pi: ExtensionAPI) {
|
|
43
|
+
// The session's bash, resolved once (lib/shell-spawn.ts). An ignored
|
|
44
|
+
// CLAUDE_CODE_GIT_BASH_PATH is reported once, at the first session start.
|
|
45
|
+
const bash = bashSpawn();
|
|
46
|
+
const shellPath = bash.spawn?.commandTransport === "stdin" ? undefined : bash.spawn?.shell;
|
|
47
|
+
let warned = false;
|
|
48
|
+
pi.on("session_start", (_event, ctx) => {
|
|
49
|
+
if (warned || !bash.warning) return;
|
|
50
|
+
warned = true;
|
|
51
|
+
if (ctx.hasUI) ctx.ui.notify(bash.warning, "warning");
|
|
52
|
+
else process.stderr.write(`${bash.warning}\n`);
|
|
53
|
+
});
|
|
54
|
+
|
|
58
55
|
// pi's definition supplies the description and TUI renderers; the executor
|
|
59
56
|
// is re-created per working directory because it closes over cwd (worktree
|
|
60
|
-
// switches change ctx.cwd mid-session).
|
|
61
|
-
|
|
62
|
-
const
|
|
57
|
+
// switches change ctx.cwd mid-session). The same bash drives pi's foreground
|
|
58
|
+
// executor, so the override applies there too.
|
|
59
|
+
const base = createBashToolDefinition(process.cwd(), { shellPath });
|
|
63
60
|
// pi's base sentence is "Optionally provide a timeout in seconds." — but the
|
|
64
61
|
// `timeout` parameter and the execute path both use milliseconds (Claude
|
|
65
62
|
// Code's Bash unit; the executor divides by 1000). The two must not
|
|
@@ -71,141 +68,14 @@ export default function bashExtension(pi: ExtensionAPI) {
|
|
|
71
68
|
"`timeout` is in milliseconds: default 120000, max 600000.",
|
|
72
69
|
);
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
// A worktree session cd-wraps input.command; the pre-wrapper original arrives
|
|
76
|
-
// over the bus keyed by toolCallId (never read from params — model-writable).
|
|
77
|
-
const originalCommands = trackOriginalCommands(pi);
|
|
78
|
-
const notify = (text: string, details: Record<string, unknown>) => notifyTask("task-notification", text, details);
|
|
79
|
-
|
|
80
|
-
const taskLogPath = (ctx: ExtensionContext, taskId: string): string | undefined => {
|
|
81
|
-
try {
|
|
82
|
-
const dir = join(ctx.sessionManager.getSessionDir(), "bash", taskId);
|
|
83
|
-
mkdirSync(dir, { recursive: true });
|
|
84
|
-
return join(dir, "output.log");
|
|
85
|
-
} catch {
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const finishLine = (summary: BashFinishSummary, timeoutSeconds?: number): string => {
|
|
91
|
-
if (summary.stopped) return "stopped";
|
|
92
|
-
if (summary.timedOut) return `failed (timed out after ${timeoutSeconds}s)`;
|
|
93
|
-
if (summary.exitCode === 0) return "completed";
|
|
94
|
-
return `failed (${summary.exitCode !== null ? `exit code ${summary.exitCode}` : `terminated by ${summary.signal ?? "unknown signal"}`})`;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
pi.registerTool({
|
|
71
|
+
registerShellTool(pi, {
|
|
98
72
|
name: "bash",
|
|
99
|
-
|
|
100
|
-
description: `${baseDescription} Pass run_in_background: true for long-running commands (builds, servers, watches): it returns a task id immediately so you can keep working, completion arrives as a
|
|
101
|
-
promptSnippet: base.promptSnippet,
|
|
102
|
-
promptGuidelines: base.promptGuidelines,
|
|
103
|
-
executionMode: base.executionMode,
|
|
104
|
-
...(() => {
|
|
105
|
-
// `● Bash(cmd)` / elbow-indented output, like every other One Code tool.
|
|
106
|
-
// Not passing base.renderCall also drops its timer state, so the
|
|
107
|
-
// misleading "Took Ns" that counted permission-prompt wait disappears.
|
|
108
|
-
const wrapped = ccWrapBuiltinRenderers<{ command?: string }>("Bash", base, { title: (a) => a?.command });
|
|
109
|
-
return {
|
|
110
|
-
renderShell: wrapped.renderShell,
|
|
111
|
-
renderCall: wrapped.renderCall as ToolDefinition<typeof BashParams>["renderCall"],
|
|
112
|
-
renderResult: ((result, options, theme, context) => {
|
|
113
|
-
// A background start returns a model-facing instruction paragraph;
|
|
114
|
-
// the transcript needs one line (Claude Code: "Running in the background").
|
|
115
|
-
const details = result.details as { taskId?: string; logPath?: string } | undefined;
|
|
116
|
-
if (details?.taskId && !context.isError) {
|
|
117
|
-
const line = options.expanded
|
|
118
|
-
? `Running in the background (task ${details.taskId}${details.logPath ? ` · log: ${details.logPath}` : ""})`
|
|
119
|
-
: "Running in the background (↓ to manage)";
|
|
120
|
-
return linesComponent(() => resultLines(theme as any, line, options.expanded, false));
|
|
121
|
-
}
|
|
122
|
-
return wrapped.renderResult(result, options, theme, context);
|
|
123
|
-
}) as ToolDefinition<typeof BashParams>["renderResult"],
|
|
124
|
-
};
|
|
125
|
-
})(),
|
|
73
|
+
ccLabel: "Bash",
|
|
74
|
+
description: `${baseDescription} Pass run_in_background: true for long-running commands (builds, servers, watches): it returns a task id immediately so you can keep working, completion arrives as a task notification, and the output is retrievable with task_output / stoppable with task_stop (both deferred — load them with tool_search; in a one-shot print/json session the call runs to completion and returns the output directly). Foreground \`sleep\` is blocked; to wait on a condition use the monitor tool (deferred — load it with tool_search select:monitor) with an until-loop.`,
|
|
126
75
|
parameters: BashParams,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const guardReason = bashGuardReason(originalCommand, { background: params.run_in_background === true });
|
|
132
|
-
if (guardReason) return { content: [{ type: "text" as const, text: guardReason }], isError: true, details: {} };
|
|
133
|
-
const timeoutSeconds =
|
|
134
|
-
params.timeout !== undefined && Number.isFinite(params.timeout) && params.timeout > 0
|
|
135
|
-
? Math.min(params.timeout, MAX_TIMEOUT_MS) / 1000
|
|
136
|
-
: undefined;
|
|
137
|
-
|
|
138
|
-
if (!params.run_in_background) {
|
|
139
|
-
// pi's executor takes seconds; the model-facing unit is milliseconds
|
|
140
|
-
// (Claude Code's Bash), so a habitual `timeout: 120000` is 2 minutes,
|
|
141
|
-
// not 33 hours (review T11).
|
|
142
|
-
return foreground(ctx.cwd).execute(
|
|
143
|
-
toolCallId,
|
|
144
|
-
{ command: params.command, timeout: timeoutSeconds },
|
|
145
|
-
signal,
|
|
146
|
-
onUpdate,
|
|
147
|
-
ctx,
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const id = generateTaskId();
|
|
152
|
-
const logPath = taskLogPath(ctx, id);
|
|
153
|
-
const description = params.description || params.command.slice(0, 80);
|
|
154
|
-
|
|
155
|
-
// One-shot modes (`-p` / `--mode json`) exit when the turn settles: a
|
|
156
|
-
// detached task would be orphaned, and its completion callback would
|
|
157
|
-
// call sendMessage on the disposed session and crash pi (measured,
|
|
158
|
-
// STEERING-REVIEW-2026-09-05 H3). Run blocking there — the same rule the
|
|
159
|
-
// Agent tool applies — and return the output in the result.
|
|
160
|
-
if (!sessionOutlivesTurn(ctx.mode)) {
|
|
161
|
-
const summary = await runBackgroundBashBlocking(
|
|
162
|
-
{ id, command: params.command, description, cwd: ctx.cwd, timeoutSeconds, logPath },
|
|
163
|
-
signal,
|
|
164
|
-
);
|
|
165
|
-
const output = persistIfLarge(summary.output, { dir: sessionResultsDir(ctx), id: `bash-${id}`, maxBytes: ONE_SHOT_OUTPUT_CAP });
|
|
166
|
-
return {
|
|
167
|
-
content: [
|
|
168
|
-
{
|
|
169
|
-
type: "text",
|
|
170
|
-
text: `Bash task ${id} (${description}) ${finishLine(summary, timeoutSeconds)}. ${oneShotNote("command")}${logPath ? ` Log: ${logPath}.` : ""}\n\n${output}`,
|
|
171
|
-
},
|
|
172
|
-
],
|
|
173
|
-
// No taskId: nothing was registered behind task_output/task_stop, and
|
|
174
|
-
// its presence is what renderResult reads as "running in the background".
|
|
175
|
-
details: { logPath },
|
|
176
|
-
isError: summary.exitCode !== 0 && !summary.stopped,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const task = startBackgroundBash({
|
|
181
|
-
id,
|
|
182
|
-
command: params.command,
|
|
183
|
-
description,
|
|
184
|
-
cwd: ctx.cwd,
|
|
185
|
-
timeoutSeconds,
|
|
186
|
-
logPath,
|
|
187
|
-
onFinished: (finishedTask, summary) => {
|
|
188
|
-
const tail = tailCap(summary.output, NOTIFY_OUTPUT_CAP).trim();
|
|
189
|
-
const where = `Full output: task_output ${id}${logPath ? ` (or read ${logPath})` : ""}.`;
|
|
190
|
-
notify(
|
|
191
|
-
systemNotification(
|
|
192
|
-
`Background bash ${id} (${description}) ${finishLine(summary, timeoutSeconds)}. ${where}${tail ? `\n\nLast output:\n${tail}` : ""}`,
|
|
193
|
-
),
|
|
194
|
-
{ taskId: id, status: finishedTask.status, exitCode: summary.exitCode, logPath },
|
|
195
|
-
);
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
pi.events.emit(TASK_REGISTER_CHANNEL, task);
|
|
199
|
-
|
|
200
|
-
return {
|
|
201
|
-
content: [
|
|
202
|
-
{
|
|
203
|
-
type: "text",
|
|
204
|
-
text: `⏳ Bash task ${id} running in background (${description}).\n\nCompletion (with output) will arrive as a system notification on its own — you do not need to wait for it or poll; keep working.${logPath ? ` To check interim output, read ${logPath}.` : ""} If your next step cannot proceed without the result, task_output with block=true waits for it. Stop with task_stop.`,
|
|
205
|
-
},
|
|
206
|
-
],
|
|
207
|
-
details: { taskId: id, logPath },
|
|
208
|
-
};
|
|
209
|
-
},
|
|
76
|
+
base,
|
|
77
|
+
foreground: perCwd((cwd: string) => createBashToolDefinition(cwd, { shellPath })),
|
|
78
|
+
guard: bashGuardReason,
|
|
79
|
+
backgroundShell: () => bash.spawn,
|
|
210
80
|
});
|
|
211
81
|
}
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { modeCycleKey } from "../lib/keys.ts";
|
|
17
18
|
import os from "node:os";
|
|
18
19
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { dirname, join } from "node:path";
|
|
20
|
+
import { basename, dirname, join } from "node:path";
|
|
20
21
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
22
|
import { getAgentDir, SettingsManager, VERSION as PI_VERSION } from "@earendil-works/pi-coding-agent";
|
|
22
23
|
import { piVersionWarning } from "../lib/pi-version.ts";
|
|
@@ -91,8 +92,9 @@ const HINTS: [key: string, what: string][] = [
|
|
|
91
92
|
// it as Claude Code's "effort" so the key and /effort agree on the name.
|
|
92
93
|
["shift+tab", "effort"],
|
|
93
94
|
// Claude Code cycles permission modes on shift+tab; pi owns that key, and
|
|
94
|
-
// ctrl+q is the one ctrl+letter both pi and terminals leave free
|
|
95
|
-
|
|
95
|
+
// ctrl+q is the one ctrl+letter both pi and terminals leave free — except
|
|
96
|
+
// on Windows and WSL, where pi takes ctrl+q and the key is alt+m.
|
|
97
|
+
[modeCycleKey(), "permissions"],
|
|
96
98
|
["ctrl+t", "thinking"],
|
|
97
99
|
["ctrl+o", "expand output"],
|
|
98
100
|
["/", "commands"],
|
|
@@ -280,7 +282,37 @@ export default function brandingExtension(pi: ExtensionAPI) {
|
|
|
280
282
|
}
|
|
281
283
|
// The label and prompt marker apply in every session; CC_NO_BANNER only
|
|
282
284
|
// disables the header.
|
|
285
|
+
// The terminal title. pi writes "π - [<session name> - ]<cwd>" at the end of
|
|
286
|
+
// its startup, on every session replacement and whenever the name changes —
|
|
287
|
+
// each AFTER our session_start handler ran, so a title set there alone lost
|
|
288
|
+
// (seen live 2026-09-19: "π - proj"). Re-assert it once the current task
|
|
289
|
+
// has drained (setTimeout 0, past pi's synchronous tail) and on every
|
|
290
|
+
// agent_start, and follow the name like pi does, brand first.
|
|
291
|
+
let lastTitle: string | undefined;
|
|
292
|
+
// A ctx captured for the deferred write is stale once the runtime is replaced
|
|
293
|
+
// (a second /clear inside the 0 ms window); pi's stale-ctx guard throws, and a
|
|
294
|
+
// throw inside a timer is uncaught — so the write is inert after shutdown.
|
|
295
|
+
let live = false;
|
|
296
|
+
const retitle = (ctx: { hasUI: boolean; cwd: string; ui: { setTitle(title: string): void } }) => {
|
|
297
|
+
if (!live || !ctx.hasUI) return;
|
|
298
|
+
const name = pi.getSessionName?.()?.trim();
|
|
299
|
+
const title = name ? `${NAME} - ${name} - ${basename(ctx.cwd)}` : NAME;
|
|
300
|
+
if (title === lastTitle) return; // agent_start fires every turn; write only on change
|
|
301
|
+
lastTitle = title;
|
|
302
|
+
ctx.ui.setTitle(title);
|
|
303
|
+
};
|
|
304
|
+
pi.on("session_info_changed", (_event, ctx) => retitle(ctx));
|
|
305
|
+
pi.on("agent_start", (_event, ctx) => retitle(ctx));
|
|
306
|
+
pi.on("session_shutdown", () => {
|
|
307
|
+
live = false;
|
|
308
|
+
});
|
|
309
|
+
|
|
283
310
|
pi.on("session_start", (_event, ctx) => {
|
|
311
|
+
// pi's own write lands after this handler, so only the deferred write
|
|
312
|
+
// sticks; a new session may carry a name (resume), so forget the last.
|
|
313
|
+
lastTitle = undefined;
|
|
314
|
+
live = true;
|
|
315
|
+
setTimeout(() => retitle(ctx), 0).unref?.();
|
|
284
316
|
ctx.ui.setHiddenThinkingLabel(THINKING_LABEL);
|
|
285
317
|
installPromptMarker(ctx as unknown as { hasUI: boolean; mode: string; ui: BrandingEditorUI });
|
|
286
318
|
// Soft drift guard: warn once at startup when the hosting pi is outside
|
|
@@ -337,7 +369,6 @@ export default function brandingExtension(pi: ExtensionAPI) {
|
|
|
337
369
|
? collectStartupSections(ctx.cwd, home, join(dirname(fileURLToPath(import.meta.url)), "..", "..", "themes"), agentDir)
|
|
338
370
|
: undefined;
|
|
339
371
|
|
|
340
|
-
ctx.ui.setTitle(NAME);
|
|
341
372
|
ctx.ui.setHeader((tui: unknown, theme: unknown) => {
|
|
342
373
|
const paint = safeThemePaint(theme);
|
|
343
374
|
// Width-memoized (pi-tui renders every mounted component every frame);
|