@bastani/atomic 0.9.11-alpha.6 → 0.9.11-alpha.7

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 (140) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/package.json +1 -1
  5. package/dist/builtin/web-access/package.json +1 -1
  6. package/dist/builtin/workflows/package.json +1 -1
  7. package/dist/builtin/workflows/skills/impeccable/SKILL.md +33 -129
  8. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +11 -3
  9. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +26 -0
  10. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +35 -0
  11. package/dist/builtin/workflows/skills/impeccable/reference/android.md +1 -1
  12. package/dist/builtin/workflows/skills/impeccable/reference/animate.md +72 -189
  13. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +10 -9
  14. package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
  15. package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +19 -108
  16. package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +59 -253
  17. package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +51 -222
  18. package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +45 -0
  19. package/dist/builtin/workflows/skills/impeccable/reference/craft.md +3 -121
  20. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +28 -20
  21. package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +97 -0
  22. package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +23 -0
  23. package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +32 -0
  24. package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  25. package/dist/builtin/workflows/skills/impeccable/reference/delight.md +47 -279
  26. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
  27. package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +53 -0
  28. package/dist/builtin/workflows/skills/impeccable/reference/document.md +60 -73
  29. package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -12
  30. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +17 -4
  31. package/dist/builtin/workflows/skills/impeccable/reference/init.md +64 -163
  32. package/dist/builtin/workflows/skills/impeccable/reference/ios.md +1 -1
  33. package/dist/builtin/workflows/skills/impeccable/reference/layout.md +52 -153
  34. package/dist/builtin/workflows/skills/impeccable/reference/live.md +44 -32
  35. package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +105 -0
  36. package/dist/builtin/workflows/skills/impeccable/reference/{product.md → operate.md} +6 -5
  37. package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +4 -4
  38. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -4
  39. package/dist/builtin/workflows/skills/impeccable/reference/polish.md +68 -212
  40. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
  41. package/dist/builtin/workflows/skills/impeccable/reference/routing.md +18 -0
  42. package/dist/builtin/workflows/skills/impeccable/reference/shape.md +38 -144
  43. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +48 -269
  44. package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +38 -0
  45. package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +1 -1
  46. package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +584 -0
  47. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +117 -9
  48. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +486 -59
  49. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +16 -45
  50. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
  51. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +143 -26
  52. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +181 -12
  53. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +3187 -182
  54. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
  55. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +282 -70
  56. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +183 -15
  57. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +22 -7
  58. package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +7 -1
  59. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +16 -2
  60. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +155 -42
  61. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2988 -155
  62. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +11 -0
  63. package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +336 -0
  64. package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +232 -0
  65. package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +90 -10
  66. package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +410 -78
  67. package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +25 -8
  68. package/dist/builtin/workflows/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
  69. package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +165 -0
  70. package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +329 -0
  71. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +20 -5
  72. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +16 -8
  73. package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +1 -0
  74. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +455 -0
  75. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
  76. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +457 -0
  77. package/dist/builtin/workflows/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
  78. package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
  79. package/dist/builtin/workflows/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
  80. package/dist/builtin/workflows/skills/impeccable/scripts/live/completion.mjs +10 -1
  81. package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +15 -0
  82. package/dist/builtin/workflows/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
  83. package/dist/builtin/workflows/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
  84. package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +109 -32
  85. package/dist/builtin/workflows/skills/impeccable/scripts/live/source-lock.mjs +105 -0
  86. package/dist/builtin/workflows/skills/impeccable/scripts/live/source-search.mjs +105 -0
  87. package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +8 -6
  88. package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
  89. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +208 -68
  90. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +563 -112
  91. package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
  92. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +198 -13
  93. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +24 -6
  94. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
  95. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +44 -14
  96. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +398 -33
  97. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +9 -5
  98. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +81 -67
  99. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +7 -2
  100. package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +76 -81
  101. package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +2 -2
  102. package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +890 -0
  103. package/dist/builtin/workflows/skills/impeccable/scripts/surface-brief.mjs +74 -0
  104. package/dist/builtin/workflows/src/extension/index.bundle.mjs +201 -22
  105. package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +23 -9
  106. package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
  107. package/dist/builtin/workflows/src/runs/foreground/postmortem-stage-chat.ts +20 -5
  108. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +10 -0
  109. package/dist/builtin/workflows/src/runs/foreground/stage-delivery-activity.ts +86 -0
  110. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +6 -2
  111. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +19 -13
  112. package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +8 -0
  113. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +16 -0
  114. package/dist/builtin/workflows/src/tui/stage-chat-view-delivery-activity.ts +121 -0
  115. package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +42 -0
  116. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +16 -0
  117. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +4 -0
  118. package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
  119. package/dist/modes/interactive/components/chat-session-host-actions.js +2 -14
  120. package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
  121. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  122. package/dist/modes/interactive/components/chat-session-host-events.js +9 -0
  123. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  124. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +25 -0
  125. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
  126. package/dist/modes/interactive/components/chat-session-host-runtime.js +53 -0
  127. package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
  128. package/dist/modes/interactive/components/chat-session-host.d.ts +13 -0
  129. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/chat-session-host.js +22 -1
  131. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  132. package/dist/modes/interactive/interactive-agent-events.js +9 -3
  133. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  134. package/docs/tui.md +7 -2
  135. package/docs/workflows.md +1 -1
  136. package/npm-shrinkwrap.json +23 -23
  137. package/package.json +2 -2
  138. package/dist/builtin/workflows/skills/impeccable/reference/brand.md +0 -108
  139. package/dist/builtin/workflows/skills/impeccable/reference/codex.md +0 -105
  140. package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -189
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Internal stage delivery-activity channel.
3
+ *
4
+ * An accepted *idle* stage delivery starts a real backend turn long before the
5
+ * public `agent_start` clears the session's ordered extension event queue. The
6
+ * attached stage chat therefore had no signal to paint `Working` with, and the
7
+ * pane looked idle while the model was already running.
8
+ *
9
+ * This channel carries only that fact — "an idle delivery was admitted and is
10
+ * now starting a turn" — as a workflow-internal event, so the chat can start
11
+ * its existing Working lifecycle without fabricating SDK lifecycle events or
12
+ * reordering the public event stream.
13
+ *
14
+ * Streaming deliveries (`steer` / `followUp`) never emit here: they join an
15
+ * in-flight turn that already owns the indicator.
16
+ *
17
+ * cross-ref:
18
+ * - src/runs/foreground/stage-runner-send-user-message.ts (emitter)
19
+ * - src/tui/stage-chat-view-delivery-activity.ts (consumer)
20
+ */
21
+
22
+ export type StageDeliveryActivityEvent =
23
+ | { readonly type: "delivery_start"; readonly deliveryId: number }
24
+ | { readonly type: "delivery_settled"; readonly deliveryId: number };
25
+
26
+ export type StageDeliveryActivityListener = (event: StageDeliveryActivityEvent) => void;
27
+
28
+ export class StageDeliveryActivity {
29
+ private readonly listeners = new Set<StageDeliveryActivityListener>();
30
+ private readonly active = new Set<number>();
31
+ private nextDeliveryId = 0;
32
+
33
+ /**
34
+ * Register `listener`, then synchronously replay every currently-active
35
+ * delivery as `delivery_start`.
36
+ *
37
+ * A stage chat can attach after a delivery was admitted — a late `/workflow
38
+ * attach`, or a remount — and would otherwise see nothing until the public
39
+ * `agent_start` finally arrives. Registering before replaying matters:
40
+ * snapshotting first would drop a start or settlement that lands in between.
41
+ */
42
+ subscribe(listener: StageDeliveryActivityListener): () => void {
43
+ this.listeners.add(listener);
44
+ for (const deliveryId of [...this.active]) {
45
+ if (!this.listeners.has(listener)) break;
46
+ listener({ type: "delivery_start", deliveryId });
47
+ }
48
+ return () => {
49
+ this.listeners.delete(listener);
50
+ };
51
+ }
52
+
53
+ /** Report an admitted idle delivery that is about to start a turn. */
54
+ start(): number {
55
+ const deliveryId = ++this.nextDeliveryId;
56
+ this.active.add(deliveryId);
57
+ this.emit({ type: "delivery_start", deliveryId });
58
+ return deliveryId;
59
+ }
60
+
61
+ /** Keep one correlated delivery active for the full asynchronous operation. */
62
+ async runWithLease<T>(operation: () => Promise<T>): Promise<T> {
63
+ const deliveryId = this.start();
64
+ try {
65
+ return await operation();
66
+ } finally {
67
+ this.settle(deliveryId);
68
+ }
69
+ }
70
+
71
+ /** Report that a delivery finished, failed, or started no turn at all. */
72
+ settle(deliveryId: number | undefined): void {
73
+ if (deliveryId === undefined || !this.active.delete(deliveryId)) return;
74
+ this.emit({ type: "delivery_settled", deliveryId });
75
+ }
76
+
77
+ /** Settle every outstanding delivery, then drop listeners. */
78
+ dispose(): void {
79
+ for (const deliveryId of [...this.active]) this.settle(deliveryId);
80
+ this.listeners.clear();
81
+ }
82
+
83
+ private emit(event: StageDeliveryActivityEvent): void {
84
+ for (const listener of [...this.listeners]) listener(event);
85
+ }
86
+ }
@@ -110,8 +110,8 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
110
110
  await controller.sendUserMessage(text, options);
111
111
  },
112
112
 
113
- async __sendUserMessage(text, options, beforeDelivery) {
114
- return controller.sendUserMessage(text, options, beforeDelivery);
113
+ async __sendUserMessage(text, options, beforeDelivery, preparation) {
114
+ return controller.sendUserMessage(text, options, beforeDelivery, preparation);
115
115
  },
116
116
 
117
117
  async steer(text) {
@@ -126,6 +126,10 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
126
126
  return controller.subscribe(listener);
127
127
  },
128
128
 
129
+ __subscribeDeliveryActivity(listener) {
130
+ return controller.subscribeDeliveryActivity(listener);
131
+ },
132
+
129
133
  get sessionFile() {
130
134
  return controller.currentSession?.sessionFile;
131
135
  },
@@ -9,8 +9,9 @@ import { structuredOutputToolErrorFromEvent } from "./stage-runner-structured-ou
9
9
  import { buildStageSessionOptions } from "./stage-runner-session-options.js";
10
10
  import { sendStageUserMessage } from "./stage-runner-send-user-message.js";
11
11
  import { StageMessageAdmission } from "./stage-runner-message-admission.js";
12
+ import { StageDeliveryActivity, type StageDeliveryActivityListener } from "./stage-delivery-activity.js";
12
13
  import { nextResumedContextOverflowFallbackIndex, terminatingToolCallId, unresolvedContextOverflowFailure, unresolvedContextOverflowMessage } from "./stage-runner-unresolved-overflow.js";
13
- import type { AgentSessionConsumer, StageModelFallbackMeta, StageRunnerOpts, StageSessionCreateOptions, StageSessionCreateResult, StageSessionEvent, StageSessionRuntime, WorkflowFastModeSettingsManager } from "./stage-runner-types.js";
14
+ import type { AgentSessionConsumer, StageModelFallbackMeta, StageRunnerOpts, StageSessionCreateOptions, StageSessionCreateResult, StageSessionEvent, StageSessionRuntime, StageUserMessagePreparation, WorkflowFastModeSettingsManager } from "./stage-runner-types.js";
14
15
  import { StageSessionReplacement } from "./stage-runner-replacement.js";
15
16
  import { StageSessionPause, type StageSessionPauseResumeResult } from "./stage-runner-pause.js";
16
17
 
@@ -44,6 +45,7 @@ export class StageSessionController {
44
45
  private sessionSettingsManager: WorkflowFastModeSettingsManager | undefined;
45
46
  private readonly replacement = new StageSessionReplacement();
46
47
  private readonly messageAdmission = new StageMessageAdmission();
48
+ private readonly deliveryActivity = new StageDeliveryActivity();
47
49
 
48
50
  constructor(
49
51
  private readonly opts: StageRunnerOpts,
@@ -106,21 +108,25 @@ export class StageSessionController {
106
108
  return session;
107
109
  }
108
110
 
109
- async sendUserMessage(content: StageUserMessageContent, options?: StageSendUserMessageOptions, beforeDelivery?: () => void):
110
- Promise<Awaited<ReturnType<typeof sendStageUserMessage>>> {
111
+ async sendUserMessage(content: StageUserMessageContent, options?: StageSendUserMessageOptions,
112
+ beforeDelivery?: () => void, preparation?: StageUserMessagePreparation): Promise<Awaited<ReturnType<typeof sendStageUserMessage>>> {
111
113
  return this.messageAdmission.run(async (release) => {
112
- const pausedDelivery = this.pauseControl.deferRunnerOwnedDelivery(
113
- () => this.sendUserMessage(content, options, beforeDelivery),
114
- );
115
- if (pausedDelivery !== undefined) {
116
- release();
117
- return pausedDelivery;
118
- }
119
- return sendStageUserMessage(
120
- await this.ensureSession("prompt"), content, options, beforeDelivery, release, this.messageAdmission,
114
+ const pausedDelivery = this.pauseControl.deferRunnerOwnedDelivery(() =>
115
+ this.sendUserMessage(content, options, beforeDelivery, preparation));
116
+ if (pausedDelivery !== undefined) { release(); return pausedDelivery; }
117
+ preparation?.beforePreparation?.();
118
+ const sessionFile = preparation?.sessionFile;
119
+ const deliver = async (activity?: StageDeliveryActivity) => sendStageUserMessage(
120
+ sessionFile === undefined ? await this.ensureSession("prompt")
121
+ : await this.ensureSessionFromFile(sessionFile, "prompt"),
122
+ content, options, beforeDelivery, release, this.messageAdmission, activity,
121
123
  );
124
+ if (this.session === undefined || sessionFile !== undefined) return this.deliveryActivity.runWithLease(() => deliver());
125
+ return deliver(this.deliveryActivity);
122
126
  });
123
127
  }
128
+ /** Internal pre-stream delivery lifecycle consumed by an attached stage chat. */
129
+ subscribeDeliveryActivity(listener: StageDeliveryActivityListener): () => void { return this.deliveryActivity.subscribe(listener); }
124
130
 
125
131
  sealGeneration(): void {
126
132
  this.generationSealed = true;
@@ -201,7 +207,7 @@ export class StageSessionController {
201
207
  this.pendingListeners.clear();
202
208
  this.unsubscribeTerminateWatcher?.();
203
209
  this.unsubscribeTerminateWatcher = undefined;
204
- this.terminatingToolCallIds.clear(); this.messageAdmission.dispose();
210
+ this.terminatingToolCallIds.clear(); this.messageAdmission.dispose(); this.deliveryActivity.dispose();
205
211
  await this.replacement.dispose();
206
212
  await disposeStageSession(this.session);
207
213
  }
@@ -1,4 +1,5 @@
1
1
  import type { StageSendUserMessageOptions, StageUserMessageContent } from "../../shared/types.js";
2
+ import type { StageDeliveryActivity } from "./stage-delivery-activity.js";
2
3
  import type { StageMessageAdmission, StageMessageTurn } from "./stage-runner-message-admission.js";
3
4
  import type { StageSessionRuntime, StageUserMessageDeliveryAction } from "./stage-runner-types.js";
4
5
 
@@ -59,6 +60,7 @@ export async function sendStageUserMessage(
59
60
  beforeDelivery?: () => void,
60
61
  promptStarted?: () => void,
61
62
  admission?: StageMessageAdmission,
63
+ activity?: StageDeliveryActivity,
62
64
  ): Promise<StageUserMessageDeliveryAction> {
63
65
  const streaming = activeSession.isStreaming || admission?.isOwned(activeSession) === true;
64
66
  const deliverAs = streaming ? options?.deliverAs ?? "followUp" : options?.deliverAs;
@@ -71,6 +73,9 @@ export async function sendStageUserMessage(
71
73
  ? createLocalPromptOwnershipObserver(activeSession, promptStarted)
72
74
  : coordinatedPromptOwnershipObserver(admission.startTurn(activeSession, promptStarted ?? (() => {})));
73
75
  ownership?.arm();
76
+ // An admitted idle delivery owns the visible Working period from here until
77
+ // it settles; a streaming delivery joins a turn that already owns it.
78
+ const deliveryId = streaming ? undefined : activity?.start();
74
79
  try {
75
80
  const delivery = activeSession.sendUserMessage(content, {
76
81
  ...(deliverAs === undefined ? {} : { deliverAs }),
@@ -89,6 +94,7 @@ export async function sendStageUserMessage(
89
94
  throw error;
90
95
  } finally {
91
96
  ownership?.dispose();
97
+ activity?.settle(deliveryId);
92
98
  }
93
99
  }
94
100
  if (typeof content !== "string") throw unsupportedContentError();
@@ -102,6 +108,7 @@ export async function sendStageUserMessage(
102
108
  ? createLocalPromptOwnershipObserver(activeSession, promptStarted)
103
109
  : coordinatedPromptOwnershipObserver(admission.startTurn(activeSession, promptStarted ?? (() => {})));
104
110
  ownership.arm();
111
+ const deliveryId = activity?.start();
105
112
  try {
106
113
  const turn = activeSession.prompt(content);
107
114
  ownership.observeStreaming();
@@ -113,5 +120,6 @@ export async function sendStageUserMessage(
113
120
  throw error;
114
121
  } finally {
115
122
  ownership.dispose();
123
+ activity?.settle(deliveryId);
116
124
  }
117
125
  }
@@ -29,6 +29,12 @@ export type WorkflowFastModeSettingsManager = {
29
29
 
30
30
  export type StageUserMessageDeliveryAction = "prompt" | "steer" | "followUp" | "handled";
31
31
 
32
+ /** Internal session preparation and side-effect-free eligibility boundary. */
33
+ export interface StageUserMessagePreparation {
34
+ readonly sessionFile?: string;
35
+ readonly beforePreparation?: () => void;
36
+ }
37
+
32
38
  export interface StageUserMessageDeliveryHooks {
33
39
  readonly beforeDelivery?: () => void;
34
40
  /** Releases serialized admission once an idle prompt synchronously owns the turn. */
@@ -152,7 +158,17 @@ export interface InternalStageContext extends StageContext {
152
158
  content: StageUserMessageContent,
153
159
  options?: StageSendUserMessageOptions,
154
160
  beforeDelivery?: () => void,
161
+ preparation?: StageUserMessagePreparation,
155
162
  ): Promise<StageUserMessageDeliveryAction>;
163
+ /**
164
+ * Internal: subscribe to workflow-owned delivery lifecycle facts for this
165
+ * stage. An accepted idle delivery reports `delivery_start` before the public
166
+ * `agent_start` reaches subscribers, so an attached chat can paint Working
167
+ * for the whole turn rather than only after the SDK event queue drains.
168
+ */
169
+ __subscribeDeliveryActivity(
170
+ listener: (event: import("./stage-delivery-activity.js").StageDeliveryActivityEvent) => void,
171
+ ): () => void;
156
172
  /** Internal: synchronously reject new detached traffic without waiting for active work. */
157
173
  __sealGeneration(): void;
158
174
  /** Internal: atomically stop detached traffic admission and drain admitted work. */
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Maps workflow-owned stage delivery lifecycle facts onto the attached chat's
3
+ * existing Working lifecycle.
4
+ *
5
+ * A workflow definition that auto-sends to its own retained stage starts a real
6
+ * backend turn well before the public `agent_start` reaches this view. Without
7
+ * this bridge the pane sits with `sdkBusy=false` and no requested render while
8
+ * the model is already running, so it looks idle.
9
+ *
10
+ * Starting the host's ordinary lifecycle here — rather than synthesising an SDK
11
+ * event — keeps `agent_start`, `turn_start`, `turn_end`, `agent_end`, retry,
12
+ * fallback, compaction, and terminal cleanup in charge of everything after
13
+ * startup, and lets a settled delivery that never produced a turn clean up.
14
+ *
15
+ * cross-ref: src/runs/foreground/stage-delivery-activity.ts
16
+ */
17
+ import type { StageDeliveryActivityEvent } from "../runs/foreground/stage-delivery-activity.js";
18
+ import { isTerminalStageChatState } from "./stage-chat-view-status.js";
19
+ import type { StageChatViewContext } from "./stage-chat-view-types.js";
20
+
21
+ export function subscribeStageChatDeliveryActivity(
22
+ ctx: StageChatViewContext,
23
+ ): (() => void) | null {
24
+ const handle = ctx.handle;
25
+ if (!handle?.subscribeDeliveryActivity) return null;
26
+ return handle.subscribeDeliveryActivity((event) => {
27
+ applyStageChatDeliveryActivityEvent(ctx, event);
28
+ });
29
+ }
30
+
31
+ export function applyStageChatDeliveryActivityEvent(
32
+ ctx: StageChatViewContext,
33
+ event: StageDeliveryActivityEvent,
34
+ ): void {
35
+ if (event.type === "delivery_start") {
36
+ // Replaces any lifecycle this same delivery id already owned; the map keeps
37
+ // one live token per delivery so settlement stays correlated. A start that
38
+ // arrives after terminal cleanup is fresh authorization for a retained
39
+ // post-mortem turn, so it also lifts the stale-start fence.
40
+ ctx.terminalLifecycleFenced = false;
41
+ ctx.deliveryLifecycles.set(event.deliveryId, ctx.chatHost.beginExternalPromptLifecycle());
42
+ return;
43
+ }
44
+ if (!ctx.deliveryLifecycles.has(event.deliveryId)) return;
45
+ const generation = ctx.deliveryLifecycles.get(event.deliveryId);
46
+ ctx.deliveryLifecycles.delete(event.deliveryId);
47
+ // A pre-turn compaction reasserts one lifecycle and hands the same token to
48
+ // every lease still open, so several deliveries can alias one generation.
49
+ // Only the last owner may settle it; otherwise the first lease to finish
50
+ // would stop Working while another accepted delivery is still running.
51
+ // Distinct generations — including stale ones — keep reaching the host's own
52
+ // generation fence exactly as before.
53
+ if (!ownsStageChatLifecycleGeneration(ctx, generation)) {
54
+ ctx.chatHost.settleExternalPromptLifecycle(generation);
55
+ }
56
+ // The map is the reference count: overlapping leases keep authorization open,
57
+ // and the last one to settle on a finished stage restores stale-start
58
+ // suppression. Otherwise the fence stayed open forever after the first
59
+ // post-terminal delivery and a late start could repaint a settled chat.
60
+ if (!hasActiveStageChatDelivery(ctx) && isStageChatContextTerminal(ctx)) {
61
+ ctx.terminalLifecycleFenced = true;
62
+ }
63
+ }
64
+
65
+ /** True while another still-active delivery references this exact lifecycle token. */
66
+ function ownsStageChatLifecycleGeneration(
67
+ ctx: StageChatViewContext,
68
+ generation: number | undefined,
69
+ ): boolean {
70
+ for (const active of ctx.deliveryLifecycles.values()) {
71
+ if (active === generation) return true;
72
+ }
73
+ return false;
74
+ }
75
+
76
+ /** True while at least one admitted delivery still owns this chat's Working period. */
77
+ export function hasActiveStageChatDelivery(ctx: StageChatViewContext): boolean {
78
+ return ctx.deliveryLifecycles.size > 0;
79
+ }
80
+
81
+ /** True when the last observed run or stage status is terminal. */
82
+ export function isStageChatContextTerminal(ctx: StageChatViewContext): boolean {
83
+ return (
84
+ isTerminalStageChatState(ctx.lastObservedRunStatus) ||
85
+ isTerminalStageChatState(ctx.lastObservedStageStatus)
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Restore delivery-owned Working after a temporary overlay ended.
91
+ *
92
+ * A pre-turn `compaction_end` clears busy state and the Working lifecycle even
93
+ * though the accepted delivery has not started its turn yet, so the pane would
94
+ * sit idle until the public `agent_start`. Reassertion preserves factual status
95
+ * text, so a compaction error still outranks ordinary Working.
96
+ */
97
+ export function reconcileStageChatDeliveryLifecycle(ctx: StageChatViewContext): void {
98
+ if (!hasActiveStageChatDelivery(ctx)) return;
99
+ if (ctx.chatHost.isStreaming() && ctx.chatHost.hasAnimationTick()) return;
100
+ const generation = ctx.chatHost.reassertExternalPromptLifecycle();
101
+ for (const deliveryId of [...ctx.deliveryLifecycles.keys()]) {
102
+ ctx.deliveryLifecycles.set(deliveryId, generation);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Drop every delivery lease admitted before a workflow terminal transition and
108
+ * arm the stale-start fence.
109
+ *
110
+ * Those turns can no longer authorize activity on this chat, and keeping them
111
+ * would let a queued late `agent_start` restart Working on a finished stage. A
112
+ * settlement for a dropped id is then an ordinary unknown settlement.
113
+ *
114
+ * The fence is armed only by an observed transition. A chat that mounts onto an
115
+ * already-terminal stage never cleaned up in-flight work, so a later
116
+ * `agent_start` there is genuine retained-session work and stays visible.
117
+ */
118
+ export function invalidateStageChatDeliveryLifecycles(ctx: StageChatViewContext): void {
119
+ ctx.deliveryLifecycles.clear();
120
+ ctx.terminalLifecycleFenced = true;
121
+ }
@@ -1,18 +1,60 @@
1
1
  import type { AgentSessionEvent } from "@bastani/atomic";
2
2
  import type { StageChatViewContext } from "./stage-chat-view-types.js";
3
3
  import { isTerminalStageChatState } from "./stage-chat-view-status.js";
4
+ import {
5
+ hasActiveStageChatDelivery,
6
+ reconcileStageChatDeliveryLifecycle,
7
+ } from "./stage-chat-view-delivery-activity.js";
4
8
 
5
9
  export function applyStageChatLiveHandleEvent(
6
10
  ctx: StageChatViewContext,
7
11
  event: AgentSessionEvent,
8
12
  ): void {
13
+ const staleTerminalStart = isStaleTerminalLifecycleStart(ctx, event);
9
14
  ctx.chatHost.applyAgentEvent(event);
15
+ if (staleTerminalStart) {
16
+ retainTerminalCleanup(ctx);
17
+ return;
18
+ }
19
+ if (isCompactionEndEvent(event)) reconcileStageChatDeliveryLifecycle(ctx);
10
20
  if (!shouldCleanupAfterLiveEvent(ctx, event)) return;
21
+ retainTerminalCleanup(ctx);
22
+ }
23
+
24
+ function retainTerminalCleanup(ctx: StageChatViewContext): void {
11
25
  const hadAnimationTick = ctx.chatHost.hasAnimationTick();
12
26
  ctx.chatHost.clearBusyForTerminalWorkflowStage();
13
27
  if (hadAnimationTick !== ctx.chatHost.hasAnimationTick()) ctx.requestRender?.();
14
28
  }
15
29
 
30
+ /**
31
+ * A turn start queued before the stage finished must not repaint Working on a
32
+ * settled stage.
33
+ *
34
+ * The fence is armed only when this chat actually observed a terminal
35
+ * transition and cleaned up in-flight work. Work admitted after that carries
36
+ * its own authorization and is left alone: a live workflow delivery lease
37
+ * (including one replayed at attach time), or a prompt this chat submitted
38
+ * itself, which is already busy by the time its `agent_start` arrives.
39
+ * `turn_start` is guarded too, otherwise it would restart the lifecycle a
40
+ * suppressed `agent_start` just stopped.
41
+ */
42
+ function isStaleTerminalLifecycleStart(
43
+ ctx: StageChatViewContext,
44
+ event: AgentSessionEvent,
45
+ ): boolean {
46
+ if (!ctx.terminalLifecycleFenced) return false;
47
+ const type = String((event as { type?: unknown }).type ?? "");
48
+ if (type !== "agent_start" && type !== "turn_start") return false;
49
+ if (hasActiveStageChatDelivery(ctx)) return false;
50
+ if (ctx.chatHost.isStreaming()) return false;
51
+ return isCurrentRunOrStageTerminal(ctx);
52
+ }
53
+
54
+ function isCompactionEndEvent(event: AgentSessionEvent): boolean {
55
+ return String((event as { type?: unknown }).type ?? "") === "compaction_end";
56
+ }
57
+
16
58
  function shouldCleanupAfterLiveEvent(
17
59
  ctx: StageChatViewContext,
18
60
  event: AgentSessionEvent,
@@ -18,6 +18,10 @@ import {
18
18
  } from "./stage-chat-view-types.js";
19
19
  import { noticeRow, noticeSummary } from "./stage-chat-view-transcript.js";
20
20
  import { applyStageChatLiveHandleEvent } from "./stage-chat-view-live-events.js";
21
+ import {
22
+ invalidateStageChatDeliveryLifecycles,
23
+ subscribeStageChatDeliveryActivity,
24
+ } from "./stage-chat-view-delivery-activity.js";
21
25
  import { replayPendingToolExecutions } from "./stage-chat-view-pending-tools.js";
22
26
  import { chatHostStyle, stageChatRenderSettings } from "./stage-chat-view-render-settings.js";
23
27
  import { hexToAnsi, RESET } from "./color-utils.js";
@@ -66,8 +70,11 @@ export function initializeStageChatView(
66
70
  ctx.lastObservedStageStatus = undefined;
67
71
  ctx.lastObservedRunStatus = undefined;
68
72
  ctx.seenNoticeIds = new Set<string>();
73
+ ctx.deliveryLifecycles = new Map<number, number | undefined>();
74
+ ctx.terminalLifecycleFenced = false;
69
75
  ctx._unsubscribeStore = null;
70
76
  ctx._unsubscribeHandle = null;
77
+ ctx._unsubscribeDeliveryActivity = null;
71
78
  ctx._unsubscribeFooterData = null;
72
79
  ctx._unregisterStageUiHost = null;
73
80
  installFocusHold(ctx);
@@ -98,6 +105,10 @@ export function initializeStageChatView(
98
105
  ctx._unsubscribeFooterData = ctx.footerData?.onBranchChange(() => ctx.requestRender?.()) ?? null;
99
106
 
100
107
  if (ctx.handle) {
108
+ // Delivery activity first: its replay must be in place before the SDK
109
+ // stream, so a synchronously replayed `agent_start` on a retained terminal
110
+ // stage can see that a live delivery authorizes it.
111
+ ctx._unsubscribeDeliveryActivity = subscribeStageChatDeliveryActivity(ctx);
101
112
  ctx._unsubscribeHandle = ctx.handle.subscribe((event) => applyStageChatLiveHandleEvent(ctx, event));
102
113
  }
103
114
  ctx.chatHost.syncAnimationTick();
@@ -209,6 +220,8 @@ function handleStoreUpdate(ctx: StageChatViewContext): void {
209
220
  }
210
221
  if (isTerminalStageChatTransition(ctx.lastObservedStageStatus, currentStageStatus) || isTerminalStageChatTransition(ctx.lastObservedRunStatus, currentRunStatus)) {
211
222
  ctx.chatHost.clearBusyForTerminalWorkflowStage();
223
+ // Leases admitted before this transition can no longer authorize activity.
224
+ invalidateStageChatDeliveryLifecycles(ctx);
212
225
  changed = true;
213
226
  }
214
227
  ctx.lastObservedRunStatus = currentRunStatus;
@@ -468,6 +481,9 @@ export function disposeStageChatView(ctx: StageChatViewContext): void {
468
481
  ctx._unsubscribeStore = null;
469
482
  ctx._unsubscribeHandle?.();
470
483
  ctx._unsubscribeHandle = null;
484
+ ctx._unsubscribeDeliveryActivity?.();
485
+ ctx._unsubscribeDeliveryActivity = null;
486
+ ctx.deliveryLifecycles.clear();
471
487
  ctx._unsubscribeFooterData?.();
472
488
  ctx._unsubscribeFooterData = null;
473
489
  releaseMountedCustomUi(ctx);
@@ -162,8 +162,12 @@ export interface StageChatViewContext {
162
162
  lastObservedStageStatus: StageStatus | undefined;
163
163
  lastObservedRunStatus: RunStatus | undefined;
164
164
  seenNoticeIds: Set<string>;
165
+ deliveryLifecycles: Map<number, number | undefined>;
166
+ /** True once this chat observed a terminal transition and cleaned up in-flight work. */
167
+ terminalLifecycleFenced: boolean;
165
168
  _unsubscribeStore: (() => void) | null;
166
169
  _unsubscribeHandle: (() => void) | null;
170
+ _unsubscribeDeliveryActivity: (() => void) | null;
167
171
  _unsubscribeFooterData: (() => void) | null;
168
172
  _unregisterStageUiHost: (() => void) | null;
169
173
  }
@@ -1 +1 @@
1
- {"version":3,"file":"chat-session-host-actions.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/chat-session-host-actions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAcpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAczE,wBAAgB,oBAAoB,CAClC,WAAW,SAAS,uBAAuB,EAC3C,KAAK,EAAE,oBAAoB,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BzD;AA4BD,wBAAsB,iBAAiB,CAAC,WAAW,SAAS,uBAAuB,EACjF,KAAK,EAAE,oBAAoB,CAAC,WAAW,CAAC,EACxC,IAAI,GAAE,MAAM,GAAG,UAAmB,EAClC,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CA8Gf;AAoBD,wBAAsB,0BAA0B,CAC9C,WAAW,SAAS,uBAAuB,EAC3C,KAAK,EAAE,oBAAoB,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAYzD;AAED,wBAAsB,oBAAoB,CAAC,WAAW,SAAS,uBAAuB,EACpF,KAAK,EAAE,oBAAoB,CAAC,WAAW,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,wBAAsB,+BAA+B,CACnD,WAAW,SAAS,uBAAuB,EAC3C,KAAK,EAAE,oBAAoB,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBzD;AAED,wBAAgB,6BAA6B,CAC3C,WAAW,SAAS,uBAAuB,EAC3C,KAAK,EAAE,oBAAoB,CAAC,WAAW,CAAC,GAAG,OAAO,CAkBnD"}
1
+ {"version":3,"file":"chat-session-host-actions.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/chat-session-host-actions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAepE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAczE,wBAAgB,oBAAoB,CAClC,WAAW,SAAS,uBAAuB,EAC3C,KAAK,EAAE,oBAAoB,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BzD;AA4BD,wBAAsB,iBAAiB,CAAC,WAAW,SAAS,uBAAuB,EACjF,KAAK,EAAE,oBAAoB,CAAC,WAAW,CAAC,EACxC,IAAI,GAAE,MAAM,GAAG,UAAmB,EAClC,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CA8Gf;AAID,wBAAsB,0BAA0B,CAC9C,WAAW,SAAS,uBAAuB,EAC3C,KAAK,EAAE,oBAAoB,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAYzD;AAED,wBAAsB,oBAAoB,CAAC,WAAW,SAAS,uBAAuB,EACpF,KAAK,EAAE,oBAAoB,CAAC,WAAW,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,wBAAsB,+BAA+B,CACnD,WAAW,SAAS,uBAAuB,EAC3C,KAAK,EAAE,oBAAoB,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBzD;AAED,wBAAgB,6BAA6B,CAC3C,WAAW,SAAS,uBAAuB,EAC3C,KAAK,EAAE,oBAAoB,CAAC,WAAW,CAAC,GAAG,OAAO,CAkBnD"}
@@ -1,6 +1,6 @@
1
1
  import { combineQueuedMessagesForEditor } from "../chat-input-actions.js";
2
2
  import { setChatSessionEditorText } from "./chat-session-host-editor.js";
3
- import { decrementOptimisticUserSignature, incrementOptimisticUserSignature, isChatSessionBashRunning, isChatSessionStreaming, notifyChatSessionStatus, notifyChatSessionWarning, requiredChatSessionCommand, startChatSessionWorkingLifecycle, stopChatSessionWorkingLifecycle, syncChatSessionAnimationTick, } from "./chat-session-host-runtime.js";
3
+ import { decrementOptimisticUserSignature, incrementOptimisticUserSignature, isChatSessionBashRunning, isChatSessionStreaming, notifyChatSessionStatus, notifyChatSessionWarning, requiredChatSessionCommand, settleChatSessionPromptLifecycle, startChatSessionWorkingLifecycle, stopChatSessionWorkingLifecycle, syncChatSessionAnimationTick, } from "./chat-session-host-runtime.js";
4
4
  import { errorMessage, parseBashInput, userMessageSignature, } from "./chat-session-host-utils.js";
5
5
  function supportsQueuedMessagePause(session) {
6
6
  return typeof session?.pauseQueuedMessages === "function"
@@ -176,19 +176,7 @@ export async function submitChatSession(state, mode = "auto", submittedText) {
176
176
  state.requestRender?.();
177
177
  }
178
178
  }
179
- function settleSubmittedPromptLifecycle(state, submittedGeneration) {
180
- if (submittedGeneration === undefined) {
181
- state.sdkBusy = false;
182
- return;
183
- }
184
- const lifecycleWasReplaced = state.workingLifecycleGeneration !== submittedGeneration;
185
- if (lifecycleWasReplaced && state.workingLifecycleActive)
186
- return;
187
- state.sdkBusy = false;
188
- if (!lifecycleWasReplaced && !isChatSessionStreaming(state)) {
189
- stopChatSessionWorkingLifecycle(state);
190
- }
191
- }
179
+ const settleSubmittedPromptLifecycle = settleChatSessionPromptLifecycle;
192
180
  export async function abortChatSessionCompaction(state) {
193
181
  try {
194
182
  await state.commands.abortCompaction?.();