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.
Files changed (300) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +222 -177
  3. package/agents/explore.md +9 -6
  4. package/agents/general-purpose.md +1 -1
  5. package/agents/plan.md +6 -4
  6. package/docs/guide/README.md +52 -0
  7. package/docs/guide/bring-your-claude-code-setup.md +128 -0
  8. package/docs/guide/configuration.md +173 -0
  9. package/docs/guide/differences-from-claude-code.md +152 -0
  10. package/docs/guide/doctor.md +88 -0
  11. package/docs/guide/hooks.md +111 -0
  12. package/docs/guide/installation.md +126 -0
  13. package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
  14. package/docs/guide/providers-and-models.md +196 -0
  15. package/docs/guide/reference.md +226 -0
  16. package/docs/guide/sessions-and-context.md +192 -0
  17. package/docs/guide/skills-plugins-and-mcp.md +178 -0
  18. package/docs/guide/subagents-and-workflows.md +195 -0
  19. package/docs/guide/tasks-and-background-work.md +113 -0
  20. package/docs/guide/terminal-interface.md +157 -0
  21. package/docs/guide/tools.md +177 -0
  22. package/docs/guide/troubleshooting.md +177 -0
  23. package/extensions/ask-user/index.ts +65 -71
  24. package/extensions/ask-user/questions.ts +15 -28
  25. package/extensions/ask-user/widget.ts +533 -0
  26. package/extensions/auto-mode/actions.ts +9 -26
  27. package/extensions/auto-mode/classifier-prompt.ts +127 -0
  28. package/extensions/auto-mode/classifier.ts +305 -227
  29. package/extensions/auto-mode/config.ts +279 -77
  30. package/extensions/auto-mode/defaults.ts +17 -60
  31. package/extensions/auto-mode/instructions.ts +2 -1
  32. package/extensions/auto-mode/model-picker.ts +53 -31
  33. package/extensions/auto-mode/model-select.ts +153 -82
  34. package/extensions/auto-mode/paths.ts +13 -1
  35. package/extensions/auto-mode/pause.ts +8 -0
  36. package/extensions/auto-mode/prompt.ts +205 -238
  37. package/extensions/auto-mode/recoverability.ts +229 -0
  38. package/extensions/auto-mode/rules.ts +92 -75
  39. package/extensions/auto-mode/safety-floor.ts +43 -12
  40. package/extensions/auto-mode/sensitive.ts +11 -0
  41. package/extensions/auto-mode/setup-run.ts +245 -0
  42. package/extensions/auto-mode/setup.ts +235 -0
  43. package/extensions/auto-mode/shell-analysis.ts +332 -26
  44. package/extensions/auto-mode/transcript.ts +102 -0
  45. package/extensions/background/index.ts +256 -43
  46. package/extensions/background/monitor-batch.ts +58 -0
  47. package/extensions/background/registry.ts +9 -0
  48. package/extensions/background/wakeup.ts +45 -0
  49. package/extensions/bash/background.ts +47 -22
  50. package/extensions/bash/guards.ts +229 -0
  51. package/extensions/bash/index.ts +81 -19
  52. package/extensions/branding/index.ts +44 -18
  53. package/extensions/branding/startup.ts +23 -8
  54. package/extensions/claude-compat/index.ts +17 -2
  55. package/extensions/claude-context/index.ts +41 -12
  56. package/extensions/clear/index.ts +27 -0
  57. package/extensions/compaction/fit.ts +121 -0
  58. package/extensions/compaction/index.ts +176 -71
  59. package/extensions/compaction/prompt.ts +21 -1
  60. package/extensions/context-budget/budget.ts +61 -0
  61. package/extensions/context-budget/index.ts +66 -0
  62. package/extensions/context-management/index.ts +40 -16
  63. package/extensions/doctor/build.ts +226 -0
  64. package/extensions/doctor/cli.ts +99 -0
  65. package/extensions/doctor/compat.ts +532 -0
  66. package/extensions/doctor/dependencies.ts +186 -0
  67. package/extensions/doctor/fix-prompt.ts +125 -0
  68. package/extensions/doctor/index.ts +308 -0
  69. package/extensions/doctor/models.ts +231 -0
  70. package/extensions/doctor/presets.ts +208 -0
  71. package/extensions/doctor/report.ts +189 -0
  72. package/extensions/doctor/update-lookup.ts +46 -0
  73. package/extensions/doctor/viewer.ts +124 -0
  74. package/extensions/effort/index.ts +41 -4
  75. package/extensions/effort/slider.ts +33 -2
  76. package/extensions/file-tracker/index.ts +143 -20
  77. package/extensions/file-tracker/replay.ts +46 -0
  78. package/extensions/file-tracker/tracker.ts +51 -4
  79. package/extensions/footer/footer-line.ts +207 -0
  80. package/extensions/footer/index.ts +152 -0
  81. package/extensions/footer/pr.ts +53 -0
  82. package/extensions/hooks/executor.ts +57 -20
  83. package/extensions/hooks/formatter-notice.ts +76 -0
  84. package/extensions/hooks/index.ts +314 -73
  85. package/extensions/hooks/matcher.ts +35 -1
  86. package/extensions/hooks/plugin-hooks.ts +3 -3
  87. package/extensions/hooks/protocol.ts +58 -0
  88. package/extensions/hooks/subagent-bridge.ts +77 -0
  89. package/extensions/hooks/trust.ts +15 -4
  90. package/extensions/init/index.ts +26 -0
  91. package/extensions/init/prompt.ts +40 -0
  92. package/extensions/interrupted/index.ts +35 -0
  93. package/extensions/interrupted/line.ts +9 -0
  94. package/extensions/lib/abort.ts +15 -0
  95. package/extensions/lib/agent-loader.ts +183 -0
  96. package/extensions/lib/anthropic-payload.ts +15 -0
  97. package/extensions/lib/atomic-write.ts +56 -0
  98. package/extensions/lib/bridge-watch.ts +20 -0
  99. package/extensions/lib/cache-retention.ts +60 -0
  100. package/extensions/lib/capability-index.ts +466 -0
  101. package/extensions/lib/claude-context.ts +318 -27
  102. package/extensions/lib/claude-settings.ts +64 -0
  103. package/extensions/lib/consent-preview.ts +30 -0
  104. package/extensions/lib/deferred.ts +144 -13
  105. package/extensions/lib/favorites.ts +43 -0
  106. package/extensions/lib/fetch-timeout.ts +15 -0
  107. package/extensions/lib/frontmatter.ts +31 -0
  108. package/extensions/lib/git.ts +44 -2
  109. package/extensions/lib/hook-gate.ts +90 -0
  110. package/extensions/lib/interrupt.ts +94 -0
  111. package/extensions/lib/mcp-overrides.ts +61 -0
  112. package/extensions/lib/mcp-share.ts +25 -0
  113. package/extensions/lib/mcp-status.ts +34 -0
  114. package/extensions/lib/memory.ts +41 -6
  115. package/extensions/lib/model-facts.generated.json +2480 -0
  116. package/extensions/lib/model-facts.ts +162 -0
  117. package/extensions/lib/model-policy.ts +145 -189
  118. package/extensions/lib/model-tier.ts +480 -64
  119. package/extensions/lib/model-unusable.ts +37 -0
  120. package/extensions/lib/notifications.ts +320 -1
  121. package/extensions/lib/one-code-settings.ts +63 -0
  122. package/extensions/lib/original-command.ts +71 -0
  123. package/extensions/lib/package-version.ts +31 -0
  124. package/extensions/lib/paths.ts +39 -4
  125. package/extensions/lib/permission-gate.ts +192 -0
  126. package/extensions/lib/persisted-output.ts +18 -0
  127. package/extensions/lib/pi-ai-estimate.ts +160 -0
  128. package/extensions/lib/pi-version.ts +1 -1
  129. package/extensions/lib/plan-mode-channels.ts +11 -0
  130. package/extensions/lib/plugin-overrides.ts +24 -0
  131. package/extensions/lib/plugin-root.ts +44 -0
  132. package/extensions/lib/plugins.ts +233 -54
  133. package/extensions/lib/prefix-warm-gate.ts +157 -0
  134. package/extensions/lib/process-tree.ts +56 -0
  135. package/extensions/lib/reminders.ts +321 -68
  136. package/extensions/lib/scratchpad.ts +11 -8
  137. package/extensions/lib/session-lifecycle.ts +28 -0
  138. package/extensions/lib/settings-channels.ts +14 -0
  139. package/extensions/lib/shell-tasks.ts +54 -0
  140. package/extensions/lib/skill-overrides.ts +86 -0
  141. package/extensions/lib/skill-scan.ts +110 -0
  142. package/extensions/lib/text-width.ts +267 -0
  143. package/extensions/lib/tui-render.ts +281 -54
  144. package/extensions/lib/usage-bus.ts +75 -0
  145. package/extensions/lib/usage-tracker.ts +67 -0
  146. package/extensions/lib/worktree-isolation.ts +170 -0
  147. package/extensions/lsp/client.ts +69 -12
  148. package/extensions/lsp/format.ts +12 -2
  149. package/extensions/lsp/index.ts +273 -84
  150. package/extensions/lsp/install-hints.ts +43 -0
  151. package/extensions/lsp/keep-alive.ts +19 -0
  152. package/extensions/lsp/plugin-servers.ts +331 -0
  153. package/extensions/lsp/watcher.ts +157 -0
  154. package/extensions/mcp/announce.ts +99 -0
  155. package/extensions/mcp/client.ts +181 -22
  156. package/extensions/mcp/config.ts +55 -24
  157. package/extensions/mcp/index.ts +586 -86
  158. package/extensions/mcp/oauth/browser.ts +26 -0
  159. package/extensions/mcp/oauth/callback.ts +104 -0
  160. package/extensions/mcp/oauth/flow.ts +68 -0
  161. package/extensions/mcp/oauth/provider.ts +147 -0
  162. package/extensions/mcp/oauth/store.ts +86 -0
  163. package/extensions/mcp/panel/keys.ts +34 -0
  164. package/extensions/mcp/panel/model.ts +62 -0
  165. package/extensions/mcp/panel/render.ts +141 -0
  166. package/extensions/mcp/panel/state.ts +108 -0
  167. package/extensions/mcp/schema.ts +83 -6
  168. package/extensions/mcp/trust.ts +340 -0
  169. package/extensions/memory/entries.ts +163 -0
  170. package/extensions/memory/index.ts +119 -2
  171. package/extensions/memory/open-external.ts +97 -0
  172. package/extensions/memory/panel.ts +124 -0
  173. package/extensions/model-default/index.ts +74 -0
  174. package/extensions/notebook/index.ts +36 -18
  175. package/extensions/notebook/notebook.ts +110 -17
  176. package/extensions/permissions/index.ts +1012 -247
  177. package/extensions/permissions/matcher.ts +488 -57
  178. package/extensions/permissions/modes.ts +55 -7
  179. package/extensions/permissions/project-trust.ts +81 -0
  180. package/extensions/permissions/protected-paths.ts +16 -3
  181. package/extensions/permissions/rule-prose.ts +79 -0
  182. package/extensions/permissions/session-grant.ts +91 -0
  183. package/extensions/permissions/settings.ts +112 -33
  184. package/extensions/permissions/subagent-gate.ts +57 -0
  185. package/extensions/plan-mode/index.ts +143 -33
  186. package/extensions/plan-mode/reminder.ts +16 -19
  187. package/extensions/plan-mode/viewer.ts +40 -31
  188. package/extensions/plugins/counts.ts +68 -0
  189. package/extensions/plugins/index.ts +496 -32
  190. package/extensions/plugins/install/install.ts +167 -0
  191. package/extensions/plugins/install/paths.ts +23 -0
  192. package/extensions/plugins/install/registry.ts +72 -0
  193. package/extensions/plugins/marketplace/git.ts +103 -0
  194. package/extensions/plugins/marketplace/official.ts +36 -0
  195. package/extensions/plugins/marketplace/parse.ts +94 -0
  196. package/extensions/plugins/marketplace/registry.ts +60 -0
  197. package/extensions/plugins/marketplace/sync.ts +146 -0
  198. package/extensions/plugins/marketplace/types.ts +128 -0
  199. package/extensions/plugins/panel/keys.ts +52 -0
  200. package/extensions/plugins/panel/render.ts +315 -0
  201. package/extensions/plugins/panel/rows.ts +230 -0
  202. package/extensions/plugins/panel/state.ts +333 -0
  203. package/extensions/recap/index.ts +215 -0
  204. package/extensions/recap/prompt.ts +44 -0
  205. package/extensions/recap/scheduler.ts +87 -0
  206. package/extensions/search-tools/index.ts +0 -0
  207. package/extensions/search-tools/tools.ts +7 -5
  208. package/extensions/skill/index.ts +360 -34
  209. package/extensions/skill/invoke.ts +185 -0
  210. package/extensions/skill/listing.ts +21 -0
  211. package/extensions/skill/panel/keys.ts +41 -0
  212. package/extensions/skill/panel/render.ts +91 -0
  213. package/extensions/skill/panel/state.ts +115 -0
  214. package/extensions/spinner/index.ts +110 -0
  215. package/extensions/spinner/line.ts +51 -0
  216. package/extensions/spinner/verbs.ts +201 -0
  217. package/extensions/subagents/agents.ts +88 -44
  218. package/extensions/subagents/default-model.ts +62 -32
  219. package/extensions/subagents/delegation-steer.ts +23 -0
  220. package/extensions/subagents/hand-back-review.ts +67 -0
  221. package/extensions/subagents/index.ts +1545 -346
  222. package/extensions/subagents/live-runs.ts +293 -0
  223. package/extensions/subagents/model-select.ts +252 -82
  224. package/extensions/subagents/outcome.ts +90 -0
  225. package/extensions/subagents/panel-keys.ts +41 -0
  226. package/extensions/subagents/panel-render.ts +357 -0
  227. package/extensions/subagents/panel-widget.ts +330 -0
  228. package/extensions/subagents/pending-claim.ts +45 -0
  229. package/extensions/subagents/prose.ts +105 -0
  230. package/extensions/subagents/runner.ts +674 -0
  231. package/extensions/subagents/runs.ts +68 -1
  232. package/extensions/subagents/send-to-main-tool.ts +55 -0
  233. package/extensions/subagents/session-turns.ts +138 -0
  234. package/extensions/subagents/shell-panel.ts +198 -0
  235. package/extensions/subagents/usage.ts +16 -4
  236. package/extensions/subagents/worktree.ts +13 -0
  237. package/extensions/system-prompt/environment.ts +5 -3
  238. package/extensions/system-prompt/git-status.ts +93 -0
  239. package/extensions/system-prompt/index.ts +23 -1
  240. package/extensions/system-prompt/template.ts +34 -4
  241. package/extensions/system-prompt/tiers/common.ts +31 -6
  242. package/extensions/system-prompt/tiers/low.ts +13 -3
  243. package/extensions/system-prompt/tiers/mid.ts +21 -8
  244. package/extensions/system-reminder/index.ts +53 -9
  245. package/extensions/tasks/index.ts +65 -16
  246. package/extensions/tasks/store.ts +76 -3
  247. package/extensions/tool-search/announce.ts +42 -0
  248. package/extensions/tool-search/index.ts +136 -13
  249. package/extensions/turn-duration/index.ts +60 -0
  250. package/extensions/turn-duration/line.ts +22 -0
  251. package/extensions/turn-duration/span.ts +41 -0
  252. package/extensions/turn-duration/verbs.ts +24 -0
  253. package/extensions/web/backends.ts +421 -0
  254. package/extensions/web/index.ts +178 -21
  255. package/extensions/web-fetch/index.ts +74 -18
  256. package/extensions/web-fetch/summarize.ts +1 -1
  257. package/extensions/workflow/agent-session.ts +182 -112
  258. package/extensions/workflow/globals.ts +129 -106
  259. package/extensions/workflow/index.ts +343 -28
  260. package/extensions/workflow/records.ts +118 -0
  261. package/extensions/workflow/run-manager.ts +80 -31
  262. package/extensions/workflow/saved-workflows.ts +2 -1
  263. package/extensions/workflow/script-worker.mjs +110 -0
  264. package/extensions/workflow/types.ts +74 -8
  265. package/extensions/workflow/viewer.ts +562 -0
  266. package/extensions/workflow/vm-runtime.ts +118 -22
  267. package/extensions/workflow/widget.ts +126 -18
  268. package/extensions/worktree/guards.ts +240 -0
  269. package/extensions/worktree/index.ts +30 -5
  270. package/extensions/worktree/rewrite.ts +32 -16
  271. package/package.json +20 -10
  272. package/skills/code-review/SKILL.md +165 -0
  273. package/skills/fewer-permission-prompts/SKILL.md +61 -0
  274. package/skills/security-review/SKILL.md +178 -0
  275. package/skills/simplify/SKILL.md +70 -0
  276. package/themes/{one-code-light.json → onecode-light.json} +6 -1
  277. package/themes/{one-code.json → onecode.json} +6 -1
  278. package/types/app.d.ts +8 -1
  279. package/types/community-packages.d.ts +45 -2
  280. package/docs/decisions/auto-mode.md +0 -526
  281. package/docs/decisions/branding.md +0 -127
  282. package/docs/decisions/code-review-remediation.md +0 -106
  283. package/docs/decisions/compaction.md +0 -140
  284. package/docs/decisions/distribution.md +0 -168
  285. package/docs/decisions/hooks.md +0 -64
  286. package/docs/decisions/lsp.md +0 -58
  287. package/docs/decisions/mcp.md +0 -104
  288. package/docs/decisions/memory-state.md +0 -170
  289. package/docs/decisions/model-policy.md +0 -126
  290. package/docs/decisions/modes.md +0 -178
  291. package/docs/decisions/skills-plugins.md +0 -56
  292. package/docs/decisions/subagents-workflows.md +0 -225
  293. package/docs/decisions/system-prompt.md +0 -98
  294. package/docs/decisions/tools.md +0 -407
  295. package/docs/decisions/tui.md +0 -217
  296. package/docs/decisions.md +0 -148
  297. package/extensions/subagents/child.ts +0 -414
  298. package/extensions/subagents/rpc-turns.ts +0 -123
  299. package/extensions/todo/index.ts +0 -133
  300. 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/plan/background-bash.md: `output()`
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 { type ChildProcess, spawn } from "node:child_process";
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
- detached: process.platform !== "win32",
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
- killTree(child);
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
- killTree(child);
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
- log?.end();
123
- finish();
124
- options.onFinished(task, { exitCode, signal, stopped: stopRequested, timedOut, output: task.output() });
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
+ }
@@ -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 follow-up system
15
- * notification. The permission gate and auto-mode classifier run before
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 { systemNotification } from "../lib/notifications.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";
28
31
  import { perCwd } from "../lib/per-cwd.ts";
29
32
  import { ccWrapBuiltinRenderers, linesComponent, resultLines } from "../lib/tui-render.ts";
30
33
 
31
- const NOTIFY_OUTPUT_CAP = 30_000;
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 notify = (text: string, details: Record<string, unknown>) => {
57
- pi.sendMessage(
58
- { customType: "task-notification", content: [{ type: "text", text }], display: true, details },
59
- { deliverAs: "followUp", triggerTurn: true },
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: `${base.description} 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.`,
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 = `Running in background (task ${details.taskId}${details.logPath ? ` · log: ${details.logPath}` : ""})`;
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: params.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: params.timeout,
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(`Background bash ${id} (${description}) ${finishLine(summary, params.timeout)}.\n\n${tailCap(summary.output, NOTIFY_OUTPUT_CAP)}`),
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 ?? "0.1.0";
322
- currentModelId = ctx.model ? `${ctx.model.id}` : currentModelId;
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
- requestHeaderRender = () => {
338
- (tui as { requestRender?: () => void } | undefined)?.requestRender?.();
339
- };
340
- return {
341
- // Rendered per paint rather than precomputed, so the mode line
342
- // follows ctrl+q cycles and the classifier pinning.
343
- render: (width: number) => [
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: currentModelId,
357
+ model: modelLabel ?? "none",
349
358
  cwd: ctx.cwd,
350
359
  mode: permissionModeDisplay(permissionStatus ?? { mode: "default", paused: false }),
351
- subagents: subagentStatus?.model
352
- ? `${shortModelName(subagentStatus.model)}${subagentStatus.via ? ` (${subagentStatus.via})` : ""}`
353
- : undefined,
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
- // Nothing is cached, so there is nothing to invalidate.
362
- invalidate: () => {},
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
  }