@bastani/atomic 0.8.19 → 0.8.20

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 (103) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/CHANGELOG.md +10 -0
  4. package/dist/builtin/mcp/package.json +2 -2
  5. package/dist/builtin/subagents/CHANGELOG.md +17 -2
  6. package/dist/builtin/subagents/agents/code-simplifier.md +1 -1
  7. package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
  8. package/dist/builtin/subagents/agents/codebase-online-researcher.md +1 -1
  9. package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
  10. package/dist/builtin/subagents/agents/debugger.md +1 -1
  11. package/dist/builtin/subagents/package.json +1 -1
  12. package/dist/builtin/subagents/skills/subagent/SKILL.md +12 -12
  13. package/dist/builtin/subagents/src/agents/agent-management.ts +16 -11
  14. package/dist/builtin/subagents/src/agents/skills.ts +13 -1
  15. package/dist/builtin/subagents/src/extension/index.ts +14 -3
  16. package/dist/builtin/subagents/src/runs/background/async-execution.ts +8 -0
  17. package/dist/builtin/subagents/src/runs/background/run-status.ts +2 -3
  18. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +11 -1
  19. package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +2 -2
  20. package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +31 -23
  21. package/dist/builtin/subagents/src/runs/foreground/execution.ts +13 -7
  22. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +160 -93
  23. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
  24. package/dist/builtin/subagents/src/runs/shared/run-history.ts +1 -1
  25. package/dist/builtin/subagents/src/shared/settings.ts +1 -0
  26. package/dist/builtin/subagents/src/shared/types.ts +78 -4
  27. package/dist/builtin/subagents/src/tui/render.ts +203 -19
  28. package/dist/builtin/web-access/CHANGELOG.md +10 -0
  29. package/dist/builtin/web-access/package.json +2 -2
  30. package/dist/builtin/workflows/CHANGELOG.md +25 -0
  31. package/dist/builtin/workflows/README.md +22 -3
  32. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +1 -1
  33. package/dist/builtin/workflows/builtin/open-claude-design.ts +12 -4
  34. package/dist/builtin/workflows/builtin/ralph.ts +2 -2
  35. package/dist/builtin/workflows/package.json +1 -1
  36. package/dist/builtin/workflows/src/extension/config-loader.ts +68 -0
  37. package/dist/builtin/workflows/src/extension/index.ts +246 -55
  38. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +372 -0
  39. package/dist/builtin/workflows/src/extension/render-call.ts +1 -1
  40. package/dist/builtin/workflows/src/extension/wiring.ts +32 -3
  41. package/dist/builtin/workflows/src/runs/background/status.ts +14 -74
  42. package/dist/builtin/workflows/src/shared/persistence-restore.ts +5 -3
  43. package/dist/builtin/workflows/src/tui/chat-surface-message.ts +3 -13
  44. package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +2 -10
  45. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +5 -5
  46. package/dist/builtin/workflows/src/tui/session-confirm.ts +6 -7
  47. package/dist/builtin/workflows/src/tui/session-picker.ts +18 -14
  48. package/dist/builtin/workflows/src/tui/status-list.ts +2 -2
  49. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +125 -30
  50. package/dist/config.d.ts +1 -0
  51. package/dist/config.d.ts.map +1 -1
  52. package/dist/config.js +1 -0
  53. package/dist/config.js.map +1 -1
  54. package/dist/core/agent-session.d.ts +4 -1
  55. package/dist/core/agent-session.d.ts.map +1 -1
  56. package/dist/core/agent-session.js +2 -1
  57. package/dist/core/agent-session.js.map +1 -1
  58. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  59. package/dist/core/atomic-guide-command.js +3 -2
  60. package/dist/core/atomic-guide-command.js.map +1 -1
  61. package/dist/core/extensions/index.d.ts +1 -1
  62. package/dist/core/extensions/index.d.ts.map +1 -1
  63. package/dist/core/extensions/index.js.map +1 -1
  64. package/dist/core/extensions/runner.d.ts +3 -2
  65. package/dist/core/extensions/runner.d.ts.map +1 -1
  66. package/dist/core/extensions/runner.js +6 -1
  67. package/dist/core/extensions/runner.js.map +1 -1
  68. package/dist/core/extensions/types.d.ts +13 -0
  69. package/dist/core/extensions/types.d.ts.map +1 -1
  70. package/dist/core/extensions/types.js.map +1 -1
  71. package/dist/core/model-resolver.d.ts.map +1 -1
  72. package/dist/core/model-resolver.js +63 -17
  73. package/dist/core/model-resolver.js.map +1 -1
  74. package/dist/core/output-guard.d.ts.map +1 -1
  75. package/dist/core/output-guard.js +29 -0
  76. package/dist/core/output-guard.js.map +1 -1
  77. package/dist/core/sdk.d.ts +3 -1
  78. package/dist/core/sdk.d.ts.map +1 -1
  79. package/dist/core/sdk.js +1 -0
  80. package/dist/core/sdk.js.map +1 -1
  81. package/dist/core/system-prompt.d.ts.map +1 -1
  82. package/dist/core/system-prompt.js +1 -1
  83. package/dist/core/system-prompt.js.map +1 -1
  84. package/dist/index.d.ts +2 -2
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +1 -1
  87. package/dist/index.js.map +1 -1
  88. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  89. package/dist/modes/interactive/interactive-mode.js +46 -13
  90. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  91. package/dist/utils/pi-user-agent.d.ts.map +1 -1
  92. package/dist/utils/pi-user-agent.js +2 -1
  93. package/dist/utils/pi-user-agent.js.map +1 -1
  94. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  95. package/dist/utils/syntax-highlight.js +1 -1
  96. package/dist/utils/syntax-highlight.js.map +1 -1
  97. package/dist/utils/tools-manager.d.ts.map +1 -1
  98. package/dist/utils/tools-manager.js +3 -5
  99. package/dist/utils/tools-manager.js.map +1 -1
  100. package/docs/models.md +52 -52
  101. package/docs/quickstart.md +2 -2
  102. package/docs/workflows.md +22 -5
  103. package/package.json +9 -9
@@ -18,6 +18,7 @@ export interface RunnerSubagentStep {
18
18
  outputMode?: "inline" | "file-only";
19
19
  sessionFile?: string;
20
20
  maxSubagentDepth?: number;
21
+ workflowStageSubagentGuard?: boolean;
21
22
  }
22
23
 
23
24
  export interface ParallelStepGroup {
@@ -54,6 +54,6 @@ export function loadRunsForAgent(agent: string): RunEntry[] {
54
54
 
55
55
  return lines
56
56
  .map((line) => { try { return JSON.parse(line) as RunEntry; } catch { return undefined; } })
57
- .filter((entry): entry is RunEntry => Boolean(entry) && entry.agent === agent)
57
+ .filter((entry): entry is RunEntry => entry !== undefined && entry.agent === agent)
58
58
  .reverse();
59
59
  }
@@ -70,6 +70,7 @@ interface ParallelTaskItem {
70
70
  /** Parallel step: multiple agents running concurrently */
71
71
  interface ParallelStep {
72
72
  parallel: ParallelTaskItem[];
73
+ cwd?: string;
73
74
  concurrency?: number;
74
75
  failFast?: boolean;
75
76
  worktree?: boolean;
@@ -5,13 +5,15 @@
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
7
  import type { Message } from "@earendil-works/pi-ai";
8
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
8
9
  import type { FSWatcher } from "node:fs";
9
10
  import type { ExtensionContext } from "@bastani/atomic";
10
- import { APP_NAME, getEnvValue } from "@bastani/atomic";
11
+ import { APP_NAME, getEnvValue, WORKFLOW_STAGE_SUBAGENT_GUARD_ENV } from "@bastani/atomic";
11
12
 
12
13
  const ENV_PREFIX = APP_NAME.toUpperCase();
13
14
  const SUBAGENT_MAX_DEPTH_ENV = `${ENV_PREFIX}_SUBAGENT_MAX_DEPTH`;
14
15
  const SUBAGENT_DEPTH_ENV = `${ENV_PREFIX}_SUBAGENT_DEPTH`;
16
+ export { WORKFLOW_STAGE_SUBAGENT_GUARD_ENV };
15
17
 
16
18
  // ============================================================================
17
19
  // Basic Types
@@ -254,6 +256,9 @@ export interface Details {
254
256
  currentStepIndex?: number; // 0-indexed current step (for running chains)
255
257
  }
256
258
 
259
+ // Upstream AgentToolResult omits the runtime isError flag that subagent tool results still emit/read.
260
+ export type SubagentToolResult = AgentToolResult<Details> & { isError?: boolean };
261
+
257
262
  // ============================================================================
258
263
  // Artifacts
259
264
  // ============================================================================
@@ -572,6 +577,7 @@ export interface RunSyncOptions {
572
577
  outputPath?: string;
573
578
  outputMode?: OutputMode;
574
579
  maxSubagentDepth?: number;
580
+ workflowStageSubagentGuard?: boolean;
575
581
  nestedRoute?: NestedRouteInfo;
576
582
  /** Override the agent's default model (format: "provider/id" or just "id") */
577
583
  modelOverride?: string;
@@ -758,19 +764,87 @@ export function resolveChildMaxSubagentDepth(parentMaxDepth: number, agentMaxDep
758
764
  return normalizedAgent === undefined ? normalizedParent : Math.min(normalizedParent, normalizedAgent);
759
765
  }
760
766
 
761
- export function checkSubagentDepth(configMaxDepth?: number): { blocked: boolean; depth: number; maxDepth: number } {
767
+ export function hasWorkflowStageSubagentGuard(): boolean {
768
+ return getEnvValue(WORKFLOW_STAGE_SUBAGENT_GUARD_ENV) === "1";
769
+ }
770
+
771
+ export function isWorkflowStageOrchestrationContext(ctx: Pick<ExtensionContext, "orchestrationContext">): boolean {
772
+ return ctx.orchestrationContext?.kind === "workflow-stage";
773
+ }
774
+
775
+ export function resolveWorkflowStageMaxSubagentDepth(
776
+ ctx: Pick<ExtensionContext, "orchestrationContext">,
777
+ configMaxDepth?: number,
778
+ ): number {
779
+ const maxDepth = resolveCurrentMaxSubagentDepth(configMaxDepth);
780
+ return isWorkflowStageOrchestrationContext(ctx)
781
+ // Workflow stages reserve one child-subagent hop; a 0-depth constraint would
782
+ // prevent the stage from delegating to its configured subagent at all.
783
+ ? Math.min(maxDepth, Math.max(1, ctx.orchestrationContext?.constraints.maxSubagentDepth ?? 1))
784
+ : maxDepth;
785
+ }
786
+
787
+ export interface SubagentDepthPolicy {
788
+ maxSubagentDepth: number;
789
+ workflowStageSubagentGuard: boolean;
790
+ }
791
+
792
+ export function resolveSubagentDepthPolicy(
793
+ ctx: Pick<ExtensionContext, "orchestrationContext">,
794
+ configMaxDepth?: number,
795
+ ): SubagentDepthPolicy {
796
+ return {
797
+ maxSubagentDepth: resolveWorkflowStageMaxSubagentDepth(ctx, configMaxDepth),
798
+ workflowStageSubagentGuard: isWorkflowStageOrchestrationContext(ctx),
799
+ };
800
+ }
801
+
802
+ function workflowStageSubagentDepthMessage(depth: number, maxDepth: number, action: "call" | "resume" = "call"): string {
803
+ return `Nested subagent ${action} blocked (depth=${depth}, max=${maxDepth}). Sub-agents inside workflow stages cannot spawn nested sub-agents.`;
804
+ }
805
+
806
+ export function subagentDepthBlockedMessage(
807
+ depth: number,
808
+ maxDepth: number,
809
+ options?: { action?: "call" | "resume"; workflowStageGuard?: boolean },
810
+ ): string {
811
+ const action = options?.action ?? "call";
812
+ if (options?.workflowStageGuard) {
813
+ return workflowStageSubagentDepthMessage(depth, maxDepth, action);
814
+ }
815
+ if (action === "resume") {
816
+ return `Nested subagent resume blocked (depth=${depth}, max=${maxDepth}). Complete the follow-up directly instead.`;
817
+ }
818
+ return `Nested subagent call blocked (depth=${depth}, max=${maxDepth}). ` +
819
+ "You are running at the maximum subagent nesting depth. " +
820
+ "Complete your current task directly without delegating to further subagents.";
821
+ }
822
+
823
+ export interface SubagentDepthCheck {
824
+ blocked: boolean;
825
+ depth: number;
826
+ maxDepth: number;
827
+ workflowStageGuard: boolean;
828
+ }
829
+
830
+ export function checkSubagentDepth(configMaxDepth?: number): SubagentDepthCheck {
762
831
  const depth = Number(getEnvValue(SUBAGENT_DEPTH_ENV) ?? "0");
763
832
  const maxDepth = resolveCurrentMaxSubagentDepth(configMaxDepth);
764
833
  const blocked = Number.isFinite(depth) && depth >= maxDepth;
765
- return { blocked, depth, maxDepth };
834
+ return { blocked, depth, maxDepth, workflowStageGuard: hasWorkflowStageSubagentGuard() };
766
835
  }
767
836
 
768
- export function getSubagentDepthEnv(maxDepth?: number): Record<string, string> {
837
+ export function getSubagentDepthEnv(maxDepth?: number, options?: { workflowStageSubagentGuard?: boolean }): Record<string, string> {
769
838
  const parentDepth = Number(getEnvValue(SUBAGENT_DEPTH_ENV) ?? "0");
839
+ // Preserve an inherited workflow-stage marker for descendants; callers that
840
+ // mutate process.env in tests must clear it to avoid intentional propagation.
770
841
  const nextDepth = Number.isFinite(parentDepth) ? parentDepth + 1 : 1;
771
842
  return {
772
843
  [SUBAGENT_DEPTH_ENV]: String(nextDepth),
773
844
  [SUBAGENT_MAX_DEPTH_ENV]: String(normalizeMaxSubagentDepth(maxDepth) ?? resolveCurrentMaxSubagentDepth()),
845
+ ...(options?.workflowStageSubagentGuard || hasWorkflowStageSubagentGuard()
846
+ ? { [WORKFLOW_STAGE_SUBAGENT_GUARD_ENV]: "1" }
847
+ : {}),
774
848
  };
775
849
  }
776
850
 
@@ -88,10 +88,28 @@ function truncLine(text: string, maxWidth: number): string {
88
88
  }
89
89
 
90
90
  const RUNNING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
91
- const STATIC_RUNNING_GLYPH = "●";
91
+
92
+ /**
93
+ * Spinner cadence (ms per frame). The running glyph is derived from wall-clock
94
+ * time so every active spinner advances smoothly and in lockstep, independent
95
+ * of how often (or how irregularly) progress data updates arrive. The animation
96
+ * timers below only schedule re-renders; the displayed frame always comes from
97
+ * the clock. This fixes the frozen/stuttering spinner from issue #1084 while
98
+ * keeping per-frame diffs to a single glyph cell so the differential renderer
99
+ * never needs a full-clear (no flicker).
100
+ */
101
+ export const RUNNING_ANIMATION_MS = 80;
92
102
 
93
103
  type ProgressSeedSource = Partial<Pick<AgentProgress, "index" | "toolCount" | "tokens" | "durationMs" | "lastActivityAt" | "currentToolStartedAt" | "turnCount">>;
94
104
 
105
+ /**
106
+ * Wall-clock-derived animation frame counter. Advances exactly one step every
107
+ * `RUNNING_ANIMATION_MS`. Exposed for tests so they can pin a deterministic now.
108
+ */
109
+ export function currentRunningFrame(now: number = Date.now()): number {
110
+ return Math.floor(now / RUNNING_ANIMATION_MS);
111
+ }
112
+
95
113
  function runningSeed(...values: Array<number | undefined>): number | undefined {
96
114
  let seed: number | undefined;
97
115
  for (const value of values) {
@@ -102,8 +120,11 @@ function runningSeed(...values: Array<number | undefined>): number | undefined {
102
120
  }
103
121
 
104
122
  function runningGlyph(seed?: number): string {
105
- if (seed === undefined) return STATIC_RUNNING_GLYPH;
106
- return RUNNING_FRAMES[Math.abs(seed) % RUNNING_FRAMES.length]!;
123
+ // Fold the wall-clock frame into the (optional) progress seed so the glyph
124
+ // advances over time. The frame is always finite, so a running entity always
125
+ // animates; the seed only offsets its starting phase between concurrent agents.
126
+ const animatedSeed = runningSeed(seed, currentRunningFrame()) ?? 0;
127
+ return RUNNING_FRAMES[Math.abs(animatedSeed) % RUNNING_FRAMES.length]!;
107
128
  }
108
129
 
109
130
  function progressRunningSeed(progress: ProgressSeedSource | undefined): number | undefined {
@@ -119,17 +140,85 @@ function progressRunningSeed(progress: ProgressSeedSource | undefined): number |
119
140
  );
120
141
  }
121
142
 
122
- interface LegacyResultAnimationContext {
123
- state: { subagentResultAnimationTimer?: ReturnType<typeof setInterval> };
143
+ type ResultAnimationState = { subagentResultAnimationTimer?: ReturnType<typeof setInterval> };
144
+
145
+ interface ResultAnimationContext {
146
+ state: ResultAnimationState;
147
+ invalidate: () => void;
124
148
  }
125
149
 
126
- export function clearLegacyResultAnimationTimer(context: LegacyResultAnimationContext): void {
150
+ type LegacyResultAnimationContext = { state: ResultAnimationState };
151
+ type ResultAnimationEntry = ResultAnimationContext;
152
+
153
+ // Registry of every live result-animation timer so they can be torn down in one
154
+ // shot on reload/shutdown even if their owning render context never re-renders.
155
+ // Each tick reads the latest `invalidate` from here so a re-sync can refresh the
156
+ // callback if the host ever swaps render contexts for the same renderable.
157
+ const resultAnimationTimers = new Map<ReturnType<typeof setInterval>, ResultAnimationEntry>();
158
+
159
+ function resultIsRunning(result: AgentToolResult<Details>): boolean {
160
+ return Boolean(
161
+ result.details?.progress?.some((entry) => entry.status === "running")
162
+ || result.details?.results.some((entry) => entry.progress?.status === "running"),
163
+ );
164
+ }
165
+
166
+ function stopResultAnimation(context: LegacyResultAnimationContext): void {
127
167
  const timer = context.state.subagentResultAnimationTimer;
128
168
  if (!timer) return;
129
169
  clearInterval(timer);
170
+ resultAnimationTimers.delete(timer);
130
171
  context.state.subagentResultAnimationTimer = undefined;
131
172
  }
132
173
 
174
+ export function clearLegacyResultAnimationTimer(context: LegacyResultAnimationContext): void {
175
+ stopResultAnimation(context);
176
+ }
177
+
178
+ /**
179
+ * Keep a running subagent result's spinner animating by scheduling a steady
180
+ * re-render while it is active, and tearing the timer down once it settles.
181
+ * The timer only calls `context.invalidate()`; the glyph value itself comes
182
+ * from {@link currentRunningFrame}, so each tick produces a single-glyph diff.
183
+ */
184
+ export function syncResultAnimation(result: AgentToolResult<Details>, context: ResultAnimationContext): void {
185
+ if (!resultIsRunning(result)) {
186
+ stopResultAnimation(context);
187
+ return;
188
+ }
189
+ const existing = context.state.subagentResultAnimationTimer;
190
+ if (existing) {
191
+ // Keep using the most recent invalidate in case the host handed us a fresh
192
+ // render context object on this re-sync.
193
+ const entry = resultAnimationTimers.get(existing);
194
+ if (entry) entry.invalidate = context.invalidate;
195
+ return;
196
+ }
197
+ const timer = setInterval(() => {
198
+ const entry = resultAnimationTimers.get(timer);
199
+ if (!entry) return;
200
+ try {
201
+ entry.invalidate();
202
+ } catch {
203
+ // A cosmetic spinner tick must never crash the host (e.g. a stale extension
204
+ // context after reload/session swap, or any other render glitch). Stop this
205
+ // timer; the next real render re-syncs and restarts it while still running.
206
+ stopResultAnimation(context);
207
+ }
208
+ }, RUNNING_ANIMATION_MS);
209
+ timer.unref?.();
210
+ context.state.subagentResultAnimationTimer = timer;
211
+ resultAnimationTimers.set(timer, { state: context.state, invalidate: context.invalidate });
212
+ }
213
+
214
+ export function stopResultAnimations(): void {
215
+ for (const [timer, entry] of resultAnimationTimers) {
216
+ clearInterval(timer);
217
+ entry.state.subagentResultAnimationTimer = undefined;
218
+ }
219
+ resultAnimationTimers.clear();
220
+ }
221
+
133
222
  function extractOutputTarget(task: string): string | undefined {
134
223
  const writeToMatch = task.match(/\[Write to:\s*([^\]\n]+)\]/i);
135
224
  if (writeToMatch?.[1]?.trim()) return writeToMatch[1].trim();
@@ -836,18 +925,95 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
836
925
  return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
837
926
  }
838
927
 
839
- function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui: unknown, theme: Theme) => Component {
840
- return (_tui, theme) => {
841
- const width = getTermWidth();
928
+ /**
929
+ * Live async-agents widget. Recomputes its lines on every render so the
930
+ * wall-clock-driven running glyph (and elapsed-time labels) stay current; the
931
+ * widget animation ticker below schedules those re-renders while jobs run.
932
+ */
933
+ class LiveWidgetComponent implements Component {
934
+ private readonly container = new Container();
935
+
936
+ constructor(
937
+ private readonly jobs: AsyncJobState[],
938
+ private readonly theme: Theme,
939
+ private readonly getExpanded: () => boolean,
940
+ ) {}
941
+
942
+ render(width: number): string[] {
943
+ const expanded = this.getExpanded();
842
944
  const lines = expanded
843
- ? buildWidgetLines(jobs, theme, width, true)
844
- : jobs.length === 1
845
- ? compactSingleWidgetLines(jobs[0]!, theme, width)
846
- : buildWidgetLines(jobs, theme, width, false);
847
- const container = new Container();
848
- for (const line of fitWidgetLineBudget(lines, theme, width, expanded)) container.addChild(new Text(line, 1, 0));
849
- return container;
850
- };
945
+ ? buildWidgetLines(this.jobs, this.theme, width, true)
946
+ : this.jobs.length === 1
947
+ ? compactSingleWidgetLines(this.jobs[0]!, this.theme, width)
948
+ : buildWidgetLines(this.jobs, this.theme, width, false);
949
+ this.container.clear();
950
+ for (const line of fitWidgetLineBudget(lines, this.theme, width, expanded)) this.container.addChild(new Text(line, 1, 0));
951
+ return this.container.render(width);
952
+ }
953
+
954
+ invalidate(): void {
955
+ this.container.invalidate();
956
+ }
957
+ }
958
+
959
+ function buildWidgetComponent(jobs: AsyncJobState[], getExpanded: () => boolean): (_tui: unknown, theme: Theme) => Component {
960
+ return (_tui, theme) => new LiveWidgetComponent(jobs, theme, getExpanded);
961
+ }
962
+
963
+ interface RenderRequestingContext {
964
+ ui: ExtensionContext["ui"] & { requestRender?: () => void };
965
+ }
966
+
967
+ // There is only ever one async-agents widget per host process, so the widget
968
+ // ticker keeps its driving context/jobs in module-level singletons.
969
+ let latestWidgetCtx: ExtensionContext | undefined;
970
+ let latestWidgetJobs: AsyncJobState[] = [];
971
+ let widgetTimer: ReturnType<typeof setInterval> | undefined;
972
+
973
+ function hasAnimatedWidgetJobs(jobs: AsyncJobState[]): boolean {
974
+ // Animate while any job — or any of its nested steps — is still running so the
975
+ // header/step spinners never freeze before the work actually settles.
976
+ return jobs.some((job) => job.status === "running" || job.steps?.some((step) => step.status === "running"));
977
+ }
978
+
979
+ function refreshAnimatedWidget(): void {
980
+ if (!latestWidgetCtx?.hasUI) return;
981
+ try {
982
+ // The cast is required because narrowing on `hasUI` above collapses `ui` to
983
+ // the base ExtensionUIContext, which does not declare the optional
984
+ // requestRender that the running interactive host actually provides.
985
+ (latestWidgetCtx as RenderRequestingContext).ui.requestRender?.();
986
+ } catch {
987
+ // Never let a cosmetic widget tick crash the host; stop on any error.
988
+ stopWidgetAnimation();
989
+ }
990
+ }
991
+
992
+ function ensureWidgetAnimation(): void {
993
+ if (widgetTimer) return;
994
+ widgetTimer = setInterval(() => {
995
+ if (!hasAnimatedWidgetJobs(latestWidgetJobs)) {
996
+ stopWidgetAnimation();
997
+ return;
998
+ }
999
+ refreshAnimatedWidget();
1000
+ }, RUNNING_ANIMATION_MS);
1001
+ widgetTimer.unref?.();
1002
+ }
1003
+
1004
+ // Stop only the ticker, keeping the last-rendered widget context/jobs intact.
1005
+ function stopWidgetTicker(): void {
1006
+ if (widgetTimer) {
1007
+ clearInterval(widgetTimer);
1008
+ widgetTimer = undefined;
1009
+ }
1010
+ }
1011
+
1012
+ // Full teardown: stop the ticker and forget the driving context/jobs entirely.
1013
+ export function stopWidgetAnimation(): void {
1014
+ stopWidgetTicker();
1015
+ latestWidgetCtx = undefined;
1016
+ latestWidgetJobs = [];
851
1017
  }
852
1018
 
853
1019
  export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false): string[] {
@@ -925,11 +1091,29 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
925
1091
  */
926
1092
  export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
927
1093
  if (jobs.length === 0) {
1094
+ stopWidgetAnimation();
928
1095
  if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
929
1096
  return;
930
1097
  }
931
- if (!ctx.hasUI) return;
932
- ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false));
1098
+ if (!ctx.hasUI) {
1099
+ stopWidgetAnimation();
1100
+ return;
1101
+ }
1102
+ latestWidgetCtx = ctx;
1103
+ latestWidgetJobs = [...jobs];
1104
+ // belowEditor: the widget animates a running glyph / elapsed labels on a
1105
+ // timer. pi-tui full-clears the screen+scrollback whenever a changed line
1106
+ // sits above the viewport fold, so an aboveEditor widget flickers once the
1107
+ // bottom region grows tall and pushes it above the fold. Rendering below the
1108
+ // editor keeps the live line within the bottom viewport (flicker-free), and
1109
+ // matches the workflow companion widget's placement (#1109).
1110
+ ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, () => ctx.ui.getToolsExpanded?.() ?? false), {
1111
+ placement: "belowEditor",
1112
+ });
1113
+ // Keep the just-rendered ctx/jobs as the last-rendered state; only the ticker
1114
+ // is conditional on whether anything is still animating.
1115
+ if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
1116
+ else stopWidgetTicker();
933
1117
  }
934
1118
 
935
1119
  function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme): Component {
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.20] - 2026-05-29
8
+
9
+ ### Changed
10
+ - Promoted the 0.8.20 prerelease changes to a stable release.
11
+
12
+ ## [0.8.20-0] - 2026-05-29
13
+
14
+ ### Changed
15
+ - Bumped `linkedom` to 0.18.12 to align with the pi 0.77.0 upgrade.
16
+
7
17
  ## [0.8.18] - 2026-05-27
8
18
 
9
19
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.8.19",
3
+ "version": "0.8.20",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@mozilla/readability": "^0.6.0",
45
- "linkedom": "^0.16.0",
45
+ "linkedom": "^0.18.12",
46
46
  "p-limit": "^6.1.0",
47
47
  "turndown": "^7.2.0",
48
48
  "unpdf": "^1.6.2"
@@ -6,6 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.20] - 2026-05-29
10
+
11
+ ### Changed
12
+
13
+ - Promoted the 0.8.20 prerelease changes to a stable release.
14
+
15
+ ## [0.8.20-0] - 2026-05-29
16
+
17
+ ### Added
18
+
19
+ - Added main-chat lifecycle steer notices for workflow completion, failure, and awaiting-input pauses with global notification config controls ([#1085](https://github.com/flora131/atomic/issues/1085)).
20
+
21
+ ### Fixed
22
+
23
+ - Fixed the background-workflow companion counter widget flickering every second while a run is active by updating a single long-lived above-editor widget in place instead of disposing and re-mounting a fresh widget factory on each elapsed-clock tick ([#1109](https://github.com/flora131/atomic/issues/1109)).
24
+ - Fixed workflow lifecycle completion/failure/awaiting-input notices crashing the TUI on narrow or freshly-resized terminals: the notice component now wraps to the render width (hard-breaking long run ids) instead of emitting a single fixed line, which pi-tui rejects with a hard "Rendered line exceeds terminal width" throw ([#1109](https://github.com/flora131/atomic/issues/1109)).
25
+ - Fixed the workflow companion counter widget triggering a full-screen redraw (clear + scrollback wipe) on every elapsed-clock tick after the terminal is resized while a run is active. The widget now mounts `belowEditor` instead of `aboveEditor`, keeping its live clock line within the bottom viewport; pi-tui full-clears the screen whenever a changed line sits above the viewport fold, and an above-editor widget was pushed above the fold once the editor/status region grew tall ([#1109](https://github.com/flora131/atomic/issues/1109)).
26
+ - Disabled workflows in non-interactive (`-p` / `--print` / `--mode json`) sessions, which bind a no-op UI surface and cannot drive workflow prompts, pickers, or the graph overlay: the `workflow` tool is removed from the model's active tool set at session start, and the `/workflow` command (reachable via `atomic -p "/workflow …"`) is refused — preventing headless runs from stalling on work that can never complete ([#1096](https://github.com/flora131/atomic/issues/1096)).
27
+ - Escaped workflow lifecycle notice text and structured response hints, isolated lifecycle send failures from store subscribers, and rejected empty lifecycle notification event lists ([#1085](https://github.com/flora131/atomic/issues/1085)).
28
+ - Fixed stage awaiting-input lifecycle notice dedupe so promptless pauses after resolved prompts are not suppressed by historical prompt metadata ([#1085](https://github.com/flora131/atomic/issues/1085)).
29
+ - Reset workflow lifecycle-notification dedupe state at chat session boundaries so reused workflow run IDs in later sessions still emit completion/failure/input notices ([#1085](https://github.com/flora131/atomic/issues/1085)).
30
+ - Warn before starting or resuming another session when workflows are still in flight, allowing users to cancel before those runs are killed and current-session workflow history is cleared ([#1082](https://github.com/flora131/atomic/issues/1082)).
31
+ - Prevented workflow stage sessions from exposing or executing the `workflow` tool while preserving stage-level subagent delegation.
32
+ - Retained completed, failed, and killed workflow runs in user-facing status/connect surfaces and changed workflow kill controls to mark runs killed without removing them from live inspection history ([#1083](https://github.com/flora131/atomic/issues/1083)).
33
+
9
34
  ## [0.8.18] - 2026-05-27
10
35
 
11
36
  ### Changed
@@ -27,6 +27,21 @@ Adding workflow files under `.atomic/workflows/` (project scope) or `~/.atomic/a
27
27
  }
28
28
  ```
29
29
 
30
+ ### Workflow lifecycle notifications
31
+
32
+ Workflow lifecycle notices are enabled by default. They send steer prompts into the main chat/model context when a run completes, fails, or pauses for input. Configure them in the same extension config file:
33
+
34
+ ```json
35
+ {
36
+ "workflowNotifications": {
37
+ "enabled": true,
38
+ "notifyOn": ["completed", "failed", "awaiting_input"]
39
+ }
40
+ }
41
+ ```
42
+
43
+ Set `enabled` to `false` to disable all notices, or narrow `notifyOn` to a non-empty list of selected events. Emitted notices use steer delivery and wake an idle model so the lifecycle update enters the model context when it happens.
44
+
30
45
  ---
31
46
 
32
47
  ## Authoring API
@@ -243,12 +258,12 @@ registry.get("alpha"); // compiled workflow definition | undefined
243
258
  | `/workflow <name> [key=value ...]` | Start a named workflow, passing optional input overrides |
244
259
  | `/workflow <name> --help` | Print the workflow's input schema |
245
260
  | `/workflow list` | List all registered workflows with descriptions |
246
- | `/workflow status [run-id]` | Show active runs or details for one run |
261
+ | `/workflow status [run-id]` | Show active plus retained terminal/current-session runs, or details for one run |
247
262
  | `/workflow connect [run-id]` | Attach to a workflow run overlay |
248
263
  | `/workflow attach [run-id] [stage]` | Open the attach/chat pane for a run or stage |
249
264
  | `/workflow pause [run-id] [stage]` | Pause a live run or stage |
250
265
  | `/workflow interrupt [run-id\|--all]` | Pause active/named/all active runs so they can resume |
251
- | `/workflow kill [run-id\|--all]` | Kill and remove active/named/all active runs from status |
266
+ | `/workflow kill [run-id\|--all]` | Kill in-flight workflow runs; killed runs are retained for inspection |
252
267
  | `/workflow resume <run-id>` | Resume paused work or re-open a run snapshot |
253
268
  | `/workflow reload` | Reload discovered workflow resources in-process |
254
269
  | `/workflow inputs <name>` | Print the input schema for a workflow |
@@ -302,7 +317,7 @@ Press **F2** while a workflow is running to open the DAG overlay for the active
302
317
 
303
318
  `@bastani/workflows` follows pi's package/extension model: pi loads `src/extension/index.ts` from the package `pi.extensions` manifest, then the extension registers the `workflow` tool, `/workflow` slash command, renderers, widget, and lifecycle hooks in-process.
304
319
 
305
- For interactive use, run workflows through `/workflow <name> [key=value ...]` or let the LLM call the `workflow` tool. For library or scripted use, call the explicit programmatic runner:
320
+ For interactive use, run workflows through `/workflow <name> [key=value ...]` or let the LLM call the `workflow` tool. Both the `/workflow` command and the `workflow` tool are disabled in non-interactive (`-p` / `--print` / `--mode json`) sessions, which bind a no-op UI surface and therefore cannot drive workflow pickers, the graph overlay, or human-in-the-loop prompts. For library or scripted use, call the explicit programmatic runner instead:
306
321
 
307
322
  ```ts
308
323
  import { runWorkflow, type WorkflowOptions } from "@bastani/workflows";
@@ -429,6 +444,10 @@ Config-based discovery (`~/.atomic/agent/extensions/workflow/config.json` or `.a
429
444
  {
430
445
  "workflows": {
431
446
  "my-team-workflows": { "path": "/shared/team/workflows" }
447
+ },
448
+ "workflowNotifications": {
449
+ "enabled": true,
450
+ "notifyOn": ["completed", "failed", "awaiting_input"]
432
451
  }
433
452
  }
434
453
  ```
@@ -401,7 +401,7 @@ export async function runDeepResearchCodebaseWorkflow(
401
401
  fallbackModels: [
402
402
  "openai-codex/gpt-5.5",
403
403
  "github-copilot/gpt-5.5",
404
- "anthropic/claude-opus-4-7",
404
+ "anthropic/claude-opus-4-8",
405
405
  "github-copilot/claude-opus-4.7",
406
406
  ],
407
407
  thinkingLevel: "high" as const,
@@ -222,12 +222,14 @@ export default defineWorkflow("open-claude-design")
222
222
  DEFAULT_MAX_REFINEMENTS,
223
223
  );
224
224
 
225
- const { runId, artifactDir, previewPath, specPath } = prepareArtifactDir(ctx.cwd);
225
+ const { runId, artifactDir, previewPath, specPath } = prepareArtifactDir(
226
+ ctx.cwd,
227
+ );
226
228
  const previewFileUrl = `file://${previewPath}`;
227
229
  const specFileUrl = `file://${specPath}`;
228
230
 
229
231
  const designModelConfig = {
230
- model: "anthropic/claude-opus-4-7",
232
+ model: "anthropic/claude-opus-4-8",
231
233
  fallbackModels: [
232
234
  "github-copilot/claude-opus-4.7",
233
235
  "anthropic/claude-sonnet-4-6",
@@ -710,7 +712,10 @@ export default defineWorkflow("open-claude-design")
710
712
  ["preview_path", previewPath],
711
713
  ["preview_file_url", previewFileUrl],
712
714
  ["current_design_and_feedback", "{previous}"],
713
- ["playwright_browser_bootstrap", PLAYWRIGHT_BROWSER_BOOTSTRAP_RULES],
715
+ [
716
+ "playwright_browser_bootstrap",
717
+ PLAYWRIGHT_BROWSER_BOOTSTRAP_RULES,
718
+ ],
714
719
  [
715
720
  "instructions",
716
721
  [
@@ -798,7 +803,10 @@ export default defineWorkflow("open-claude-design")
798
803
  ],
799
804
  ["preview_path", previewPath],
800
805
  ["preview_file_url", previewFileUrl],
801
- ["playwright_browser_bootstrap", PLAYWRIGHT_BROWSER_BOOTSTRAP_RULES],
806
+ [
807
+ "playwright_browser_bootstrap",
808
+ PLAYWRIGHT_BROWSER_BOOTSTRAP_RULES,
809
+ ],
802
810
  [
803
811
  "instructions",
804
812
  [
@@ -417,7 +417,7 @@ async function runRalphWorkflow(
417
417
  fallbackModels: [
418
418
  "openai-codex/gpt-5.5",
419
419
  "github-copilot/gpt-5.5",
420
- "anthropic/claude-opus-4-7",
420
+ "anthropic/claude-opus-4-8",
421
421
  "github-copilot/claude-opus-4.7",
422
422
  ],
423
423
  thinkingLevel: "high" as const,
@@ -453,7 +453,7 @@ async function runRalphWorkflow(
453
453
  fallbackModels: [
454
454
  "openai-codex/gpt-5.5",
455
455
  "github-copilot/gpt-5.5",
456
- "anthropic/claude-opus-4-7",
456
+ "anthropic/claude-opus-4-8",
457
457
  "github-copilot/claude-opus-4.7",
458
458
  ],
459
459
  thinkingLevel: "high" as const,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.8.19",
3
+ "version": "0.8.20",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [