@bastani/atomic 0.8.21 → 0.8.22-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/CHANGELOG.md +40 -9
  2. package/dist/builtin/intercom/broker/broker.ts +3 -3
  3. package/dist/builtin/intercom/config.ts +3 -3
  4. package/dist/builtin/intercom/index.ts +1 -1
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/intercom/ui/compose.ts +2 -2
  7. package/dist/builtin/mcp/host-html-template.ts +0 -3
  8. package/dist/builtin/mcp/package.json +1 -1
  9. package/dist/builtin/subagents/CHANGELOG.md +13 -4
  10. package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -9
  11. package/dist/builtin/subagents/agents/debugger.md +6 -6
  12. package/dist/builtin/subagents/package.json +1 -1
  13. package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +1 -1
  14. package/dist/builtin/subagents/skills/browser-use/SKILL.md +234 -0
  15. package/dist/builtin/subagents/skills/browser-use/references/cdp-python.md +76 -0
  16. package/dist/builtin/subagents/skills/browser-use/references/multi-session.md +92 -0
  17. package/dist/builtin/subagents/skills/subagent/SKILL.md +4 -4
  18. package/dist/builtin/subagents/src/agents/skills.ts +19 -1
  19. package/dist/builtin/subagents/src/extension/index.ts +24 -22
  20. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +7 -1
  21. package/dist/builtin/subagents/src/runs/background/async-execution.ts +23 -7
  22. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +98 -3
  23. package/dist/builtin/subagents/src/runs/background/async-status.ts +3 -1
  24. package/dist/builtin/subagents/src/runs/background/run-status.ts +1 -1
  25. package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +3 -0
  26. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +37 -12
  27. package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +15 -15
  28. package/dist/builtin/subagents/src/runs/foreground/execution.ts +26 -2
  29. package/dist/builtin/subagents/src/runs/shared/nested-render.ts +1 -1
  30. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +7 -0
  31. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +28 -1
  32. package/dist/builtin/subagents/src/shared/fast-mode.ts +80 -0
  33. package/dist/builtin/subagents/src/shared/formatters.ts +4 -2
  34. package/dist/builtin/subagents/src/shared/types.ts +4 -2
  35. package/dist/builtin/subagents/src/shared/utils.ts +3 -61
  36. package/dist/builtin/subagents/src/tui/render.ts +303 -157
  37. package/dist/builtin/web-access/package.json +1 -1
  38. package/dist/builtin/workflows/CHANGELOG.md +95 -35
  39. package/dist/builtin/workflows/README.md +228 -41
  40. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +535 -541
  41. package/dist/builtin/workflows/builtin/goal.ts +39 -25
  42. package/dist/builtin/workflows/builtin/open-claude-design.ts +66 -69
  43. package/dist/builtin/workflows/builtin/ralph.ts +21 -21
  44. package/dist/builtin/workflows/package.json +6 -5
  45. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
  46. package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +2 -2
  47. package/dist/builtin/workflows/src/extension/discovery.ts +25 -146
  48. package/dist/builtin/workflows/src/extension/dispatcher.ts +72 -24
  49. package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +363 -0
  50. package/dist/builtin/workflows/src/extension/index.ts +690 -352
  51. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +99 -62
  52. package/dist/builtin/workflows/src/extension/render-call.ts +2 -1
  53. package/dist/builtin/workflows/src/extension/render-result.ts +9 -3
  54. package/dist/builtin/workflows/src/extension/renderers.ts +5 -3
  55. package/dist/builtin/workflows/src/extension/runtime.ts +68 -33
  56. package/dist/builtin/workflows/src/extension/status-writer.ts +1 -1
  57. package/dist/builtin/workflows/src/extension/wiring.ts +34 -13
  58. package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +142 -0
  59. package/dist/builtin/workflows/src/extension/workflow-schema.ts +4 -4
  60. package/dist/builtin/workflows/src/index.ts +2 -0
  61. package/dist/builtin/workflows/src/intercom/result-intercom.ts +1 -1
  62. package/dist/builtin/workflows/src/runs/background/runner.ts +6 -4
  63. package/dist/builtin/workflows/src/runs/background/status.ts +45 -21
  64. package/dist/builtin/workflows/src/runs/foreground/executor.ts +624 -52
  65. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +1 -1
  66. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +80 -24
  67. package/dist/builtin/workflows/src/runs/shared/validate-inputs.ts +61 -24
  68. package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +32 -10
  69. package/dist/builtin/workflows/src/sdk-surface.ts +6 -0
  70. package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +178 -0
  71. package/dist/builtin/workflows/src/shared/persistence-restore.ts +92 -12
  72. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +21 -3
  73. package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +1 -2
  74. package/dist/builtin/workflows/src/shared/run-visibility.ts +9 -0
  75. package/dist/builtin/workflows/src/shared/schema-introspection.ts +121 -0
  76. package/dist/builtin/workflows/src/shared/serializable.ts +132 -0
  77. package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +91 -9
  78. package/dist/builtin/workflows/src/shared/store-types.ts +31 -3
  79. package/dist/builtin/workflows/src/shared/store.ts +58 -14
  80. package/dist/builtin/workflows/src/shared/types.ts +105 -40
  81. package/dist/builtin/workflows/src/tui/chat-surface-message.ts +129 -13
  82. package/dist/builtin/workflows/src/tui/chat-surface.ts +6 -1
  83. package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +3 -2
  84. package/dist/builtin/workflows/src/tui/graph-canvas.ts +1 -1
  85. package/dist/builtin/workflows/src/tui/graph-view.ts +91 -65
  86. package/dist/builtin/workflows/src/tui/inline-form-card.ts +1 -1
  87. package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +3 -2
  88. package/dist/builtin/workflows/src/tui/inputs-overlay.ts +3 -2
  89. package/dist/builtin/workflows/src/tui/inputs-picker.ts +8 -7
  90. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
  91. package/dist/builtin/workflows/src/tui/node-card.ts +34 -8
  92. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -11
  93. package/dist/builtin/workflows/src/tui/prompt-card.ts +98 -50
  94. package/dist/builtin/workflows/src/tui/session-list.ts +7 -2
  95. package/dist/builtin/workflows/src/tui/session-picker.ts +2 -0
  96. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +226 -55
  97. package/dist/builtin/workflows/src/tui/status-helpers.ts +2 -0
  98. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +37 -158
  99. package/dist/builtin/workflows/src/tui/toast.ts +2 -2
  100. package/dist/builtin/workflows/src/tui/widget.ts +53 -12
  101. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +270 -19
  102. package/dist/builtin/workflows/src/tui/workflow-notice-card.ts +184 -0
  103. package/dist/builtin/workflows/src/workflows/define-workflow.ts +138 -43
  104. package/dist/config.d.ts +9 -0
  105. package/dist/config.d.ts.map +1 -1
  106. package/dist/config.js +45 -0
  107. package/dist/config.js.map +1 -1
  108. package/dist/core/agent-session.d.ts +27 -9
  109. package/dist/core/agent-session.d.ts.map +1 -1
  110. package/dist/core/agent-session.js +196 -17
  111. package/dist/core/agent-session.js.map +1 -1
  112. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  113. package/dist/core/atomic-guide-command.js +2 -2
  114. package/dist/core/atomic-guide-command.js.map +1 -1
  115. package/dist/core/codex-fast-mode.d.ts +36 -0
  116. package/dist/core/codex-fast-mode.d.ts.map +1 -0
  117. package/dist/core/codex-fast-mode.js +117 -0
  118. package/dist/core/codex-fast-mode.js.map +1 -0
  119. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  120. package/dist/core/compaction/branch-summarization.js +1 -1
  121. package/dist/core/compaction/branch-summarization.js.map +1 -1
  122. package/dist/core/compaction/compaction.d.ts.map +1 -1
  123. package/dist/core/compaction/compaction.js +1 -1
  124. package/dist/core/compaction/compaction.js.map +1 -1
  125. package/dist/core/extensions/index.d.ts +4 -1
  126. package/dist/core/extensions/index.d.ts.map +1 -1
  127. package/dist/core/extensions/index.js +1 -0
  128. package/dist/core/extensions/index.js.map +1 -1
  129. package/dist/core/extensions/loader.d.ts +7 -2
  130. package/dist/core/extensions/loader.d.ts.map +1 -1
  131. package/dist/core/extensions/loader.js +23 -8
  132. package/dist/core/extensions/loader.js.map +1 -1
  133. package/dist/core/extensions/reactive-widget.d.ts +58 -0
  134. package/dist/core/extensions/reactive-widget.d.ts.map +1 -0
  135. package/dist/core/extensions/reactive-widget.js +182 -0
  136. package/dist/core/extensions/reactive-widget.js.map +1 -0
  137. package/dist/core/extensions/runner.d.ts.map +1 -1
  138. package/dist/core/extensions/runner.js +1 -0
  139. package/dist/core/extensions/runner.js.map +1 -1
  140. package/dist/core/extensions/types.d.ts +26 -12
  141. package/dist/core/extensions/types.d.ts.map +1 -1
  142. package/dist/core/extensions/types.js.map +1 -1
  143. package/dist/core/messages.d.ts +1 -1
  144. package/dist/core/messages.d.ts.map +1 -1
  145. package/dist/core/messages.js +8 -2
  146. package/dist/core/messages.js.map +1 -1
  147. package/dist/core/model-registry.d.ts +4 -0
  148. package/dist/core/model-registry.d.ts.map +1 -1
  149. package/dist/core/model-registry.js +11 -0
  150. package/dist/core/model-registry.js.map +1 -1
  151. package/dist/core/resource-loader.d.ts +9 -1
  152. package/dist/core/resource-loader.d.ts.map +1 -1
  153. package/dist/core/resource-loader.js +49 -21
  154. package/dist/core/resource-loader.js.map +1 -1
  155. package/dist/core/sdk.d.ts.map +1 -1
  156. package/dist/core/sdk.js +22 -13
  157. package/dist/core/sdk.js.map +1 -1
  158. package/dist/core/session-manager.d.ts +7 -5
  159. package/dist/core/session-manager.d.ts.map +1 -1
  160. package/dist/core/session-manager.js +5 -3
  161. package/dist/core/session-manager.js.map +1 -1
  162. package/dist/core/settings-manager.d.ts +16 -0
  163. package/dist/core/settings-manager.d.ts.map +1 -1
  164. package/dist/core/settings-manager.js +64 -5
  165. package/dist/core/settings-manager.js.map +1 -1
  166. package/dist/core/slash-commands.d.ts.map +1 -1
  167. package/dist/core/slash-commands.js +1 -0
  168. package/dist/core/slash-commands.js.map +1 -1
  169. package/dist/core/system-prompt.d.ts.map +1 -1
  170. package/dist/core/system-prompt.js +7 -4
  171. package/dist/core/system-prompt.js.map +1 -1
  172. package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
  173. package/dist/core/tools/ask-user-question/ask-user-question.js +2 -2
  174. package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
  175. package/dist/index.d.ts +4 -3
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +3 -2
  178. package/dist/index.js.map +1 -1
  179. package/dist/main.d.ts +3 -0
  180. package/dist/main.d.ts.map +1 -1
  181. package/dist/main.js +12 -0
  182. package/dist/main.js.map +1 -1
  183. package/dist/modes/interactive/chat-input-actions.d.ts.map +1 -1
  184. package/dist/modes/interactive/chat-input-actions.js.map +1 -1
  185. package/dist/modes/interactive/components/diff.d.ts.map +1 -1
  186. package/dist/modes/interactive/components/diff.js +0 -1
  187. package/dist/modes/interactive/components/diff.js.map +1 -1
  188. package/dist/modes/interactive/components/fast-mode-selector.d.ts +27 -0
  189. package/dist/modes/interactive/components/fast-mode-selector.d.ts.map +1 -0
  190. package/dist/modes/interactive/components/fast-mode-selector.js +105 -0
  191. package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -0
  192. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  193. package/dist/modes/interactive/components/footer.js +7 -12
  194. package/dist/modes/interactive/components/footer.js.map +1 -1
  195. package/dist/modes/interactive/components/index.d.ts +1 -0
  196. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  197. package/dist/modes/interactive/components/index.js +1 -0
  198. package/dist/modes/interactive/components/index.js.map +1 -1
  199. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  200. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  201. package/dist/modes/interactive/interactive-mode.js +132 -30
  202. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  203. package/dist/modes/print-mode.d.ts.map +1 -1
  204. package/dist/modes/print-mode.js +53 -6
  205. package/dist/modes/print-mode.js.map +1 -1
  206. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  207. package/dist/modes/rpc/rpc-mode.js +3 -0
  208. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  209. package/docs/compaction.md +1 -1
  210. package/docs/custom-provider.md +2 -2
  211. package/docs/development.md +2 -2
  212. package/docs/docs.json +2 -2
  213. package/docs/extensions.md +18 -13
  214. package/docs/providers.md +5 -1
  215. package/docs/quickstart.md +5 -3
  216. package/docs/rpc.md +5 -5
  217. package/docs/sdk.md +12 -12
  218. package/docs/settings.md +18 -0
  219. package/docs/themes.md +6 -6
  220. package/docs/tui.md +20 -18
  221. package/docs/usage.md +2 -0
  222. package/docs/workflows.md +403 -39
  223. package/examples/extensions/qna.ts +2 -2
  224. package/package.json +4 -4
  225. package/dist/builtin/subagents/skills/playwright-cli/SKILL.md +0 -392
  226. package/dist/builtin/subagents/skills/playwright-cli/references/element-attributes.md +0 -23
  227. package/dist/builtin/subagents/skills/playwright-cli/references/playwright-tests.md +0 -39
  228. package/dist/builtin/subagents/skills/playwright-cli/references/request-mocking.md +0 -87
  229. package/dist/builtin/subagents/skills/playwright-cli/references/running-code.md +0 -241
  230. package/dist/builtin/subagents/skills/playwright-cli/references/session-management.md +0 -225
  231. package/dist/builtin/subagents/skills/playwright-cli/references/spec-driven-testing.md +0 -305
  232. package/dist/builtin/subagents/skills/playwright-cli/references/storage-state.md +0 -275
  233. package/dist/builtin/subagents/skills/playwright-cli/references/test-generation.md +0 -134
  234. package/dist/builtin/subagents/skills/playwright-cli/references/tracing.md +0 -139
  235. package/dist/builtin/subagents/skills/playwright-cli/references/video-recording.md +0 -143
@@ -40,6 +40,15 @@
40
40
  * live Atomic widgets without reintroducing a high-frequency spinner.
41
41
  */
42
42
 
43
+ import {
44
+ decideReactiveWidgetAction,
45
+ installReactiveWidget,
46
+ type ReactiveWidgetAction,
47
+ type ReactiveWidgetFactory,
48
+ type ReactiveWidgetRenderState,
49
+ type ReactiveWidgetTimerApi,
50
+ type ReactiveWidgetTimerHandle,
51
+ } from "@bastani/atomic";
43
52
  import type { Store } from "../shared/store.js";
44
53
  import type { StoreSnapshot } from "../shared/store-types.js";
45
54
  import { buildThemedWidgetLines, nextWidgetRefreshDelayMs } from "./widget.js";
@@ -49,15 +58,9 @@ export interface PiTheme {
49
58
  bold(text: string): string;
50
59
  }
51
60
 
52
- export type WidgetFactory = (
53
- tui: unknown,
54
- theme: PiTheme | unknown,
55
- ) => Component & { dispose?(): void };
56
-
57
- export interface Component {
58
- render(width: number): string[];
59
- dispose?(): void;
60
- }
61
+ export type WidgetFactory = ReactiveWidgetFactory<unknown>;
62
+ export type WidgetAction = ReactiveWidgetAction;
63
+ export type WidgetRenderState = ReactiveWidgetRenderState;
61
64
 
62
65
  interface UiSlice {
63
66
  setWidget?: (
@@ -68,14 +71,8 @@ interface UiSlice {
68
71
  requestRender?: () => void;
69
72
  }
70
73
 
71
- interface TimerHandle {
72
- unref?: () => void;
73
- }
74
-
75
- interface TimerApi {
76
- setTimeout(handler: () => void, delayMs: number): TimerHandle;
77
- clearTimeout(handle: TimerHandle): void;
78
- }
74
+ interface TimerApi extends ReactiveWidgetTimerApi {}
75
+ interface TimerHandle extends ReactiveWidgetTimerHandle {}
79
76
 
80
77
  const defaultTimerApi: TimerApi = {
81
78
  setTimeout: (handler, delayMs) => setTimeout(handler, delayMs) as TimerHandle,
@@ -97,59 +94,11 @@ function isStale(err: unknown): boolean {
97
94
  return err instanceof Error && err.message.includes(STALE_CONTEXT);
98
95
  }
99
96
 
100
- /** The four ways a refresh can reach (or skip) the terminal. */
101
- export type WidgetAction = "mount" | "unmount" | "update" | "none";
102
-
103
- export interface WidgetRenderState {
104
- /** Whether the widget is currently mounted (factory installed via setWidget). */
105
- readonly mounted: boolean;
106
- /** Plain preview lines last rendered while mounted (empty when not mounted). */
107
- readonly lines: readonly string[];
108
- }
109
-
110
- function linesEqual(a: readonly string[], b: readonly string[]): boolean {
111
- if (a.length !== b.length) return false;
112
- for (let i = 0; i < a.length; i++) {
113
- if (a[i] !== b[i]) return false;
114
- }
115
- return true;
116
- }
117
-
118
- /**
119
- * Pure decision: given the previous mount state + previously rendered lines
120
- * and the next preview lines, decide how a refresh should reach the terminal.
121
- * - hidden → visible => "mount" (setWidget(factory))
122
- * - visible → hidden => "unmount" (setWidget(undefined))
123
- * - visible → visible, changed => "update" (requestRender only — no remount)
124
- * - visible → visible, identical => "none" (skip the redundant repaint)
125
- * - hidden → hidden => "none"
126
- *
127
- * No UI/terminal dependency, so it is unit-testable without a real terminal.
128
- */
129
97
  export function decideWidgetAction(
130
98
  prev: WidgetRenderState,
131
99
  nextLines: readonly string[],
132
100
  ): WidgetAction {
133
- const nextVisible = nextLines.length > 0;
134
- if (!prev.mounted) return nextVisible ? "mount" : "none";
135
- if (!nextVisible) return "unmount";
136
- return linesEqual(prev.lines, nextLines) ? "none" : "update";
137
- }
138
-
139
- /**
140
- * Build the long-lived widget factory closure. Pi invokes the factory once
141
- * with `(this.ui, theme)` at mount; the returned component is reused across
142
- * renders. It closes over a *live* snapshot getter rather than a captured
143
- * snapshot, so each `requestRender()` recomputes lines from the latest
144
- * snapshot (and a fresh `Date.now()`) with no dispose/remount.
145
- */
146
- function widgetFactory(getSnap: () => StoreSnapshot): WidgetFactory {
147
- return (_tui, theme) => {
148
- return {
149
- render: (width: number) =>
150
- buildThemedWidgetLines(getSnap(), theme as PiTheme | undefined, width),
151
- };
152
- };
101
+ return decideReactiveWidgetAction(prev, nextLines);
153
102
  }
154
103
 
155
104
  export function installStoreWidget(
@@ -160,96 +109,25 @@ export function installStoreWidget(
160
109
  const ui = pi.ui;
161
110
  if (!ui?.setWidget) return () => {};
162
111
 
163
- let disposed = false;
164
- let refreshTimer: TimerHandle | undefined;
165
- // In-place widget state: whether the long-lived component is mounted, the
166
- // latest snapshot it should render, and the last plain preview lines used
167
- // to detect no-op refreshes.
168
- let mounted = false;
169
- let currentSnap: StoreSnapshot;
170
- let lastLines: readonly string[] = [];
171
-
172
- const clearRefreshTimer = (): void => {
173
- if (refreshTimer === undefined) return;
174
- timers.clearTimeout(refreshTimer);
175
- refreshTimer = undefined;
176
- };
177
-
178
- const scheduleRefresh = (snap: StoreSnapshot): void => {
179
- const delayMs = nextWidgetRefreshDelayMs(snap);
180
- if (delayMs === undefined) return;
181
- refreshTimer = timers.setTimeout(() => {
182
- refreshTimer = undefined;
183
- rerender();
184
- }, delayMs);
185
- refreshTimer.unref?.();
186
- };
187
-
188
- const rerender = (): void => {
189
- if (disposed) return;
190
- clearRefreshTimer();
191
- try {
192
- currentSnap = storeInstance.snapshot();
193
- const nextLines = buildThemedWidgetLines(currentSnap, undefined);
194
- const action = decideWidgetAction({ mounted, lines: lastLines }, nextLines);
195
-
196
- switch (action) {
197
- case "mount":
198
- // Mount the single long-lived component. It reads `currentSnap`
199
- // through the getter on every subsequent render, so we never need
200
- // to re-issue setWidget to refresh content.
201
- ui.setWidget?.(WIDGET_KEY, widgetFactory(() => currentSnap), {
202
- // belowEditor (not aboveEditor): the widget renders a live elapsed
203
- // clock that re-renders every second. pi-tui's differential
204
- // renderer does a full screen+scrollback clear whenever a *changed*
205
- // line sits ABOVE the viewport fold (tui.js `firstChanged <
206
- // prevViewportTop -> fullRender(true)`). An aboveEditor widget is
207
- // pushed above the fold when the bottom region (usage meter,
208
- // multi-line editor, below-editor widgets, footer) grows tall,
209
- // so each clock tick then repainted the whole screen — the resize
210
- // flicker in #1109. belowEditor keeps the widget among the last
211
- // rendered lines (always within the bottom viewport), so its
212
- // per-second tick is a clean in-place differential redraw.
213
- placement: "belowEditor",
214
- });
215
- ui.requestRender?.();
216
- mounted = true;
217
- break;
218
- case "unmount":
219
- ui.setWidget?.(WIDGET_KEY, undefined);
220
- ui.requestRender?.();
221
- mounted = false;
222
- break;
223
- case "update":
224
- // In place — NO setWidget, NO dispose/remount (the #1109 flicker fix).
225
- ui.requestRender?.();
226
- break;
227
- case "none":
228
- break;
229
- }
230
-
231
- lastLines = nextLines;
232
- scheduleRefresh(currentSnap);
233
- } catch (err) {
234
- if (isStale(err)) return;
235
- throw err;
236
- }
237
- };
238
-
239
- const unsubscribe = storeInstance.subscribe(() => rerender());
240
- rerender();
241
-
242
- return () => {
243
- disposed = true;
244
- clearRefreshTimer();
245
- unsubscribe();
246
- try {
247
- ui.setWidget?.(WIDGET_KEY, undefined);
248
- ui.requestRender?.();
249
- } catch (err) {
250
- if (!isStale(err)) throw err;
251
- }
252
- };
112
+ const requestRender = ui.requestRender;
113
+ const controller = installReactiveWidget<StoreSnapshot, unknown>({
114
+ ui: {
115
+ setWidget: (key, factory, opts) => ui.setWidget?.(key, factory, opts),
116
+ ...(requestRender ? { requestRender: () => requestRender.call(ui) } : {}),
117
+ },
118
+ key: WIDGET_KEY,
119
+ placement: "belowEditor",
120
+ timers,
121
+ getSnapshot: () => storeInstance.snapshot(),
122
+ subscribe: (listener) => storeInstance.subscribe(() => listener()),
123
+ getPreviewLines: (snap, now) => buildThemedWidgetLines(snap, undefined, 120, now),
124
+ render: (snap, { theme, width, now }) =>
125
+ buildThemedWidgetLines(snap, theme as PiTheme | undefined, width, now),
126
+ getNextRefreshDelayMs: (snap, now) => nextWidgetRefreshDelayMs(snap, now),
127
+ isStaleError: isStale,
128
+ });
129
+
130
+ return () => controller.dispose();
253
131
  }
254
132
 
255
133
  interface ToolExecutionStartPayload {
@@ -372,7 +250,8 @@ export function installToolExecutionHooks(pi: LiveWidgetAPI, storeInstance: Stor
372
250
  function recordToolEnd(payload: unknown): void {
373
251
  if (!isToolExecutionPayload(payload)) return;
374
252
 
375
- const ids = findActiveAskCall(payload) ?? resolveIds(payload, true);
253
+ const activeAskCall = findActiveAskCall(payload);
254
+ const ids = activeAskCall ?? resolveIds(payload, false);
376
255
  if (!ids) return;
377
256
 
378
257
  storeInstance.recordToolEnd(ids.runId, ids.stageId, {
@@ -382,7 +261,7 @@ export function installToolExecutionHooks(pi: LiveWidgetAPI, storeInstance: Stor
382
261
  endedAt: payload.endedAt ?? payload.ended_at ?? Date.now(),
383
262
  output: payload.output,
384
263
  });
385
- recordAskUserQuestionEnd(payload, ids);
264
+ recordAskUserQuestionEnd(payload, activeAskCall ?? ids);
386
265
  }
387
266
 
388
267
  const safeStart = safelyHandle(recordToolStart);
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Visual: solid status-coloured pill (`success`/`warning`/`info`/`error`)
5
5
  * with `backgroundElement` foreground and bold weight — same vocabulary as
6
- * the header pill (DESIGN.md §5 Mode Pills). Icon glyphs from the canonical
7
- * Unicode set (`✓ ✗ ⚠ ℹ`).
6
+ * the header pill (DESIGN.md §5 Mode Pills). Icon glyphs use the same
7
+ * cross-platform text symbols as the rest of the workflow UI (`✓ ✗ ⚠ ℹ`).
8
8
  */
9
9
  import type { GraphTheme } from "./graph-theme.js";
10
10
  import { hexBg, hexToAnsi, RESET, BOLD } from "./color-utils.js";
@@ -27,12 +27,14 @@ import type {
27
27
  RunSnapshot,
28
28
  } from "../shared/store-types.js";
29
29
  import { elapsedRunMs } from "../shared/timing.js";
30
+ import { topLevelWorkflowRuns } from "../shared/run-visibility.js";
30
31
  import type { PiTheme } from "./store-widget-installer.js";
31
32
  import { renderRoundedBoxLines } from "./chat-surface.js";
32
33
  import type { FlatBandBadge } from "./chat-surface.js";
33
34
  import { deriveGraphTheme } from "./graph-theme.js";
34
35
  import type { GraphTheme } from "./graph-theme.js";
35
36
  import { hexToAnsi, RESET, BOLD } from "./color-utils.js";
37
+ import { statusIcon } from "./status-helpers.js";
36
38
 
37
39
  // ---------------------------------------------------------------------------
38
40
  // Tunables
@@ -81,17 +83,36 @@ interface RunCounts {
81
83
  awaiting: number;
82
84
  }
83
85
 
84
- function countRuns(runs: readonly RunSnapshot[]): RunCounts {
86
+ function runAwaitsInput(run: RunSnapshot): boolean {
87
+ return (
88
+ run.endedAt === undefined &&
89
+ (run.pendingPrompt !== undefined || run.stages.some((s) => s.status === "awaiting_input"))
90
+ );
91
+ }
92
+
93
+ /**
94
+ * A top-level run "needs attention" when it OR any of its nested
95
+ * `ctx.workflow()` descendants is awaiting human input. Nested child runs are
96
+ * hidden from the widget, but each carries `rootRunId` pointing at the ultimate
97
+ * top-level run, so a hidden child's awaiting (HiL) state surfaces on the
98
+ * visible ancestor instead of vanishing with it.
99
+ */
100
+ function subtreeAwaitsInput(root: RunSnapshot, allRuns: readonly RunSnapshot[]): boolean {
101
+ if (runAwaitsInput(root)) return true;
102
+ return allRuns.some((run) => run.rootRunId === root.id && runAwaitsInput(run));
103
+ }
104
+
105
+ function countRuns(
106
+ runs: readonly RunSnapshot[],
107
+ allRuns: readonly RunSnapshot[] = runs,
108
+ ): RunCounts {
85
109
  const counts: RunCounts = { active: 0, paused: 0, done: 0, failed: 0, awaiting: 0 };
86
110
  for (const r of runs) {
87
111
  if (r.endedAt === undefined && r.status === "paused") counts.paused++;
88
112
  else if (r.endedAt === undefined) counts.active++;
89
113
  else if (r.status === "completed") counts.done++;
90
114
  else if (r.status === "failed" || r.status === "killed") counts.failed++;
91
- if (
92
- r.endedAt === undefined &&
93
- (r.pendingPrompt !== undefined || r.stages.some((s) => s.status === "awaiting_input"))
94
- ) {
115
+ if (r.endedAt === undefined && subtreeAwaitsInput(r, allRuns)) {
95
116
  counts.awaiting++;
96
117
  }
97
118
  }
@@ -120,7 +141,12 @@ export function nextWidgetRefreshDelayMs(
120
141
  }
121
142
 
122
143
  function selectDisplayRuns(snap: StoreSnapshot, now: number): RunSnapshot[] {
123
- const all = snap.runs as readonly RunSnapshot[];
144
+ // Only surface top-level workflows. Nested `ctx.workflow()` child runs carry
145
+ // a `parentRunId`, and they are already represented inline as flattened
146
+ // stages of their parent's graph, so listing them here would show the same
147
+ // composition three times (root + parent + child). Matches the visibility
148
+ // rule `statusRuns`/the `status` action already apply.
149
+ const all = topLevelWorkflowRuns(snap.runs);
124
150
  const active = all.filter((r) => isActive(r));
125
151
  const recent = all.filter((r) => recentlyEnded(r, now));
126
152
  // Most recently started first within each bucket; active runs precede recent.
@@ -223,10 +249,10 @@ function countBadges(counts: RunCounts, theme: GraphTheme): FlatBandBadge[] {
223
249
  badges.push({ text: `❚❚ ${counts.paused} paused`, fg: theme.warning });
224
250
  }
225
251
  // Awaiting input is shown in Sky per DESIGN.md status semantics: a live
226
- // human-in-the-loop request that requires attention. Spell it out in the
227
- // widget title instead of relying on the glyph alone.
252
+ // human-in-the-loop request that requires attention. Mirror the graph node's
253
+ // question-mark status glyph, then keep as the attach/respond action hint.
228
254
  if (counts.awaiting > 0) {
229
- badges.push({ text: `↵ ${counts.awaiting} needs attention (attach to workflow with \`/workflow connect\`)`, fg: theme.info });
255
+ badges.push({ text: `${statusIcon("awaiting_input")} ${counts.awaiting} needs attention (attach to workflow with \`/workflow connect\`)`, fg: theme.info });
230
256
  }
231
257
  if (counts.done > 0) {
232
258
  badges.push({ text: `✓ ${counts.done} complete`, fg: theme.success });
@@ -237,6 +263,20 @@ function countBadges(counts: RunCounts, theme: GraphTheme): FlatBandBadge[] {
237
263
  return badges;
238
264
  }
239
265
 
266
+ function formatTitleBadges(
267
+ badges: readonly FlatBandBadge[],
268
+ theme: GraphTheme,
269
+ themed: boolean,
270
+ ): string {
271
+ if (badges.length === 0) return "";
272
+ if (!themed) return badges.map((b) => b.text).join(" ");
273
+
274
+ const fallbackFg = hexToAnsi(theme.border);
275
+ return badges
276
+ .map((b) => `${b.fg ? hexToAnsi(b.fg) : fallbackFg}${b.text}${RESET}${fallbackFg}`)
277
+ .join(" ");
278
+ }
279
+
240
280
  // ---------------------------------------------------------------------------
241
281
  // Themed rendering (ANSI + Catppuccin)
242
282
  // ---------------------------------------------------------------------------
@@ -313,12 +353,12 @@ export function buildThemedWidgetLines(
313
353
  snap: StoreSnapshot,
314
354
  piTheme: PiTheme | undefined,
315
355
  width = 120,
356
+ now = Date.now(),
316
357
  ): string[] {
317
- const now = Date.now();
318
358
  const display = selectDisplayRuns(snap, now);
319
359
  if (display.length === 0) return [];
320
360
 
321
- const counts = countRuns(snap.runs as readonly RunSnapshot[]);
361
+ const counts = countRuns(topLevelWorkflowRuns(snap.runs), snap.runs);
322
362
  // Active + recently-ended dominate the badge counts so a finished run
323
363
  // visually persists for a beat before dropping off.
324
364
  const visibleCounts: RunCounts = {
@@ -340,7 +380,8 @@ export function buildThemedWidgetLines(
340
380
  const total = counts.active + counts.paused + counts.done + counts.failed;
341
381
  const subtitle = `${total} run${total === 1 ? "" : "s"}`;
342
382
 
343
- const badges = countBadges(visibleCounts, graphTheme).map((b) => b.text).join(" ");
383
+ const badgeList = countBadges(visibleCounts, graphTheme);
384
+ const badges = formatTitleBadges(badgeList, graphTheme, themed);
344
385
  const title = `BACKGROUND ${subtitle}${badges ? ` ${badges}` : ""}`;
345
386
  const body: string[] = [];
346
387