@bastani/atomic 0.9.3-alpha.1 → 0.9.3-alpha.2

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 (172) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +15 -0
  3. package/dist/builtin/cursor/README.md +2 -1
  4. package/dist/builtin/cursor/package.json +2 -2
  5. package/dist/builtin/cursor/src/cursor-models-raw.json +2 -9
  6. package/dist/builtin/cursor/src/model-mapper.ts +14 -3
  7. package/dist/builtin/cursor/src/proto/protobuf-codec-base64.ts +22 -0
  8. package/dist/builtin/cursor/src/proto/protobuf-codec-request.ts +53 -13
  9. package/dist/builtin/cursor/src/proto/protobuf-codec-wire.ts +24 -7
  10. package/dist/builtin/cursor/src/proto/protobuf-codec.ts +3 -2
  11. package/dist/builtin/cursor/src/stream.ts +5 -11
  12. package/dist/builtin/cursor/src/transport-types.ts +3 -0
  13. package/dist/builtin/cursor/src/transport.ts +1 -0
  14. package/dist/builtin/intercom/package.json +1 -1
  15. package/dist/builtin/mcp/package.json +1 -1
  16. package/dist/builtin/subagents/CHANGELOG.md +9 -0
  17. package/dist/builtin/subagents/package.json +1 -1
  18. package/dist/builtin/subagents/src/extension/fanout-child.ts +1 -0
  19. package/dist/builtin/subagents/src/extension/index.ts +6 -3
  20. package/dist/builtin/subagents/src/extension/schemas.ts +0 -5
  21. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +1 -4
  22. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +15 -1
  23. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +35 -1
  24. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  25. package/dist/builtin/subagents/src/shared/types-async.ts +1 -0
  26. package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +27 -5
  27. package/dist/builtin/subagents/src/tui/render-layout.ts +27 -4
  28. package/dist/builtin/subagents/src/tui/render-result-animation.ts +22 -31
  29. package/dist/builtin/subagents/src/tui/render-result-compact.ts +6 -6
  30. package/dist/builtin/subagents/src/tui/render-result.ts +20 -19
  31. package/dist/builtin/subagents/src/tui/render-status-progress.ts +3 -3
  32. package/dist/builtin/subagents/src/tui/render-widget.ts +46 -7
  33. package/dist/builtin/subagents/src/tui/render.ts +2 -2
  34. package/dist/builtin/web-access/package.json +1 -1
  35. package/dist/builtin/workflows/CHANGELOG.md +43 -0
  36. package/dist/builtin/workflows/README.md +1 -1
  37. package/dist/builtin/workflows/package.json +1 -1
  38. package/dist/builtin/workflows/src/authoring.d.ts +1 -1
  39. package/dist/builtin/workflows/src/durable/backend.ts +343 -0
  40. package/dist/builtin/workflows/src/durable/child-primitive.ts +79 -0
  41. package/dist/builtin/workflows/src/durable/dbos-backend.ts +421 -0
  42. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +171 -0
  43. package/dist/builtin/workflows/src/durable/factory.ts +96 -0
  44. package/dist/builtin/workflows/src/durable/file-backend.ts +433 -0
  45. package/dist/builtin/workflows/src/durable/index.ts +73 -0
  46. package/dist/builtin/workflows/src/durable/resume-catalog.ts +217 -0
  47. package/dist/builtin/workflows/src/durable/resume-runtime.ts +299 -0
  48. package/dist/builtin/workflows/src/durable/scoped-backend.ts +171 -0
  49. package/dist/builtin/workflows/src/durable/stage-primitive.ts +284 -0
  50. package/dist/builtin/workflows/src/durable/tool-primitive.ts +180 -0
  51. package/dist/builtin/workflows/src/durable/types.ts +168 -0
  52. package/dist/builtin/workflows/src/durable/ui-primitive.ts +96 -0
  53. package/dist/builtin/workflows/src/engine/options.ts +3 -0
  54. package/dist/builtin/workflows/src/engine/primitives/parallel.ts +2 -2
  55. package/dist/builtin/workflows/src/engine/primitives/task.ts +4 -4
  56. package/dist/builtin/workflows/src/engine/primitives/ui.ts +22 -8
  57. package/dist/builtin/workflows/src/engine/primitives/workflow.ts +8 -0
  58. package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +69 -0
  59. package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +31 -0
  60. package/dist/builtin/workflows/src/engine/run.ts +148 -6
  61. package/dist/builtin/workflows/src/engine/runtime.ts +8 -2
  62. package/dist/builtin/workflows/src/extension/extension-factory.ts +6 -12
  63. package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +5 -1
  64. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +3 -0
  65. package/dist/builtin/workflows/src/extension/runtime.ts +48 -9
  66. package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +143 -4
  67. package/dist/builtin/workflows/src/runs/background/quit.ts +61 -0
  68. package/dist/builtin/workflows/src/runs/background/status.ts +1 -0
  69. package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +5 -5
  70. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +74 -33
  71. package/dist/builtin/workflows/src/runs/foreground/executor-stage-context.ts +20 -1
  72. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +8 -7
  73. package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
  74. package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +1 -1
  75. package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +19 -2
  76. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +4 -0
  77. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -10
  78. package/dist/builtin/workflows/src/runs/foreground/stage-runner-options.ts +5 -1
  79. package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +25 -0
  80. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +3 -0
  81. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +16 -0
  82. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +20 -0
  83. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +23 -1
  84. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +30 -1
  85. package/dist/builtin/workflows/src/shared/store-public-types.ts +6 -2
  86. package/dist/builtin/workflows/src/shared/store-run-methods.ts +12 -6
  87. package/dist/builtin/workflows/src/shared/types.ts +55 -0
  88. package/dist/builtin/workflows/src/tui/graph-view-constants.ts +1 -1
  89. package/dist/builtin/workflows/src/tui/graph-view-graph-render.ts +41 -0
  90. package/dist/builtin/workflows/src/tui/graph-view-input.ts +82 -24
  91. package/dist/builtin/workflows/src/tui/graph-view-render.ts +7 -0
  92. package/dist/builtin/workflows/src/tui/graph-view-state.ts +22 -2
  93. package/dist/builtin/workflows/src/tui/graph-view-types.ts +4 -5
  94. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +9 -11
  95. package/dist/builtin/workflows/src/tui/stage-chat-view-footer-status.ts +9 -3
  96. package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +11 -2
  97. package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +35 -0
  98. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +51 -17
  99. package/dist/builtin/workflows/src/tui/stage-chat-view-status.ts +36 -0
  100. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +5 -1
  101. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +3 -1
  102. package/dist/builtin/workflows/src/tui/status-list.ts +14 -2
  103. package/dist/builtin/workflows/src/tui/widget.ts +23 -8
  104. package/dist/builtin/workflows/src/tui/workflow-attach-pane-types.ts +5 -4
  105. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
  106. package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +151 -0
  107. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  108. package/dist/core/extensions/loader-virtual-modules.js +47 -30
  109. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  110. package/dist/core/messages.d.ts +1 -0
  111. package/dist/core/messages.d.ts.map +1 -1
  112. package/dist/core/messages.js +46 -1
  113. package/dist/core/messages.js.map +1 -1
  114. package/dist/core/sdk.d.ts.map +1 -1
  115. package/dist/core/sdk.js +12 -0
  116. package/dist/core/sdk.js.map +1 -1
  117. package/dist/core/session-manager-core.d.ts +15 -7
  118. package/dist/core/session-manager-core.d.ts.map +1 -1
  119. package/dist/core/session-manager-core.js +20 -9
  120. package/dist/core/session-manager-core.js.map +1 -1
  121. package/dist/core/session-manager-entries.d.ts +2 -2
  122. package/dist/core/session-manager-entries.d.ts.map +1 -1
  123. package/dist/core/session-manager-entries.js +9 -3
  124. package/dist/core/session-manager-entries.js.map +1 -1
  125. package/dist/core/session-manager-history.d.ts.map +1 -1
  126. package/dist/core/session-manager-history.js +2 -1
  127. package/dist/core/session-manager-history.js.map +1 -1
  128. package/dist/core/session-manager-list.d.ts +3 -3
  129. package/dist/core/session-manager-list.d.ts.map +1 -1
  130. package/dist/core/session-manager-list.js +27 -8
  131. package/dist/core/session-manager-list.js.map +1 -1
  132. package/dist/core/session-manager-storage.d.ts +3 -1
  133. package/dist/core/session-manager-storage.d.ts.map +1 -1
  134. package/dist/core/session-manager-storage.js +55 -12
  135. package/dist/core/session-manager-storage.js.map +1 -1
  136. package/dist/core/session-manager-tool-dependencies.d.ts +10 -0
  137. package/dist/core/session-manager-tool-dependencies.d.ts.map +1 -0
  138. package/dist/core/session-manager-tool-dependencies.js +133 -0
  139. package/dist/core/session-manager-tool-dependencies.js.map +1 -0
  140. package/dist/core/session-manager-types.d.ts +22 -0
  141. package/dist/core/session-manager-types.d.ts.map +1 -1
  142. package/dist/core/session-manager-types.js.map +1 -1
  143. package/dist/core/session-manager.d.ts +2 -2
  144. package/dist/core/session-manager.d.ts.map +1 -1
  145. package/dist/core/session-manager.js +1 -1
  146. package/dist/core/session-manager.js.map +1 -1
  147. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +1 -0
  148. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
  149. package/dist/modes/interactive/components/chat-session-host-runtime.js +12 -0
  150. package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
  151. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts +4 -0
  152. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts.map +1 -0
  153. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js +131 -0
  154. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js.map +1 -0
  155. package/dist/modes/interactive/components/chat-session-host.d.ts +2 -0
  156. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  157. package/dist/modes/interactive/components/chat-session-host.js +7 -1
  158. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  159. package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
  160. package/dist/modes/interactive/components/chat-transcript.js +15 -4
  161. package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
  162. package/dist/modes/interactive/components/tool-execution.d.ts +3 -0
  163. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  164. package/dist/modes/interactive/components/tool-execution.js +26 -0
  165. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  166. package/docs/compaction.md +2 -0
  167. package/docs/models.md +1 -1
  168. package/docs/providers.md +2 -1
  169. package/docs/session-format.md +6 -0
  170. package/docs/sessions.md +6 -0
  171. package/docs/workflows.md +105 -3
  172. package/package.json +4 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.3-alpha.2] - 2026-06-27
6
+
7
+ ### Fixed
8
+
9
+ - Fixed custom tool renderer disposal to honor renderer-owned cleanup callbacks, preventing stale animation registry entries after terminal workflow tool rows are finalized ([#1518](https://github.com/bastani-inc/atomic/issues/1518)).
10
+ - Hardened session replay and LLM conversion so persisted context-compaction filters cannot leave orphaned `toolResult` messages after deleting their paired assistant `toolCall`, preventing GitHub Copilot Claude/Anthropic replay failures after repeated subagent runs ([#1527](https://github.com/bastani-inc/atomic/issues/1527)).
11
+
5
12
  ## [0.9.3-alpha.1] - 2026-06-25
6
13
 
7
14
  ### Breaking Changes
@@ -13,6 +20,7 @@
13
20
 
14
21
  - Added one-time first-run onboarding that explains Atomic workflows, uses an onboarding editor placeholder, lets users opt into normal chat with `/chat`, preserves other slash commands, saves a pre-login pasted task in memory only, and hands the first ready ticket/spec/task to the normal coding-agent session with `goal`/`ralph` workflow-routing guidance.
15
22
  - Added first-run onboarding routing guidance that raises the parent session to high reasoning when supported, asks the coding agent to first make a text-only scope estimate from tickets/GitHub issues/specs, routes directly when the task is clearly tiny or small with high confidence, and only uses targeted read-only `codebase-locator`/`codebase-analyzer`/`codebase-pattern-finder` probing when referenced context must be read or scope is medium, large, unclear, risky, or not obviously tiny before choosing `goal` or `ralph`.
23
+ - Excluded workflow-created (internal) sessions from the standard `/resume`, `atomic -r`, and `--continue` history by marking their `SessionHeader` with `internal: true` and optional `workflow: { runId, stageId, stageName }` linkage, while keeping them resumable via `/workflow resume`/`workflow({ action: "resume" })` and direct `--session <path>` access; added `includeInternal` opt-ins to `SessionManager.list`/`listAll`/`continueRecent`, a `markSessionInternal` method, robust full-line `readSessionHeader`, and regression tests ([#1504](https://github.com/bastani-inc/atomic/issues/1504)).
16
24
  - Added a first-class `search` built-in and exposed `find`/`search` in normal coding sessions ([#1483](https://github.com/bastani-inc/atomic/issues/1483)).
17
25
  - Added hashline snapshot anchors across `read`, `search`, `write`, and successful `edit` results, plus hashline line-range/block/multi-section edit scripts with stale-tag safety checks and snapshot-based recovery for non-overlapping file drift, empty-replace validation, and fresh post-mutation tags for follow-up edits ([#1483](https://github.com/bastani-inc/atomic/issues/1483)).
18
26
  - Added disabled-by-default `bashInterceptor.enabled` settings support with built-in shell anti-pattern rules, a `/settings` **Bash Interceptor** toggle, and optional `user_bash` extension routing, without changing the default local-execution behavior ([#1483](https://github.com/bastani-inc/atomic/issues/1483)).
@@ -30,6 +38,7 @@
30
38
 
31
39
  ### Fixed
32
40
 
41
+ - Fixed extension loading in Windows package tests by lazy-initializing compiled-binary virtual modules, avoiding eager source-barrel resolution before Jiti aliases are installed.
33
42
  - Fixed the first-run onboarding input placeholder so it uses muted TUI text and still renders a visible cursor while empty, making the startup composer read as an editable field instead of static copy.
34
43
  - Fixed `@` file-reference autocomplete in the first-run onboarding editor before the asynchronous `fd` readiness check completes by falling back to the built-in synchronous path completer while preserving `@` prefixes and quoted paths.
35
44
  - Fixed workflow config/discovery isolation so `ATOMIC_CODING_AGENT_DIR` prevents home-global workflows from shadowing the bundled first-run onboarding `goal` and `ralph` targets.
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.3-alpha.2] - 2026-06-27
6
+
7
+ ### Added
8
+
9
+ - Added scoped Cursor image-input support for known multimodal Claude, Composer, Gemini, GPT, and Kimi model families (`claude-`, `composer-`, `gemini-`, `gpt-`, `kimi-`), plus `cursor/grok-4.3`, including selected-image request serialization and mixed text/image MCP tool-result serialization.
10
+ - Added `cursor/grok-4.3` to the estimated fallback catalog.
11
+
12
+ ### Fixed
13
+
14
+ - Rejected empty or malformed base64 image payloads during Cursor selected-image and MCP image serialization with sanitized local errors, while accepting valid MIME-wrapped base64 with line whitespace.
15
+
16
+ ### Removed
17
+
18
+ - Removed outdated Cursor Grok 4.20 entries from the estimated fallback catalog and no longer advertise Grok-family Cursor IDs as image-capable.
19
+
5
20
  ## [0.9.2] - 2026-06-23
6
21
 
7
22
  ### Changed
@@ -12,7 +12,8 @@ The unavoidable Atomic-specific integration difference is the provider surface:
12
12
 
13
13
  ## Limitations
14
14
 
15
- - Text input only. Vision/image content is rejected.
15
+ - Image input is supported only for known multimodal Cursor Claude, Composer, Gemini, GPT, and Kimi model families (IDs beginning `claude-`, `composer-`, `gemini-`, `gpt-`, or `kimi-`), plus `grok-4.3`; text-only Cursor models still reject images.
16
+ - User images and mixed text/image MCP tool results are serialized for image-capable Cursor models. Image payloads must be non-empty standard base64; MIME-style line wrapping whitespace is accepted and stripped before serialization.
16
17
  - Cursor's private API may change without notice.
17
18
  - HTTP/2 transport requires the bundled `@bastani/atomic-natives` Rust/N-API native client for the current platform.
18
19
  - Credentials are OAuth-only. Do not pass Cursor tokens via command-line args, environment variables, logs, or local proxy processes.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/cursor",
3
- "version": "0.9.3-alpha.1",
3
+ "version": "0.9.3-alpha.2",
4
4
  "private": true,
5
5
  "description": "Experimental first-party Atomic extension for Cursor OAuth, model discovery, and streaming provider registration.",
6
6
  "contributors": [
@@ -40,7 +40,7 @@
40
40
  }
41
41
  },
42
42
  "dependencies": {
43
- "@bastani/atomic-natives": "0.9.3-alpha.1",
43
+ "@bastani/atomic-natives": "0.9.3-alpha.2",
44
44
  "@bufbuild/protobuf": "^2.0.0"
45
45
  }
46
46
  }
@@ -560,15 +560,8 @@
560
560
  "maxTokens": 64000
561
561
  },
562
562
  {
563
- "id": "grok-4-20",
564
- "name": "Grok 4.20",
565
- "reasoning": false,
566
- "contextWindow": 200000,
567
- "maxTokens": 64000
568
- },
569
- {
570
- "id": "grok-4-20-thinking",
571
- "name": "Grok 4.20 Thinking",
563
+ "id": "grok-4.3",
564
+ "name": "Grok 4.3",
572
565
  "reasoning": false,
573
566
  "contextWindow": 200000,
574
567
  "maxTokens": 64000
@@ -22,6 +22,8 @@ export interface CursorModelCatalog {
22
22
  readonly models: readonly CursorUsableModel[];
23
23
  }
24
24
 
25
+ export type CursorModelInput = ["text"] | ["text", "image"];
26
+
25
27
  export interface CursorProviderModelDefinition {
26
28
  readonly id: string;
27
29
  readonly name: string;
@@ -29,7 +31,7 @@ export interface CursorProviderModelDefinition {
29
31
  readonly baseUrl?: string;
30
32
  readonly reasoning: boolean;
31
33
  readonly thinkingLevelMap?: ThinkingLevelMap;
32
- readonly input: ["text"];
34
+ readonly input: CursorModelInput;
33
35
  readonly cost: { readonly input: number; readonly output: number; readonly cacheRead: number; readonly cacheWrite: number };
34
36
  readonly contextWindow: number;
35
37
  readonly maxTokens: number;
@@ -95,7 +97,7 @@ export function mapCursorCatalogToProviderModels(catalog: CursorModelCatalog): C
95
97
  baseUrl: CURSOR_API_BASE_URL,
96
98
  reasoning: supportsReasoning,
97
99
  thinkingLevelMap: supportsEffort ? buildThinkingLevelMap(effortVariants, group.primaryId) : undefined,
98
- input: ["text"],
100
+ input: cursorModelInput(group.primaryId),
99
101
  cost: subscriptionCost(),
100
102
  contextWindow: chooseLargestNumber(group.variants.map((variant) => variant.contextWindow)) ?? ESTIMATED_CONTEXT_WINDOW,
101
103
  maxTokens: chooseLargestNumber(group.variants.map((variant) => variant.maxTokens)) ?? ESTIMATED_MAX_TOKENS,
@@ -250,11 +252,20 @@ function chooseDisplayName(variants: readonly CursorVariant[], baseId: string, p
250
252
  ?? titleCaseModelId(baseId);
251
253
  }
252
254
 
255
+ function cursorModelInput(id: string): CursorModelInput {
256
+ return supportsImageInputModelId(id) ? ["text", "image"] : ["text"];
257
+ }
258
+
259
+ function supportsImageInputModelId(id: string): boolean {
260
+ const variant = parseCursorVariant({ id });
261
+ return variant.baseId === "grok-4.3" || /^(claude|composer|gemini|gpt|kimi)(-|$)/iu.test(variant.baseId);
262
+ }
263
+
253
264
  function supportsReasoningModelId(id: string): boolean {
254
265
  const variant = parseCursorVariant({ id });
255
266
  if (variant.effort || variant.thinking) return true;
256
267
  if (variant.baseId === "default") return true;
257
- return /^(claude|composer|gemini|gpt|grok|kimi)(-|$)/iu.test(variant.baseId);
268
+ return /^(claude|composer|gemini|gpt|kimi)(-|$)/iu.test(variant.baseId);
258
269
  }
259
270
 
260
271
  function titleCaseModelId(id: string): string {
@@ -0,0 +1,22 @@
1
+ export interface CursorImageBase64Context {
2
+ readonly kind: string;
3
+ readonly mimeType: string;
4
+ readonly index?: number;
5
+ }
6
+
7
+ // Cursor image protobuf serialization accepts canonical standard base64 after MIME line wrapping is removed.
8
+ const BASE64_WHITESPACE_PATTERN = /[\t\n\f\r ]+/gu;
9
+ const STANDARD_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;
10
+
11
+ export function decodeStrictBase64ImageData(data: string, context: CursorImageBase64Context): Uint8Array {
12
+ const normalized = data.replace(BASE64_WHITESPACE_PATTERN, "");
13
+ if (normalized.length === 0 || normalized.length % 4 !== 0 || !STANDARD_BASE64_PATTERN.test(normalized)) {
14
+ throwInvalidBase64ImageData(context);
15
+ }
16
+ return Buffer.from(normalized, "base64");
17
+ }
18
+
19
+ function throwInvalidBase64ImageData(context: CursorImageBase64Context): never {
20
+ const index = context.index === undefined ? "" : ` at index ${context.index}`;
21
+ throw new Error(`Invalid ${context.kind} base64 image data${index} for MIME type ${context.mimeType}`);
22
+ }
@@ -1,7 +1,8 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
3
+ import type { ImageContent } from "@earendil-works/pi-ai";
3
4
  import { parseJsonObject, type JsonObject } from "../config.js";
4
- import type { CursorRunRequest } from "../transport.js";
5
+ import type { CursorRunRequest, CursorToolResultContent } from "../transport.js";
5
6
  import {
6
7
  AgentClientMessageSchema,
7
8
  AgentConversationTurnStructureSchema,
@@ -16,14 +17,17 @@ import {
16
17
  McpToolDefinitionSchema,
17
18
  ModelDetailsSchema,
18
19
  SelectedContextSchema,
20
+ SelectedImageSchema,
19
21
  ToolCallSchema,
20
22
  UserMessageActionSchema,
21
23
  UserMessageSchema,
22
24
  type ConversationStateStructure,
23
25
  type McpToolDefinition,
26
+ type SelectedImage,
24
27
  type UserMessage,
25
28
  } from "./agent_pb.js";
26
29
  import { encodeMcpArgsMap, encodeProtobufValue, serializableJsonValue } from "./protobuf-codec-json.js";
30
+ import { decodeStrictBase64ImageData } from "./protobuf-codec-base64.js";
27
31
  import { createMcpToolCallResult } from "./protobuf-codec-wire.js";
28
32
 
29
33
  export interface ParsedAssistantTextStep {
@@ -36,13 +40,14 @@ export interface ParsedToolCallStep {
36
40
  readonly toolCallId: string;
37
41
  readonly toolName: string;
38
42
  readonly arguments: JsonObject;
39
- result?: { readonly content: string; readonly isError: boolean };
43
+ result?: { readonly content: string | readonly CursorToolResultContent[]; readonly isError: boolean; readonly fallbackText?: string };
40
44
  }
41
45
 
42
46
  export type ParsedTurnStep = ParsedAssistantTextStep | ParsedToolCallStep;
43
47
 
44
48
  export interface ParsedTurn {
45
49
  readonly userText: string;
50
+ readonly userImages: readonly ImageContent[];
46
51
  readonly steps: ParsedTurnStep[];
47
52
  }
48
53
 
@@ -69,6 +74,7 @@ export function buildCursorRequest(
69
74
  conversationId: string,
70
75
  checkpoint: Uint8Array | null,
71
76
  existingBlobStore?: Map<string, Uint8Array>,
77
+ userImages: readonly ImageContent[] = [],
72
78
  ): { readonly requestBytes: Uint8Array; readonly blobStore: Map<string, Uint8Array> } {
73
79
  const blobStore = new Map<string, Uint8Array>(existingBlobStore ?? []);
74
80
  const systemBlobId = storeAsBlob(textEncoder.encode(JSON.stringify({ role: "system", content: systemPrompt })), blobStore);
@@ -76,7 +82,7 @@ export function buildCursorRequest(
76
82
  const conversationState = checkpoint
77
83
  ? fromBinary(ConversationStateStructureSchema, checkpoint)
78
84
  : buildConversationState(turns, blobStore, systemBlobId, selectedContextBlob);
79
- const userMessage = createUserMessage(userText, selectedContextBlob);
85
+ const userMessage = createUserMessage(userText, selectedContextBlob, userImages);
80
86
  const action = create(ConversationActionSchema, {
81
87
  action: { case: "userMessageAction", value: create(UserMessageActionSchema, { userMessage }) },
82
88
  });
@@ -96,7 +102,7 @@ function buildConversationState(
96
102
  ): ConversationStateStructure {
97
103
  const turnBlobIds: Uint8Array[] = [];
98
104
  for (const turn of turns) {
99
- const userMessage = createUserMessage(turn.userText, selectedContextBlob);
105
+ const userMessage = createUserMessage(turn.userText, selectedContextBlob, turn.userImages);
100
106
  const userMessageBlobId = storeAsBlob(toBinary(UserMessageSchema, userMessage), blobStore);
101
107
  const stepBlobIds = turn.steps.map((step) => storeAsBlob(buildTurnStepBytes(step), blobStore));
102
108
  const agentTurn = create(AgentConversationTurnStructureSchema, {
@@ -127,18 +133,39 @@ function buildConversationState(
127
133
  });
128
134
  }
129
135
 
130
- function createUserMessage(text: string, selectedContextBlob: Uint8Array): UserMessage {
136
+ function createUserMessage(text: string, selectedContextBlob: Uint8Array, images: readonly ImageContent[] = []): UserMessage {
131
137
  const messageId = randomUUID();
138
+ const selectedImages = images.map((image, index) => createSelectedImage(image, index));
132
139
  return create(UserMessageSchema, {
133
140
  text,
134
141
  messageId,
135
- selectedContext: create(SelectedContextSchema, {}),
142
+ selectedContext: create(SelectedContextSchema, { selectedImages }),
136
143
  mode: 1,
137
144
  selectedContextBlob,
138
145
  correlationId: messageId,
139
146
  });
140
147
  }
141
148
 
149
+ function createSelectedImage(image: ImageContent, index: number): SelectedImage {
150
+ const data = decodeStrictBase64ImageData(image.data, { kind: "selected image", mimeType: image.mimeType, index });
151
+ const hash = createHash("sha256").update(data).digest("hex").slice(0, 16);
152
+ return create(SelectedImageSchema, {
153
+ uuid: randomUUID(),
154
+ path: `/atomic/inline-images/${hash}-${index}.${extensionForMimeType(image.mimeType)}`,
155
+ mimeType: image.mimeType,
156
+ dataOrBlobId: { case: "data", value: data },
157
+ });
158
+ }
159
+
160
+ function extensionForMimeType(mimeType: string): string {
161
+ const normalized = mimeType.toLowerCase();
162
+ if (normalized === "image/jpeg" || normalized === "image/jpg") return "jpg";
163
+ if (normalized === "image/png") return "png";
164
+ if (normalized === "image/gif") return "gif";
165
+ if (normalized === "image/webp") return "webp";
166
+ return "img";
167
+ }
168
+
142
169
  function buildTurnStepBytes(step: ParsedTurnStep): Uint8Array {
143
170
  if (step.kind === "assistantText") {
144
171
  return toBinary(ConversationStepSchema, create(ConversationStepSchema, {
@@ -154,7 +181,7 @@ function buildTurnStepBytes(step: ParsedTurnStep): Uint8Array {
154
181
  providerIdentifier: CURSOR_PROTO_CLIENT_NAME,
155
182
  toolName,
156
183
  }),
157
- ...(step.result ? { result: createMcpToolCallResult(step.result.content, step.result.isError) } : {}),
184
+ ...(step.result ? { result: createMcpToolCallResult(step.result.content, step.result.isError, step.result.fallbackText ?? "") } : {}),
158
185
  });
159
186
  return toBinary(ConversationStepSchema, create(ConversationStepSchema, {
160
187
  message: {
@@ -166,20 +193,22 @@ function buildTurnStepBytes(step: ParsedTurnStep): Uint8Array {
166
193
 
167
194
  export function parseHistoricalTurns(messages: readonly CursorRunRequest["context"]["messages"][number][]): readonly ParsedTurn[] {
168
195
  const turns: ParsedTurn[] = [];
169
- let currentTurn: { userText: string; steps: ParsedTurnStep[]; toolCallById: Map<string, ParsedToolCallStep> } | undefined;
170
- const ensureTurn = (): { userText: string; steps: ParsedTurnStep[]; toolCallById: Map<string, ParsedToolCallStep> } => {
171
- currentTurn ??= { userText: "", steps: [], toolCallById: new Map() };
196
+ let currentTurn: { userText: string; userImages: readonly ImageContent[]; steps: ParsedTurnStep[]; toolCallById: Map<string, ParsedToolCallStep> } | undefined;
197
+ const ensureTurn = (): { userText: string; userImages: readonly ImageContent[]; steps: ParsedTurnStep[]; toolCallById: Map<string, ParsedToolCallStep> } => {
198
+ currentTurn ??= { userText: "", userImages: [], steps: [], toolCallById: new Map() };
172
199
  return currentTurn;
173
200
  };
174
201
  const flushTurn = (): void => {
175
202
  if (!currentTurn) return;
176
- if (currentTurn.userText || currentTurn.steps.length > 0) turns.push({ userText: currentTurn.userText, steps: currentTurn.steps });
203
+ if (currentTurn.userText || currentTurn.userImages.length > 0 || currentTurn.steps.length > 0) {
204
+ turns.push({ userText: currentTurn.userText, userImages: currentTurn.userImages, steps: currentTurn.steps });
205
+ }
177
206
  currentTurn = undefined;
178
207
  };
179
208
  for (const message of messages) {
180
209
  if (message.role === "user") {
181
210
  flushTurn();
182
- currentTurn = { userText: textFromMessage(message), steps: [], toolCallById: new Map() };
211
+ currentTurn = { userText: textFromMessage(message), userImages: imagesFromUserMessage(message), steps: [], toolCallById: new Map() };
183
212
  } else if (message.role === "assistant") {
184
213
  const turn = ensureTurn();
185
214
  for (const part of message.content) {
@@ -199,7 +228,7 @@ export function parseHistoricalTurns(messages: readonly CursorRunRequest["contex
199
228
  turn.steps.push(step);
200
229
  turn.toolCallById.set(step.toolCallId, step);
201
230
  }
202
- step.result = { content: rawToolResultText(message), isError: message.isError };
231
+ step.result = { content: message.content, isError: message.isError, fallbackText: rawToolResultText(message) };
203
232
  }
204
233
  }
205
234
  flushTurn();
@@ -221,6 +250,17 @@ export function extractCurrentActionText(request: CursorRunRequest): string {
221
250
  return last ? textFromMessage(last) : "";
222
251
  }
223
252
 
253
+ export function extractCurrentActionImages(request: CursorRunRequest): readonly ImageContent[] {
254
+ const last = request.context.messages.at(-1);
255
+ if (!last || last.role !== "user") return [];
256
+ return imagesFromUserMessage(last);
257
+ }
258
+
259
+ function imagesFromUserMessage(message: Extract<CursorRunRequest["context"]["messages"][number], { readonly role: "user" }>): readonly ImageContent[] {
260
+ if (typeof message.content === "string") return [];
261
+ return message.content.filter((part): part is ImageContent => part.type === "image");
262
+ }
263
+
224
264
  function rawToolResultText(message: Extract<CursorRunRequest["context"]["messages"][number], { readonly role: "toolResult" }>): string {
225
265
  return message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n");
226
266
  }
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { create, toBinary } from "@bufbuild/protobuf";
3
- import type { CursorControlMessage, CursorProtocolMessage, CursorServerMessage } from "../transport.js";
3
+ import type { CursorControlMessage, CursorProtocolMessage, CursorServerMessage, CursorToolResultContent } from "../transport.js";
4
4
  import {
5
5
  AgentClientMessageSchema,
6
6
  BackgroundShellSpawnResultSchema,
@@ -17,6 +17,7 @@ import {
17
17
  LsRejectedSchema,
18
18
  LsResultSchema,
19
19
  McpErrorSchema,
20
+ McpImageContentSchema,
20
21
  McpResultSchema,
21
22
  McpSuccessSchema,
22
23
  McpTextContentSchema,
@@ -40,6 +41,7 @@ import {
40
41
  type KvServerMessage,
41
42
  type McpToolDefinition,
42
43
  } from "./agent_pb.js";
44
+ import { decodeStrictBase64ImageData } from "./protobuf-codec-base64.js";
43
45
  import { decodeMcpArgsMap } from "./protobuf-codec-json.js";
44
46
 
45
47
  const NATIVE_EXEC_REJECT_REASON = "Tool not available in this environment. Use the MCP tools provided instead.";
@@ -203,29 +205,44 @@ export function encodeExecClientMessage(execNumericId: number | undefined, execI
203
205
  return toBinary(AgentClientMessageSchema, clientMessage);
204
206
  }
205
207
 
206
- export function createMcpToolResult(text: string, isError: boolean): ReturnType<typeof create<typeof McpResultSchema>> {
208
+ export function createMcpToolResult(content: string | readonly CursorToolResultContent[], isError: boolean, fallbackText = ""): ReturnType<typeof create<typeof McpResultSchema>> {
209
+ const text = typeof content === "string" ? content : fallbackText;
207
210
  if (isError) {
208
211
  return create(McpResultSchema, { result: { case: "error", value: create(McpErrorSchema, { error: text }) } });
209
212
  }
210
213
  return create(McpResultSchema, {
211
214
  result: {
212
215
  case: "success",
213
- value: createMcpSuccess(text),
216
+ value: createMcpSuccess(content, fallbackText),
214
217
  },
215
218
  });
216
219
  }
217
220
 
218
- export function createMcpToolCallResult(text: string, isError: boolean): ReturnType<typeof create<typeof McpToolResultSchema>> {
221
+ export function createMcpToolCallResult(content: string | readonly CursorToolResultContent[], isError: boolean, fallbackText = ""): ReturnType<typeof create<typeof McpToolResultSchema>> {
222
+ const text = typeof content === "string" ? content : fallbackText;
219
223
  if (isError) {
220
224
  return create(McpToolResultSchema, { result: { case: "error", value: create(McpToolErrorSchema, { error: text }) } });
221
225
  }
222
- return create(McpToolResultSchema, { result: { case: "success", value: createMcpSuccess(text) } });
226
+ return create(McpToolResultSchema, { result: { case: "success", value: createMcpSuccess(content, fallbackText) } });
223
227
  }
224
228
 
225
- function createMcpSuccess(text: string): ReturnType<typeof create<typeof McpSuccessSchema>> {
229
+ function createMcpSuccess(content: string | readonly CursorToolResultContent[], fallbackText: string): ReturnType<typeof create<typeof McpSuccessSchema>> {
230
+ const items = typeof content === "string"
231
+ ? [createTextContentItem(content)]
232
+ : content.map((part, index) => part.type === "text" ? createTextContentItem(part.text) : createImageContentItem(part.data, part.mimeType, index));
226
233
  return create(McpSuccessSchema, {
227
- content: [create(McpToolResultContentItemSchema, { content: { case: "text", value: create(McpTextContentSchema, { text }) } })],
234
+ content: items.length > 0 ? items : [createTextContentItem(fallbackText)],
228
235
  isError: false,
229
236
  });
230
237
  }
231
238
 
239
+ function createTextContentItem(text: string): ReturnType<typeof create<typeof McpToolResultContentItemSchema>> {
240
+ return create(McpToolResultContentItemSchema, { content: { case: "text", value: create(McpTextContentSchema, { text }) } });
241
+ }
242
+
243
+ function createImageContentItem(data: string, mimeType: string, index: number): ReturnType<typeof create<typeof McpToolResultContentItemSchema>> {
244
+ return create(McpToolResultContentItemSchema, {
245
+ content: { case: "image", value: create(McpImageContentSchema, { data: decodeStrictBase64ImageData(data, { kind: "MCP image", mimeType, index }), mimeType }) },
246
+ });
247
+ }
248
+
@@ -15,7 +15,7 @@ import {
15
15
  type McpToolDefinition,
16
16
  type ModelDetails,
17
17
  } from "./agent_pb.js";
18
- import { blobKey, buildCursorRequest, buildMcpToolDefinitions, extractCurrentActionText, parseHistoricalTurns } from "./protobuf-codec-request.js";
18
+ import { blobKey, buildCursorRequest, buildMcpToolDefinitions, extractCurrentActionImages, extractCurrentActionText, parseHistoricalTurns } from "./protobuf-codec-request.js";
19
19
  import { createMcpToolResult, decodeAgentServerMessage, encodeExecClientMessage, encodeKvClientMessage, encodeNativeExecRejection, encodeRequestContextResult } from "./protobuf-codec-wire.js";
20
20
 
21
21
  // Cursor protocol codec intentionally follows the MIT-licensed
@@ -67,6 +67,7 @@ export class CursorProtobufProtocolCodec implements CursorProtocolCodec {
67
67
  conversationIdValue,
68
68
  storedState?.checkpoint ?? null,
69
69
  storedState?.blobStore,
70
+ extractCurrentActionImages(request),
70
71
  );
71
72
  this.#blobStores.set(request.requestId, payload.blobStore);
72
73
  this.#toolDefinitions.set(request.requestId, buildMcpToolDefinitions(request));
@@ -141,7 +142,7 @@ export class CursorProtobufProtocolCodec implements CursorProtocolCodec {
141
142
  }
142
143
 
143
144
  encodeToolResult(result: CursorToolResultMessage): Uint8Array {
144
- const mcpResult = createMcpToolResult(result.text, result.isError);
145
+ const mcpResult = createMcpToolResult(result.content ?? result.text, result.isError, result.text);
145
146
  return encodeExecClientMessage(result.execNumericId, result.execId, "mcpResult", mcpResult);
146
147
  }
147
148
 
@@ -88,8 +88,8 @@ export class CursorStreamAdapter {
88
88
  if (!options?.apiKey) {
89
89
  throw new Error("Cursor OAuth credentials are required. Run /login and select Cursor.");
90
90
  }
91
- if (hasImageInput(context)) {
92
- throw new Error("Cursor provider currently supports text input only; vision/image content is unsupported.");
91
+ if (hasImageInput(context) && !model.input.includes("image")) {
92
+ throw new Error(`Cursor model ${model.id} does not support image input.`);
93
93
  }
94
94
  if (options.signal?.aborted) {
95
95
  throw new CursorStreamAbortError();
@@ -216,16 +216,10 @@ export class CursorStreamAdapter {
216
216
  }
217
217
  }
218
218
  class CursorStreamAbortError extends Error {
219
- constructor() {
220
- super("Cursor stream aborted.");
221
- this.name = "CursorStreamAbortError";
222
- }
219
+ constructor() { super("Cursor stream aborted."); this.name = "CursorStreamAbortError"; }
223
220
  }
224
221
  class CursorStreamTimeoutError extends Error {
225
- constructor() {
226
- super("Cursor stream timed out while waiting for provider output.");
227
- this.name = "CursorStreamTimeoutError";
228
- }
222
+ constructor() { super("Cursor stream timed out while waiting for provider output."); this.name = "CursorStreamTimeoutError"; }
229
223
  }
230
224
  interface CursorPendingMessageRead {
231
225
  readonly promise: Promise<IteratorResult<CursorServerMessage>>;
@@ -329,7 +323,7 @@ function getTrailingToolResults(context: Context): CursorToolResultMessage[] {
329
323
  for (let index = context.messages.length - 1; index >= 0; index--) {
330
324
  const message = context.messages[index];
331
325
  if (!message || message.role !== "toolResult") break;
332
- results.unshift({ toolCallId: message.toolCallId, toolName: message.toolName, text: textFromToolResult(message), isError: message.isError });
326
+ results.unshift({ toolCallId: message.toolCallId, toolName: message.toolName, text: textFromToolResult(message), content: message.content, isError: message.isError });
333
327
  }
334
328
  return results;
335
329
  }
@@ -47,10 +47,13 @@ export type CursorControlMessage =
47
47
 
48
48
  export type CursorProtocolMessage = CursorServerMessage | CursorControlMessage;
49
49
 
50
+ export type CursorToolResultContent = Extract<Context["messages"][number], { role: "toolResult" }>["content"][number];
51
+
50
52
  export interface CursorToolResultMessage {
51
53
  readonly toolCallId: string;
52
54
  readonly toolName: string;
53
55
  readonly text: string;
56
+ readonly content?: readonly CursorToolResultContent[];
54
57
  readonly isError: boolean;
55
58
  readonly execId?: string;
56
59
  readonly execNumericId?: number;
@@ -18,6 +18,7 @@ export type {
18
18
  CursorRunStream,
19
19
  CursorServerMessage,
20
20
  CursorToolCallMessage,
21
+ CursorToolResultContent,
21
22
  CursorToolResultMessage,
22
23
  CursorTransportLifecycleSnapshot,
23
24
  CursorWriteOptions,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.3-alpha.1",
3
+ "version": "0.9.3-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.3-alpha.1",
3
+ "version": "0.9.3-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
6
6
  "contributors": [
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.3-alpha.2] - 2026-06-27
6
+
7
+ ### Fixed
8
+
9
+ - Prevented the async subagent status widget from briefly unmounting during reset-and-hydrate cycles when active background runs are still present, including Atomic host updates that deliver fresh UI context wrappers for the same logical session ([#1517](https://github.com/bastani-inc/atomic/issues/1517)).
10
+ - Fixed live subagent result animation cleanup to register a host-row disposer, so terminal workflow cleanup evicts animation registry entries instead of only clearing intervals ([#1518](https://github.com/bastani-inc/atomic/issues/1518)).
11
+ - Synced recent upstream subagent hardening so compact delegated tool-call summaries are preserved, fanout children keep their live nested subagent call/result history, duplicate concurrent subagent dispatches are rejected, provider-hostile chain schema conditionals are removed, and failed foreground runs include captured child output for diagnostics ([#1527](https://github.com/bastani-inc/atomic/issues/1527)).
12
+ - Eliminated the foreground subagent widget flicker that appeared once a running subagent panel grew tall enough to reach or exceed the terminal viewport. The live compact result no longer animates its spinner on an 80ms wall-clock timer; instead it shows an activity "pulse" glyph that advances exactly once per real progress update. Because the panel renders into chat scrollback, a timer-driven spinner cell that scrolled above pi-tui's viewport fold forced a destructive full-screen + scrollback clear on every tick — driving the indicator off genuine updates keeps line diffs tied to content that actually changed, so the differential renderer repaints only when progress does and never strobes.
13
+
5
14
  ## [0.9.3-alpha.1] - 2026-06-25
6
15
 
7
16
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.3-alpha.1",
3
+ "version": "0.9.3-alpha.2",
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": [
@@ -34,6 +34,7 @@ export function createChildSafeState(): SubagentState {
34
34
  baseCwd: "",
35
35
  currentSessionId: null,
36
36
  asyncJobs: new Map(),
37
+ subagentInProgress: false,
37
38
  foregroundRuns: new Map(),
38
39
  foregroundControls: new Map(),
39
40
  lastForegroundControlId: null,
@@ -10,7 +10,7 @@ import { discoverAgents } from "../agents/agents.ts";
10
10
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
11
11
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
12
12
  import { cleanupOldChainDirs } from "../shared/settings.ts";
13
- import { renderLiveSubagentResult, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, type SubagentResultRenderState } from "../tui/render.ts";
13
+ import { advanceResultPulseFrame, renderLiveSubagentResult, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, type SubagentResultRenderState } from "../tui/render.ts";
14
14
  import { SubagentParams } from "./schemas.ts";
15
15
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
16
16
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
@@ -69,7 +69,7 @@ type SubagentToolRenderState = SubagentResultRenderState;
69
69
  function rebuildSlashResultContainer(
70
70
  container: Container,
71
71
  result: AgentToolResult<Details>,
72
- options: { expanded: boolean; now?: number },
72
+ options: { expanded: boolean; now?: number; pulseFrame?: number },
73
73
  theme: ExtensionContext["ui"]["theme"],
74
74
  ): void {
75
75
  container.clear();
@@ -87,12 +87,14 @@ function createSlashResultComponent(
87
87
  const container = new Container();
88
88
  let lastVersion = -1;
89
89
  let lastSnapshotNow = 0;
90
+ let pulseFrame = 0;
90
91
  container.render = (width: number): string[] => {
91
92
  const snapshot = getSlashRenderableSnapshot(details);
92
93
  if (snapshot.version !== lastVersion) {
93
94
  lastVersion = snapshot.version;
94
95
  lastSnapshotNow = Date.now();
95
- rebuildSlashResultContainer(container, snapshot.result, { ...options, now: lastSnapshotNow }, theme);
96
+ pulseFrame = advanceResultPulseFrame(pulseFrame);
97
+ rebuildSlashResultContainer(container, snapshot.result, { ...options, now: lastSnapshotNow, pulseFrame }, theme);
96
98
  }
97
99
  return Container.prototype.render.call(container, width);
98
100
  };
@@ -179,6 +181,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
179
181
  baseCwd: "",
180
182
  currentSessionId: null,
181
183
  asyncJobs: new Map(),
184
+ subagentInProgress: false,
182
185
  foregroundRuns: new Map(),
183
186
  foregroundControls: new Map(),
184
187
  lastForegroundControlId: null,
@@ -137,11 +137,6 @@ const ChainItem = Type.Object({
137
137
  }, {
138
138
  description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
139
139
  additionalProperties: false,
140
- allOf: [
141
- { if: { required: ["expand"] }, then: { required: ["parallel", "collect"], properties: { parallel: { type: "object" } } } },
142
- { if: { required: ["collect"] }, then: { required: ["expand", "parallel"], properties: { parallel: { type: "object" } } } },
143
- { not: { required: ["expand"], properties: { parallel: { type: "array", items: {} } } } },
144
- ],
145
140
  });
146
141
 
147
142
  const ControlOverrides = Type.Object({
@@ -395,10 +395,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
395
395
  state.foregroundControls?.clear();
396
396
  state.lastForegroundControlId = null;
397
397
  state.resultFileCoalescer.clear();
398
- if (ctx?.hasUI) {
399
- state.lastUiContext = ctx;
400
- rerenderWidget(ctx, []);
401
- }
398
+ if (ctx?.hasUI) state.lastUiContext = ctx;
402
399
  };
403
400
 
404
401
  return { ensurePoller, handleStarted, handleComplete, resetJobs, hydrateActiveJobs };