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
@@ -12,20 +12,47 @@
12
12
  *
13
13
  * Orchestration is opt-in, like Claude Code: the tool description gates it,
14
14
  * and the literal keyword "ultracode" in a user message arms the turn via a
15
- * system reminder.
15
+ * system reminder (skipped while `/effort ultracode` has the standing block
16
+ * on — see `effort/`). The keyword is read by pi's `input` event, which only
17
+ * a prompt sent to an idle session fires; a message queued mid-turn does not
18
+ * arm (docs/upstream_prs.md #14).
16
19
  */
17
20
 
18
- import { existsSync, readFileSync } from "node:fs";
21
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
22
  import os from "node:os";
20
23
  import { join } from "node:path";
21
24
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
22
25
  import { Type } from "typebox";
26
+ import { recordUsage } from "../lib/usage-bus.ts";
27
+ import { MODEL_UNUSABLE_CHANNEL, type ModelUnusableEvent } from "../lib/model-unusable.ts";
28
+ import { whenAborted } from "../lib/abort.ts";
29
+ import { createTaskNotifier, oneShotNote, sessionOutlivesTurn } from "../lib/notifications.ts";
23
30
  import { REMINDER_CHANNEL } from "../lib/reminders.ts";
31
+ import { ULTRACODE_MODE_CHANNEL } from "../effort/slider.ts";
32
+ import { PERMISSION_STATUS_CHANNEL } from "../permissions/modes.ts";
33
+ import { watchPermissionBridge } from "../permissions/subagent-gate.ts";
34
+ import { watchHookBridge } from "../hooks/subagent-bridge.ts";
24
35
  import { applicableSubagentDefault, loadSubagentDefault } from "../subagents/default-model.ts";
25
- import { discoverSavedWorkflows, findSavedWorkflow } from "./saved-workflows.ts";
36
+ import { discoverSavedWorkflows, findSavedWorkflow, workflowDirs } from "./saved-workflows.ts";
26
37
  import { buildRunReport, WorkflowRunManager } from "./run-manager.ts";
27
- import { ccToolRenderers, customMessageText, notificationComponent } from "../lib/tui-render.ts";
38
+ import {
39
+ ccToolRenderers,
40
+ customMessageText,
41
+ notificationComponent,
42
+ safeThemeBold,
43
+ safeThemePaint,
44
+ truncateLine,
45
+ } from "../lib/tui-render.ts";
28
46
  import { WorkflowScriptError } from "./types.ts";
47
+ import {
48
+ clampViewerState,
49
+ decodeStatusKey,
50
+ decodeViewerKey,
51
+ initialViewerState,
52
+ planSave,
53
+ renderViewer,
54
+ type ViewerRunSnapshot,
55
+ } from "./viewer.ts";
29
56
  import { WorkflowWidget } from "./widget.ts";
30
57
 
31
58
  /**
@@ -39,7 +66,7 @@ const ULTRACODE_REMINDER =
39
66
  /**
40
67
  * Ported from Claude Code's Workflow tool description, adapted to this harness
41
68
  * (runId + follow-up message instead of a task id + task-notification,
42
- * `/workflows stop` instead of TaskStop, `subagent` as the single-agent
69
+ * `/workflows stop` instead of TaskStop, `Agent` as the single-agent
43
70
  * alternative). The authoring guidance is the substantive part: it is what
44
71
  * makes the model reach for pipeline() over barriers and pick sane fan-out
45
72
  * sizes, so it is kept rather than trimmed.
@@ -50,10 +77,12 @@ A workflow structures work across many agents — to be comprehensive (decompose
50
77
 
51
78
  ONLY call this tool when the user has explicitly opted into multi-agent orchestration. Workflows can spawn dozens of agents and consume a large amount of tokens; the user must request that scale, not have it inferred. Explicit opt-in means one of:
52
79
  - The user included the keyword "ultracode" in their prompt (you'll see a system-reminder confirming it).
80
+ - Ultracode is on for the session (a system-reminder confirms it), which makes the opt-in standing for every turn until it is turned off.
53
81
  - The user directly asked you to run a workflow or use multi-agent orchestration in their own words ("use a workflow", "run a workflow", "fan out agents", "orchestrate this with subagents"). The ask must be in the user's words — a task that would merely benefit from a workflow does not count.
82
+ - The user invoked a skill or slash command whose instructions tell you to call Workflow.
54
83
  - The user asked you to run a specific named or saved workflow.
55
84
 
56
- For any other task — even one that would clearly benefit from parallelism — do NOT call this tool. Use the \`subagent\` tool for individual subagents, or briefly describe what a multi-agent workflow could do and ask the user whether to run it.
85
+ For any other task — even one that would clearly benefit from parallelism — do NOT call this tool. Use the \`Agent\` tool for individual subagents, or briefly describe what a multi-agent workflow could do and how much it would roughly cost, and ask the user whether to run it.
57
86
 
58
87
  When you do call it, the right move is often **hybrid**: scout inline first (list the files, find the targets, scope the diff) to discover the work-list, then call workflow to pipeline over it. You don't need to know the shape before the *task* — only before the *orchestration step*.
59
88
 
@@ -89,7 +118,7 @@ Script body hooks:
89
118
  - log(message: string): void — emit a progress message to the user.
90
119
  - phase(title: string): void — start a new phase; subsequent agent() calls are grouped under it.
91
120
  - args: any — the value passed as this tool's \`args\` input, verbatim. Pass arrays/objects as actual JSON values, NOT as a JSON-encoded string.
92
- - budget: {total: number|null, spent(): number, remaining(): number} — the run's token target from \`tokenBudget\`. \`total\` is null if none was set. The target is a HARD ceiling: once spent() reaches total, further agent() calls throw. Use for dynamic loops: \`while (budget.total && budget.remaining() > 50_000) { ... }\`.
121
+ - budget: {total: number|null, spent(): number, remaining(): number} — the run's output-token target from \`tokenBudget\`, shared with nested workflows. \`total\` is null if none was set. Once spent() reaches total, new and queued agent() calls throw before starting. Already-running agents may finish above the target; this is not a hard cap on provider usage. Use for dynamic loops: \`while (budget.total && budget.remaining() > 50_000) { ... }\`.
93
122
  - workflow(nameOrRef: string | {scriptPath: string}, args?: any): Promise<any> — run another workflow inline as a sub-step. Nesting is one level only.
94
123
 
95
124
  DEFAULT TO pipeline(). Only reach for a barrier (parallel between stages) when you genuinely need ALL prior-stage results together — dedup/merge across the full result set, early-exit on a zero count, or a prompt that references "the other findings". A barrier is NOT justified by "I need to flatten/map/filter first" (do it inside a pipeline stage) or "the stages are conceptually separate".
@@ -165,7 +194,7 @@ const WorkflowParams = Type.Object({
165
194
  tokenBudget: Type.Optional(
166
195
  Type.Integer({
167
196
  minimum: 1000,
168
- description: "Hard output-token ceiling for the run, exposed to the script as budget {total, spent(), remaining()}",
197
+ description: "Output-token target shared by the run and nested workflows. Stops new and queued agents once reached; already-running agents may exceed it. Exposed as budget {total, spent(), remaining()}",
169
198
  }),
170
199
  ),
171
200
  sync: Type.Optional(
@@ -174,35 +203,59 @@ const WorkflowParams = Type.Object({
174
203
  });
175
204
 
176
205
  export default function workflowExtension(pi: ExtensionAPI) {
206
+ // Models the account refused this session (classifier/subagent reports, lib/model-unusable.ts):
207
+ // workflow agents share the subagent selector, so they resolve around them too.
208
+ const unusableModels = new Set<string>();
209
+ pi.events.on(MODEL_UNUSABLE_CHANNEL, (data) => {
210
+ unusableModels.add((data as ModelUnusableEvent).model);
211
+ });
212
+ const notifyTask = createTaskNotifier(pi);
177
213
  const manager = new WorkflowRunManager();
178
214
  let lastCtx: ExtensionContext | undefined;
179
215
  const widget = new WorkflowWidget(manager, () => lastCtx);
180
216
  const deliveredRuns = new Set<string>();
217
+ let viewerOpen = false;
218
+
219
+ // Parent permission bridge for workflow agents' gates — same bridge the
220
+ // subagent runner uses; see AgentRunnerOptions.getPermissionBridge.
221
+ const getPermissionBridge = watchPermissionBridge(pi);
222
+ const getHookBridge = watchHookBridge(pi);
223
+
224
+ const openViewer = async (ctx: ExtensionContext, opts?: { height?: "full" | "half"; runIndex?: number }) => {
225
+ if (viewerOpen) return;
226
+ viewerOpen = true;
227
+ try {
228
+ await openWorkflowViewer(ctx, manager, opts);
229
+ } finally {
230
+ viewerOpen = false;
231
+ }
232
+ };
181
233
 
182
234
  const deliverResult = (runId: string) => {
183
235
  const handle = manager.get(runId);
184
236
  if (!handle || deliveredRuns.has(runId)) return;
185
237
  deliveredRuns.add(runId);
186
- pi.sendMessage(
187
- {
188
- customType: "workflow-result",
189
- content: [{ type: "text", text: buildRunReport(handle) }],
190
- display: true,
191
- details: { runId: handle.runId, name: handle.meta.name, status: handle.status },
192
- },
193
- { deliverAs: "followUp", triggerTurn: true },
194
- );
238
+ notifyTask("one-code:workflow-result", buildRunReport(handle), { runId: handle.runId, name: handle.meta.name, status: handle.status });
195
239
  };
196
240
 
197
- pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) =>
241
+ pi.registerMessageRenderer("one-code:workflow-result", (message, { expanded }, theme) =>
198
242
  notificationComponent(theme, customMessageText(message.content), expanded),
199
243
  );
200
244
 
201
245
  pi.registerTool({
202
246
  name: "workflow",
203
247
  label: "Workflow",
204
- ...ccToolRenderers<{ name?: string; scriptPath?: string; script?: string; resumeFromRunId?: string }>("Workflow", {
248
+ ...ccToolRenderers<
249
+ { name?: string; scriptPath?: string; script?: string; resumeFromRunId?: string },
250
+ { runId?: string; background?: boolean }
251
+ >("Workflow", {
205
252
  title: (a) => a?.name ?? a?.scriptPath ?? (a?.resumeFromRunId ? `resume ${a.resumeFromRunId}` : a?.script ? "inline script" : undefined),
253
+ // The model-facing start text is instructions, not information — show
254
+ // the user Claude Code's hint line instead.
255
+ result: (result, _args, isError) =>
256
+ !isError && result.details?.background
257
+ ? `Running in background · /workflows to monitor and save · ${result.details.runId ?? ""}`.trimEnd()
258
+ : undefined,
206
259
  }),
207
260
  description: WORKFLOW_TOOL_DESCRIPTION,
208
261
  promptSnippet: "Run a script that orchestrates many subagents (opt-in ultracode mode)",
@@ -247,12 +300,28 @@ export default function workflowExtension(pi: ExtensionAPI) {
247
300
  cwd: ctx.cwd,
248
301
  sessionDir,
249
302
  defaultModel: ctx.model,
250
- configuredDefaultModel: configuredDefault?.spec,
303
+ configuredDefault,
251
304
  defaultEffort: ctx.thinkingLevel,
305
+ getPermissionBridge,
306
+ getHookBridge,
307
+ // Workflow agents run in their own sessions; their spend reaches the footer only through the bus.
308
+ onUsage: (cost) => recordUsage(pi, "subagent", { cost: { total: cost } }),
309
+ unusableModels: () => unusableModels,
310
+ onModelUnusable: (model, reason) => {
311
+ if (unusableModels.has(model)) return;
312
+ unusableModels.add(model);
313
+ pi.events.emit(MODEL_UNUSABLE_CHANNEL, { model, reason } satisfies ModelUnusableEvent);
314
+ },
252
315
  });
253
316
  widget.attach(handle);
254
317
 
255
- if (params.sync) {
318
+ // One-shot modes (`-p` / `--mode json`) exit when the turn settles:
319
+ // session_shutdown aborts every run, so a backgrounded workflow never
320
+ // finished and its promised follow-up never came (LIFECYCLE-REVIEW
321
+ // M1, measured: no result, no journal). Run it to completion there,
322
+ // the rule bash and Agent apply, and return the report in the result.
323
+ const forcedSync = !params.sync && !sessionOutlivesTurn(ctx.mode);
324
+ if (params.sync || forcedSync) {
256
325
  const onProgress = () => {
257
326
  onUpdate?.({
258
327
  content: [{ type: "text", text: handle.recentEvents.slice(-8).join("\n") || "starting…" }],
@@ -260,17 +329,17 @@ export default function workflowExtension(pi: ExtensionAPI) {
260
329
  });
261
330
  };
262
331
  handle.on("progress", onProgress);
263
- const onAbort = () => handle.abort("tool call aborted");
264
- signal?.addEventListener("abort", onAbort, { once: true });
332
+ const unhookAbort = whenAborted(signal, () => handle.abort("tool call aborted"));
265
333
  try {
266
334
  await handle.finished;
267
335
  } finally {
268
- signal?.removeEventListener("abort", onAbort);
336
+ unhookAbort();
269
337
  handle.removeListener("progress", onProgress);
270
338
  }
271
339
  deliveredRuns.add(handle.runId); // sync result goes in the tool result, not a followUp
340
+ const oneShotPrefix = forcedSync ? `${oneShotNote("workflow")}\n\n` : "";
272
341
  return {
273
- content: [{ type: "text", text: buildRunReport(handle) }],
342
+ content: [{ type: "text", text: `${oneShotPrefix}${buildRunReport(handle)}` }],
274
343
  details: { runId: handle.runId, status: handle.status, scriptPath: handle.scriptPath },
275
344
  isError: handle.status !== "completed",
276
345
  };
@@ -302,14 +371,18 @@ export default function workflowExtension(pi: ExtensionAPI) {
302
371
  });
303
372
 
304
373
  pi.registerCommand("workflows", {
305
- description: "List workflow runs and saved workflows; stop or inspect runs",
374
+ description: "Open the workflow viewer; list, stop, or inspect runs",
306
375
  getArgumentCompletions: (prefix) => {
307
- const items = ["stop ", "log "].filter((c) => c.startsWith(prefix));
376
+ const items = ["stop ", "log ", "list"].filter((c) => c.startsWith(prefix));
308
377
  return items.length ? items.map((c) => ({ value: c, label: c.trim() })) : null;
309
378
  },
310
379
  handler: async (args, ctx) => {
311
380
  lastCtx = ctx;
312
381
  const [action, runId] = args.trim().split(/\s+/);
382
+ if (!action && ctx.hasUI) {
383
+ await openViewer(ctx, { height: "full" });
384
+ return;
385
+ }
313
386
  if (action === "stop" && runId) {
314
387
  const stopped = manager.abort(runId, "stopped via /workflows");
315
388
  ctx.ui.notify(stopped ? `Stopping ${runId}…` : `No running workflow ${runId}`, stopped ? "info" : "error");
@@ -347,18 +420,260 @@ export default function workflowExtension(pi: ExtensionAPI) {
347
420
  },
348
421
  });
349
422
 
423
+ // While ultracode MODE is on (/effort ultracode) the standing block already
424
+ // carries the opt-in, so the keyword's single-turn one-shot is skipped: one
425
+ // instruction for one fact. `input` fires for prompt() only — a message the
426
+ // user queues while a turn is running (pi's steer()/followUp()) is expanded
427
+ // without it and does not arm the turn (upstream ask #14; the mode covers it).
428
+ let ultracodeMode = false;
429
+ pi.events.on(ULTRACODE_MODE_CHANNEL, (data) => {
430
+ ultracodeMode = (data as { active?: boolean })?.active === true;
431
+ });
350
432
  pi.on("input", (event) => {
351
- if (/\bultracode\b/i.test(event.text)) {
433
+ if (!ultracodeMode && /\bultracode\b/i.test(event.text)) {
352
434
  pi.events.emit(REMINDER_CHANNEL, { text: ULTRACODE_REMINDER, scope: "next-turn" });
353
435
  }
354
436
  return undefined;
355
437
  });
356
438
 
439
+ // Down-arrow soft focus for the below-editor status strip (Claude Code's
440
+ // bottom workflow entry): with the editor focused and empty, ↓ highlights
441
+ // the newest run's row; ↑/↓ move, enter opens the half-screen viewer, x
442
+ // stops the run, esc — or typing anything — returns to the editor. The
443
+ // listener runs before the focused component sees the byte, so every
444
+ // consume is guarded by "the core editor really has focus" (identity
445
+ // against the captured baseline) to never steal keys from dialogs.
446
+ let inputHookRegistered = false;
447
+ const DOWN_KEYS = new Set(["\x1b[B", "\x1bOB"]);
448
+ const registerInputHook = (registerCtx: ExtensionContext) => {
449
+ if (inputHookRegistered || !registerCtx.hasUI) return;
450
+ inputHookRegistered = true;
451
+ const leave = () => widget.setFocus(undefined);
452
+ try {
453
+ registerCtx.ui.onTerminalInput((data) => {
454
+ // Session switches replace the ExtensionContext; the hook registers
455
+ // once, so it must act through the freshest ctx, not its closure.
456
+ const ctx = lastCtx ?? registerCtx;
457
+ if (viewerOpen || !widget.rowCount()) {
458
+ if (widget.focusIndex !== undefined) leave();
459
+ return undefined;
460
+ }
461
+ if (widget.focusIndex === undefined) {
462
+ if (!DOWN_KEYS.has(data) || !widget.editorFocusedAndIdle(ctx)) return undefined;
463
+ widget.setFocus(0);
464
+ return { consume: true };
465
+ }
466
+ // Focus moved to a dialog/overlay since the strip took soft focus —
467
+ // drop it and let the dialog have the key.
468
+ if (!widget.editorFocused()) {
469
+ leave();
470
+ return undefined;
471
+ }
472
+ const key = decodeStatusKey(data);
473
+ if (!key) {
474
+ leave();
475
+ return undefined; // typing resumes in the editor, byte included
476
+ }
477
+ switch (key) {
478
+ case "up":
479
+ if (widget.focusIndex === 0) leave();
480
+ else widget.setFocus(widget.focusIndex - 1);
481
+ return { consume: true };
482
+ case "down":
483
+ widget.setFocus(Math.min(widget.rowCount() - 1, widget.focusIndex + 1));
484
+ return { consume: true };
485
+ case "leave":
486
+ leave();
487
+ // While the model streams, esc must still interrupt it — drop the
488
+ // soft focus and let the byte through; consumed only when idle.
489
+ return ctx.isIdle() ? { consume: true } : undefined;
490
+ case "stop": {
491
+ const run = widget.selectedRun();
492
+ if (run) manager.abort(run.runId, "stopped from status strip");
493
+ return { consume: true };
494
+ }
495
+ case "open": {
496
+ const runIndex = widget.focusIndex;
497
+ leave();
498
+ void openViewer(ctx, { height: "half", runIndex });
499
+ return { consume: true };
500
+ }
501
+ }
502
+ });
503
+ } catch {
504
+ // Mode without raw terminal input (print/RPC) — the strip is view-only.
505
+ }
506
+ };
507
+
357
508
  pi.on("session_start", (_event, ctx) => {
358
509
  lastCtx = ctx;
510
+ registerInputHook(ctx);
359
511
  });
360
512
 
513
+ // The permission-mode badge is a below-editor widget too, re-set on every
514
+ // mode change; setWidget order is last-write order, so re-set the strip
515
+ // whenever the badge changes to keep it beneath the mode line (CC's order).
516
+ pi.events.on(PERMISSION_STATUS_CHANNEL, () => widget.refresh());
517
+
361
518
  pi.on("session_shutdown", () => {
519
+ // Fires on /clear, /new and /resume too (findings §8): after this the ctx
520
+ // is invalid, so nothing below may paint on it.
362
521
  manager.abortAll("session ended");
522
+ widget.dispose();
523
+ lastCtx = undefined;
524
+ });
525
+ }
526
+
527
+ /**
528
+ * The interactive workflow viewer (Claude Code's /workflows screen). All
529
+ * layout/key logic is pure in viewer.ts; this component owns only mutable
530
+ * state, the repaint ticker, and the save-to-.claude/workflows fs work.
531
+ *
532
+ * Liveness comes from a 500ms ticker (invalidate + requestRender) instead of
533
+ * per-RunHandle subscriptions — it also picks up runs that start while the
534
+ * viewer is open, and the memoized render keeps the per-frame cost at a map
535
+ * lookup (findings §15).
536
+ */
537
+ async function openWorkflowViewer(
538
+ ctx: ExtensionContext,
539
+ manager: WorkflowRunManager,
540
+ opts?: { height?: "full" | "half"; runIndex?: number },
541
+ ): Promise<void> {
542
+ await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
543
+ const paint = { fg: safeThemePaint(theme), bold: safeThemeBold(theme) };
544
+
545
+ // Newest run first, so the viewer opens on the latest.
546
+ const snapshots = (): ViewerRunSnapshot[] => manager.snapshots();
547
+
548
+ // Full (the /workflows tab) fills the terminal; half (opened from the
549
+ // status strip) leaves the transcript visible above, like Claude Code.
550
+ const viewerHeight = (rows: number): number =>
551
+ opts?.height === "half" ? Math.max(12, Math.min(Math.floor(rows / 2), 30)) : Math.max(12, rows - 2);
552
+
553
+ const state = initialViewerState(opts?.runIndex ?? 0);
554
+ let cache: { width: number; lines: string[] } | undefined;
555
+ const repaint = () => {
556
+ cache = undefined;
557
+ tui.requestRender();
558
+ };
559
+ const ticker = setInterval(repaint, 500);
560
+ ticker.unref?.();
561
+
562
+ const save = (runs: ViewerRunSnapshot[]) => {
563
+ const run = runs[state.runIndex];
564
+ const handle = run && manager.get(run.runId);
565
+ if (!handle) return;
566
+ try {
567
+ const source = readFileSync(handle.scriptPath, "utf8");
568
+ const dir = workflowDirs(ctx.cwd, os.homedir()).project;
569
+ const safeName = run.name.replace(/[^A-Za-z0-9._-]/g, "-");
570
+ const target = join(dir, `${safeName}.js`);
571
+ const exists = existsSync(target);
572
+ const sameContent = exists && readFileSync(target, "utf8") === source;
573
+ const plan = planSave({ name: safeName, exists, sameContent, confirmed: state.confirmOverwrite });
574
+ if (plan.action === "write") {
575
+ mkdirSync(dir, { recursive: true });
576
+ writeFileSync(target, source, "utf8");
577
+ state.confirmOverwrite = false;
578
+ } else if (plan.action === "confirm") {
579
+ state.confirmOverwrite = true;
580
+ }
581
+ state.notice = plan.notice;
582
+ } catch (error) {
583
+ state.notice = `save failed: ${(error as Error).message}`;
584
+ state.confirmOverwrite = false;
585
+ }
586
+ };
587
+
588
+ return {
589
+ render: (width: number) => {
590
+ if (cache?.width === width) return cache.lines;
591
+ const height = viewerHeight(tui.terminal.rows);
592
+ const lines = renderViewer({ runs: snapshots(), state, width, height, now: Date.now() }, paint).map(
593
+ (line) => truncateLine(line, width),
594
+ );
595
+ cache = { width, lines };
596
+ return lines;
597
+ },
598
+ handleInput: (data: string) => {
599
+ const key = decodeViewerKey(data);
600
+ if (!key) return;
601
+ const close = () => {
602
+ clearInterval(ticker);
603
+ done(null);
604
+ };
605
+ if (key.kind === "close") return close();
606
+ if (key.kind !== "save") {
607
+ state.notice = undefined;
608
+ state.confirmOverwrite = false;
609
+ }
610
+ switch (key.kind) {
611
+ case "up":
612
+ case "down": {
613
+ const delta = key.kind === "up" ? -1 : 1;
614
+ if (state.level === "phases") state.phaseCursor += delta;
615
+ else state.agentCursor += delta;
616
+ clampViewerState(state, snapshots());
617
+ state.detailScroll = 0;
618
+ state.promptExpanded = false;
619
+ break;
620
+ }
621
+ case "enter":
622
+ // Phases level drills into the phase; agents level toggles the prompt.
623
+ if (state.level === "phases") {
624
+ state.level = "agents";
625
+ state.agentCursor = 0;
626
+ state.detailScroll = 0;
627
+ state.promptExpanded = false;
628
+ clampViewerState(state, snapshots()); // falls back if the phase has no agents
629
+ } else {
630
+ state.promptExpanded = !state.promptExpanded;
631
+ }
632
+ break;
633
+ case "back":
634
+ if (state.level === "agents") {
635
+ state.level = "phases";
636
+ state.detailScroll = 0;
637
+ state.promptExpanded = false;
638
+ break;
639
+ }
640
+ return close();
641
+ case "pageUp":
642
+ case "pageDown":
643
+ // Clamped against the detail's real length at render time.
644
+ state.detailScroll = Math.max(0, state.detailScroll + (key.kind === "pageUp" ? -8 : 8));
645
+ break;
646
+ case "nextRun": {
647
+ const count = manager.list().length;
648
+ state.runIndex = count ? (state.runIndex + 1) % count : 0;
649
+ state.level = "phases";
650
+ state.phaseCursor = 0;
651
+ state.agentCursor = 0;
652
+ state.detailScroll = 0;
653
+ state.promptExpanded = false;
654
+ break;
655
+ }
656
+ case "stop": {
657
+ const run = snapshots()[state.runIndex];
658
+ const stopped = run && manager.abort(run.runId, "stopped from viewer");
659
+ state.notice = stopped ? `stopping ${run.runId}…` : "workflow is not running";
660
+ break;
661
+ }
662
+ case "save":
663
+ save(snapshots());
664
+ break;
665
+ }
666
+ repaint();
667
+ },
668
+ invalidate: () => {
669
+ cache = undefined;
670
+ },
671
+ // Covers every dismissal path that isn't the user pressing esc while
672
+ // focused (session end, another overlay taking over) — without it the
673
+ // ticker outlives the component.
674
+ dispose: () => {
675
+ clearInterval(ticker);
676
+ },
677
+ };
363
678
  });
364
679
  }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Per-agent run records (pure): folds the RunProgressEvent stream into
3
+ * AgentRecord[] so the viewer can show each agent's prompt, model, activity,
4
+ * and outcome. RunHandle owns one store and applies every recorded event;
5
+ * timestamps are stamped here at apply time (host side — the vm's Date block
6
+ * does not apply).
7
+ */
8
+
9
+ import type { AgentRecord, RunProgressEvent } from "./types.ts";
10
+
11
+ /** The per-agent members of the event union (the ones records care about). */
12
+ type AgentEvent = Extract<RunProgressEvent, { callIndex: number }>;
13
+
14
+ function recordKey(callIndex: number, source?: string): string {
15
+ return `${source ?? ""}#${callIndex}`;
16
+ }
17
+
18
+ export const ACTIVITY_CAP = 100;
19
+ export const PREVIEW_CAP = 2000;
20
+
21
+ /** Human-readable preview of an agent's returned value, hard-capped. */
22
+ export function previewValue(value: unknown, cap = PREVIEW_CAP, truncationMarker = "…"): string {
23
+ let text: string;
24
+ if (typeof value === "string") {
25
+ text = value;
26
+ } else if (value === undefined) {
27
+ text = "(no value)";
28
+ } else {
29
+ try {
30
+ text = JSON.stringify(value, null, 2);
31
+ } catch {
32
+ text = String(value);
33
+ }
34
+ }
35
+ return text.length > cap ? `${text.slice(0, cap)}${truncationMarker}` : text;
36
+ }
37
+
38
+ export class AgentRecordStore {
39
+ // Keyed by (source, callIndex): a nested workflow() child restarts its own
40
+ // callIndex at 0, so callIndex alone would merge its agents into the parent's.
41
+ private byKey = new Map<string, AgentRecord>();
42
+ private order: string[] = [];
43
+
44
+ constructor(private readonly now: () => number = () => Date.now()) {}
45
+
46
+ /** Records in first-seen order (callIndex order for sequential scripts). */
47
+ list(): AgentRecord[] {
48
+ return this.order.map((key) => this.byKey.get(key) as AgentRecord);
49
+ }
50
+
51
+ get(callIndex: number, source?: string): AgentRecord | undefined {
52
+ return this.byKey.get(recordKey(callIndex, source));
53
+ }
54
+
55
+ apply(event: RunProgressEvent): void {
56
+ switch (event.type) {
57
+ case "agentStart": {
58
+ const record = this.ensure(event);
59
+ record.status = "running";
60
+ record.startedAt ??= this.now();
61
+ record.prompt = event.prompt;
62
+ break;
63
+ }
64
+ case "agentUpdate": {
65
+ const record = this.ensure(event);
66
+ if (event.model) record.model = event.model;
67
+ if (event.tool) {
68
+ record.activity.push(event.tool);
69
+ record.toolCalls++;
70
+ if (record.activity.length > ACTIVITY_CAP) record.activity.shift();
71
+ }
72
+ break;
73
+ }
74
+ case "agentEnd": {
75
+ const record = this.ensure(event);
76
+ record.finishedAt = this.now();
77
+ if (event.prompt !== undefined) record.prompt ??= event.prompt;
78
+ if (event.tokens) record.tokens = event.tokens;
79
+ if (event.cost !== undefined) record.cost = event.cost;
80
+ if (event.preview !== undefined) record.outcome = event.preview;
81
+ if (event.replayed) {
82
+ record.status = "replayed";
83
+ } else if (event.text) {
84
+ record.status = "failed";
85
+ record.error = event.text;
86
+ } else {
87
+ record.status = "done";
88
+ }
89
+ break;
90
+ }
91
+ default:
92
+ break;
93
+ }
94
+ }
95
+
96
+ private ensure(event: AgentEvent): AgentRecord {
97
+ const key = recordKey(event.callIndex, event.source);
98
+ const record = this.byKey.get(key);
99
+ if (!record) {
100
+ const created: AgentRecord = {
101
+ callIndex: event.callIndex,
102
+ source: event.source,
103
+ label: event.label ?? `agent ${event.callIndex + 1}`,
104
+ phase: event.phase,
105
+ status: "running",
106
+ activity: [],
107
+ toolCalls: 0,
108
+ };
109
+ this.byKey.set(key, created);
110
+ this.order.push(key);
111
+ return created;
112
+ }
113
+ // Later events may carry the label/phase a bare agentUpdate lacked.
114
+ if (event.label) record.label = event.label;
115
+ if (event.phase && !record.phase) record.phase = event.phase;
116
+ return record;
117
+ }
118
+ }