@bastani/atomic 0.9.5-alpha.8 → 0.9.5-alpha.9

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 (207) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/builtin/cursor/package.json +2 -2
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/CHANGELOG.md +11 -0
  5. package/dist/builtin/mcp/README.md +6 -6
  6. package/dist/builtin/mcp/direct-tools.ts +41 -23
  7. package/dist/builtin/mcp/index.ts +31 -4
  8. package/dist/builtin/mcp/init.ts +4 -43
  9. package/dist/builtin/mcp/metadata-hydration.ts +37 -0
  10. package/dist/builtin/mcp/package.json +1 -1
  11. package/dist/builtin/mcp/proxy-info-modes.ts +67 -14
  12. package/dist/builtin/mcp/startup-warmup.ts +98 -0
  13. package/dist/builtin/mcp/tool-result-renderer.ts +3 -3
  14. package/dist/builtin/subagents/CHANGELOG.md +12 -0
  15. package/dist/builtin/subagents/package.json +1 -1
  16. package/dist/builtin/subagents/src/extension/index.ts +14 -25
  17. package/dist/builtin/subagents/src/extension/startup-maintenance.ts +89 -0
  18. package/dist/builtin/subagents/src/runs/background/async-execution-common.ts +23 -18
  19. package/dist/builtin/subagents/src/runs/background/result-watcher.ts +20 -10
  20. package/dist/builtin/subagents/src/runs/background/subagent-runner-streaming.ts +13 -3
  21. package/dist/builtin/subagents/src/runs/foreground/execution-attempt.ts +10 -2
  22. package/dist/builtin/subagents/src/runs/shared/pi-spawn.ts +28 -1
  23. package/dist/builtin/subagents/src/tui/render-widget.ts +29 -0
  24. package/dist/builtin/web-access/package.json +1 -1
  25. package/dist/builtin/workflows/CHANGELOG.md +18 -0
  26. package/dist/builtin/workflows/README.md +1 -1
  27. package/dist/builtin/workflows/builtin/goal-runner.ts +39 -1
  28. package/dist/builtin/workflows/package.json +1 -1
  29. package/dist/builtin/workflows/skills/impeccable/SKILL.md +16 -13
  30. package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +1 -0
  31. package/dist/builtin/workflows/skills/impeccable/reference/adapt.native.md +58 -0
  32. package/dist/builtin/workflows/skills/impeccable/reference/android.md +40 -0
  33. package/dist/builtin/workflows/skills/impeccable/reference/animate.md +2 -0
  34. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +2 -0
  35. package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +139 -0
  36. package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +69 -62
  37. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +21 -7
  38. package/dist/builtin/workflows/skills/impeccable/reference/document.md +1 -1
  39. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +92 -0
  40. package/dist/builtin/workflows/skills/impeccable/reference/init.md +29 -7
  41. package/dist/builtin/workflows/skills/impeccable/reference/ios.md +45 -0
  42. package/dist/builtin/workflows/skills/impeccable/reference/layout.md +25 -1
  43. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +23 -1
  44. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +2 -1
  45. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +75 -14
  46. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +1 -1
  47. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +31 -0
  48. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +64 -0
  49. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +118 -11
  50. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +16 -11
  51. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +66 -0
  52. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +42 -10
  53. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/fonts.mjs +30 -0
  54. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +4 -6
  55. package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +660 -0
  56. package/dist/builtin/workflows/skills/impeccable/scripts/hook-before-edit.mjs +516 -0
  57. package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +1764 -0
  58. package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +61 -0
  59. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +4 -2
  60. package/dist/builtin/workflows/skills/impeccable/scripts/lib/is-generated.mjs +2 -2
  61. package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +13 -16
  62. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +17 -11
  63. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +104 -37
  64. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +9 -0
  65. package/dist/builtin/workflows/src/durable/backend.ts +12 -0
  66. package/dist/builtin/workflows/src/durable/dbos-backend.ts +17 -1
  67. package/dist/builtin/workflows/src/durable/resume-catalog.ts +12 -0
  68. package/dist/builtin/workflows/src/durable/resume-runtime.ts +5 -2
  69. package/dist/builtin/workflows/src/durable/types.ts +16 -0
  70. package/dist/builtin/workflows/src/engine/primitives/task.ts +1 -0
  71. package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +1 -2
  72. package/dist/builtin/workflows/src/engine/run-returned-status.ts +50 -16
  73. package/dist/builtin/workflows/src/engine/run.ts +20 -19
  74. package/dist/builtin/workflows/src/engine/runtime.ts +5 -0
  75. package/dist/builtin/workflows/src/extension/extension-factory.ts +3 -0
  76. package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +9 -2
  77. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +137 -27
  78. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +22 -6
  79. package/dist/builtin/workflows/src/extension/public-types.ts +1 -1
  80. package/dist/builtin/workflows/src/extension/workflow-command-completions.ts +13 -1
  81. package/dist/builtin/workflows/src/extension/workflow-command-registration.ts +19 -4
  82. package/dist/builtin/workflows/src/extension/workflow-command-surfaces.ts +9 -0
  83. package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +48 -23
  84. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +20 -8
  85. package/dist/builtin/workflows/src/extension/workflow-tool.ts +24 -8
  86. package/dist/builtin/workflows/src/runs/background/status.ts +4 -3
  87. package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +29 -8
  88. package/dist/builtin/workflows/src/runs/foreground/executor-hil.ts +2 -1
  89. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +3 -2
  90. package/dist/builtin/workflows/src/runs/shared/worktree-git.ts +155 -20
  91. package/dist/builtin/workflows/src/runs/shared/worktree-types.ts +6 -0
  92. package/dist/builtin/workflows/src/runs/shared/worktree.ts +2 -1
  93. package/dist/builtin/workflows/src/shared/returned-run-status.ts +116 -0
  94. package/dist/builtin/workflows/src/shared/workflow-failures-decisions.ts +2 -0
  95. package/dist/builtin/workflows/src/tui/graph-view-render-helpers.ts +21 -3
  96. package/dist/builtin/workflows/src/tui/graph-view-state.ts +3 -0
  97. package/dist/builtin/workflows/src/tui/graph-view-types.ts +3 -0
  98. package/dist/builtin/workflows/src/tui/status-list.ts +22 -15
  99. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +4 -4
  100. package/dist/core/agent-session-auto-compaction.d.ts +23 -3
  101. package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
  102. package/dist/core/agent-session-auto-compaction.js +129 -17
  103. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  104. package/dist/core/agent-session-events.d.ts.map +1 -1
  105. package/dist/core/agent-session-events.js +7 -0
  106. package/dist/core/agent-session-events.js.map +1 -1
  107. package/dist/core/agent-session-methods.d.ts +6 -3
  108. package/dist/core/agent-session-methods.d.ts.map +1 -1
  109. package/dist/core/agent-session-methods.js.map +1 -1
  110. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  111. package/dist/core/agent-session-prompt.js +1 -1
  112. package/dist/core/agent-session-prompt.js.map +1 -1
  113. package/dist/core/agent-session.d.ts +4 -2
  114. package/dist/core/agent-session.d.ts.map +1 -1
  115. package/dist/core/agent-session.js +4 -2
  116. package/dist/core/agent-session.js.map +1 -1
  117. package/dist/core/extensions/loader-virtual-modules.d.ts +8 -1
  118. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  119. package/dist/core/extensions/loader-virtual-modules.js +2 -1
  120. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  121. package/dist/core/footer-data-provider.d.ts +6 -0
  122. package/dist/core/footer-data-provider.d.ts.map +1 -1
  123. package/dist/core/footer-data-provider.js +72 -29
  124. package/dist/core/footer-data-provider.js.map +1 -1
  125. package/dist/core/openai-responses-payload-sanitizer.d.ts +1 -0
  126. package/dist/core/openai-responses-payload-sanitizer.d.ts.map +1 -1
  127. package/dist/core/openai-responses-payload-sanitizer.js +24 -9
  128. package/dist/core/openai-responses-payload-sanitizer.js.map +1 -1
  129. package/dist/core/slash-commands.d.ts +2 -0
  130. package/dist/core/slash-commands.d.ts.map +1 -1
  131. package/dist/core/slash-commands.js +141 -0
  132. package/dist/core/slash-commands.js.map +1 -1
  133. package/dist/index.d.ts +1 -0
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +1 -0
  136. package/dist/index.js.map +1 -1
  137. package/dist/main-deferred-startup.d.ts.map +1 -1
  138. package/dist/main-deferred-startup.js +2 -2
  139. package/dist/main-deferred-startup.js.map +1 -1
  140. package/dist/main-early-input.d.ts.map +1 -1
  141. package/dist/main-early-input.js +7 -0
  142. package/dist/main-early-input.js.map +1 -1
  143. package/dist/main.d.ts.map +1 -1
  144. package/dist/main.js +1 -1
  145. package/dist/main.js.map +1 -1
  146. package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
  147. package/dist/modes/interactive/components/chat-session-host-actions.js +0 -1
  148. package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
  149. package/dist/modes/interactive/interactive-agent-events.d.ts.map +1 -1
  150. package/dist/modes/interactive/interactive-agent-events.js +1 -0
  151. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  152. package/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
  153. package/dist/modes/interactive/interactive-autocomplete.js +21 -9
  154. package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
  155. package/dist/modes/interactive/interactive-deferred-startup.d.ts.map +1 -1
  156. package/dist/modes/interactive/interactive-deferred-startup.js +41 -28
  157. package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -1
  158. package/dist/modes/interactive/interactive-hotkeys-debug.d.ts.map +1 -1
  159. package/dist/modes/interactive/interactive-hotkeys-debug.js +1 -0
  160. package/dist/modes/interactive/interactive-hotkeys-debug.js.map +1 -1
  161. package/dist/modes/interactive/interactive-input-handling.d.ts.map +1 -1
  162. package/dist/modes/interactive/interactive-input-handling.js +41 -6
  163. package/dist/modes/interactive/interactive-input-handling.js.map +1 -1
  164. package/dist/modes/interactive/interactive-mode-base.d.ts +3 -0
  165. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  166. package/dist/modes/interactive/interactive-mode-base.js +3 -0
  167. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  168. package/dist/modes/interactive/interactive-mode-deps.d.ts +1 -1
  169. package/dist/modes/interactive/interactive-mode-deps.d.ts.map +1 -1
  170. package/dist/modes/interactive/interactive-mode-deps.js +1 -1
  171. package/dist/modes/interactive/interactive-mode-deps.js.map +1 -1
  172. package/dist/modes/interactive/interactive-mode-surface.d.ts +2 -2
  173. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  174. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  175. package/dist/modes/interactive/interactive-model-routing.d.ts.map +1 -1
  176. package/dist/modes/interactive/interactive-model-routing.js +3 -1
  177. package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
  178. package/dist/modes/interactive/interactive-process-lifecycle.d.ts.map +1 -1
  179. package/dist/modes/interactive/interactive-process-lifecycle.js +0 -3
  180. package/dist/modes/interactive/interactive-process-lifecycle.js.map +1 -1
  181. package/dist/modes/interactive/interactive-render-chat.d.ts.map +1 -1
  182. package/dist/modes/interactive/interactive-render-chat.js +15 -1
  183. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  184. package/dist/modes/interactive/interactive-session-routing.d.ts.map +1 -1
  185. package/dist/modes/interactive/interactive-session-routing.js +5 -1
  186. package/dist/modes/interactive/interactive-session-routing.js.map +1 -1
  187. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  188. package/dist/modes/interactive/interactive-startup.js +20 -21
  189. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  190. package/dist/modes/interactive/theme/global-theme.d.ts.map +1 -1
  191. package/dist/modes/interactive/theme/global-theme.js +26 -2
  192. package/dist/modes/interactive/theme/global-theme.js.map +1 -1
  193. package/dist/utils/fs-watch.d.ts +24 -1
  194. package/dist/utils/fs-watch.d.ts.map +1 -1
  195. package/dist/utils/fs-watch.js +60 -5
  196. package/dist/utils/fs-watch.js.map +1 -1
  197. package/docs/compaction.md +5 -1
  198. package/docs/development.md +4 -0
  199. package/docs/extensions.md +5 -0
  200. package/docs/json.md +1 -1
  201. package/docs/quickstart.md +1 -1
  202. package/docs/settings.md +1 -1
  203. package/docs/subagents.md +4 -0
  204. package/docs/windows.md +4 -0
  205. package/docs/workflows.md +8 -6
  206. package/npm-shrinkwrap.json +23 -23
  207. package/package.json +2 -2
@@ -0,0 +1,98 @@
1
+ import type { McpExtensionState } from "./state.ts";
2
+ import { getMissingConfiguredDirectToolServers } from "./direct-tools.ts";
3
+ import { loadMetadataCache } from "./metadata-cache.ts";
4
+ import { buildToolMetadata } from "./tool-metadata.ts";
5
+ import { parallelLimit } from "./utils.ts";
6
+ import { logger } from "./logger.ts";
7
+ import { updateMetadataCache, updateStatusBar } from "./init.ts";
8
+
9
+ export interface McpStartupWarmupOptions {
10
+ shouldContinue?: () => boolean;
11
+ onDirectToolsChanged?: () => void | Promise<void>;
12
+ onSettled?: () => void;
13
+ }
14
+
15
+ export interface McpStartupWarmupHandle {
16
+ readonly promise: Promise<void>;
17
+ cancel(): void;
18
+ }
19
+
20
+ function deferToMacrotask(): Promise<void> {
21
+ return new Promise((resolve) => setImmediate(resolve));
22
+ }
23
+
24
+ export function scheduleMcpStartupWarmup(
25
+ state: McpExtensionState,
26
+ options: McpStartupWarmupOptions = {},
27
+ ): McpStartupWarmupHandle {
28
+ let cancelled = false;
29
+ const shouldContinue = (): boolean => !cancelled && (options.shouldContinue?.() ?? true);
30
+
31
+ const promise = (async () => {
32
+ await deferToMacrotask();
33
+ if (!shouldContinue()) return;
34
+
35
+ const envDirect = process.env.MCP_DIRECT_TOOLS;
36
+ if (envDirect === "__none__") return;
37
+
38
+ const envDirectTools = envDirect?.split(",").map((item) => item.trim()).filter(Boolean);
39
+ const missingCacheServers = getMissingConfiguredDirectToolServers(state.config, loadMetadataCache(), envDirectTools)
40
+ .filter((name) => state.manager.getConnection(name)?.status !== "connected");
41
+ if (missingCacheServers.length === 0) return;
42
+
43
+ const prefix = state.config.settings?.toolPrefix ?? "server";
44
+ const results = await parallelLimit(missingCacheServers, 10, async (name) => {
45
+ if (!shouldContinue()) return { name, ok: false };
46
+ const definition = state.config.mcpServers[name];
47
+ if (!definition) return { name, ok: false };
48
+ try {
49
+ const connection = await state.manager.connect(name, definition);
50
+ if (!shouldContinue()) {
51
+ if (state.manager.getConnection(name) === connection) {
52
+ await state.manager.close(name);
53
+ }
54
+ return { name, ok: false };
55
+ }
56
+ if (connection.status === "needs-auth") return { name, ok: false };
57
+ const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
58
+ state.toolMetadata.set(name, metadata);
59
+ updateMetadataCache(state, name);
60
+ return { name, ok: true };
61
+ } catch (error) {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ logger.debug(`MCP: background direct-tools warmup failed for ${name}: ${message}`);
64
+ return { name, ok: false };
65
+ }
66
+ });
67
+
68
+ if (!shouldContinue()) return;
69
+ const warmed = results.filter((result) => result.ok).map((result) => result.name);
70
+ if (warmed.length === 0) return;
71
+ updateStatusBar(state);
72
+ if (!shouldContinue()) return;
73
+ await options.onDirectToolsChanged?.();
74
+ if (!shouldContinue()) return;
75
+ if (state.ui) {
76
+ state.ui.notify(`MCP: direct tools for ${warmed.join(", ")} are now available`, "info");
77
+ }
78
+ })()
79
+ .catch((error) => {
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ logger.debug(`MCP: background startup warmup failed: ${message}`);
82
+ })
83
+ .finally(() => {
84
+ try {
85
+ options.onSettled?.();
86
+ } catch (error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ logger.debug(`MCP: background startup warmup settled callback failed: ${message}`);
89
+ }
90
+ });
91
+
92
+ return {
93
+ promise,
94
+ cancel() {
95
+ cancelled = true;
96
+ },
97
+ };
98
+ }
@@ -1,7 +1,7 @@
1
- import type { AgentToolResult, ToolRenderResultOptions } from "@bastani/atomic";
1
+ import { keyText, type AgentToolResult, type ToolRenderResultOptions } from "@bastani/atomic";
2
2
  import { Text } from "@earendil-works/pi-tui";
3
3
 
4
- type McpToolResultDetails = Record<string, unknown> & { error?: unknown };
4
+ export type McpToolResultDetails = Record<string, unknown> & { error?: unknown };
5
5
  type McpToolContentBlock = AgentToolResult<McpToolResultDetails>["content"][number];
6
6
 
7
7
  interface RenderTheme {
@@ -58,7 +58,7 @@ export function renderMcpToolResult(
58
58
  .map((line) => line === "…" ? theme.fg("muted", line) : theme.fg("toolOutput", line))
59
59
  .join("\n");
60
60
  const hint = display.truncated && !options.expanded
61
- ? `\n${theme.fg("muted", "(CTRL+O Expand)")}`
61
+ ? `\n${theme.fg("muted", `(${keyText("app.tools.expand")} Expand)`)}`
62
62
  : "";
63
63
 
64
64
  return new Text(`${output}${hint}`, 0, 0);
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.5-alpha.9] - 2026-07-09
6
+
7
+ ### Changed
8
+
9
+ - Improved subagent extension startup by deferring old-run cleanup scans, result watcher startup, and existing-result priming to macrotasks while preserving immediate `subagent` tool and slash-command registration.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed background async result watching on Windows to use Atomic's safe watcher path strategy and fall back to polling when native `fs.watch` would receive an unsafe or non-canonical watcher path.
14
+ - Fixed async/background subagent status visibility while the fullscreen workflow graph is active by publishing the live async summary through the shared status surface in addition to the below-editor widget.
15
+ - Fixed foreground and background subagent startup diagnostics so missing or invalid child `cwd` values are reported as working-directory problems before spawn, while runtime spawn failures include the attempted command and cwd instead of ambiguous `ENOENT` output.
16
+
5
17
  ## [0.9.5-alpha.8] - 2026-07-08
6
18
 
7
19
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.5-alpha.8",
3
+ "version": "0.9.5-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -7,21 +7,18 @@ import { APP_NAME, getEnvValue } from "@bastani/atomic";
7
7
  import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@bastani/atomic";
8
8
  import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
9
9
  import { discoverAgents } from "../agents/agents.ts";
10
- import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
10
+ import { getArtifactsDir } from "../shared/artifacts.ts";
11
11
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
12
- import { cleanupOldChainDirs } from "../shared/settings.ts";
13
12
  import { advanceResultPulseFrame, renderLiveSubagentResult, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, type SubagentResultRenderState } from "../tui/render.ts";
14
13
  import { SubagentParams } from "./schemas.ts";
15
14
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
16
15
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
17
- import { createResultWatcher } from "../runs/background/result-watcher.ts";
18
16
  import { registerSlashCommands } from "../slash/slash-commands.ts";
19
17
  import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.ts";
20
18
  import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
21
19
  import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
22
20
  import { inspectSubagentStatus } from "../runs/background/run-status.ts";
23
21
  import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/background/notify.ts";
24
- import { cleanupOldNestedRuntimeDirs } from "../runs/shared/nested-events.ts";
25
22
  import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
26
23
  import registerFanoutChildSubagentExtension from "./fanout-child.ts";
27
24
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
@@ -29,6 +26,7 @@ import { loadConfig } from "./config.ts";
29
26
  import { DEFAULT_PROMPT_GUIDANCE } from "./prompt-guidance.ts";
30
27
  import { type Details, type SubagentState, ASYNC_DIR, DEFAULT_ARTIFACT_CONFIG, RESULTS_DIR, SLASH_RESULT_TYPE, SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_ASYNC_STARTED_EVENT, SUBAGENT_CONTROL_EVENT, WIDGET_KEY } from "../shared/types.ts";
31
28
  import { clearPendingForegroundControlNotices, formatSubagentControlNotice, handleSubagentControlNotice, SUBAGENT_CONTROL_MESSAGE_TYPE, type SubagentControlMessageDetails } from "./control-notices.ts";
29
+ import { createSubagentStartupMaintenance } from "./startup-maintenance.ts";
32
30
  export { loadConfig } from "./config.ts";
33
31
  function getSubagentSessionRoot(parentSessionFile: string | null): string {
34
32
  if (parentSessionFile) {
@@ -171,12 +169,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
171
169
  }
172
170
  ensureAccessibleDir(RESULTS_DIR);
173
171
  ensureAccessibleDir(ASYNC_DIR);
174
- cleanupOldChainDirs();
175
172
  const config = loadConfig();
176
173
  const asyncByDefault = config.asyncByDefault === true;
177
174
  const tempArtifactsDir = getArtifactsDir(null);
178
- cleanupAllArtifactDirs(DEFAULT_ARTIFACT_CONFIG.cleanupDays);
179
- cleanupOldNestedRuntimeDirs(DEFAULT_ARTIFACT_CONFIG.cleanupDays);
180
175
  const state: SubagentState = {
181
176
  baseCwd: "",
182
177
  currentSessionId: null,
@@ -197,16 +192,16 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
197
192
  clear: () => {},
198
193
  },
199
194
  };
200
- const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
201
- pi,
202
- state,
203
- RESULTS_DIR,
204
- 10 * 60 * 1000,
205
- );
206
- startResultWatcher();
207
- primeExistingResults();
195
+ const maintenance = createSubagentStartupMaintenance(pi, state, {
196
+ resultsDir: RESULTS_DIR,
197
+ artifactCleanupDays: DEFAULT_ARTIFACT_CONFIG.cleanupDays,
198
+ resultTtlMs: 10 * 60 * 1000,
199
+ });
200
+ maintenance.scheduleStartupCleanup();
201
+ maintenance.startResultWatcherDeferred();
202
+ maintenance.primeExistingResultsDeferred();
208
203
  const runtimeCleanup = () => {
209
- stopResultWatcher();
204
+ maintenance.stop();
210
205
  stopWidgetAnimation();
211
206
  stopResultAnimations();
212
207
  clearPendingForegroundControlNotices(state);
@@ -430,13 +425,7 @@ DIAGNOSTICS:
430
425
  if (state.asyncJobs.size > 0) ensurePoller();
431
426
  });
432
427
  const cleanupSessionArtifacts = (ctx: ExtensionContext) => {
433
- try {
434
- const sessionFile = ctx.sessionManager.getSessionFile();
435
- if (sessionFile) {
436
- cleanupOldArtifacts(getArtifactsDir(sessionFile), DEFAULT_ARTIFACT_CONFIG.cleanupDays);
437
- }
438
- } catch {
439
- }
428
+ maintenance.cleanupSessionArtifactsDeferred(ctx);
440
429
  };
441
430
  const resetSessionState = (ctx: ExtensionContext) => {
442
431
  state.baseCwd = ctx.cwd;
@@ -447,7 +436,7 @@ DIAGNOSTICS:
447
436
  resetJobs(ctx);
448
437
  hydrateActiveJobs(ctx);
449
438
  restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
450
- primeExistingResults();
439
+ maintenance.primeExistingResultsDeferred();
451
440
  };
452
441
  pi.on("session_start", (_event, ctx) => {
453
442
  resetSessionState(ctx);
@@ -462,7 +451,7 @@ DIAGNOSTICS:
462
451
  if (globalStore[eventUnsubscribeStoreKey] === eventUnsubscribes) {
463
452
  delete globalStore[eventUnsubscribeStoreKey];
464
453
  }
465
- stopResultWatcher();
454
+ maintenance.stop();
466
455
  stopWidgetAnimation();
467
456
  stopResultAnimations();
468
457
  if (state.poller) clearInterval(state.poller);
@@ -0,0 +1,89 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@bastani/atomic";
2
+ import type { SubagentState } from "../shared/types.ts";
3
+ import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
4
+ import { cleanupOldChainDirs } from "../shared/settings.ts";
5
+ import { cleanupOldNestedRuntimeDirs } from "../runs/shared/nested-events.ts";
6
+ import { createResultWatcher } from "../runs/background/result-watcher.ts";
7
+
8
+ export interface SubagentStartupMaintenance {
9
+ scheduleStartupCleanup(): void;
10
+ startResultWatcherDeferred(): void;
11
+ primeExistingResultsDeferred(): void;
12
+ cleanupSessionArtifactsDeferred(ctx: ExtensionContext): void;
13
+ stop(): void;
14
+ }
15
+
16
+ function scheduleMacrotask(task: () => void): () => void {
17
+ let cancelled = false;
18
+ const handle = setImmediate(() => {
19
+ if (!cancelled) task();
20
+ });
21
+ handle.unref?.();
22
+ return () => {
23
+ cancelled = true;
24
+ clearImmediate(handle);
25
+ };
26
+ }
27
+
28
+ function swallowCleanup(task: () => void): void {
29
+ try {
30
+ task();
31
+ } catch {
32
+ }
33
+ }
34
+
35
+ export function createSubagentStartupMaintenance(
36
+ pi: ExtensionAPI,
37
+ state: SubagentState,
38
+ options: {
39
+ resultsDir: string;
40
+ artifactCleanupDays: number;
41
+ resultTtlMs: number;
42
+ },
43
+ ): SubagentStartupMaintenance {
44
+ const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
45
+ pi,
46
+ state,
47
+ options.resultsDir,
48
+ options.resultTtlMs,
49
+ );
50
+ const cancelTasks = new Set<() => void>();
51
+ const schedule = (task: () => void): void => {
52
+ const cancel = scheduleMacrotask(() => {
53
+ cancelTasks.delete(cancel);
54
+ task();
55
+ });
56
+ cancelTasks.add(cancel);
57
+ };
58
+
59
+ return {
60
+ scheduleStartupCleanup() {
61
+ schedule(() => {
62
+ swallowCleanup(cleanupOldChainDirs);
63
+ swallowCleanup(() => cleanupAllArtifactDirs(options.artifactCleanupDays));
64
+ swallowCleanup(() => cleanupOldNestedRuntimeDirs(options.artifactCleanupDays));
65
+ });
66
+ },
67
+ startResultWatcherDeferred() {
68
+ schedule(startResultWatcher);
69
+ },
70
+ primeExistingResultsDeferred() {
71
+ schedule(primeExistingResults);
72
+ },
73
+ cleanupSessionArtifactsDeferred(ctx) {
74
+ let sessionFile: string | null | undefined;
75
+ try {
76
+ sessionFile = ctx.sessionManager.getSessionFile();
77
+ } catch {
78
+ return;
79
+ }
80
+ if (!sessionFile) return;
81
+ schedule(() => swallowCleanup(() => cleanupOldArtifacts(getArtifactsDir(sessionFile), options.artifactCleanupDays)));
82
+ },
83
+ stop() {
84
+ for (const cancel of cancelTasks) cancel();
85
+ cancelTasks.clear();
86
+ stopResultWatcher();
87
+ },
88
+ };
89
+ }
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import { createRequire } from "node:module";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { APP_NAME } from "@bastani/atomic";
7
- import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
7
+ import { formatPiSpawnError, resolvePiPackageRoot, validatePiSpawnCwd } from "../shared/pi-spawn.ts";
8
8
  import { getAsyncConfigPath, TEMP_ROOT_DIR, type SubagentRunMode } from "../../shared/types.ts";
9
9
  import type { AsyncExecutionResult, AsyncSpawnResult } from "./async-execution-types.ts";
10
10
 
@@ -85,33 +85,38 @@ export function writeAsyncRunnerConfig(cfg: object, suffix: string): string {
85
85
  }
86
86
 
87
87
  export function spawnRunner(cfg: object, suffix: string, cwd: string): AsyncSpawnResult {
88
+ const cwdValidation = validatePiSpawnCwd(cwd);
89
+ if (!cwdValidation.ok) return { error: cwdValidation.error };
90
+
88
91
  if (!jitiCliPath) {
89
92
  return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
90
93
  }
91
94
 
92
- try {
93
- const cwdStats = fs.statSync(cwd);
94
- if (!cwdStats.isDirectory()) {
95
- return { error: `cwd is not a directory: ${cwd}` };
96
- }
97
- } catch {
98
- return { error: `cwd does not exist: ${cwd}` };
99
- }
100
-
101
95
  const cfgPath = writeAsyncRunnerConfig(cfg, suffix);
102
96
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
103
97
 
104
- const proc = spawn(process.execPath, [jitiCliPath, runner, cfgPath], {
105
- cwd,
106
- detached: true,
107
- stdio: "ignore",
108
- windowsHide: true,
109
- });
98
+ const spawnSpec = { command: process.execPath, args: [jitiCliPath, runner, cfgPath] };
99
+ let proc: ReturnType<typeof spawn>;
100
+ try {
101
+ proc = spawn(spawnSpec.command, spawnSpec.args, {
102
+ cwd,
103
+ detached: true,
104
+ stdio: "ignore",
105
+ windowsHide: true,
106
+ });
107
+ } catch (error) {
108
+ const spawnError = error instanceof Error ? error : new Error(String(error));
109
+ return { error: formatPiSpawnError(spawnError, spawnSpec, cwd) };
110
+ }
110
111
  proc.on("error", (error) => {
111
- console.error(`[${APP_NAME}-subagents] async spawn failed: ${error.message}`);
112
+ console.error(`[${APP_NAME}-subagents] async spawn failed: ${formatPiSpawnError(error, spawnSpec, cwd)}`);
112
113
  });
113
114
  if (typeof proc.pid !== "number") {
114
- return { error: `async runner did not produce a pid for cwd: ${cwd}` };
115
+ const noPidError = Object.assign(
116
+ new Error(`spawn ${spawnSpec.command} failed before assigning a pid`),
117
+ fs.existsSync(spawnSpec.command) ? {} : { code: "ENOENT" },
118
+ );
119
+ return { error: formatPiSpawnError(noPidError, spawnSpec, cwd) };
115
120
  }
116
121
  proc.unref();
117
122
  return { pid: proc.pid };
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { isSafeFsWatchPathError, watchWithErrorHandler } from "@bastani/atomic";
3
4
  import { buildCompletionKey, markSeenWithTtl } from "./completion-dedupe.ts";
4
5
  import { createFileCoalescer } from "../../shared/file-coalescer.ts";
5
6
  import {
@@ -21,7 +22,9 @@ import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-
21
22
  const WATCHER_RESTART_DELAY_MS = 3000;
22
23
  const POLL_INTERVAL_MS = 3000;
23
24
 
24
- type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "watch">;
25
+ type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "watch"> & {
26
+ realpathSync?: typeof fs.realpathSync;
27
+ };
25
28
 
26
29
  type ResultWatcherTimers = {
27
30
  setTimeout: typeof setTimeout;
@@ -30,9 +33,12 @@ type ResultWatcherTimers = {
30
33
  clearInterval: typeof clearInterval;
31
34
  };
32
35
 
36
+ type ResultWatcherSafeWatch = typeof watchWithErrorHandler;
37
+
33
38
  type ResultWatcherDeps = {
34
39
  fs?: ResultWatcherFs;
35
40
  timers?: ResultWatcherTimers;
41
+ safeWatch?: ResultWatcherSafeWatch;
36
42
  };
37
43
 
38
44
  type ResultFileChild = {
@@ -88,7 +94,7 @@ function isNotFoundError(error: unknown): boolean {
88
94
 
89
95
  function shouldFallBackToPolling(error: unknown): boolean {
90
96
  const code = getErrorCode(error);
91
- return code === "EMFILE" || code === "ENOSPC";
97
+ return code === "EMFILE" || code === "ENOSPC" || isSafeFsWatchPathError(error);
92
98
  }
93
99
 
94
100
  export function createResultWatcher(
@@ -104,6 +110,7 @@ export function createResultWatcher(
104
110
  } {
105
111
  const fsApi = deps.fs ?? fs;
106
112
  const timers = deps.timers ?? { setTimeout, clearTimeout, setInterval, clearInterval };
113
+ const safeWatch = deps.safeWatch ?? watchWithErrorHandler;
107
114
 
108
115
  const handleResult = async (file: string) => {
109
116
  const resultPath = path.join(resultsDir, file);
@@ -264,13 +271,7 @@ export function createResultWatcher(
264
271
  state.watcherRestartTimer = null;
265
272
  }
266
273
  try {
267
- state.watcher = fsApi.watch(resultsDir, (ev, file) => {
268
- if (ev !== "rename" || !file) return;
269
- const fileName = file.toString();
270
- if (!fileName.endsWith(".json")) return;
271
- state.resultFileCoalescer.schedule(fileName);
272
- });
273
- state.watcher.on("error", (error) => {
274
+ const handleWatcherError = (error: Error) => {
274
275
  if (shouldFallBackToPolling(error)) {
275
276
  startPollingFallback(error);
276
277
  return;
@@ -279,8 +280,17 @@ export function createResultWatcher(
279
280
  state.watcher?.close();
280
281
  state.watcher = null;
281
282
  scheduleRestart();
283
+ };
284
+ state.watcher = safeWatch(resultsDir, (ev, file) => {
285
+ if (ev !== "rename" || !file) return;
286
+ const fileName = file.toString();
287
+ if (!fileName.endsWith(".json")) return;
288
+ state.resultFileCoalescer.schedule(fileName);
289
+ }, handleWatcherError, {
290
+ watch: fsApi.watch,
291
+ realpathSyncNative: fsApi.realpathSync?.native,
282
292
  });
283
- state.watcher.unref?.();
293
+ state.watcher?.unref?.();
284
294
  } catch (error) {
285
295
  if (shouldFallBackToPolling(error)) {
286
296
  startPollingFallback(error);
@@ -5,7 +5,7 @@ import { appendJsonl } from "../../shared/artifacts.ts";
5
5
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
6
6
  import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
7
7
  import { getSubagentDepthEnv } from "../../shared/types.ts";
8
- import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
8
+ import { formatPiSpawnError, getPiSpawnCommand, validatePiSpawnCwd } from "../shared/pi-spawn.ts";
9
9
  import {
10
10
  assistantStopReason,
11
11
  isAssistantFailureStopReason,
@@ -29,6 +29,17 @@ export function runPiStreaming(
29
29
  registerInterrupt?: (interrupt: (() => void) | undefined) => void,
30
30
  onChildEvent?: (event: ChildEvent) => void,
31
31
  ): Promise<RunPiStreamingResult> {
32
+ const cwdValidation = validatePiSpawnCwd(cwd);
33
+ if (!cwdValidation.ok) {
34
+ return Promise.resolve({
35
+ stderr: "",
36
+ exitCode: 1,
37
+ messages: [],
38
+ usage: emptyUsage(),
39
+ error: cwdValidation.error,
40
+ finalOutput: "",
41
+ });
42
+ }
32
43
  return new Promise((resolve) => {
33
44
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
34
45
  const spawnEnv = {
@@ -273,8 +284,7 @@ export function runPiStreaming(
273
284
  attemptWatchdog.clear();
274
285
  outputStream.end();
275
286
  const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
276
- const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
277
- const finalError = error ?? assistantError ?? spawnErrorMessage;
287
+ const finalError = error ?? assistantError ?? formatPiSpawnError(spawnError, spawnSpec, cwd);
278
288
  resolve({
279
289
  stderr,
280
290
  exitCode: 1,
@@ -14,7 +14,7 @@ import { extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "
14
14
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
15
15
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
16
16
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
17
- import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
17
+ import { formatPiSpawnError, getPiSpawnCommand, validatePiSpawnCwd } from "../shared/pi-spawn.ts";
18
18
  import { assistantStopReason, isAssistantFailureStopReason, shouldStartSubagentFinalDrain } from "../shared/final-drain.ts";
19
19
  import { modelFailureMessage } from "../shared/model-fallback.ts";
20
20
  import { createAttemptWatchdog } from "../shared/attempt-watchdog.ts";
@@ -124,6 +124,14 @@ export async function runSingleAttempt(
124
124
  workflowStageSubagentGuard: options.workflowStageSubagentGuard,
125
125
  }),
126
126
  };
127
+ const cwdValidation = validatePiSpawnCwd(runCwd);
128
+ if (!cwdValidation.ok) {
129
+ cleanupTempDir(tempDir);
130
+ result.error = cwdValidation.error;
131
+ return finalizeSingleAttempt({
132
+ result, progress, exitCode: 1, interruptedByControl, allControlEvents: controlRuntime.allControlEvents, options, shared, startTime,
133
+ });
134
+ }
127
135
  const exitCode = await new Promise<number>((resolve) => {
128
136
  const spawnSpec = getPiSpawnCommand(args);
129
137
  const proc = spawn(spawnSpec.command, spawnSpec.args, {
@@ -418,7 +426,7 @@ export async function runSingleAttempt(
418
426
  attemptWatchdog.clear();
419
427
  void jsonlWriter.close().catch(() => undefined);
420
428
  cleanupTempDir(tempDir);
421
- if (!result.error) result.error = error instanceof Error ? error.message : String(error);
429
+ if (!result.error) result.error = formatPiSpawnError(error, spawnSpec, runCwd);
422
430
  finish(1);
423
431
  });
424
432
 
@@ -42,9 +42,36 @@ export interface PiSpawnDeps {
42
42
  resolvePackageJson?: () => string;
43
43
  resolvePackageEntry?: () => string;
44
44
  piPackageRoot?: string;
45
+ statSync?: (filePath: string) => fs.Stats;
45
46
  }
46
47
 
47
- interface PiSpawnCommand {
48
+ export type PiSpawnCwdValidationResult = { ok: true } | { ok: false; error: string };
49
+
50
+ export function validatePiSpawnCwd(cwd: string, deps: PiSpawnDeps = {}): PiSpawnCwdValidationResult {
51
+ const statSync = deps.statSync ?? fs.statSync;
52
+ try {
53
+ const cwdStats = statSync(cwd);
54
+ if (!cwdStats.isDirectory()) return { ok: false, error: `cwd is not a directory: ${cwd}` };
55
+ return { ok: true };
56
+ } catch (error) {
57
+ const fsError = error as NodeJS.ErrnoException;
58
+ if (fsError.code === "ENOENT") return { ok: false, error: `cwd does not exist: ${cwd}` };
59
+ if (fsError.code === "ENOTDIR") return { ok: false, error: `cwd path contains a non-directory component: ${cwd}` };
60
+ const details = fsError.message ? ` (${fsError.message})` : "";
61
+ return { ok: false, error: `cwd is not accessible: ${cwd}${details}` };
62
+ }
63
+ }
64
+
65
+ export function formatPiSpawnError(error: Error, spawnSpec: PiSpawnCommand, cwd: string): string {
66
+ const spawnError = error as NodeJS.ErrnoException;
67
+ const message = error.message || String(error);
68
+ if (spawnError.code === "ENOENT") {
69
+ return `failed to spawn subagent runtime '${spawnSpec.command}' from cwd '${cwd}': runtime executable was not found or could not be launched (${message})`;
70
+ }
71
+ return `failed to spawn subagent runtime '${spawnSpec.command}' from cwd '${cwd}': ${message}`;
72
+ }
73
+
74
+ export interface PiSpawnCommand {
48
75
  command: string;
49
76
  args: string[];
50
77
  }
@@ -137,6 +137,28 @@ function getWidgetOwnerKey(ctx: ExtensionContext): string {
137
137
  return `${sessionOwner}|cwd:${cwdOwner}`;
138
138
  }
139
139
 
140
+ function widgetStatusText(jobs: AsyncJobState[]): string | undefined {
141
+ if (jobs.length === 0) return undefined;
142
+ const counts = new Map<string, number>();
143
+ for (const job of jobs) counts.set(job.status, (counts.get(job.status) ?? 0) + 1);
144
+ const ordered = ["running", "queued", "complete", "failed", "paused"];
145
+ const parts = ordered
146
+ .map((status) => {
147
+ const count = counts.get(status) ?? 0;
148
+ return count > 0 ? `${count} ${status}` : undefined;
149
+ })
150
+ .filter((part): part is string => part !== undefined);
151
+ return `Async agents: ${parts.join(", ")}`;
152
+ }
153
+
154
+ function setWidgetStatus(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
155
+ try {
156
+ ctx.ui.setStatus?.(WIDGET_KEY, widgetStatusText(jobs));
157
+ } catch {
158
+ // Status mirroring must never prevent the primary widget render path.
159
+ }
160
+ }
161
+
140
162
  function requestWidgetRender(ctx: ExtensionContext): void {
141
163
  (ctx as RenderRequestingContext).ui.requestRender?.();
142
164
  }
@@ -150,6 +172,11 @@ function unmountWidgetBestEffort(ctx: ExtensionContext | undefined): void {
150
172
  // reload/session rebinding, but local state still needs to move on so the
151
173
  // next status update can mount cleanly on the active UI context.
152
174
  }
175
+ try {
176
+ ctx.ui.setStatus?.(WIDGET_KEY, undefined);
177
+ } catch {
178
+ // Status mirroring cleanup must never block primary widget teardown.
179
+ }
153
180
  }
154
181
 
155
182
  function widgetSnapshotKey(jobs: AsyncJobState[]): string {
@@ -253,6 +280,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
253
280
  // host session disposal owns cross-owner empty-session teardown.
254
281
  return;
255
282
  }
283
+ if (ctx.hasUI) setWidgetStatus(ctx, []);
256
284
  stopWidgetAnimation();
257
285
  return;
258
286
  }
@@ -271,6 +299,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
271
299
  mountedWidgetOwnerKey = undefined;
272
300
  widgetMounted = false;
273
301
  }
302
+ setWidgetStatus(ctx, jobs);
274
303
  if (!widgetMounted) {
275
304
  // belowEditor keeps the live async widget pinned to the bottom viewport,
276
305
  // matching the workflow companion widget's placement (#1109). The pulse frame
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.5-alpha.8",
3
+ "version": "0.9.5-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [