one-code-extension 0.1.4 → 0.3.1
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/LICENSE +20 -0
- package/README.md +222 -177
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +128 -0
- package/docs/guide/configuration.md +173 -0
- package/docs/guide/differences-from-claude-code.md +152 -0
- package/docs/guide/doctor.md +88 -0
- package/docs/guide/hooks.md +111 -0
- package/docs/guide/installation.md +126 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
- package/docs/guide/providers-and-models.md +196 -0
- package/docs/guide/reference.md +226 -0
- package/docs/guide/sessions-and-context.md +192 -0
- package/docs/guide/skills-plugins-and-mcp.md +178 -0
- package/docs/guide/subagents-and-workflows.md +195 -0
- package/docs/guide/tasks-and-background-work.md +113 -0
- package/docs/guide/terminal-interface.md +157 -0
- package/docs/guide/tools.md +177 -0
- package/docs/guide/troubleshooting.md +177 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -5,16 +5,18 @@
|
|
|
5
5
|
* A background command spawns detached in its own process group, spools
|
|
6
6
|
* stdout+stderr to memory (tail-capped) and to a log file when one is given,
|
|
7
7
|
* and satisfies the BackgroundTask contract so task_output/task_stop work
|
|
8
|
-
* unchanged. Robustness rules from docs/
|
|
8
|
+
* unchanged. Robustness rules from docs/features/tools/records/background-bash.md: `output()`
|
|
9
9
|
* never returns an empty body for a finished task (an explicitly-marked
|
|
10
10
|
* "(no output)" beats an ambiguous blank a weak model reads as failure), and
|
|
11
11
|
* the completion callback receives exactly what `output()` returns, so the
|
|
12
12
|
* notification and task_output can never disagree.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
16
|
import { createWriteStream } from "node:fs";
|
|
17
17
|
import type { BackgroundTask } from "../background/registry.ts";
|
|
18
|
+
import { whenAborted } from "../lib/abort.ts";
|
|
19
|
+
import { detachedSpawnOptions, KILL_GRACE_MS, stopProcessTree } from "../lib/process-tree.ts";
|
|
18
20
|
|
|
19
21
|
export const STORED_OUTPUT_CAP = 200_000;
|
|
20
22
|
|
|
@@ -46,24 +48,11 @@ export interface StartBackgroundBashOptions {
|
|
|
46
48
|
onFinished(task: BackgroundTask, summary: BashFinishSummary): void;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
function killTree(child: ChildProcess): void {
|
|
50
|
-
if (child.pid == null) return;
|
|
51
|
-
// Detached → own process group; negative pid signals the whole tree.
|
|
52
|
-
try {
|
|
53
|
-
process.kill(-child.pid, "SIGTERM");
|
|
54
|
-
} catch {
|
|
55
|
-
try {
|
|
56
|
-
child.kill("SIGTERM");
|
|
57
|
-
} catch {
|
|
58
|
-
// Already gone.
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
51
|
export function startBackgroundBash(options: StartBackgroundBashOptions): BackgroundTask {
|
|
52
|
+
// Own process group, so stop/timeout can signal the whole tree (lib/process-tree.ts).
|
|
64
53
|
const child = spawn(process.env.SHELL || "/bin/sh", ["-c", options.command], {
|
|
65
54
|
cwd: options.cwd,
|
|
66
|
-
|
|
55
|
+
...detachedSpawnOptions(),
|
|
67
56
|
stdio: ["ignore", "pipe", "pipe"],
|
|
68
57
|
});
|
|
69
58
|
|
|
@@ -93,13 +82,17 @@ export function startBackgroundBash(options: StartBackgroundBashOptions): Backgr
|
|
|
93
82
|
id: options.id,
|
|
94
83
|
kind: "bash",
|
|
95
84
|
description: options.description,
|
|
85
|
+
command: options.command,
|
|
96
86
|
status: "running",
|
|
97
87
|
startedAt: Date.now(),
|
|
98
88
|
logPath: options.logPath,
|
|
89
|
+
ownUI: true, // rendered live by the subagents panel's shell manager
|
|
99
90
|
output: () => stored || (task.status === "running" ? "" : EMPTY_OUTPUT_MARKER),
|
|
100
91
|
stop: () => {
|
|
101
92
|
stopRequested = true;
|
|
102
|
-
|
|
93
|
+
// SIGTERM the tree, SIGKILL after the grace: a command that traps TERM
|
|
94
|
+
// would otherwise never close, and a blocking one-shot run never return.
|
|
95
|
+
stopProcessTree(child, KILL_GRACE_MS);
|
|
103
96
|
},
|
|
104
97
|
finished,
|
|
105
98
|
};
|
|
@@ -108,7 +101,7 @@ export function startBackgroundBash(options: StartBackgroundBashOptions): Backgr
|
|
|
108
101
|
if (options.timeoutSeconds && options.timeoutSeconds > 0) {
|
|
109
102
|
timer = setTimeout(() => {
|
|
110
103
|
timedOut = true;
|
|
111
|
-
|
|
104
|
+
stopProcessTree(child, KILL_GRACE_MS);
|
|
112
105
|
}, options.timeoutSeconds * 1000);
|
|
113
106
|
timer.unref?.();
|
|
114
107
|
}
|
|
@@ -119,9 +112,16 @@ export function startBackgroundBash(options: StartBackgroundBashOptions): Backgr
|
|
|
119
112
|
if (timer) clearTimeout(timer);
|
|
120
113
|
task.status = status;
|
|
121
114
|
task.finishedAt = Date.now();
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
const complete = () => {
|
|
116
|
+
finish();
|
|
117
|
+
options.onFinished(task, { exitCode, signal, stopped: stopRequested, timedOut, output: task.output() });
|
|
118
|
+
};
|
|
119
|
+
// end() flushes asynchronously; `finished` must not resolve while the
|
|
120
|
+
// log file is still short of what output() returns, or a reader sent to
|
|
121
|
+
// logPath by the completion notification can see a truncated file. The
|
|
122
|
+
// callback also fires if the stream errors, so this cannot hang.
|
|
123
|
+
if (log) log.end(complete);
|
|
124
|
+
else complete();
|
|
125
125
|
};
|
|
126
126
|
|
|
127
127
|
child.on("error", (error) => {
|
|
@@ -134,3 +134,28 @@ export function startBackgroundBash(options: StartBackgroundBashOptions): Backgr
|
|
|
134
134
|
|
|
135
135
|
return task;
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Run a background command to completion and resolve with its summary — the
|
|
140
|
+
* shape the one-shot modes need (`-p` / `--mode json`): the process exits when
|
|
141
|
+
* the turn settles, so a detached task would be orphaned with its completion
|
|
142
|
+
* undelivered — and, worse, its late callback would call `sendMessage` on the
|
|
143
|
+
* disposed session and crash pi (STEERING-REVIEW-2026-09-05 H3). The tool's
|
|
144
|
+
* abort signal stops the process tree; the summary then reports `stopped`.
|
|
145
|
+
*/
|
|
146
|
+
export function runBackgroundBashBlocking(
|
|
147
|
+
options: Omit<StartBackgroundBashOptions, "onFinished">,
|
|
148
|
+
signal?: AbortSignal,
|
|
149
|
+
): Promise<BashFinishSummary> {
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
let unhook = () => {};
|
|
152
|
+
const task = startBackgroundBash({
|
|
153
|
+
...options,
|
|
154
|
+
onFinished: (_task, summary) => {
|
|
155
|
+
unhook();
|
|
156
|
+
resolve(summary);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
unhook = whenAborted(signal, () => task.stop());
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic pre-execution guards for the bash tool (pure).
|
|
3
|
+
*
|
|
4
|
+
* Each guard recognises a workflow anti-pattern BEFORE the command runs and
|
|
5
|
+
* refuses it with an instructive error: what tripped it, why it is a problem,
|
|
6
|
+
* the sanctioned alternative, and a clause closing the obvious workaround.
|
|
7
|
+
* These are steering, not security — the permission gate and the auto-mode
|
|
8
|
+
* classifier still apply to everything that passes. The contract mirrors the
|
|
9
|
+
* auto-mode pre-gate's: a guard concludes only on a positive parse, so an
|
|
10
|
+
* unparseable command always passes through.
|
|
11
|
+
*
|
|
12
|
+
* Guards, in order:
|
|
13
|
+
* - interactive guard (foreground + background): a command that needs a TTY
|
|
14
|
+
* (`vim`, `git rebase -i`, `git add -p`, `watch`) hangs until the timeout —
|
|
15
|
+
* or worse, silently no-ops — in this pipe-connected shell.
|
|
16
|
+
* - wait guard (foreground): a command that LEADS with `sleep` exists only to
|
|
17
|
+
* wait and stalls the whole session. Provably-short sleeps (< 2s) are
|
|
18
|
+
* legitimate pacing and pass; a sleep deeper in a chain is a pause inside
|
|
19
|
+
* real work and is left alone.
|
|
20
|
+
* - poll-loop guard (foreground): a `while`/`until`/`for` loop that sleeps is
|
|
21
|
+
* the same stall spelled as a loop — the monitor tool exists to run the
|
|
22
|
+
* until-loop without occupying the session.
|
|
23
|
+
* - orphan guard (foreground): `nohup`/`setsid`/a top-level `&` detaches a
|
|
24
|
+
* process nothing manages, losing its output and exit status. A command
|
|
25
|
+
* that ends with `wait` reaps its own children and passes.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
gitSubcommand,
|
|
30
|
+
leadTokens,
|
|
31
|
+
parseCommand,
|
|
32
|
+
resolvePayload,
|
|
33
|
+
type Segment,
|
|
34
|
+
type Token,
|
|
35
|
+
} from "../auto-mode/shell-analysis.ts";
|
|
36
|
+
|
|
37
|
+
const clip = (text: string, max = 200): string => (text.length > max ? `${text.slice(0, max)}…` : text);
|
|
38
|
+
|
|
39
|
+
export function bashGuardReason(command: string, opts: { background: boolean }): string | undefined {
|
|
40
|
+
// A heredoc/here-string body is data, but parseCommand tokenizes it as live
|
|
41
|
+
// shell — every guard would misfire on body text (a literal `vim` or
|
|
42
|
+
// `while … sleep` inside a document). Judge only the command that PRECEDES
|
|
43
|
+
// the first `<<`: `sleep 600 <<< ""` is still a sleep (review T14), while a
|
|
44
|
+
// `cat <<EOF` body is never read.
|
|
45
|
+
const lead = command.split(/<</)[0];
|
|
46
|
+
if (lead !== command && !lead.trim()) return undefined;
|
|
47
|
+
const { segments, parseFailed } = parseCommand(lead);
|
|
48
|
+
if (parseFailed || segments.length === 0) return undefined;
|
|
49
|
+
const interactive = interactiveReason(segments);
|
|
50
|
+
if (interactive || opts.background) return interactive;
|
|
51
|
+
return waitReason(segments) ?? pollLoopReason(lead, segments) ?? orphanReason(lead, segments);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Wait guard
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Seconds a `sleep` invocation provably lasts (GNU sleep sums multiple
|
|
59
|
+
* duration arguments), or undefined when any argument is not a literal.
|
|
60
|
+
*/
|
|
61
|
+
function sleepSeconds(args: Token[]): number | undefined {
|
|
62
|
+
if (args.length === 0) return undefined;
|
|
63
|
+
let total = 0;
|
|
64
|
+
for (const arg of args) {
|
|
65
|
+
const match = /^(\d+(?:\.\d+)?|\.\d+)([smhd]?)$/.exec(arg.value);
|
|
66
|
+
if (!match) return undefined;
|
|
67
|
+
total += Number(match[1]) * ({ "": 1, s: 1, m: 60, h: 3600, d: 86400 }[match[2]] ?? 1);
|
|
68
|
+
}
|
|
69
|
+
return total;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A segment's raw slice can keep half of the operator that ended the previous
|
|
73
|
+
// one (`a && b` → second raw is `& b`) — strip it for a clean echo.
|
|
74
|
+
|
|
75
|
+
function waitReason(segments: Segment[]): string | undefined {
|
|
76
|
+
// Measure the run of LEADING sleep segments: `sleep 1 && sleep 1 && …` is
|
|
77
|
+
// one wait spelled as a chain, so the pacing exemption applies only to a
|
|
78
|
+
// single provably-short sleep. Unprovable durations (`sleep "$DELAY"`)
|
|
79
|
+
// count as long — the model controls the literal.
|
|
80
|
+
let leadSleeps = 0;
|
|
81
|
+
let total: number | undefined = 0;
|
|
82
|
+
for (const seg of segments) {
|
|
83
|
+
const { command: cmd, args } = resolvePayload(leadTokens(seg));
|
|
84
|
+
if (cmd !== "sleep") break;
|
|
85
|
+
leadSleeps++;
|
|
86
|
+
const seconds = sleepSeconds(args);
|
|
87
|
+
total = total === undefined || seconds === undefined ? undefined : total + seconds;
|
|
88
|
+
}
|
|
89
|
+
if (leadSleeps === 0) return undefined;
|
|
90
|
+
if (leadSleeps === 1 && total !== undefined && total < 2) return undefined;
|
|
91
|
+
const rest = segments.slice(leadSleeps).map((seg) => seg.raw).filter(Boolean).join("; ");
|
|
92
|
+
const shown = segments.slice(0, leadSleeps).map((seg) => seg.raw).join(" && ");
|
|
93
|
+
const echo = rest ? `\`${shown}\` followed by: ${clip(rest)}` : `standalone \`${shown}\``;
|
|
94
|
+
return (
|
|
95
|
+
`Blocked: ${echo}. A foreground sleep stalls the whole session while it runs. ` +
|
|
96
|
+
"To wait for a command you started, run it with run_in_background: true — its completion arrives as a system notification on its own, so you never need to poll. " +
|
|
97
|
+
"To wait for a condition, use the monitor tool with an until-loop (e.g. `until <check>; do sleep 2; done`) — monitor is deferred, load it with tool_search select:monitor. " +
|
|
98
|
+
"If you genuinely need a delay (rate limiting, deliberate pacing), keep it under 2 seconds. " +
|
|
99
|
+
"Do not chain shorter sleeps to work around this block."
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Poll-loop guard
|
|
105
|
+
|
|
106
|
+
function pollLoopReason(command: string, segments: Segment[]): string | undefined {
|
|
107
|
+
const first = leadTokens(segments[0])[0]?.value;
|
|
108
|
+
if (first !== "while" && first !== "until" && first !== "for") return undefined;
|
|
109
|
+
// `sleep` counts only in command position (segment lead, or right after a
|
|
110
|
+
// `do`/`then`/`else` keyword) — `echo sleep` is data, not a wait.
|
|
111
|
+
const sleeps = segments.some((seg) => {
|
|
112
|
+
const { command: cmd } = resolvePayload(leadTokens(seg));
|
|
113
|
+
return cmd === "sleep";
|
|
114
|
+
});
|
|
115
|
+
if (!sleeps) return undefined;
|
|
116
|
+
return (
|
|
117
|
+
`Blocked: a foreground polling loop (\`${clip(command, 160)}\`). It occupies the whole session while it spins. ` +
|
|
118
|
+
"Use the monitor tool to run the until-loop for you (deferred — load it with tool_search select:monitor) — it returns when the condition holds — or start the underlying work with run_in_background: true and let its completion notification arrive on its own. " +
|
|
119
|
+
"Do not shorten the sleep or unroll the loop to work around this block."
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Orphan guard
|
|
125
|
+
|
|
126
|
+
/** True when the command backgrounds something with a top-level unquoted `&`. */
|
|
127
|
+
export function hasBackgroundAmp(command: string): boolean {
|
|
128
|
+
let inSingle = false;
|
|
129
|
+
let inDouble = false;
|
|
130
|
+
let inAnsiC = false; // $'…' honors backslash-escaped quotes, unlike '…'
|
|
131
|
+
let escape = false;
|
|
132
|
+
for (let i = 0; i < command.length; i++) {
|
|
133
|
+
const ch = command[i];
|
|
134
|
+
if (escape) {
|
|
135
|
+
escape = false;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (inAnsiC) {
|
|
139
|
+
if (ch === "\\") escape = true;
|
|
140
|
+
else if (ch === "'") inAnsiC = false;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (ch === "'" && !inSingle && !inDouble && command[i - 1] === "$") {
|
|
144
|
+
inAnsiC = true;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (ch === "\\" && !inSingle) {
|
|
148
|
+
escape = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (ch === "'" && !inDouble) {
|
|
152
|
+
inSingle = !inSingle;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (ch === '"' && !inSingle) {
|
|
156
|
+
inDouble = !inDouble;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (inSingle || inDouble || ch !== "&") continue;
|
|
160
|
+
const prev = command[i - 1];
|
|
161
|
+
const next = command[i + 1];
|
|
162
|
+
if (next === "&") {
|
|
163
|
+
i++; // logical &&
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (next === ">") continue; // &> redirect
|
|
167
|
+
if (prev === ">" || prev === "|") continue; // 2>&1 fd duplication, |& pipe
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function orphanReason(command: string, segments: Segment[]): string | undefined {
|
|
174
|
+
const message = (via: string) =>
|
|
175
|
+
`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 system notification, output stays readable with task_output, and it can be stopped with task_stop. " +
|
|
177
|
+
"If you need shell-level parallelism inside one command, end it with `wait` so the children are reaped.";
|
|
178
|
+
|
|
179
|
+
for (const seg of segments) {
|
|
180
|
+
const { peeled } = resolvePayload(leadTokens(seg));
|
|
181
|
+
const detacher = peeled.find((p) => p === "nohup" || p === "setsid");
|
|
182
|
+
if (detacher) return message(`\`${detacher}\``);
|
|
183
|
+
}
|
|
184
|
+
// Only a `wait` that comes LAST reaps the children — `wait; job &` still
|
|
185
|
+
// orphans the job it precedes.
|
|
186
|
+
const lastCmd = resolvePayload(leadTokens(segments[segments.length - 1])).command;
|
|
187
|
+
if (hasBackgroundAmp(command) && lastCmd !== "wait") return message("`&`");
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Interactive guard
|
|
193
|
+
|
|
194
|
+
const EDITORS = new Set(["vi", "vim", "nvim", "view", "vimdiff", "nano", "pico", "emacs"]);
|
|
195
|
+
|
|
196
|
+
function interactiveReason(segments: Segment[]): string | undefined {
|
|
197
|
+
for (const seg of segments) {
|
|
198
|
+
const { command: cmd, args } = resolvePayload(leadTokens(seg));
|
|
199
|
+
if (EDITORS.has(cmd)) {
|
|
200
|
+
if (cmd === "emacs" && args.some((a) => ["--batch", "-batch", "--script"].includes(a.value))) continue;
|
|
201
|
+
return (
|
|
202
|
+
`Blocked: \`${cmd}\` is an interactive editor and this shell has no TTY — it would hang until the timeout. ` +
|
|
203
|
+
"Read files with the read tool and change them with edit/write."
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (cmd === "watch") {
|
|
207
|
+
return (
|
|
208
|
+
"Blocked: `watch` needs an interactive terminal, which this shell does not have. " +
|
|
209
|
+
"Use the monitor tool (deferred — load it with tool_search select:monitor) to wait for a condition, or run_in_background: true with task_output to check on long-running output."
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (cmd !== "git") continue;
|
|
213
|
+
const { sub, rest } = gitSubcommand(args);
|
|
214
|
+
const flags = new Set(rest.map((t) => t.value));
|
|
215
|
+
if (sub === "rebase" && (flags.has("-i") || flags.has("--interactive"))) {
|
|
216
|
+
return (
|
|
217
|
+
"Blocked: `git rebase -i` opens an interactive editor, which this shell cannot provide — it would hang until the timeout. " +
|
|
218
|
+
"Drive it non-interactively instead: `GIT_SEQUENCE_EDITOR=: git rebase …` accepts the todo list as-is, or restructure history with `git rebase --onto`, `git commit --amend`, or `git cherry-pick`."
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (sub === "add" && ["-i", "--interactive", "-p", "--patch"].some((f) => flags.has(f))) {
|
|
222
|
+
return (
|
|
223
|
+
"Blocked: interactive `git add` prompts on a TTY this shell does not have — it would read EOF and stage nothing while appearing to succeed. " +
|
|
224
|
+
"Stage whole files with `git add <paths>`, or stage a partial change non-interactively with `git apply --cached` and a crafted diff."
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
package/extensions/bash/index.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* A background run spawns detached, returns a task id immediately, spools
|
|
12
12
|
* output to `<sessionDir>/bash/<taskId>/output.log`, registers on the shared
|
|
13
13
|
* background registry (task_output/task_stop just work — they are
|
|
14
|
-
* kind-agnostic), and announces completion as a
|
|
15
|
-
*
|
|
14
|
+
* kind-agnostic), and announces completion as a steered system notification
|
|
15
|
+
* (lib/notifications.ts). The permission gate and auto-mode classifier run before
|
|
16
16
|
* execute like any bash call — a background command is NOT auto-allowed, and
|
|
17
17
|
* the gate fires before anything detaches.
|
|
18
18
|
*/
|
|
@@ -23,18 +23,26 @@ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-w
|
|
|
23
23
|
import { createBashToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
24
24
|
import { Type } from "typebox";
|
|
25
25
|
import { generateTaskId, TASK_REGISTER_CHANNEL } from "../background/registry.ts";
|
|
26
|
-
import { type BashFinishSummary, startBackgroundBash, tailCap } from "./background.ts";
|
|
27
|
-
import {
|
|
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";
|
|
28
31
|
import { perCwd } from "../lib/per-cwd.ts";
|
|
29
32
|
import { ccWrapBuiltinRenderers, linesComponent, resultLines } from "../lib/tui-render.ts";
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
// The completion notification carries status + exit code + where the output is,
|
|
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;
|
|
32
42
|
|
|
33
43
|
const BashParams = Type.Object({
|
|
34
44
|
command: Type.String({ description: "Bash command to execute" }),
|
|
35
|
-
timeout: Type.Optional(
|
|
36
|
-
Type.Number({ description: "Timeout in seconds (optional; also applies to background runs)" }),
|
|
37
|
-
),
|
|
45
|
+
timeout: Type.Optional(Type.Number({ description: "Optional timeout in milliseconds (max 600000)" })),
|
|
38
46
|
run_in_background: Type.Optional(
|
|
39
47
|
Type.Boolean({
|
|
40
48
|
description:
|
|
@@ -52,13 +60,22 @@ export default function bashExtension(pi: ExtensionAPI) {
|
|
|
52
60
|
// switches change ctx.cwd mid-session).
|
|
53
61
|
const base = createBashToolDefinition(process.cwd());
|
|
54
62
|
const foreground = perCwd(createBashToolDefinition);
|
|
63
|
+
// pi's base sentence is "Optionally provide a timeout in seconds." — but the
|
|
64
|
+
// `timeout` parameter and the execute path both use milliseconds (Claude
|
|
65
|
+
// Code's Bash unit; the executor divides by 1000). The two must not
|
|
66
|
+
// contradict, or a model that trusts the description sends `timeout: 120`
|
|
67
|
+
// and gets a 120 ms deadline (TOOL-FIDELITY-REVIEW-2026-09-07 H3). Swap in
|
|
68
|
+
// CC's Bash sentence.
|
|
69
|
+
const baseDescription = base.description.replace(
|
|
70
|
+
"Optionally provide a timeout in seconds.",
|
|
71
|
+
"`timeout` is in milliseconds: default 120000, max 600000.",
|
|
72
|
+
);
|
|
55
73
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
74
|
+
const notifyTask = createTaskNotifier(pi);
|
|
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);
|
|
62
79
|
|
|
63
80
|
const taskLogPath = (ctx: ExtensionContext, taskId: string): string | undefined => {
|
|
64
81
|
try {
|
|
@@ -80,7 +97,7 @@ export default function bashExtension(pi: ExtensionAPI) {
|
|
|
80
97
|
pi.registerTool({
|
|
81
98
|
name: "bash",
|
|
82
99
|
label: base.label,
|
|
83
|
-
description: `${
|
|
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 system 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.`,
|
|
84
101
|
promptSnippet: base.promptSnippet,
|
|
85
102
|
promptGuidelines: base.promptGuidelines,
|
|
86
103
|
executionMode: base.executionMode,
|
|
@@ -97,7 +114,9 @@ export default function bashExtension(pi: ExtensionAPI) {
|
|
|
97
114
|
// the transcript needs one line (Claude Code: "Running in the background").
|
|
98
115
|
const details = result.details as { taskId?: string; logPath?: string } | undefined;
|
|
99
116
|
if (details?.taskId && !context.isError) {
|
|
100
|
-
const line =
|
|
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)";
|
|
101
120
|
return linesComponent(() => resultLines(theme as any, line, options.expanded, false));
|
|
102
121
|
}
|
|
103
122
|
return wrapped.renderResult(result, options, theme, context);
|
|
@@ -106,10 +125,23 @@ export default function bashExtension(pi: ExtensionAPI) {
|
|
|
106
125
|
})(),
|
|
107
126
|
parameters: BashParams,
|
|
108
127
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
128
|
+
// Guards check the model's original command — a worktree session rewrites
|
|
129
|
+
// input.command to `cd '<wt>' && (…)`, which would hide the real lead.
|
|
130
|
+
const originalCommand = commandToEvaluate(originalCommands, toolCallId, params.command);
|
|
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
|
+
|
|
109
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).
|
|
110
142
|
return foreground(ctx.cwd).execute(
|
|
111
143
|
toolCallId,
|
|
112
|
-
{ command: params.command, timeout:
|
|
144
|
+
{ command: params.command, timeout: timeoutSeconds },
|
|
113
145
|
signal,
|
|
114
146
|
onUpdate,
|
|
115
147
|
ctx,
|
|
@@ -119,16 +151,46 @@ export default function bashExtension(pi: ExtensionAPI) {
|
|
|
119
151
|
const id = generateTaskId();
|
|
120
152
|
const logPath = taskLogPath(ctx, id);
|
|
121
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
|
+
|
|
122
180
|
const task = startBackgroundBash({
|
|
123
181
|
id,
|
|
124
182
|
command: params.command,
|
|
125
183
|
description,
|
|
126
184
|
cwd: ctx.cwd,
|
|
127
|
-
timeoutSeconds
|
|
185
|
+
timeoutSeconds,
|
|
128
186
|
logPath,
|
|
129
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})` : ""}.`;
|
|
130
190
|
notify(
|
|
131
|
-
systemNotification(
|
|
191
|
+
systemNotification(
|
|
192
|
+
`Background bash ${id} (${description}) ${finishLine(summary, timeoutSeconds)}. ${where}${tail ? `\n\nLast output:\n${tail}` : ""}`,
|
|
193
|
+
),
|
|
132
194
|
{ taskId: id, status: finishedTask.status, exitCode: summary.exitCode, logPath },
|
|
133
195
|
);
|
|
134
196
|
},
|
|
@@ -20,11 +20,14 @@ import { dirname, join } from "node:path";
|
|
|
20
20
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { getAgentDir, SettingsManager, VERSION as PI_VERSION } from "@earendil-works/pi-coding-agent";
|
|
22
22
|
import { piVersionWarning } from "../lib/pi-version.ts";
|
|
23
|
+
import { extensionVersion } from "../lib/package-version.ts";
|
|
23
24
|
import {
|
|
25
|
+
formatModel,
|
|
26
|
+
formatModelSpec,
|
|
27
|
+
isRealModel,
|
|
24
28
|
PERMISSION_STATUS_CHANNEL,
|
|
25
29
|
permissionModeDisplay,
|
|
26
30
|
type PermissionStatus,
|
|
27
|
-
shortModelName,
|
|
28
31
|
} from "../permissions/modes.ts";
|
|
29
32
|
import { SUBAGENT_STATUS_CHANNEL, type SubagentStatus } from "../subagents/model-select.ts";
|
|
30
33
|
import {
|
|
@@ -34,7 +37,7 @@ import {
|
|
|
34
37
|
shouldDefaultHideThinking,
|
|
35
38
|
type StartupSection,
|
|
36
39
|
} from "./startup.ts";
|
|
37
|
-
import { safeThemePaint, truncateLine } from "../lib/tui-render.ts";
|
|
40
|
+
import { linesComponent, safeThemePaint, truncateLine } from "../lib/tui-render.ts";
|
|
38
41
|
import { markAssistantMarkdown } from "./assistant-marker.ts";
|
|
39
42
|
import { PROMPT_PADDING, PromptEditor } from "./prompt-editor.ts";
|
|
40
43
|
|
|
@@ -298,11 +301,13 @@ export default function brandingExtension(pi: ExtensionAPI) {
|
|
|
298
301
|
let permissionStatus: PermissionStatus | undefined;
|
|
299
302
|
let subagentStatus: SubagentStatus | undefined;
|
|
300
303
|
let currentModelId: string | undefined;
|
|
304
|
+
let currentModelProvider: string | undefined;
|
|
301
305
|
let requestHeaderRender: (() => void) | undefined;
|
|
302
306
|
// The model line goes stale the same way the mode line did once the header
|
|
303
307
|
// re-renders live, so it follows ctrl+p / /model changes too.
|
|
304
308
|
pi.on("model_select", (event) => {
|
|
305
309
|
currentModelId = event.model?.id ?? currentModelId;
|
|
310
|
+
currentModelProvider = event.model?.provider ?? currentModelProvider;
|
|
306
311
|
requestHeaderRender?.();
|
|
307
312
|
});
|
|
308
313
|
pi.events.on(PERMISSION_STATUS_CHANNEL, (data) => {
|
|
@@ -318,8 +323,9 @@ export default function brandingExtension(pi: ExtensionAPI) {
|
|
|
318
323
|
// Only the TUI has a header to replace; rpc/print modes have no chrome.
|
|
319
324
|
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
320
325
|
|
|
321
|
-
const version = process.env.CC_VERSION ??
|
|
322
|
-
currentModelId = ctx.model
|
|
326
|
+
const version = process.env.CC_VERSION ?? extensionVersion();
|
|
327
|
+
currentModelId = ctx.model?.id ?? currentModelId;
|
|
328
|
+
currentModelProvider = ctx.model?.provider ?? currentModelProvider;
|
|
323
329
|
|
|
324
330
|
// With pi's own listing silenced, the banner carries compact sections
|
|
325
331
|
// instead (minus the internal [Extensions] noise).
|
|
@@ -334,33 +340,53 @@ export default function brandingExtension(pi: ExtensionAPI) {
|
|
|
334
340
|
ctx.ui.setTitle(NAME);
|
|
335
341
|
ctx.ui.setHeader((tui: unknown, theme: unknown) => {
|
|
336
342
|
const paint = safeThemePaint(theme);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
render
|
|
343
|
+
// Width-memoized (pi-tui renders every mounted component every frame);
|
|
344
|
+
// the mode/model/subagent lines stay live because each of their
|
|
345
|
+
// updates invalidates the cache before requesting a repaint.
|
|
346
|
+
const component = linesComponent((width) => {
|
|
347
|
+
// Read live each render (model_select invalidates the cache). isRealModel
|
|
348
|
+
// narrows currentModelId to string; the label is undefined when no
|
|
349
|
+
// provider is configured, which both fields below render as "none" — a
|
|
350
|
+
// fresh machine must not show pi's placeholder `(unknown) unknown` (M3).
|
|
351
|
+
const modelLabel = isRealModel(currentModelId) ? formatModel(currentModelProvider ?? "", currentModelId) : undefined;
|
|
352
|
+
return [
|
|
344
353
|
"",
|
|
345
354
|
...bannerLines(
|
|
346
355
|
{
|
|
347
356
|
version,
|
|
348
|
-
model:
|
|
357
|
+
model: modelLabel ?? "none",
|
|
349
358
|
cwd: ctx.cwd,
|
|
350
359
|
mode: permissionModeDisplay(permissionStatus ?? { mode: "default", paused: false }),
|
|
351
|
-
subagents:
|
|
352
|
-
|
|
353
|
-
|
|
360
|
+
subagents:
|
|
361
|
+
modelLabel === undefined
|
|
362
|
+
? "none"
|
|
363
|
+
: subagentStatus?.model
|
|
364
|
+
? `${formatModelSpec(subagentStatus.model)}${subagentStatus.via ? ` (${subagentStatus.via})` : ""}`
|
|
365
|
+
: undefined,
|
|
354
366
|
sections,
|
|
355
367
|
},
|
|
356
368
|
paint,
|
|
357
369
|
width,
|
|
358
370
|
),
|
|
359
371
|
"",
|
|
360
|
-
]
|
|
361
|
-
|
|
362
|
-
|
|
372
|
+
];
|
|
373
|
+
});
|
|
374
|
+
requestHeaderRender = () => {
|
|
375
|
+
component.invalidate();
|
|
376
|
+
(tui as { requestRender?: () => void } | undefined)?.requestRender?.();
|
|
363
377
|
};
|
|
378
|
+
return component;
|
|
364
379
|
});
|
|
380
|
+
|
|
381
|
+
// A fresh machine has no provider configured; pi's own hint prints two
|
|
382
|
+
// absolute doc paths inside node_modules. Replace it with a One Code
|
|
383
|
+
// onboarding line that names the command that actually connects a provider
|
|
384
|
+
// and points at the guide, not pi's files (distribution review M3).
|
|
385
|
+
if (!isRealModel(ctx.model?.id)) {
|
|
386
|
+
ctx.ui.notify(
|
|
387
|
+
"No model provider yet. Run /login to connect one (free options at https://github.com/IsuruMaduranga/one-code/blob/master/docs/guide/providers-and-models.md).",
|
|
388
|
+
"warning",
|
|
389
|
+
);
|
|
390
|
+
}
|
|
365
391
|
});
|
|
366
392
|
}
|