@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
@@ -11,7 +11,7 @@ import type {
11
11
  MountedStageCustomUi,
12
12
  StageUiBroker,
13
13
  } from "../shared/stage-ui-broker.js";
14
- import type { StageNotice, StageStatus } from "../shared/store-types.js";
14
+ import type { RunStatus, StageNotice, StageStatus } from "../shared/store-types.js";
15
15
  import type { GraphTheme } from "./graph-theme.js";
16
16
  import type { PromptCardState } from "./prompt-card.js";
17
17
 
@@ -19,6 +19,7 @@ export const VIEW_LINE_COUNT = 32;
19
19
  export const PROMPT_SCROLL_STEP_ROWS = 4;
20
20
  export const HEADER_ROWS = 1;
21
21
  export const SEP_ROWS = 1;
22
+ export const STAGE_CHAT_MOUSE_SCROLL_TOGGLE_LABEL = "ctrl+t";
22
23
 
23
24
  export function isReadOnlyArchiveStatus(status: StageStatus): boolean {
24
25
  return status === "completed" || status === "failed" || status === "skipped";
@@ -144,6 +145,9 @@ export interface StageChatViewContext {
144
145
  promptScrollOffset: number;
145
146
  promptMaxScroll: number;
146
147
  localPaused: boolean;
148
+ mouseScrollCaptureEnabled: boolean;
149
+ lastObservedStageStatus: StageStatus | undefined;
150
+ lastObservedRunStatus: RunStatus | undefined;
147
151
  seenNoticeIds: Set<string>;
148
152
  _unsubscribeStore: (() => void) | null;
149
153
  _unsubscribeHandle: (() => void) | null;
@@ -95,6 +95,7 @@ export class StageChatView implements Component, Focusable {
95
95
  private promptScrollOffset!: StageChatViewContext["promptScrollOffset"];
96
96
  private promptMaxScroll!: StageChatViewContext["promptMaxScroll"];
97
97
  private localPaused!: StageChatViewContext["localPaused"];
98
+ private mouseScrollCaptureEnabled!: StageChatViewContext["mouseScrollCaptureEnabled"];
98
99
  private seenNoticeIds!: StageChatViewContext["seenNoticeIds"];
99
100
  private _unsubscribeStore!: StageChatViewContext["_unsubscribeStore"];
100
101
  private _unsubscribeHandle!: StageChatViewContext["_unsubscribeHandle"];
@@ -177,7 +178,7 @@ export class StageChatView implements Component, Focusable {
177
178
  }
178
179
 
179
180
  wantsMouseScrollTracking(): boolean {
180
- return true;
181
+ return this.mouseScrollCaptureEnabled;
181
182
  }
182
183
 
183
184
  handleInput(data: string): boolean {
@@ -220,6 +221,7 @@ export class StageChatView implements Component, Focusable {
220
221
  void this.promptEditorSubmitFromEnter;
221
222
  void this.promptScrollOffset;
222
223
  void this.promptMaxScroll;
224
+ void this.mouseScrollCaptureEnabled;
223
225
  void this.seenNoticeIds;
224
226
  void this._unsubscribeStore;
225
227
  void this._unsubscribeHandle;
@@ -56,6 +56,10 @@ export interface RenderStatusListOpts {
56
56
  width?: number;
57
57
  }
58
58
 
59
+ function isQuitRun(run: RunSnapshot): boolean {
60
+ return run.endedAt === undefined && run.status === "paused" && run.exitReason === "quit";
61
+ }
62
+
59
63
  /**
60
64
  * Render a list of run snapshots as the canonical rounded `BACKGROUND`
61
65
  * surface: one panel plus one card per run.
@@ -157,6 +161,7 @@ function renderRunEntry(
157
161
 
158
162
  function runAccent(run: RunSnapshot, theme?: GraphTheme): string {
159
163
  if (!theme) return "#000000";
164
+ if (isQuitRun(run)) return theme.warning;
160
165
  switch (run.status) {
161
166
  case "completed": return theme.success;
162
167
  case "running": return theme.warning;
@@ -172,6 +177,7 @@ function runAccent(run: RunSnapshot, theme?: GraphTheme): string {
172
177
  }
173
178
 
174
179
  function runTrailing(run: RunSnapshot, theme?: GraphTheme): { text: string; fg?: string } | undefined {
180
+ if (isQuitRun(run)) return { text: "○ quit", fg: theme?.warning };
175
181
  switch (run.status) {
176
182
  case "completed": return { text: "✓ completed", fg: theme?.success };
177
183
  case "running": return { text: "● running", fg: theme?.warning };
@@ -205,6 +211,7 @@ function runCardMeta(run: RunSnapshot, now: number): string {
205
211
  ? fmtDuration(elapsedRunMs(run, now))
206
212
  : undefined;
207
213
 
214
+ if (isQuitRun(run)) return "resumable via /workflow resume";
208
215
  if (run.status === "running") {
209
216
  if (isChain) parts.push(`${done}/${total}`);
210
217
  const labels = runningStageLabels(run);
@@ -315,15 +322,17 @@ function effectiveWidth(width?: number): number {
315
322
  interface Counts {
316
323
  active: number;
317
324
  paused: number;
325
+ quit: number;
318
326
  completed: number;
319
327
  failed: number;
320
328
  pending: number;
321
329
  }
322
330
 
323
331
  function countBuckets(runs: readonly RunSnapshot[]): Counts {
324
- const c: Counts = { active: 0, paused: 0, completed: 0, failed: 0, pending: 0 };
332
+ const c: Counts = { active: 0, paused: 0, quit: 0, completed: 0, failed: 0, pending: 0 };
325
333
  for (const r of runs) {
326
- if (r.endedAt === undefined) {
334
+ if (isQuitRun(r)) c.quit++;
335
+ else if (r.endedAt === undefined) {
327
336
  if (r.status === "pending") c.pending++;
328
337
  else if (r.status === "paused") c.paused++;
329
338
  else if (r.status === "running") c.active++;
@@ -343,6 +352,7 @@ function themedBadges(c: Counts, theme: GraphTheme): FlatBandBadge[] {
343
352
  // Keep the word label: the pause glyph is less familiar than the other
344
353
  // status glyphs, so this intentional asymmetry improves scanability.
345
354
  if (c.paused > 0) out.push({ text: `❚❚ ${c.paused} paused`, fg: theme.warning });
355
+ if (c.quit > 0) out.push({ text: `${c.quit} quit`, fg: theme.warning });
346
356
  if (c.pending > 0) out.push({ text: `○ ${c.pending}`, fg: theme.dim });
347
357
  if (c.failed > 0) out.push({ text: `⊘ ${c.failed}`, fg: theme.error });
348
358
  return out;
@@ -355,6 +365,7 @@ function plainBadges(c: Counts): FlatBandBadge[] {
355
365
  // Keep the word label: the pause glyph is less familiar than the other
356
366
  // status glyphs, so this intentional asymmetry improves scanability.
357
367
  if (c.paused > 0) out.push({ text: `❚❚ ${c.paused} paused` });
368
+ if (c.quit > 0) out.push({ text: `${c.quit} quit` });
358
369
  if (c.pending > 0) out.push({ text: `○ ${c.pending}` });
359
370
  if (c.failed > 0) out.push({ text: `⊘ ${c.failed}` });
360
371
  return out;
@@ -381,6 +392,7 @@ function emptyStateLine(theme?: GraphTheme): string {
381
392
  }
382
393
 
383
394
  function statusIconForRun(run: RunSnapshot): string {
395
+ if (isQuitRun(run)) return "○";
384
396
  switch (run.status) {
385
397
  case "completed": return "✓";
386
398
  case "skipped": return "⊘";
@@ -73,9 +73,14 @@ function recentlyEnded(run: RunSnapshot, now: number): boolean {
73
73
  return run.endedAt !== undefined && now - run.endedAt <= RECENT_ENDED_WINDOW_MS;
74
74
  }
75
75
 
76
+ function isQuitRun(run: RunSnapshot): boolean {
77
+ return run.endedAt === undefined && run.status === "paused" && run.exitReason === "quit";
78
+ }
79
+
76
80
  interface RunCounts {
77
81
  active: number;
78
82
  paused: number;
83
+ quit: number;
79
84
  done: number;
80
85
  failed: number;
81
86
  /** Runs with a pending HIL prompt — surfaced as a separate badge so the
@@ -106,9 +111,10 @@ function countRuns(
106
111
  runs: readonly RunSnapshot[],
107
112
  allRuns: readonly RunSnapshot[] = runs,
108
113
  ): RunCounts {
109
- const counts: RunCounts = { active: 0, paused: 0, done: 0, failed: 0, awaiting: 0 };
114
+ const counts: RunCounts = { active: 0, paused: 0, quit: 0, done: 0, failed: 0, awaiting: 0 };
110
115
  for (const r of runs) {
111
- if (r.endedAt === undefined && r.status === "paused") counts.paused++;
116
+ if (isQuitRun(r)) counts.quit++;
117
+ else if (r.endedAt === undefined && r.status === "paused") counts.paused++;
112
118
  else if (r.endedAt === undefined) counts.active++;
113
119
  else if (r.status === "completed" || r.status === "skipped" || r.status === "cancelled" || r.status === "blocked") counts.done++;
114
120
  else if (r.status === "failed" || r.status === "killed") counts.failed++;
@@ -165,6 +171,7 @@ function shortId(run: RunSnapshot): string {
165
171
  }
166
172
 
167
173
  function statusGlyph(run: RunSnapshot): string {
174
+ if (isQuitRun(run)) return "○";
168
175
  switch (run.status) {
169
176
  case "running":
170
177
  return "●";
@@ -188,6 +195,7 @@ function statusGlyph(run: RunSnapshot): string {
188
195
  }
189
196
 
190
197
  function statusFg(run: RunSnapshot, theme: GraphTheme): string {
198
+ if (isQuitRun(run)) return theme.warning;
191
199
  switch (run.status) {
192
200
  case "running":
193
201
  case "paused":
@@ -237,6 +245,7 @@ function metaLine(run: RunSnapshot, now: number): string {
237
245
  if (run.endedAt !== undefined) {
238
246
  return elapsedLabel(run, now);
239
247
  }
248
+ if (isQuitRun(run)) return "quit · resumable via /workflow resume";
240
249
  const parts: string[] = [modeLabel(run)];
241
250
  const prog = progressLabel(run);
242
251
  if (prog) parts.push(prog);
@@ -257,6 +266,9 @@ function countBadges(counts: RunCounts, theme: GraphTheme): FlatBandBadge[] {
257
266
  if (counts.paused > 0) {
258
267
  badges.push({ text: `❚❚ ${counts.paused} paused`, fg: theme.warning });
259
268
  }
269
+ if (counts.quit > 0) {
270
+ badges.push({ text: `${counts.quit} quit`, fg: theme.warning });
271
+ }
260
272
  // Awaiting input is shown in Sky per DESIGN.md status semantics: a live
261
273
  // human-in-the-loop request that requires attention. Mirror the graph node's
262
274
  // question-mark status glyph, then keep ↵ as the attach/respond action hint.
@@ -332,16 +344,18 @@ function themedCollapsed(
332
344
  const dim = hexToAnsi(theme.dim);
333
345
  const muted = hexToAnsi(theme.textMuted);
334
346
  const warning = hexToAnsi(theme.warning);
335
- const total = counts.active + counts.paused + counts.done + counts.failed;
347
+ const total = counts.active + counts.paused + counts.quit + counts.done + counts.failed;
336
348
  const active = counts.active;
337
349
  const paused = counts.paused > 0 ? `${dim} · ${RESET}${warning}${counts.paused} ❚❚${RESET}` : "";
338
- return ` ${mauve}▾${RESET} ${muted}${total} background${RESET}${dim} · ${RESET}${warning}${active} ●${RESET}${paused}`;
350
+ const quit = counts.quit > 0 ? `${dim} · ${RESET}${warning}${counts.quit} quit${RESET}` : "";
351
+ return ` ${mauve}▾${RESET} ${muted}${total} background${RESET}${dim} · ${RESET}${warning}${active} ●${RESET}${paused}${quit}`;
339
352
  }
340
353
 
341
354
  function plainCollapsed(counts: RunCounts): string {
342
- const total = counts.active + counts.paused + counts.done + counts.failed;
355
+ const total = counts.active + counts.paused + counts.quit + counts.done + counts.failed;
343
356
  const paused = counts.paused > 0 ? ` · ${counts.paused} ❚❚` : "";
344
- return ` ▾ ${total} background · ${counts.active} ●${paused}`;
357
+ const quit = counts.quit > 0 ? ` · ${counts.quit} quit` : "";
358
+ return ` ▾ ${total} background · ${counts.active} ●${paused}${quit}`;
345
359
  }
346
360
 
347
361
  // ---------------------------------------------------------------------------
@@ -372,7 +386,8 @@ export function buildThemedWidgetLines(
372
386
  // visually persists for a beat before dropping off.
373
387
  const visibleCounts: RunCounts = {
374
388
  active: display.filter((r) => r.endedAt === undefined && r.status !== "paused").length,
375
- paused: display.filter((r) => r.endedAt === undefined && r.status === "paused").length,
389
+ paused: display.filter((r) => r.endedAt === undefined && r.status === "paused" && !isQuitRun(r)).length,
390
+ quit: display.filter(isQuitRun).length,
376
391
  done: display.filter((r) => r.endedAt !== undefined && (r.status === "completed" || r.status === "skipped" || r.status === "cancelled" || r.status === "blocked")).length,
377
392
  failed: display.filter((r) => r.endedAt !== undefined && (r.status === "failed" || r.status === "killed")).length,
378
393
  awaiting: counts.awaiting,
@@ -386,7 +401,7 @@ export function buildThemedWidgetLines(
386
401
  return [themed ? themedCollapsed(visibleCounts, graphTheme) : plainCollapsed(visibleCounts)];
387
402
  }
388
403
 
389
- const total = counts.active + counts.paused + counts.done + counts.failed;
404
+ const total = counts.active + counts.paused + counts.quit + counts.done + counts.failed;
390
405
  const subtitle = `${total} run${total === 1 ? "" : "s"}`;
391
406
 
392
407
  const badgeList = countBadges(visibleCounts, graphTheme);
@@ -29,8 +29,8 @@ export interface WorkflowAttachPaneOpts {
29
29
  onClose: () => void;
30
30
  /** Called when the user requests the host to hide the popup. */
31
31
  onHide?: () => void;
32
- /** Called when the user kills the active run (q in graph mode). */
33
- onKill?: (runId: string) => void;
32
+ /** Called when the user quits/detaches the active run (q in graph mode). */
33
+ onQuit?: (runId: string) => void;
34
34
  /** Called when the user resolves a HIL prompt via the graph view. */
35
35
  onPromptResolve?: (runId: string, promptId: string, response: unknown) => void;
36
36
  /** Live pi-tui host objects used by attached stage chat to reuse coding-agent editor UI. */
@@ -73,8 +73,9 @@ export interface WorkflowAttachPaneOpts {
73
73
  requestFocus?: () => void;
74
74
  /**
75
75
  * Host hook for terminal mouse reporting. Graph mode uses wheel input
76
- * for canvas scrolling; stage-chat mode uses it for transcript history
77
- * scrolling and drops non-wheel mouse bytes before they reach the editor.
76
+ * for canvas scrolling. Stage-chat mode captures wheel input by default so
77
+ * transcript/prompt scrolling stays inside the active workflow chat; ctrl+t
78
+ * toggles copy mode, which disables capture for terminal text selection.
78
79
  */
79
80
  setMouseScrollTracking?: (enabled: boolean) => void;
80
81
  /** Optional clock injection for deterministic transition-quarantine tests. */
@@ -19,7 +19,6 @@
19
19
  * - src/tui/stage-chat-view.ts (chat mode Component)
20
20
  * - src/runs/foreground/stage-control-registry.ts (live handles)
21
21
  */
22
-
23
22
  import type { Component, EditorComponent, EditorTheme, TUI } from "@earendil-works/pi-tui";
24
23
  import type { ChatMessageRenderOptions, ReadonlyFooterDataProvider } from "@bastani/atomic";
25
24
  import type { Store } from "../shared/store.js";
@@ -39,7 +38,6 @@ import type { StageUiBroker } from "../shared/stage-ui-broker.js";
39
38
  import type { StageSnapshot, StoreSnapshot } from "../shared/store-types.js";
40
39
  import { expandWorkflowGraph } from "../shared/expanded-workflow-graph.js";
41
40
  import { WORKFLOW_STATUS_KEY } from "./workflow-status.js";
42
-
43
41
  /**
44
42
  * Surface used to write Pi's footer/status tag while the attach pane is
45
43
  * mounted. Passing `undefined` clears the slot — required on dispose so
@@ -50,7 +48,6 @@ import { WORKFLOW_STATUS_KEY } from "./workflow-status.js";
50
48
  */
51
49
  import type { AttachUiStatusSurface, WorkflowAttachPaneMode, WorkflowAttachPaneOpts } from "./workflow-attach-pane-types.js";
52
50
  const ENTER_TRANSITION_QUARANTINE_MS = 200;
53
-
54
51
  export class WorkflowAttachPane implements Component {
55
52
  private store: Store;
56
53
  private theme: GraphTheme;
@@ -60,7 +57,7 @@ export class WorkflowAttachPane implements Component {
60
57
  private uiStatus: AttachUiStatusSurface | undefined;
61
58
  private onClose: () => void;
62
59
  private onHide?: () => void;
63
- private onKill?: (runId: string) => void;
60
+ private onQuit?: (runId: string) => void;
64
61
  private onPromptResolve?: (runId: string, promptId: string, response: unknown) => void;
65
62
  private getViewportRows?: () => number | undefined;
66
63
  private hostRequestRender?: () => void;
@@ -73,7 +70,6 @@ export class WorkflowAttachPane implements Component {
73
70
  private getChatRenderSettings?: () => Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd">> | undefined;
74
71
  private footerData?: ReadonlyFooterDataProvider;
75
72
  private now: () => number;
76
-
77
73
  private mode: WorkflowAttachPaneMode = "graph";
78
74
  private visible = true;
79
75
  private graphView: GraphView;
@@ -97,7 +93,7 @@ export class WorkflowAttachPane implements Component {
97
93
  this.uiStatus = opts.uiStatus;
98
94
  this.onClose = opts.onClose;
99
95
  this.onHide = opts.onHide;
100
- this.onKill = opts.onKill;
96
+ this.onQuit = opts.onQuit;
101
97
  this.onPromptResolve = opts.onPromptResolve;
102
98
  this.getViewportRows = opts.getViewportRows;
103
99
  this.hostRequestRender = opts.requestRender;
@@ -130,7 +126,7 @@ export class WorkflowAttachPane implements Component {
130
126
  graphTheme: this.theme,
131
127
  onClose: this.onClose,
132
128
  onHide: this.onHide,
133
- onKill: this.onKill,
129
+ onQuit: this.onQuit,
134
130
  onPromptResolve: this.onPromptResolve,
135
131
  onStageAttach: (runId, stageId) => this._attachToStage(runId, stageId, {
136
132
  suppressInitialPromptSubmit: true,
@@ -366,7 +362,11 @@ export class WorkflowAttachPane implements Component {
366
362
  if (!this.visible) return false;
367
363
  if (this.mode === "stage-chat" && this.chatView) {
368
364
  if (this._shouldQuarantineStagePromptEnter(data)) return true;
369
- return this.chatView.handleInput(data);
365
+ const beforeMouseTracking = this.chatView.wantsMouseScrollTracking();
366
+ const handled = this.chatView.handleInput(data);
367
+ const afterMouseTracking = this.chatView?.wantsMouseScrollTracking();
368
+ if (afterMouseTracking !== undefined && afterMouseTracking !== beforeMouseTracking) this._syncMouseScrollTracking();
369
+ return handled;
370
370
  }
371
371
  if (this._shouldQuarantineGraphEnter(data)) return true;
372
372
  return this.graphView.handleInput(data);
@@ -0,0 +1,151 @@
1
+ import { SessionSelectorComponent, type SessionInfo } from "@bastani/atomic";
2
+ import type { ResumableWorkflowEntry } from "../durable/types.js";
3
+ import type {
4
+ PiCustomComponent,
5
+ PiCustomOverlayFactoryTui,
6
+ PiCustomOverlayFunction,
7
+ } from "../extension/wiring.js";
8
+ import type { RunSnapshot, StageSnapshot } from "../shared/store-types.js";
9
+
10
+ export type WorkflowResumeSelectorResult =
11
+ | { kind: "live"; runId: string }
12
+ | { kind: "durable"; workflowId: string }
13
+ | { kind: "close" };
14
+
15
+ export interface WorkflowResumeSelectorUiSurface {
16
+ custom?: PiCustomOverlayFunction;
17
+ }
18
+
19
+ interface WorkflowResumeSelectorItem {
20
+ readonly result: Exclude<WorkflowResumeSelectorResult, { kind: "close" }>;
21
+ readonly session: SessionInfo;
22
+ }
23
+
24
+ function latestStageTimestamp(stage: StageSnapshot): number {
25
+ return stage.endedAt ?? stage.startedAt ?? 0;
26
+ }
27
+
28
+ function latestRunTimestamp(run: RunSnapshot): number {
29
+ const stageTimes = run.stages.map(latestStageTimestamp);
30
+ return Math.max(run.endedAt ?? 0, run.resumedAt ?? 0, run.pausedAt ?? 0, run.startedAt, ...stageTimes);
31
+ }
32
+
33
+ function completedStageCount(run: RunSnapshot): number {
34
+ return run.stages.filter((stage) => stage.status === "completed" || stage.status === "failed").length;
35
+ }
36
+
37
+ function liveRunSession(run: RunSnapshot): WorkflowResumeSelectorItem {
38
+ const completed = completedStageCount(run);
39
+ const total = run.stages.length;
40
+ const modified = new Date(latestRunTimestamp(run));
41
+ const firstMessage = `${run.name} ${run.status} ${completed}/${total} stages`;
42
+ return {
43
+ result: { kind: "live", runId: run.id },
44
+ session: {
45
+ path: `workflow-live:${run.id}`,
46
+ id: run.id,
47
+ cwd: "Live workflow runs",
48
+ created: new Date(run.startedAt),
49
+ modified,
50
+ messageCount: total,
51
+ firstMessage,
52
+ allMessagesText: `${run.id} ${run.name} ${run.status} ${completed}/${total} stages`,
53
+ },
54
+ };
55
+ }
56
+
57
+ function durableWorkflowSession(entry: ResumableWorkflowEntry): WorkflowResumeSelectorItem {
58
+ const checkpointText = `${entry.completedCheckpoints} checkpoints`;
59
+ const promptText = `${entry.pendingPrompts} prompts`;
60
+ const firstMessage = `${entry.name} ${entry.status} ${checkpointText} ${promptText}`;
61
+ return {
62
+ result: { kind: "durable", workflowId: entry.workflowId },
63
+ session: {
64
+ path: `workflow-durable:${entry.workflowId}`,
65
+ id: entry.workflowId,
66
+ cwd: "Durable workflow runs",
67
+ created: new Date(entry.createdAt),
68
+ modified: new Date(entry.updatedAt),
69
+ messageCount: entry.completedCheckpoints,
70
+ firstMessage,
71
+ allMessagesText: `${entry.workflowId} ${entry.name} ${entry.status} ${checkpointText} ${promptText}`,
72
+ },
73
+ };
74
+ }
75
+
76
+ export function workflowResumeSelectorItems(
77
+ liveRuns: readonly RunSnapshot[],
78
+ durableEntries: readonly ResumableWorkflowEntry[],
79
+ ): WorkflowResumeSelectorItem[] {
80
+ const liveIds = new Set(liveRuns.map((run) => run.id));
81
+ return [
82
+ ...liveRuns.map(liveRunSession),
83
+ ...durableEntries
84
+ .filter((entry) => !liveIds.has(entry.workflowId))
85
+ .map(durableWorkflowSession),
86
+ ];
87
+ }
88
+
89
+ export function openWorkflowResumeSelector(
90
+ ui: WorkflowResumeSelectorUiSurface,
91
+ liveRuns: readonly RunSnapshot[],
92
+ durableEntries: readonly ResumableWorkflowEntry[],
93
+ ): Promise<WorkflowResumeSelectorResult> {
94
+ const custom = ui.custom;
95
+ if (typeof custom !== "function") return Promise.resolve({ kind: "close" });
96
+
97
+ const items = workflowResumeSelectorItems(liveRuns, durableEntries);
98
+
99
+ const resultByPath = new Map(items.map((item) => [item.session.path, item.result]));
100
+ const sessions = items.map((item) => item.session);
101
+ const loadSessions = async (onProgress?: (loaded: number, total: number) => void): Promise<SessionInfo[]> => {
102
+ onProgress?.(sessions.length, sessions.length);
103
+ return [...sessions];
104
+ };
105
+
106
+ return new Promise<WorkflowResumeSelectorResult>((resolve) => {
107
+ let settled = false;
108
+ const settle = (result: WorkflowResumeSelectorResult, done?: (result: undefined) => void): void => {
109
+ if (settled) return;
110
+ settled = true;
111
+ resolve(result);
112
+ done?.(undefined);
113
+ };
114
+
115
+ const factory = (
116
+ tui: PiCustomOverlayFactoryTui,
117
+ _theme: unknown,
118
+ _keys: unknown,
119
+ done: (result: undefined) => void,
120
+ ): PiCustomComponent => {
121
+ const selector = new SessionSelectorComponent(
122
+ loadSessions,
123
+ loadSessions,
124
+ (path) => settle(resultByPath.get(path) ?? { kind: "close" }, done),
125
+ () => settle({ kind: "close" }, done),
126
+ () => settle({ kind: "close" }, done),
127
+ () => tui.requestRender?.(),
128
+ { showRenameHint: false },
129
+ );
130
+ // Workflow rows are synthetic SessionInfo records. Reuse the /resume
131
+ // selector chrome, but never let its session-file delete action touch a
132
+ // path derived from a workflow id.
133
+ selector.getSessionList().onDeleteSession = async () => {
134
+ tui.requestRender?.();
135
+ };
136
+ selector.focused = true;
137
+
138
+ return {
139
+ render: (width) => selector.render(width),
140
+ handleInput: (data) => selector.handleInput(data),
141
+ invalidate: () => {
142
+ selector.invalidate?.();
143
+ tui.requestRender?.();
144
+ },
145
+ dispose: () => settle({ kind: "close" }),
146
+ };
147
+ };
148
+
149
+ void custom(factory, { overlay: false });
150
+ });
151
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"loader-virtual-modules.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader-virtual-modules.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA4BnD,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CAOrE;AAoED,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,mBAAmB,GAC/B,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAmBvC","sourcesContent":["import * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport * as _bundledPiAgentCore from \"@earendil-works/pi-agent-core\";\nimport * as _bundledPiAi from \"@earendil-works/pi-ai\";\nimport * as _bundledPiAiOauth from \"@earendil-works/pi-ai/oauth\";\nimport * as _bundledPiTui from \"@earendil-works/pi-tui\";\nimport { createJiti } from \"jiti/static\";\nimport * as _bundledTypebox from \"typebox\";\nimport * as _bundledTypeboxCompile from \"typebox/compile\";\nimport * as _bundledTypeboxValue from \"typebox/value\";\nimport { isBunBinary } from \"../../config.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\n// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,\n// avoiding a circular dependency. Extensions can import from the Atomic package\n// name (or upstream-compatible pi package names).\nimport * as _bundledPiCodingAgent from \"../../index.ts\";\nimport type { ExtensionFactory } from \"./types.ts\";\n\n/** Modules available to extensions via virtualModules (for compiled Bun binary) */\nconst VIRTUAL_MODULES: Record<string, unknown> = {\n typebox: _bundledTypebox,\n \"typebox/compile\": _bundledTypeboxCompile,\n \"typebox/value\": _bundledTypeboxValue,\n \"@sinclair/typebox\": _bundledTypebox,\n \"@sinclair/typebox/compile\": _bundledTypeboxCompile,\n \"@sinclair/typebox/value\": _bundledTypeboxValue,\n \"@earendil-works/pi-agent-core\": _bundledPiAgentCore,\n \"@earendil-works/pi-tui\": _bundledPiTui,\n \"@earendil-works/pi-ai\": _bundledPiAi,\n \"@earendil-works/pi-ai/oauth\": _bundledPiAiOauth,\n \"@bastani/atomic\": _bundledPiCodingAgent,\n \"@mariozechner/pi-agent-core\": _bundledPiAgentCore,\n \"@mariozechner/pi-tui\": _bundledPiTui,\n \"@mariozechner/pi-ai\": _bundledPiAi,\n \"@mariozechner/pi-ai/oauth\": _bundledPiAiOauth,\n};\n\nconst require = createRequire(import.meta.url);\nlet _aliases: Record<string, string> | null = null;\n\nlet extensionCacheCwd: string | undefined;\nlet extensionCacheGeneration = 0;\nconst extensionCache = new Map<string, ExtensionFactory>();\n\nexport interface ExtensionCacheToken {\n cwd: string;\n generation: number;\n}\n\nexport function clearExtensionCache(): void {\n extensionCache.clear();\n extensionCacheCwd = undefined;\n extensionCacheGeneration++;\n}\n\nexport function useExtensionCacheCwd(cwd: string): ExtensionCacheToken {\n const resolvedCwd = resolvePath(cwd);\n if (extensionCacheCwd !== undefined && extensionCacheCwd !== resolvedCwd) {\n clearExtensionCache();\n }\n extensionCacheCwd = resolvedCwd;\n return { cwd: resolvedCwd, generation: extensionCacheGeneration };\n}\n\nfunction isCurrentCacheToken(cacheToken: ExtensionCacheToken | undefined): cacheToken is ExtensionCacheToken {\n return (\n cacheToken !== undefined &&\n extensionCacheCwd === cacheToken.cwd &&\n extensionCacheGeneration === cacheToken.generation\n );\n}\n\nfunction extensionImportSpecifier(extensionPath: string, cacheToken: ExtensionCacheToken | undefined): string {\n const url = pathToFileURL(extensionPath);\n const cacheKey = cacheToken ? `${cacheToken.generation}:${cacheToken.cwd}` : `${Date.now()}:${Math.random()}`;\n url.searchParams.set(\"atomicExtensionCache\", cacheKey);\n return url.href;\n}\n\n/**\n * Get aliases for jiti (used in Node.js/development mode).\n * In Bun binary mode, virtualModules is used instead.\n */\nfunction getAliases(): Record<string, string> {\n if (_aliases) return _aliases;\n\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n const typeboxEntry = require.resolve(\"typebox\");\n const typeboxCompileEntry = require.resolve(\"typebox/compile\");\n const typeboxValueEntry = require.resolve(\"typebox/value\");\n\n const packagesRoot = path.resolve(__dirname, \"../../../../\");\n const resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => {\n const workspacePath = path.join(packagesRoot, workspaceRelativePath);\n if (fs.existsSync(workspacePath)) {\n return workspacePath;\n }\n return fileURLToPath(import.meta.resolve(specifier));\n };\n\n const piCodingAgentEntry = packageIndex;\n const piAgentCoreEntry = resolveWorkspaceOrImport(\"agent/dist/index.js\", \"@earendil-works/pi-agent-core\");\n const piTuiEntry = resolveWorkspaceOrImport(\"tui/dist/index.js\", \"@earendil-works/pi-tui\");\n const piAiEntry = resolveWorkspaceOrImport(\"ai/dist/index.js\", \"@earendil-works/pi-ai\");\n const piAiOauthEntry = resolveWorkspaceOrImport(\"ai/dist/oauth.js\", \"@earendil-works/pi-ai/oauth\");\n\n _aliases = {\n \"@bastani/atomic\": piCodingAgentEntry,\n \"@earendil-works/pi-coding-agent\": piCodingAgentEntry,\n \"@earendil-works/pi-agent-core\": piAgentCoreEntry,\n \"@earendil-works/pi-tui\": piTuiEntry,\n \"@earendil-works/pi-ai\": piAiEntry,\n \"@earendil-works/pi-ai/oauth\": piAiOauthEntry,\n \"@mariozechner/pi-agent-core\": piAgentCoreEntry,\n \"@mariozechner/pi-tui\": piTuiEntry,\n \"@mariozechner/pi-ai\": piAiEntry,\n \"@mariozechner/pi-ai/oauth\": piAiOauthEntry,\n typebox: typeboxEntry,\n \"typebox/compile\": typeboxCompileEntry,\n \"typebox/value\": typeboxValueEntry,\n \"@sinclair/typebox\": typeboxEntry,\n \"@sinclair/typebox/compile\": typeboxCompileEntry,\n \"@sinclair/typebox/value\": typeboxValueEntry,\n };\n\n return _aliases;\n}\n\nexport async function loadExtensionModule(\n extensionPath: string,\n cacheToken?: ExtensionCacheToken,\n): Promise<ExtensionFactory | undefined> {\n if (isCurrentCacheToken(cacheToken)) {\n const cachedFactory = extensionCache.get(extensionPath);\n if (cachedFactory) return cachedFactory;\n }\n\n const forceTransformedImports = isBunBinary || process.platform === \"win32\";\n const jiti = createJiti(import.meta.url, {\n moduleCache: false,\n ...(forceTransformedImports ? { fsCache: false, tryNative: false } : {}),\n ...(isBunBinary ? { virtualModules: VIRTUAL_MODULES } : { alias: getAliases() }),\n });\n const module = await jiti.import(extensionImportSpecifier(extensionPath, cacheToken), { default: true });\n const factory = module as ExtensionFactory;\n if (typeof factory !== \"function\") return undefined;\n if (isCurrentCacheToken(cacheToken)) {\n extensionCache.set(extensionPath, factory);\n }\n return factory;\n}\n"]}
1
+ {"version":3,"file":"loader-virtual-modules.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader-virtual-modules.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA4DnD,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CAOrE;AAoED,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,mBAAmB,GAC/B,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAmBvC","sourcesContent":["import * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { createJiti } from \"jiti/static\";\nimport { isBunBinary } from \"../../config.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport type { ExtensionFactory } from \"./types.ts\";\n\nconst require = createRequire(import.meta.url);\nlet _virtualModules: Record<string, object> | null = null;\nlet _virtualModulesPromise: Promise<Record<string, object>> | null = null;\n\nasync function loadVirtualModules(): Promise<Record<string, object>> {\n const [typebox, typeboxCompile, typeboxValue, piAgentCore, piTui, piAi, piAiOauth, piCodingAgent] = await Promise.all([\n import(\"typebox\"),\n import(\"typebox/compile\"),\n import(\"typebox/value\"),\n import(\"@earendil-works/pi-agent-core\"),\n import(\"@earendil-works/pi-tui\"),\n import(\"@earendil-works/pi-ai\"),\n import(\"@earendil-works/pi-ai/oauth\"),\n // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,\n // avoiding a circular dependency while preserving the package-name extension import path.\n import(\"../../index.ts\"),\n ]);\n\n return {\n typebox,\n \"typebox/compile\": typeboxCompile,\n \"typebox/value\": typeboxValue,\n \"@sinclair/typebox\": typebox,\n \"@sinclair/typebox/compile\": typeboxCompile,\n \"@sinclair/typebox/value\": typeboxValue,\n \"@earendil-works/pi-agent-core\": piAgentCore,\n \"@earendil-works/pi-tui\": piTui,\n \"@earendil-works/pi-ai\": piAi,\n \"@earendil-works/pi-ai/oauth\": piAiOauth,\n \"@bastani/atomic\": piCodingAgent,\n \"@mariozechner/pi-agent-core\": piAgentCore,\n \"@mariozechner/pi-tui\": piTui,\n \"@mariozechner/pi-ai\": piAi,\n \"@mariozechner/pi-ai/oauth\": piAiOauth,\n };\n}\n\n/** Modules available to extensions via virtualModules (for compiled Bun binary). */\nasync function getVirtualModules(): Promise<Record<string, object>> {\n if (_virtualModules) return _virtualModules;\n _virtualModulesPromise ??= loadVirtualModules().then(\n (virtualModules) => {\n _virtualModules = virtualModules;\n return virtualModules;\n },\n (error: Error) => {\n _virtualModulesPromise = null;\n throw error;\n },\n );\n return _virtualModulesPromise;\n}\nlet _aliases: Record<string, string> | null = null;\n\nlet extensionCacheCwd: string | undefined;\nlet extensionCacheGeneration = 0;\nconst extensionCache = new Map<string, ExtensionFactory>();\n\nexport interface ExtensionCacheToken {\n cwd: string;\n generation: number;\n}\n\nexport function clearExtensionCache(): void {\n extensionCache.clear();\n extensionCacheCwd = undefined;\n extensionCacheGeneration++;\n}\n\nexport function useExtensionCacheCwd(cwd: string): ExtensionCacheToken {\n const resolvedCwd = resolvePath(cwd);\n if (extensionCacheCwd !== undefined && extensionCacheCwd !== resolvedCwd) {\n clearExtensionCache();\n }\n extensionCacheCwd = resolvedCwd;\n return { cwd: resolvedCwd, generation: extensionCacheGeneration };\n}\n\nfunction isCurrentCacheToken(cacheToken: ExtensionCacheToken | undefined): cacheToken is ExtensionCacheToken {\n return (\n cacheToken !== undefined &&\n extensionCacheCwd === cacheToken.cwd &&\n extensionCacheGeneration === cacheToken.generation\n );\n}\n\nfunction extensionImportSpecifier(extensionPath: string, cacheToken: ExtensionCacheToken | undefined): string {\n const url = pathToFileURL(extensionPath);\n const cacheKey = cacheToken ? `${cacheToken.generation}:${cacheToken.cwd}` : `${Date.now()}:${Math.random()}`;\n url.searchParams.set(\"atomicExtensionCache\", cacheKey);\n return url.href;\n}\n\n/**\n * Get aliases for jiti (used in Node.js/development mode).\n * In Bun binary mode, virtualModules is used instead.\n */\nfunction getAliases(): Record<string, string> {\n if (_aliases) return _aliases;\n\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n const typeboxEntry = require.resolve(\"typebox\");\n const typeboxCompileEntry = require.resolve(\"typebox/compile\");\n const typeboxValueEntry = require.resolve(\"typebox/value\");\n\n const packagesRoot = path.resolve(__dirname, \"../../../../\");\n const resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => {\n const workspacePath = path.join(packagesRoot, workspaceRelativePath);\n if (fs.existsSync(workspacePath)) {\n return workspacePath;\n }\n return fileURLToPath(import.meta.resolve(specifier));\n };\n\n const piCodingAgentEntry = packageIndex;\n const piAgentCoreEntry = resolveWorkspaceOrImport(\"agent/dist/index.js\", \"@earendil-works/pi-agent-core\");\n const piTuiEntry = resolveWorkspaceOrImport(\"tui/dist/index.js\", \"@earendil-works/pi-tui\");\n const piAiEntry = resolveWorkspaceOrImport(\"ai/dist/index.js\", \"@earendil-works/pi-ai\");\n const piAiOauthEntry = resolveWorkspaceOrImport(\"ai/dist/oauth.js\", \"@earendil-works/pi-ai/oauth\");\n\n _aliases = {\n \"@bastani/atomic\": piCodingAgentEntry,\n \"@earendil-works/pi-coding-agent\": piCodingAgentEntry,\n \"@earendil-works/pi-agent-core\": piAgentCoreEntry,\n \"@earendil-works/pi-tui\": piTuiEntry,\n \"@earendil-works/pi-ai\": piAiEntry,\n \"@earendil-works/pi-ai/oauth\": piAiOauthEntry,\n \"@mariozechner/pi-agent-core\": piAgentCoreEntry,\n \"@mariozechner/pi-tui\": piTuiEntry,\n \"@mariozechner/pi-ai\": piAiEntry,\n \"@mariozechner/pi-ai/oauth\": piAiOauthEntry,\n typebox: typeboxEntry,\n \"typebox/compile\": typeboxCompileEntry,\n \"typebox/value\": typeboxValueEntry,\n \"@sinclair/typebox\": typeboxEntry,\n \"@sinclair/typebox/compile\": typeboxCompileEntry,\n \"@sinclair/typebox/value\": typeboxValueEntry,\n };\n\n return _aliases;\n}\n\nexport async function loadExtensionModule(\n extensionPath: string,\n cacheToken?: ExtensionCacheToken,\n): Promise<ExtensionFactory | undefined> {\n if (isCurrentCacheToken(cacheToken)) {\n const cachedFactory = extensionCache.get(extensionPath);\n if (cachedFactory) return cachedFactory;\n }\n\n const forceTransformedImports = isBunBinary || process.platform === \"win32\";\n const jiti = createJiti(import.meta.url, {\n moduleCache: false,\n ...(forceTransformedImports ? { fsCache: false, tryNative: false } : {}),\n ...(isBunBinary ? { virtualModules: await getVirtualModules() } : { alias: getAliases() }),\n });\n const module = await jiti.import(extensionImportSpecifier(extensionPath, cacheToken), { default: true });\n const factory = module as ExtensionFactory;\n if (typeof factory !== \"function\") return undefined;\n if (isCurrentCacheToken(cacheToken)) {\n extensionCache.set(extensionPath, factory);\n }\n return factory;\n}\n"]}
@@ -2,39 +2,56 @@ import * as fs from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as path from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
- import * as _bundledPiAgentCore from "@earendil-works/pi-agent-core";
6
- import * as _bundledPiAi from "@earendil-works/pi-ai";
7
- import * as _bundledPiAiOauth from "@earendil-works/pi-ai/oauth";
8
- import * as _bundledPiTui from "@earendil-works/pi-tui";
9
5
  import { createJiti } from "jiti/static";
10
- import * as _bundledTypebox from "typebox";
11
- import * as _bundledTypeboxCompile from "typebox/compile";
12
- import * as _bundledTypeboxValue from "typebox/value";
13
6
  import { isBunBinary } from "../../config.js";
14
7
  import { resolvePath } from "../../utils/paths.js";
15
- // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
16
- // avoiding a circular dependency. Extensions can import from the Atomic package
17
- // name (or upstream-compatible pi package names).
18
- import * as _bundledPiCodingAgent from "../../index.js";
19
- /** Modules available to extensions via virtualModules (for compiled Bun binary) */
20
- const VIRTUAL_MODULES = {
21
- typebox: _bundledTypebox,
22
- "typebox/compile": _bundledTypeboxCompile,
23
- "typebox/value": _bundledTypeboxValue,
24
- "@sinclair/typebox": _bundledTypebox,
25
- "@sinclair/typebox/compile": _bundledTypeboxCompile,
26
- "@sinclair/typebox/value": _bundledTypeboxValue,
27
- "@earendil-works/pi-agent-core": _bundledPiAgentCore,
28
- "@earendil-works/pi-tui": _bundledPiTui,
29
- "@earendil-works/pi-ai": _bundledPiAi,
30
- "@earendil-works/pi-ai/oauth": _bundledPiAiOauth,
31
- "@bastani/atomic": _bundledPiCodingAgent,
32
- "@mariozechner/pi-agent-core": _bundledPiAgentCore,
33
- "@mariozechner/pi-tui": _bundledPiTui,
34
- "@mariozechner/pi-ai": _bundledPiAi,
35
- "@mariozechner/pi-ai/oauth": _bundledPiAiOauth,
36
- };
37
8
  const require = createRequire(import.meta.url);
9
+ let _virtualModules = null;
10
+ let _virtualModulesPromise = null;
11
+ async function loadVirtualModules() {
12
+ const [typebox, typeboxCompile, typeboxValue, piAgentCore, piTui, piAi, piAiOauth, piCodingAgent] = await Promise.all([
13
+ import("typebox"),
14
+ import("typebox/compile"),
15
+ import("typebox/value"),
16
+ import("@earendil-works/pi-agent-core"),
17
+ import("@earendil-works/pi-tui"),
18
+ import("@earendil-works/pi-ai"),
19
+ import("@earendil-works/pi-ai/oauth"),
20
+ // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
21
+ // avoiding a circular dependency while preserving the package-name extension import path.
22
+ import("../../index.js"),
23
+ ]);
24
+ return {
25
+ typebox,
26
+ "typebox/compile": typeboxCompile,
27
+ "typebox/value": typeboxValue,
28
+ "@sinclair/typebox": typebox,
29
+ "@sinclair/typebox/compile": typeboxCompile,
30
+ "@sinclair/typebox/value": typeboxValue,
31
+ "@earendil-works/pi-agent-core": piAgentCore,
32
+ "@earendil-works/pi-tui": piTui,
33
+ "@earendil-works/pi-ai": piAi,
34
+ "@earendil-works/pi-ai/oauth": piAiOauth,
35
+ "@bastani/atomic": piCodingAgent,
36
+ "@mariozechner/pi-agent-core": piAgentCore,
37
+ "@mariozechner/pi-tui": piTui,
38
+ "@mariozechner/pi-ai": piAi,
39
+ "@mariozechner/pi-ai/oauth": piAiOauth,
40
+ };
41
+ }
42
+ /** Modules available to extensions via virtualModules (for compiled Bun binary). */
43
+ async function getVirtualModules() {
44
+ if (_virtualModules)
45
+ return _virtualModules;
46
+ _virtualModulesPromise ??= loadVirtualModules().then((virtualModules) => {
47
+ _virtualModules = virtualModules;
48
+ return virtualModules;
49
+ }, (error) => {
50
+ _virtualModulesPromise = null;
51
+ throw error;
52
+ });
53
+ return _virtualModulesPromise;
54
+ }
38
55
  let _aliases = null;
39
56
  let extensionCacheCwd;
40
57
  let extensionCacheGeneration = 0;
@@ -118,7 +135,7 @@ export async function loadExtensionModule(extensionPath, cacheToken) {
118
135
  const jiti = createJiti(import.meta.url, {
119
136
  moduleCache: false,
120
137
  ...(forceTransformedImports ? { fsCache: false, tryNative: false } : {}),
121
- ...(isBunBinary ? { virtualModules: VIRTUAL_MODULES } : { alias: getAliases() }),
138
+ ...(isBunBinary ? { virtualModules: await getVirtualModules() } : { alias: getAliases() }),
122
139
  });
123
140
  const module = await jiti.import(extensionImportSpecifier(extensionPath, cacheToken), { default: true });
124
141
  const factory = module;