@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.
- package/CHANGELOG.md +19 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +10 -0
- package/dist/builtin/mcp/package.json +2 -2
- package/dist/builtin/subagents/CHANGELOG.md +17 -2
- package/dist/builtin/subagents/agents/code-simplifier.md +1 -1
- package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/debugger.md +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/skills/subagent/SKILL.md +12 -12
- package/dist/builtin/subagents/src/agents/agent-management.ts +16 -11
- package/dist/builtin/subagents/src/agents/skills.ts +13 -1
- package/dist/builtin/subagents/src/extension/index.ts +14 -3
- package/dist/builtin/subagents/src/runs/background/async-execution.ts +8 -0
- package/dist/builtin/subagents/src/runs/background/run-status.ts +2 -3
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +11 -1
- package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +2 -2
- package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +31 -23
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +13 -7
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +160 -93
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
- package/dist/builtin/subagents/src/runs/shared/run-history.ts +1 -1
- package/dist/builtin/subagents/src/shared/settings.ts +1 -0
- package/dist/builtin/subagents/src/shared/types.ts +78 -4
- package/dist/builtin/subagents/src/tui/render.ts +203 -19
- package/dist/builtin/web-access/CHANGELOG.md +10 -0
- package/dist/builtin/web-access/package.json +2 -2
- package/dist/builtin/workflows/CHANGELOG.md +25 -0
- package/dist/builtin/workflows/README.md +22 -3
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +1 -1
- package/dist/builtin/workflows/builtin/open-claude-design.ts +12 -4
- package/dist/builtin/workflows/builtin/ralph.ts +2 -2
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/config-loader.ts +68 -0
- package/dist/builtin/workflows/src/extension/index.ts +246 -55
- package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +372 -0
- package/dist/builtin/workflows/src/extension/render-call.ts +1 -1
- package/dist/builtin/workflows/src/extension/wiring.ts +32 -3
- package/dist/builtin/workflows/src/runs/background/status.ts +14 -74
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +5 -3
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +3 -13
- package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +2 -10
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +5 -5
- package/dist/builtin/workflows/src/tui/session-confirm.ts +6 -7
- package/dist/builtin/workflows/src/tui/session-picker.ts +18 -14
- package/dist/builtin/workflows/src/tui/status-list.ts +2 -2
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +125 -30
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +4 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +3 -2
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +3 -2
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +6 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +13 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +63 -17
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/output-guard.d.ts.map +1 -1
- package/dist/core/output-guard.js +29 -0
- package/dist/core/output-guard.js.map +1 -1
- package/dist/core/sdk.d.ts +3 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +46 -13
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/pi-user-agent.d.ts.map +1 -1
- package/dist/utils/pi-user-agent.js +2 -1
- package/dist/utils/pi-user-agent.js.map +1 -1
- package/dist/utils/syntax-highlight.d.ts.map +1 -1
- package/dist/utils/syntax-highlight.js +1 -1
- package/dist/utils/syntax-highlight.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +3 -5
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/models.md +52 -52
- package/docs/quickstart.md +2 -2
- package/docs/workflows.md +22 -5
- package/package.json +9 -9
|
@@ -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 =>
|
|
57
|
+
.filter((entry): entry is RunEntry => entry !== undefined && entry.agent === agent)
|
|
58
58
|
.reverse();
|
|
59
59
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
123
|
-
|
|
143
|
+
type ResultAnimationState = { subagentResultAnimationTimer?: ReturnType<typeof setInterval> };
|
|
144
|
+
|
|
145
|
+
interface ResultAnimationContext {
|
|
146
|
+
state: ResultAnimationState;
|
|
147
|
+
invalidate: () => void;
|
|
124
148
|
}
|
|
125
149
|
|
|
126
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
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)
|
|
932
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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-
|
|
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(
|
|
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-
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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-
|
|
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-
|
|
456
|
+
"anthropic/claude-opus-4-8",
|
|
457
457
|
"github-copilot/claude-opus-4.7",
|
|
458
458
|
],
|
|
459
459
|
thinkingLevel: "high" as const,
|