@bastani/atomic 0.8.25 → 0.8.26-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/builtin/intercom/CHANGELOG.md +12 -0
- package/dist/builtin/intercom/index-heavy.ts +1754 -0
- package/dist/builtin/intercom/index.ts +374 -1746
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/result-renderers.ts +77 -0
- package/dist/builtin/mcp/CHANGELOG.md +16 -0
- package/dist/builtin/mcp/index.ts +151 -57
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +13 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +8 -3
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +42 -4
- package/dist/builtin/subagents/src/runs/shared/acceptance.ts +2 -1
- package/dist/builtin/subagents/src/runs/shared/worktree.ts +2 -2
- package/dist/builtin/web-access/CHANGELOG.md +12 -0
- package/dist/builtin/web-access/index-heavy.ts +2060 -0
- package/dist/builtin/web-access/index.ts +182 -2274
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/web-access/result-renderers.ts +364 -0
- package/dist/builtin/workflows/CHANGELOG.md +21 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/research-codebase/SKILL.md +28 -9
- package/dist/builtin/workflows/src/extension/index.ts +13 -3
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +59 -3
- package/dist/builtin/workflows/src/runs/shared/worktree.ts +2 -2
- package/dist/builtin/workflows/src/shared/store.ts +61 -7
- package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +12 -3
- package/dist/builtin/workflows/src/tui/inline-form-store.ts +17 -6
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +37 -2
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +13 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +7 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +13 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +3 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +14 -7
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +17 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/timings.d.ts +9 -0
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +28 -1
- package/dist/core/timings.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts +1 -0
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +36 -4
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +4 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +22 -9
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/git-env.d.ts +10 -0
- package/dist/utils/git-env.d.ts.map +1 -0
- package/dist/utils/git-env.js +33 -0
- package/dist/utils/git-env.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/web-access",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.26-alpha.2",
|
|
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": [
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@bastani/atomic";
|
|
2
|
+
import { Box, Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import { formatSeconds } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
type ToolResultRenderer = NonNullable<ToolDefinition["renderResult"]>;
|
|
6
|
+
type ToolRenderResultArgs = Parameters<ToolResultRenderer>;
|
|
7
|
+
type ToolRenderResult = ReturnType<ToolResultRenderer>;
|
|
8
|
+
type RenderedResult = ToolRenderResultArgs[0];
|
|
9
|
+
type TextContentBlock = Extract<RenderedResult["content"][number], { type: "text" }>;
|
|
10
|
+
|
|
11
|
+
type QueryDetail = {
|
|
12
|
+
query: string;
|
|
13
|
+
provider: string | null;
|
|
14
|
+
answer: string | null;
|
|
15
|
+
sources: Array<{ title: string; url: string }>;
|
|
16
|
+
error: string | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type WebSearchResultDetails = {
|
|
20
|
+
queryCount?: number;
|
|
21
|
+
successfulQueries?: number;
|
|
22
|
+
totalResults?: number;
|
|
23
|
+
error?: string;
|
|
24
|
+
fetchId?: string;
|
|
25
|
+
fetchUrls?: string[];
|
|
26
|
+
phase?: string;
|
|
27
|
+
progress?: number;
|
|
28
|
+
currentQuery?: string;
|
|
29
|
+
curated?: boolean;
|
|
30
|
+
curatedFrom?: number;
|
|
31
|
+
curatedQueries?: QueryDetail[];
|
|
32
|
+
cancelled?: boolean;
|
|
33
|
+
cancelReason?: string;
|
|
34
|
+
summary?: {
|
|
35
|
+
text: string;
|
|
36
|
+
workflow: "summary-review";
|
|
37
|
+
model: string | null;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
tokenEstimate: number;
|
|
40
|
+
fallbackUsed: boolean;
|
|
41
|
+
fallbackReason?: string;
|
|
42
|
+
edited?: boolean;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type CodeSearchResultDetails = {
|
|
47
|
+
query?: string;
|
|
48
|
+
maxTokens?: number;
|
|
49
|
+
error?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type FetchContentResultDetails = {
|
|
53
|
+
urlCount?: number;
|
|
54
|
+
successful?: number;
|
|
55
|
+
totalChars?: number;
|
|
56
|
+
error?: string;
|
|
57
|
+
title?: string;
|
|
58
|
+
truncated?: boolean;
|
|
59
|
+
responseId?: string;
|
|
60
|
+
phase?: string;
|
|
61
|
+
progress?: number;
|
|
62
|
+
hasImage?: boolean;
|
|
63
|
+
imageCount?: number;
|
|
64
|
+
prompt?: string;
|
|
65
|
+
timestamp?: string;
|
|
66
|
+
frames?: number;
|
|
67
|
+
duration?: number;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type GetSearchContentResultDetails = {
|
|
71
|
+
error?: string;
|
|
72
|
+
query?: string;
|
|
73
|
+
url?: string;
|
|
74
|
+
title?: string;
|
|
75
|
+
resultCount?: number;
|
|
76
|
+
contentLength?: number;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function isTextContentBlock(block: RenderedResult["content"][number]): block is TextContentBlock {
|
|
80
|
+
return block.type === "text";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function firstTextContent(result: RenderedResult): string {
|
|
84
|
+
return result.content.find(isTextContentBlock)?.text ?? "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function progressBar(progress: number): string {
|
|
88
|
+
const filled = Math.floor(progress * 10);
|
|
89
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const renderWebSearchResult: ToolResultRenderer = (result, { expanded, isPartial }, theme) => {
|
|
93
|
+
const details = result.details as WebSearchResultDetails | undefined;
|
|
94
|
+
|
|
95
|
+
if (isPartial) {
|
|
96
|
+
if (details?.phase === "curating") {
|
|
97
|
+
return new Text(theme.fg("accent", "waiting for summary approval..."), 0, 0);
|
|
98
|
+
}
|
|
99
|
+
if (details?.phase === "searching") {
|
|
100
|
+
const progress = details?.progress ?? 0;
|
|
101
|
+
const bar = progressBar(progress);
|
|
102
|
+
const query = details?.currentQuery || "";
|
|
103
|
+
const display = query.length > 40 ? query.slice(0, 37) + "..." : query;
|
|
104
|
+
return new Text(theme.fg("accent", `[${bar}] ${display}`), 0, 0);
|
|
105
|
+
}
|
|
106
|
+
const progress = details?.progress ?? 0;
|
|
107
|
+
const bar = progressBar(progress);
|
|
108
|
+
return new Text(theme.fg("accent", `[${bar}] ${details?.phase || "searching"}`), 0, 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (details?.error) {
|
|
112
|
+
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let statusLine: string;
|
|
116
|
+
const queryInfo = details?.queryCount === 1 ? "" : `${details?.successfulQueries}/${details?.queryCount} queries, `;
|
|
117
|
+
statusLine = theme.fg("success", `${queryInfo}${details?.totalResults ?? 0} sources`);
|
|
118
|
+
if (details?.curated && details?.curatedFrom) {
|
|
119
|
+
statusLine += theme.fg("muted", ` (${details.queryCount}/${details.curatedFrom} queries curated)`);
|
|
120
|
+
}
|
|
121
|
+
if (details?.fetchId && details?.fetchUrls) {
|
|
122
|
+
statusLine += theme.fg("muted", ` (fetching ${details.fetchUrls.length} URLs)`);
|
|
123
|
+
} else if (details?.fetchId) {
|
|
124
|
+
statusLine += theme.fg("muted", " (content ready)");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Build expanded lines first so collapsed view can reference total count
|
|
128
|
+
const lines = [statusLine];
|
|
129
|
+
if (details?.summary?.text) {
|
|
130
|
+
lines.push("");
|
|
131
|
+
lines.push(theme.fg("accent", `── Summary (${details.summary.workflow}) ` + "─".repeat(32)));
|
|
132
|
+
lines.push("");
|
|
133
|
+
for (const line of details.summary.text.split("\n")) {
|
|
134
|
+
lines.push(` ${line}`);
|
|
135
|
+
}
|
|
136
|
+
lines.push("");
|
|
137
|
+
const metaParts = [
|
|
138
|
+
details.summary.model ? `model=${details.summary.model}` : "model=deterministic",
|
|
139
|
+
`duration=${details.summary.durationMs}ms`,
|
|
140
|
+
`tokens~${details.summary.tokenEstimate}`,
|
|
141
|
+
details.summary.fallbackUsed ? "fallback=true" : "fallback=false",
|
|
142
|
+
details.summary.edited ? "edited=true" : "edited=false",
|
|
143
|
+
];
|
|
144
|
+
if (details.summary.fallbackReason) {
|
|
145
|
+
metaParts.push(`reason=${details.summary.fallbackReason}`);
|
|
146
|
+
}
|
|
147
|
+
lines.push(theme.fg("dim", " " + metaParts.join(" · ")));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const queryDetails = details?.curatedQueries;
|
|
151
|
+
if (queryDetails?.length) {
|
|
152
|
+
const kept = queryDetails.length;
|
|
153
|
+
const from = details?.curatedFrom ?? kept;
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push(theme.fg("accent", `\u2500\u2500 Curated Results (${kept} of ${from} queries kept) ` + "\u2500".repeat(24)));
|
|
156
|
+
|
|
157
|
+
for (const cq of queryDetails) {
|
|
158
|
+
lines.push("");
|
|
159
|
+
const dq = cq.query.length > 65 ? cq.query.slice(0, 62) + "..." : cq.query;
|
|
160
|
+
const providerLabel = cq.provider ? ` (${cq.provider})` : "";
|
|
161
|
+
lines.push(theme.fg("accent", ` "${dq}"${providerLabel}`));
|
|
162
|
+
|
|
163
|
+
if (cq.error) {
|
|
164
|
+
lines.push(theme.fg("error", ` ${cq.error}`));
|
|
165
|
+
} else if (cq.answer) {
|
|
166
|
+
lines.push("");
|
|
167
|
+
for (const line of cq.answer.split("\n")) {
|
|
168
|
+
lines.push(` ${line}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (cq.sources.length > 0) {
|
|
173
|
+
lines.push("");
|
|
174
|
+
for (const s of cq.sources) {
|
|
175
|
+
const domain = s.url.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
176
|
+
const title = s.title.length > 50 ? s.title.slice(0, 47) + "..." : s.title;
|
|
177
|
+
lines.push(theme.fg("muted", ` \u25b8 ${title}`) + theme.fg("dim", ` \u00b7 ${domain}`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
lines.push("");
|
|
182
|
+
} else {
|
|
183
|
+
const textContent = firstTextContent(result);
|
|
184
|
+
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
185
|
+
for (const line of preview.split("\n")) {
|
|
186
|
+
lines.push(theme.fg("dim", line));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (details?.fetchUrls && details.fetchUrls.length > 0) {
|
|
191
|
+
if (details.curated) {
|
|
192
|
+
lines.push(theme.fg("muted", `Fetching ${details.fetchUrls.length} URLs in background`));
|
|
193
|
+
} else {
|
|
194
|
+
lines.push(theme.fg("muted", "Fetching:"));
|
|
195
|
+
for (const u of details.fetchUrls.slice(0, 5)) {
|
|
196
|
+
const display = u.length > 60 ? u.slice(0, 57) + "..." : u;
|
|
197
|
+
lines.push(theme.fg("dim", " " + display));
|
|
198
|
+
}
|
|
199
|
+
if (details.fetchUrls.length > 5) {
|
|
200
|
+
lines.push(theme.fg("dim", ` ... and ${details.fetchUrls.length - 5} more`));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const totalLines = lines.length;
|
|
206
|
+
|
|
207
|
+
if (!expanded) {
|
|
208
|
+
const box = new Box(1, 0, (t) => theme.bg("toolSuccessBg", t));
|
|
209
|
+
box.addChild(new Text(statusLine, 0, 0));
|
|
210
|
+
|
|
211
|
+
let collapsedLines = 1; // statusLine
|
|
212
|
+
const summaryPreview = details?.summary?.text?.trim() || "";
|
|
213
|
+
if (summaryPreview) {
|
|
214
|
+
const preview = summaryPreview.length > 120 ? summaryPreview.slice(0, 117) + "..." : summaryPreview;
|
|
215
|
+
box.addChild(new Text(theme.fg("dim", preview), 0, 0));
|
|
216
|
+
collapsedLines++;
|
|
217
|
+
} else if (details?.curatedQueries?.length) {
|
|
218
|
+
for (const cq of details.curatedQueries.slice(0, 3)) {
|
|
219
|
+
const dq = cq.query.length > 55 ? cq.query.slice(0, 52) + "..." : cq.query;
|
|
220
|
+
const srcCount = cq.sources?.length ?? 0;
|
|
221
|
+
const suffix = cq.error ? theme.fg("error", " (error)") : theme.fg("dim", ` · ${srcCount} sources`);
|
|
222
|
+
box.addChild(new Text(theme.fg("accent", ` "${dq}"`) + suffix, 0, 0));
|
|
223
|
+
collapsedLines++;
|
|
224
|
+
}
|
|
225
|
+
if (details.curatedQueries.length > 3) {
|
|
226
|
+
box.addChild(new Text(theme.fg("dim", ` ... and ${details.curatedQueries.length - 3} more`), 0, 0));
|
|
227
|
+
collapsedLines++;
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
const textContent = firstTextContent(result);
|
|
231
|
+
const firstContentLine = textContent.split("\n").find(l => {
|
|
232
|
+
const t = l.trim();
|
|
233
|
+
return t && !t.startsWith("[") && !t.startsWith("#") && !t.startsWith("---");
|
|
234
|
+
});
|
|
235
|
+
const fallbackLine = (firstContentLine?.trim() || "").replace(/\*\*/g, "");
|
|
236
|
+
if (fallbackLine) {
|
|
237
|
+
const preview = fallbackLine.length > 120 ? fallbackLine.slice(0, 117) + "..." : fallbackLine;
|
|
238
|
+
box.addChild(new Text(theme.fg("dim", preview), 0, 0));
|
|
239
|
+
collapsedLines++;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const moreLines = Math.max(0, totalLines - collapsedLines);
|
|
243
|
+
if (moreLines > 0) {
|
|
244
|
+
box.addChild(new Text(theme.fg("muted", `\n... (${moreLines} more lines, ${totalLines} total, CTRL+O Expand)`), 0, 0));
|
|
245
|
+
}
|
|
246
|
+
return box;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export const renderCodeSearchResult: ToolResultRenderer = (result, { expanded }, theme) => {
|
|
253
|
+
const details = result.details as CodeSearchResultDetails | undefined;
|
|
254
|
+
if (details?.error) {
|
|
255
|
+
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const summary = theme.fg("success", "code context returned") +
|
|
259
|
+
theme.fg("muted", ` (${details?.maxTokens ?? 5000} tokens max)`);
|
|
260
|
+
if (!expanded) return new Text(summary, 0, 0);
|
|
261
|
+
|
|
262
|
+
const textContent = firstTextContent(result);
|
|
263
|
+
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
264
|
+
return new Text(summary + "\n" + theme.fg("dim", preview), 0, 0);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export const renderFetchContentResult: ToolResultRenderer = (result, { expanded, isPartial }, theme) => {
|
|
268
|
+
const details = result.details as FetchContentResultDetails | undefined;
|
|
269
|
+
|
|
270
|
+
if (isPartial) {
|
|
271
|
+
const progress = details?.progress ?? 0;
|
|
272
|
+
const bar = progressBar(progress);
|
|
273
|
+
return new Text(theme.fg("accent", `[${bar}] ${details?.phase || "fetching"}`), 0, 0);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (details?.error) {
|
|
277
|
+
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (details?.urlCount === 1) {
|
|
281
|
+
const title = details?.title || "Untitled";
|
|
282
|
+
const imgCount = details?.imageCount ?? (details?.hasImage ? 1 : 0);
|
|
283
|
+
const imageBadge = imgCount > 1
|
|
284
|
+
? theme.fg("accent", ` [${imgCount} images]`)
|
|
285
|
+
: imgCount === 1
|
|
286
|
+
? theme.fg("accent", " [image]")
|
|
287
|
+
: "";
|
|
288
|
+
let statusLine = theme.fg("success", title) + theme.fg("muted", ` (${details?.totalChars ?? 0} chars)`) + imageBadge;
|
|
289
|
+
if (details?.truncated) {
|
|
290
|
+
statusLine += theme.fg("warning", " [truncated]");
|
|
291
|
+
}
|
|
292
|
+
if (typeof details?.duration === "number") {
|
|
293
|
+
statusLine += theme.fg("muted", ` | ${formatSeconds(Math.floor(details.duration))} total`);
|
|
294
|
+
}
|
|
295
|
+
const textContent = firstTextContent(result);
|
|
296
|
+
if (!expanded) {
|
|
297
|
+
const brief = textContent.length > 200 ? textContent.slice(0, 200) + "..." : textContent;
|
|
298
|
+
return new Text(statusLine + "\n" + theme.fg("dim", brief), 0, 0);
|
|
299
|
+
}
|
|
300
|
+
const lines = [statusLine];
|
|
301
|
+
if (details?.prompt) {
|
|
302
|
+
const display = details.prompt.length > 250 ? details.prompt.slice(0, 247) + "..." : details.prompt;
|
|
303
|
+
lines.push(theme.fg("dim", ` prompt: "${display}"`));
|
|
304
|
+
}
|
|
305
|
+
if (details?.timestamp) {
|
|
306
|
+
lines.push(theme.fg("dim", ` timestamp: ${details.timestamp}`));
|
|
307
|
+
}
|
|
308
|
+
if (typeof details?.frames === "number") {
|
|
309
|
+
lines.push(theme.fg("dim", ` frames: ${details.frames}`));
|
|
310
|
+
}
|
|
311
|
+
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
312
|
+
lines.push(theme.fg("dim", preview));
|
|
313
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const countColor = (details?.successful ?? 0) > 0 ? "success" : "error";
|
|
317
|
+
const statusLine = theme.fg(countColor, `${details?.successful}/${details?.urlCount} URLs`) + theme.fg("muted", " (content stored)");
|
|
318
|
+
if (!expanded) {
|
|
319
|
+
return new Text(statusLine, 0, 0);
|
|
320
|
+
}
|
|
321
|
+
const textContent = firstTextContent(result);
|
|
322
|
+
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
323
|
+
return new Text(statusLine + "\n" + theme.fg("dim", preview), 0, 0);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export const renderGetSearchContentResult: ToolResultRenderer = (result, { expanded }, theme) => {
|
|
327
|
+
const details = result.details as GetSearchContentResultDetails | undefined;
|
|
328
|
+
|
|
329
|
+
if (details?.error) {
|
|
330
|
+
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let statusLine: string;
|
|
334
|
+
if (details?.query) {
|
|
335
|
+
statusLine = theme.fg("success", `"${details.query}"`) + theme.fg("muted", ` (${details.resultCount} results)`);
|
|
336
|
+
} else {
|
|
337
|
+
statusLine = theme.fg("success", details?.title || "Content") + theme.fg("muted", ` (${details?.contentLength ?? 0} chars)`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!expanded) {
|
|
341
|
+
return new Text(statusLine, 0, 0);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const textContent = firstTextContent(result);
|
|
345
|
+
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
346
|
+
return new Text(statusLine + "\n" + theme.fg("dim", preview), 0, 0);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export function renderWebAccessToolResult(name: string, args: ToolRenderResultArgs): ToolRenderResult {
|
|
350
|
+
switch (name) {
|
|
351
|
+
case "web_search":
|
|
352
|
+
return renderWebSearchResult(...args);
|
|
353
|
+
case "code_search":
|
|
354
|
+
return renderCodeSearchResult(...args);
|
|
355
|
+
case "fetch_content":
|
|
356
|
+
return renderFetchContentResult(...args);
|
|
357
|
+
case "get_search_content":
|
|
358
|
+
return renderGetSearchContentResult(...args);
|
|
359
|
+
default: {
|
|
360
|
+
const theme = args[2];
|
|
361
|
+
return new Text(theme.fg("error", `Result renderer not found: ${name}`), 0, 0);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -6,6 +6,27 @@ 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.26-alpha.2] - 2026-06-05
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Updated the `research-codebase` skill to capture a `breaking_changes_allowed` compatibility posture before research fanout, carry it through sub-agent prompts, and record it in research documents so downstream specs and workflows do not preserve legacy APIs by default when breaking changes are allowed ([#1225](https://github.com/bastani-inc/atomic/issues/1225)).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Fixed stage-local workflow HIL `input` and `editor` prompts losing draft text across Ctrl+D detach/reattach; drafts are kept live-only in memory and cleared when the prompt or run/stage exits ([#1179](https://github.com/bastani-inc/atomic/issues/1179)).
|
|
18
|
+
- Fixed workflow worktree Git commands to strip ambient repository-local Git environment variables before inspecting or creating targeted worktrees.
|
|
19
|
+
- Suppressed intermediate model fallback failure warnings from successful workflow stages while preserving final failures and raw per-attempt diagnostics ([#1226](https://github.com/bastani-inc/atomic/issues/1226)).
|
|
20
|
+
|
|
21
|
+
## [0.8.26-alpha.1] - 2026-06-05
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Fixed the inline-form "snapshot lost" renderer and the `workflow.run.start`/`workflow.run.end` banner renderers returning bare strings, which crashed the host TUI with `child.render is not a function` when resuming a session containing persisted workflow custom messages. These renderers now return proper render components ([#1236](https://github.com/bastani-inc/atomic/issues/1236)).
|
|
26
|
+
- Fixed the workflow input form (the `/workflow <name>` argument selector) leaking into model context: spawning the picker and exiting without running the workflow no longer sends the form to the LLM. The input-form card is now emitted with `excludeFromContext` since it is transient UI, not conversation.
|
|
27
|
+
- Fixed the workflow input widget re-rendering in chat after `/resume`. Inline-form state is now cleared on `session_start`, and a rehydrated `workflows:input-form` card whose backing state is gone now renders nothing (returns `null`) instead of a stale form or "snapshot lost" placeholder.
|
|
28
|
+
- Stage sessions now emit `session_shutdown` before `dispose()` (mirroring the host `AgentSessionRuntime` teardown) so bound extensions receive a graceful shutdown signal instead of being silently invalidated. This stops disposed stage sessions from leaking child MCP servers and from triggering spurious stale-context "MCP initialization failed" errors when an extension's deferred `session_start` work races with stage disposal.
|
|
29
|
+
|
|
9
30
|
## [0.8.25] - 2026-06-04
|
|
10
31
|
|
|
11
32
|
### Changed
|
|
@@ -24,14 +24,24 @@ The user's research question/request is: **$ARGUMENTS**
|
|
|
24
24
|
- **CRITICAL**: Read these files yourself in the main context before spawning any sub-tasks
|
|
25
25
|
- This ensures you have full context before decomposing the research
|
|
26
26
|
|
|
27
|
-
2. **
|
|
27
|
+
2. **Determine the compatibility posture:**
|
|
28
|
+
- Before decomposing the research request, identify whether this project must preserve backward compatibility for real downstream users.
|
|
29
|
+
- If the user explicitly allows breaking changes, public API changes, cleanup, or says there are no real users/downstream dependencies, set `breaking_changes_allowed: true`.
|
|
30
|
+
- If the user mentions production users, published APIs, downstream consumers, migration safety, or compatibility requirements, set `breaking_changes_allowed: false`.
|
|
31
|
+
- If the posture is not inferable from the request, ask the user once before continuing, using the available structured question tool when possible.
|
|
32
|
+
- Carry this posture into the research plan, every sub-agent prompt, the final research document frontmatter, and the `## Compatibility Context` section.
|
|
33
|
+
- When `breaking_changes_allowed: true`, document existing legacy behavior, compatibility shims, optional flags, and public APIs as current state, not as constraints future specs must preserve unless the user explicitly asks for preservation.
|
|
34
|
+
- When `breaking_changes_allowed: false`, document public APIs, compatibility-sensitive surfaces, downstream callers, migration constraints, and behavior that future work must preserve.
|
|
35
|
+
|
|
36
|
+
3. **Analyze and decompose the research question:**
|
|
28
37
|
- Break the research question down into composable research areas
|
|
29
38
|
- Take time to ultrathink about the underlying patterns, connections, and architectural implications the user might be seeking
|
|
30
39
|
- Identify specific components, patterns, or concepts to investigate
|
|
31
40
|
- Create a research plan using TodoWrite to track all subtasks
|
|
41
|
+
- Include the compatibility posture in the plan so later synthesis and spec creation inherit the same constraint.
|
|
32
42
|
- Consider which directories, files, or architectural patterns are relevant
|
|
33
43
|
|
|
34
|
-
|
|
44
|
+
4. **Spawn parallel sub-agent tasks:**
|
|
35
45
|
- Create multiple Task agents to research different aspects concurrently
|
|
36
46
|
- We now have specialized agents that know how to do specific research tasks:
|
|
37
47
|
|
|
@@ -67,8 +77,9 @@ The user's research question/request is: **$ARGUMENTS**
|
|
|
67
77
|
- Each agent knows its job - just tell it what you're looking for
|
|
68
78
|
- Don't write detailed prompts about HOW to search - the agents already know
|
|
69
79
|
- Remind agents they are documenting, not evaluating or improving
|
|
80
|
+
- Include `breaking_changes_allowed: true` or `breaking_changes_allowed: false` in each sub-agent prompt so compatibility-sensitive findings are documented with the right posture.
|
|
70
81
|
|
|
71
|
-
|
|
82
|
+
5. **Wait for all sub-agents to complete and synthesize:**
|
|
72
83
|
- IMPORTANT: Wait for ALL sub-agent tasks to complete before proceeding
|
|
73
84
|
- Compile all sub-agent results (both codebase and research findings)
|
|
74
85
|
- Prioritize live codebase findings as primary source of truth
|
|
@@ -79,7 +90,7 @@ The user's research question/request is: **$ARGUMENTS**
|
|
|
79
90
|
- Answer the user's research question with concrete evidence
|
|
80
91
|
- **If findings reveal the original question was misframed** (e.g., the system works differently than assumed, or the components don't exist where expected), flag this to the user before finalizing the document. This is valuable signal — don't bury it.
|
|
81
92
|
|
|
82
|
-
|
|
93
|
+
6. **Generate research document:**
|
|
83
94
|
- Follow the directory structure for research documents:
|
|
84
95
|
|
|
85
96
|
```
|
|
@@ -117,6 +128,8 @@ research/
|
|
|
117
128
|
status: complete
|
|
118
129
|
last_updated: !`date '+%Y-%m-%d'`
|
|
119
130
|
last_updated_by: [Researcher name]
|
|
131
|
+
breaking_changes_allowed: [true or false]
|
|
132
|
+
compatibility_context: "[Short explanation of downstream-user/API compatibility posture]"
|
|
120
133
|
---
|
|
121
134
|
|
|
122
135
|
# Research
|
|
@@ -125,6 +138,10 @@ research/
|
|
|
125
138
|
|
|
126
139
|
[Original user query]
|
|
127
140
|
|
|
141
|
+
## Compatibility Context
|
|
142
|
+
|
|
143
|
+
[State whether breaking changes are allowed. If true, note that existing compatibility shims, optional flags, legacy APIs, and public APIs are documented as current state rather than preservation constraints. If false, summarize compatibility-sensitive surfaces, downstream users/callers, migration constraints, and behavior future work must preserve.]
|
|
144
|
+
|
|
128
145
|
## Summary
|
|
129
146
|
|
|
130
147
|
[High-level documentation of what was found, answering the user's question by describing what exists]
|
|
@@ -167,19 +184,19 @@ research/
|
|
|
167
184
|
[Any areas that need further investigation]
|
|
168
185
|
```
|
|
169
186
|
|
|
170
|
-
|
|
187
|
+
7. **Add GitHub permalinks (if applicable):**
|
|
171
188
|
- Check if on main branch or if commit is pushed: `git branch --show-current` and `git status`
|
|
172
189
|
- If on main/master or pushed, generate GitHub permalinks:
|
|
173
190
|
- Get repo info: `gh repo view --json owner,name`
|
|
174
191
|
- Create permalinks: `https://github.com/{owner}/{repo}/blob/{commit}/{file}#L{line}`
|
|
175
192
|
- Replace local file references with permalinks in the document
|
|
176
193
|
|
|
177
|
-
|
|
194
|
+
8. **Present findings:**
|
|
178
195
|
- Present a concise summary of findings to the user
|
|
179
196
|
- Include key file references for easy navigation
|
|
180
197
|
- Ask if they have follow-up questions or need clarification
|
|
181
198
|
|
|
182
|
-
|
|
199
|
+
9. **Handle follow-up questions:**
|
|
183
200
|
|
|
184
201
|
- If the user has follow-up questions, append to the same research document
|
|
185
202
|
- Update the frontmatter fields `last_updated` and `last_updated_by` to reflect the update
|
|
@@ -207,10 +224,12 @@ research/
|
|
|
207
224
|
- **REMEMBER**: Document what IS, not what SHOULD BE
|
|
208
225
|
- **NO RECOMMENDATIONS**: Only describe the current state of the codebase
|
|
209
226
|
- **File reading**: Always read mentioned files FULLY (no limit/offset) before spawning sub-tasks
|
|
227
|
+
- **Compatibility posture**: Always determine `breaking_changes_allowed` before decomposing the question. This is a single project/research posture, not a request to add compatibility flags. Use it to document whether old APIs and shims are constraints for future work.
|
|
210
228
|
- **Critical ordering**: Follow the numbered steps exactly
|
|
211
229
|
- ALWAYS read mentioned files first before spawning sub-tasks (step 1)
|
|
212
|
-
- ALWAYS
|
|
213
|
-
- ALWAYS
|
|
230
|
+
- ALWAYS determine compatibility posture before decomposing the question (step 2)
|
|
231
|
+
- ALWAYS wait for all sub-agents to complete before synthesizing (step 5)
|
|
232
|
+
- ALWAYS gather metadata before writing the document (as part of step 6)
|
|
214
233
|
- NEVER write the research document with placeholder values
|
|
215
234
|
|
|
216
235
|
- **Frontmatter consistency**:
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
openInlineInputsForm,
|
|
55
55
|
registerInlineFormRenderer,
|
|
56
56
|
} from "../tui/inline-form-overlay.js";
|
|
57
|
+
import { clearForms } from "../tui/inline-form-store.js";
|
|
57
58
|
import {
|
|
58
59
|
registerChatSurfaceRenderer,
|
|
59
60
|
emitChatSurface,
|
|
@@ -166,7 +167,7 @@ export interface PiMessageRenderOptions {
|
|
|
166
167
|
expanded: boolean;
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
export type PiMessageRendererResult = string | PiMessageRenderComponent | undefined;
|
|
170
|
+
export type PiMessageRendererResult = string | PiMessageRenderComponent | null | undefined;
|
|
170
171
|
export type PiMessageRenderer = (
|
|
171
172
|
payload: unknown,
|
|
172
173
|
options?: PiMessageRenderOptions,
|
|
@@ -3756,11 +3757,14 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3756
3757
|
// duplicating it into chat scroll just creates visual noise and pushes
|
|
3757
3758
|
// older chat content out of view every time a stage transitions.
|
|
3758
3759
|
if (typeof pi.registerMessageRenderer === "function") {
|
|
3760
|
+
// Wrap the string-producing banners in a render component: the host adds a
|
|
3761
|
+
// renderer's result directly as a TUI child, so a bare string would crash
|
|
3762
|
+
// `Container.render()` with "child.render is not a function".
|
|
3759
3763
|
pi.registerMessageRenderer("workflow.run.start", (payload) =>
|
|
3760
|
-
renderRunBanner(payload as RunStartPayload),
|
|
3764
|
+
dynamicTextRenderComponent(() => renderRunBanner(payload as RunStartPayload)),
|
|
3761
3765
|
);
|
|
3762
3766
|
pi.registerMessageRenderer("workflow.run.end", (payload) =>
|
|
3763
|
-
renderRunSummary(payload as RunEndPayload),
|
|
3767
|
+
dynamicTextRenderComponent(() => renderRunSummary(payload as RunEndPayload)),
|
|
3764
3768
|
);
|
|
3765
3769
|
// Inline workflow-input form (Option C in the design conversation):
|
|
3766
3770
|
// a sticky chat-history card driven by a custom EditorComponent. The
|
|
@@ -3834,6 +3838,12 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3834
3838
|
persistence: persistenceRef.current,
|
|
3835
3839
|
});
|
|
3836
3840
|
store.clear();
|
|
3841
|
+
// Drop any inline input-form state from a previous session in this pi
|
|
3842
|
+
// process. A resumed/replaced session must not render a stale live form,
|
|
3843
|
+
// and rehydrated `workflows:input-form` cards then resolve to no backing
|
|
3844
|
+
// state so their renderer suppresses output (input widget hidden after
|
|
3845
|
+
// /resume).
|
|
3846
|
+
clearForms();
|
|
3837
3847
|
resetWorkflowLifecycleNotificationState(lifecycleNotificationState);
|
|
3838
3848
|
resetWorkflowHilAnswerNotificationState(hilAnswerNotificationState);
|
|
3839
3849
|
stageControlRegistry.clear();
|
|
@@ -285,6 +285,57 @@ function terminatingToolResultText(
|
|
|
285
285
|
return undefined;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* A stage session backed by a real Atomic `AgentSession` exposes its
|
|
290
|
+
* `extensionRunner`. When workflow wiring binds extensions to a stage session it
|
|
291
|
+
* replays the `session_start` lifecycle (see wiring.ts `bindExtensions`), so
|
|
292
|
+
* extensions such as MCP begin per-session initialization. Tearing that session
|
|
293
|
+
* down with `dispose()` alone invalidates the extension runtime WITHOUT emitting
|
|
294
|
+
* `session_shutdown`, so those extensions never receive a graceful teardown
|
|
295
|
+
* signal: MCP, for example, logs a spurious stale-context "initialization
|
|
296
|
+
* failed" error when its deferred init races with disposal, and leaves any child
|
|
297
|
+
* MCP servers running.
|
|
298
|
+
*
|
|
299
|
+
* The test stub session (createTestAgentSession) has no `extensionRunner`, so the
|
|
300
|
+
* capability is optional and feature-detected at runtime.
|
|
301
|
+
*/
|
|
302
|
+
type StageSessionExtensionRunner = {
|
|
303
|
+
hasHandlers(eventType: string): boolean;
|
|
304
|
+
emit(event: { readonly type: "session_shutdown"; readonly reason: "quit" }): Promise<unknown>;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
function stageSessionExtensionRunner(
|
|
308
|
+
current: StageSessionRuntime,
|
|
309
|
+
): StageSessionExtensionRunner | undefined {
|
|
310
|
+
const runner = (current as StageSessionRuntime & { extensionRunner?: StageSessionExtensionRunner })
|
|
311
|
+
.extensionRunner;
|
|
312
|
+
if (runner && typeof runner.hasHandlers === "function" && typeof runner.emit === "function") {
|
|
313
|
+
return runner;
|
|
314
|
+
}
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Dispose a stage session, mirroring the host `AgentSessionRuntime` teardown:
|
|
320
|
+
* emit `session_shutdown` before `dispose()` whenever the session exposes a
|
|
321
|
+
* compatible extension runner, so extensions tear down per-session resources
|
|
322
|
+
* (and bump their lifecycle generation) instead of being silently invalidated.
|
|
323
|
+
* A throwing shutdown handler must never strand the session, so disposal always
|
|
324
|
+
* runs.
|
|
325
|
+
*/
|
|
326
|
+
async function disposeStageSession(current: StageSessionRuntime | undefined): Promise<void> {
|
|
327
|
+
if (!current) return;
|
|
328
|
+
const runner = stageSessionExtensionRunner(current);
|
|
329
|
+
if (runner?.hasHandlers("session_shutdown")) {
|
|
330
|
+
try {
|
|
331
|
+
await runner.emit({ type: "session_shutdown", reason: "quit" });
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error("atomic-workflows: stage session_shutdown handler failed", error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
await current.dispose();
|
|
337
|
+
}
|
|
338
|
+
|
|
288
339
|
function asAgentSession(activeSession: StageSessionRuntime | undefined): AgentSession | undefined {
|
|
289
340
|
if (!activeSession) return undefined;
|
|
290
341
|
const candidate = activeSession as StageSessionRuntime & Partial<Pick<AgentSession, "state" | "sessionManager" | "modelRegistry" | "getContextUsage">>;
|
|
@@ -535,6 +586,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
535
586
|
let selectedModel: string | undefined;
|
|
536
587
|
const modelAttempts: WorkflowModelAttempt[] = [];
|
|
537
588
|
const modelWarnings: string[] = [];
|
|
589
|
+
const pendingFallbackWarnings: string[] = [];
|
|
538
590
|
const modelCatalog = opts.models === undefined
|
|
539
591
|
? undefined
|
|
540
592
|
: {
|
|
@@ -675,7 +727,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
675
727
|
unsubscribeTerminateWatcher?.();
|
|
676
728
|
unsubscribeTerminateWatcher = undefined;
|
|
677
729
|
terminatingToolCallIds.clear();
|
|
678
|
-
await current
|
|
730
|
+
await disposeStageSession(current);
|
|
679
731
|
}
|
|
680
732
|
|
|
681
733
|
async function promptWithPauseResume(
|
|
@@ -745,15 +797,19 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
745
797
|
try {
|
|
746
798
|
await promptWithPauseResume(activeSession, text, sdkOptions);
|
|
747
799
|
modelAttempts.push({ model: candidate.id, success: true, ...modelAttemptReasoning(candidate) });
|
|
800
|
+
pendingFallbackWarnings.length = 0;
|
|
748
801
|
return;
|
|
749
802
|
} catch (err) {
|
|
750
803
|
const message = errorMessage(err);
|
|
751
804
|
modelAttempts.push({ model: candidate.id, success: false, ...modelAttemptReasoning(candidate), error: message });
|
|
752
805
|
if (signal?.aborted || !isRetryableModelFailure(message) || index === candidates.length - 1) {
|
|
806
|
+
modelWarnings.push(...pendingFallbackWarnings);
|
|
807
|
+
pendingFallbackWarnings.length = 0;
|
|
808
|
+
notifyModelFallbackMetaChange();
|
|
753
809
|
throw err;
|
|
754
810
|
}
|
|
755
811
|
const nextCandidate = candidates[index + 1]!;
|
|
756
|
-
|
|
812
|
+
pendingFallbackWarnings.push(`[fallback] ${candidateLabel(candidate)} failed: ${message}. Retrying with ${candidateLabel(nextCandidate)}.`);
|
|
757
813
|
await disposeCurrentSession();
|
|
758
814
|
index += 1;
|
|
759
815
|
}
|
|
@@ -895,7 +951,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
895
951
|
unsubscribeTerminateWatcher?.();
|
|
896
952
|
unsubscribeTerminateWatcher = undefined;
|
|
897
953
|
terminatingToolCallIds.clear();
|
|
898
|
-
await session
|
|
954
|
+
await disposeStageSession(session);
|
|
899
955
|
},
|
|
900
956
|
|
|
901
957
|
__getLastAssistantText() {
|