@frockbot/plugin-shell 0.3.22 → 0.3.24

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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The building view, as the two surfaces that draw it are written.
3
+ *
4
+ * Bun has no single-file-component loader, so a `.vue` file is read as text
5
+ * here — the same thing `ComputerCard.test.ts` does. It proves the wiring the
6
+ * unit tests for the projection cannot: that the panel and the phone chip both
7
+ * read the one projection, and that neither still shows the fixed line it used
8
+ * to show for the whole of a build.
9
+ */
10
+ import { describe, expect, test } from "bun:test";
11
+ import { parse } from "@vue/compiler-sfc";
12
+
13
+ const canvas = await Bun.file(
14
+ new URL("./AppletCanvas.vue", import.meta.url),
15
+ ).text();
16
+ const app = await Bun.file(
17
+ new URL("./FrockBotApp.vue", import.meta.url),
18
+ ).text();
19
+ const styles = await Bun.file(new URL("./styles.css", import.meta.url)).text();
20
+
21
+ describe("the Applet canvas while the Bot is still building", () => {
22
+ test("the file parses as a single-file component", () => {
23
+ expect(parse(canvas).errors).toEqual([]);
24
+ });
25
+
26
+ test("the panel draws the projection rather than a fixed line", () => {
27
+ expect(canvas).toContain('from "./applet-progress.js"');
28
+ expect(canvas).toContain('data-testid="applet-canvas-progress"');
29
+ expect(canvas).toContain("progress.label");
30
+ expect(canvas).toContain("progress.failure");
31
+ expect(canvas).toContain("progress.output");
32
+ // The line the panel used to show for minutes on end, whatever happened.
33
+ expect(canvas).not.toContain("Not published yet");
34
+ });
35
+
36
+ test("the progress block is announced and is gone once the Applet runs", () => {
37
+ expect(canvas).toContain('role="status"');
38
+ expect(canvas).toContain(
39
+ 'v-if="applet && !loading && progress && beingBuilt"',
40
+ );
41
+ });
42
+
43
+ test("the build output is monospace and cannot grow without bound", () => {
44
+ expect(canvas).toContain("font-family: var(--frock-font-mono);");
45
+ expect(canvas).toContain("max-height: 180px;");
46
+ });
47
+
48
+ test("the working dot stops moving where motion is not wanted", () => {
49
+ expect(canvas).toContain("@media (prefers-reduced-motion: reduce)");
50
+ expect(canvas).toContain(
51
+ ".applet-canvas-progress-dot {\n animation: none;",
52
+ );
53
+ });
54
+ });
55
+
56
+ describe("the phone's way in", () => {
57
+ test("the chip carries the same line and opens the same panel", () => {
58
+ expect(app).toContain('from "./applet-progress.js"');
59
+ expect(app).toContain("appletChipStatus");
60
+ // The one overlay: the right panel drawer, opened by the one toggle.
61
+ expect(app).toContain('class="applet-chip"');
62
+ expect(app).toContain('@click="toggleRightPanel"');
63
+ });
64
+
65
+ test("the chip has room for a second line", () => {
66
+ expect(styles).toContain(".applet-chip-status {");
67
+ expect(styles).toContain("min-height: var(--frock-control-md);");
68
+ });
69
+ });
@@ -0,0 +1,300 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ APPLET_PROGRESS_OUTPUT_LINES_V1,
4
+ appletCommandOutputV1,
5
+ appletIsBeingBuiltV1,
6
+ appletProgressV1,
7
+ } from "./applet-progress.js";
8
+ import type { WebToolActivity } from "../shared.js";
9
+
10
+ const draft = {
11
+ appletId: "u1abc.todo",
12
+ displayName: "Weekly Todos",
13
+ status: "draft" as const,
14
+ tools: [],
15
+ createdAt: "2026-09-05T00:00:00.000Z",
16
+ };
17
+ const published = {
18
+ ...draft,
19
+ status: "published" as const,
20
+ currentGenerationId: "generation-2",
21
+ };
22
+
23
+ const sourceOf = (...paths: string[]) => ({
24
+ appletId: draft.appletId,
25
+ truncated: false,
26
+ files: paths.map((path) => ({
27
+ path,
28
+ text: "export default class {}",
29
+ generationId: "w-1",
30
+ changedAt: "2026-09-05T00:01:00.000Z",
31
+ })),
32
+ });
33
+
34
+ function tool(
35
+ name: string,
36
+ status: WebToolActivity["status"],
37
+ extra: Partial<WebToolActivity> = {},
38
+ ): WebToolActivity {
39
+ return { id: `${name}-${status}`, name, status, ...extra };
40
+ }
41
+
42
+ describe("what the Bot is doing to the focused Applet", () => {
43
+ test("no Applet is no line at all", () => {
44
+ expect(appletProgressV1({})).toBeUndefined();
45
+ expect(appletProgressV1({ applet: null })).toBeUndefined();
46
+ });
47
+
48
+ test("a draft nothing has been said about yet reads as still being built", () => {
49
+ // The same words the Applets list uses, so the two surfaces agree.
50
+ expect(appletProgressV1({ applet: draft })).toMatchObject({
51
+ stage: "unknown",
52
+ label: "Still being built",
53
+ working: false,
54
+ });
55
+ });
56
+
57
+ test("the create call is the first thing worth saying", () => {
58
+ expect(
59
+ appletProgressV1({
60
+ applet: draft,
61
+ tools: [tool("applets/applet_create", "completed")],
62
+ })?.stage,
63
+ ).toBe("created");
64
+ });
65
+
66
+ test("a native tool name counts the same as a namespaced one", () => {
67
+ expect(
68
+ appletProgressV1({
69
+ applet: draft,
70
+ tools: [tool("applet_create", "running")],
71
+ }),
72
+ ).toMatchObject({ stage: "created", working: true });
73
+ });
74
+
75
+ test("source on the Workspace means the Bot has been writing", () => {
76
+ expect(
77
+ appletProgressV1({
78
+ applet: draft,
79
+ source: sourceOf("server.ts", "ui.tsx"),
80
+ tools: [tool("applets/applet_create", "completed")],
81
+ })?.label,
82
+ ).toBe("Writing the code");
83
+ });
84
+
85
+ test("a check that passed moves the line on and shows what it printed", () => {
86
+ const progress = appletProgressV1({
87
+ applet: draft,
88
+ source: sourceOf("server.ts"),
89
+ tools: [
90
+ tool("computer_exec", "completed", {
91
+ text: "applet check: no problems found",
92
+ }),
93
+ ],
94
+ });
95
+ expect(progress).toMatchObject({
96
+ stage: "checking",
97
+ label: "The code checks out",
98
+ done: true,
99
+ });
100
+ expect(progress?.output).toEqual(["applet check: no problems found"]);
101
+ expect(progress?.failure).toBeUndefined();
102
+ });
103
+
104
+ test("a check that found errors is a failure in plain words", () => {
105
+ const progress = appletProgressV1({
106
+ applet: draft,
107
+ tools: [
108
+ tool("computer_exec", "failed", {
109
+ text: "ui.tsx:3:1 Unexpected any\napplet check: 1 error(s)",
110
+ }),
111
+ ],
112
+ });
113
+ expect(progress?.stage).toBe("checking");
114
+ expect(progress?.failure).toBe("The code has problems that need fixing.");
115
+ expect(progress?.output).toEqual([
116
+ "ui.tsx:3:1 Unexpected any",
117
+ "applet check: 1 error(s)",
118
+ ]);
119
+ });
120
+
121
+ test("a build is recognised by the files it says it wrote", () => {
122
+ const progress = appletProgressV1({
123
+ applet: draft,
124
+ tools: [
125
+ tool("computer_exec", "completed", {
126
+ text: [
127
+ "dist/server.js 0a1b2c3d4e5f",
128
+ "dist/ui.html f5e4d3c2b1a0",
129
+ "dist/manifest.json 1 tool(s): add_todo",
130
+ ].join("\n"),
131
+ }),
132
+ ],
133
+ });
134
+ expect(progress).toMatchObject({
135
+ stage: "building",
136
+ label: "Built and ready to go live",
137
+ done: true,
138
+ });
139
+ expect(progress?.output).toHaveLength(3);
140
+ });
141
+
142
+ test("a shell command that is not the Applet CLI says nothing", () => {
143
+ expect(
144
+ appletCommandOutputV1("total 0\ndrwxr-xr-x 2 box box"),
145
+ ).toBeUndefined();
146
+ expect(appletCommandOutputV1(undefined)).toBeUndefined();
147
+ expect(
148
+ appletProgressV1({
149
+ applet: draft,
150
+ source: sourceOf("server.ts"),
151
+ tools: [tool("computer_exec", "completed", { text: "hello" })],
152
+ })?.stage,
153
+ ).toBe("writing");
154
+ });
155
+
156
+ test("a shell command still running is the Bot working, not a new step", () => {
157
+ const progress = appletProgressV1({
158
+ applet: draft,
159
+ source: sourceOf("server.ts"),
160
+ tools: [tool("computer_exec", "running")],
161
+ });
162
+ expect(progress).toMatchObject({ stage: "writing", working: true });
163
+ });
164
+
165
+ test("output is bounded, newest lines kept", () => {
166
+ const lines = Array.from(
167
+ { length: 40 },
168
+ (_, index) => `ui.tsx:${index}:1 problem`,
169
+ );
170
+ const progress = appletProgressV1({
171
+ applet: draft,
172
+ tools: [
173
+ tool("computer_exec", "failed", {
174
+ text: [...lines, "applet check: 40 error(s)"].join("\n"),
175
+ }),
176
+ ],
177
+ });
178
+ expect(progress?.output).toHaveLength(APPLET_PROGRESS_OUTPUT_LINES_V1);
179
+ expect(progress?.output?.at(-1)).toBe("applet check: 40 error(s)");
180
+ });
181
+
182
+ test("a very long line is cut rather than allowed to run off the panel", () => {
183
+ const progress = appletProgressV1({
184
+ applet: draft,
185
+ tools: [
186
+ tool("computer_exec", "failed", {
187
+ text: `applet check: 1 error(s)\n${"x".repeat(500)}`,
188
+ }),
189
+ ],
190
+ });
191
+ expect(progress?.output?.at(-1)?.length).toBe(200);
192
+ expect(progress?.output?.at(-1)?.endsWith("…")).toBe(true);
193
+ });
194
+
195
+ test("a publish in flight is the last step, and clears an earlier complaint", () => {
196
+ const progress = appletProgressV1({
197
+ applet: draft,
198
+ tools: [
199
+ tool("computer_exec", "failed", { text: "applet check: 2 error(s)" }),
200
+ tool("applets/applet_publish", "running", {
201
+ input: { appletId: draft.appletId },
202
+ }),
203
+ ],
204
+ });
205
+ expect(progress).toMatchObject({
206
+ stage: "publishing",
207
+ label: "Getting it ready to open",
208
+ working: true,
209
+ });
210
+ expect(progress?.failure).toBeUndefined();
211
+ });
212
+
213
+ test("a publish that was refused says why, in the words it was refused with", () => {
214
+ const progress = appletProgressV1({
215
+ applet: draft,
216
+ tools: [
217
+ tool("applets/applet_publish", "failed", {
218
+ input: { appletId: draft.appletId },
219
+ text: 'Publishing u1abc.todo failed: "add_todo" is already a tool of "Shopping"',
220
+ }),
221
+ ],
222
+ });
223
+ expect(progress?.stage).toBe("publishing");
224
+ expect(progress?.failure).toBe(
225
+ 'Publishing u1abc.todo failed: "add_todo" is already a tool of "Shopping"',
226
+ );
227
+ });
228
+
229
+ test("a publish of another Applet never moves this one's line", () => {
230
+ expect(
231
+ appletProgressV1({
232
+ applet: draft,
233
+ tools: [
234
+ tool("applets/applet_publish", "running", {
235
+ input: { appletId: "u1abc.other" },
236
+ }),
237
+ ],
238
+ })?.stage,
239
+ ).toBe("unknown");
240
+ });
241
+
242
+ test("a live Applet is ready, and the canvas stops showing progress", () => {
243
+ const progress = appletProgressV1({ applet: published });
244
+ expect(progress).toMatchObject({
245
+ stage: "published",
246
+ label: "Ready to use",
247
+ done: true,
248
+ });
249
+ expect(appletIsBeingBuiltV1(progress)).toBe(false);
250
+ expect(appletIsBeingBuiltV1(appletProgressV1({ applet: draft }))).toBe(
251
+ true,
252
+ );
253
+ expect(appletIsBeingBuiltV1(undefined)).toBe(false);
254
+ });
255
+
256
+ test("a step still going never claims it finished", () => {
257
+ const progress = appletProgressV1({
258
+ applet: draft,
259
+ tools: [
260
+ tool("computer_exec", "completed", {
261
+ text: "applet check: no problems found",
262
+ }),
263
+ tool("computer_exec", "running"),
264
+ ],
265
+ });
266
+ expect(progress).toMatchObject({ done: false, working: true });
267
+ expect(progress?.label).toBe("Checking the code");
268
+ });
269
+
270
+ test("a Turn still running says so even when no Applet tool has been called", () => {
271
+ expect(appletProgressV1({ applet: draft, running: true })?.working).toBe(
272
+ true,
273
+ );
274
+ });
275
+
276
+ test("the outcome the Applet authority recorded is used when it has one", () => {
277
+ const progress = appletProgressV1({
278
+ applet: draft,
279
+ build: {
280
+ status: "failed",
281
+ command: "build",
282
+ summary: "dist/manifest.json is invalid",
283
+ diagnostics: ["hashes.server must be a sha-256 hex digest"],
284
+ },
285
+ });
286
+ expect(progress).toMatchObject({
287
+ stage: "building",
288
+ failure: "dist/manifest.json is invalid",
289
+ });
290
+ expect(progress?.output).toEqual([
291
+ "hashes.server must be a sha-256 hex digest",
292
+ ]);
293
+ });
294
+
295
+ test("an unknown recorded outcome is not a step that happened", () => {
296
+ expect(
297
+ appletProgressV1({ applet: draft, build: { status: "unknown" } })?.stage,
298
+ ).toBe("unknown");
299
+ });
300
+ });
@@ -0,0 +1,339 @@
1
+ /**
2
+ * What the Bot is doing to the focused Applet, in words a person reads.
3
+ *
4
+ * Building an Applet takes minutes: the Bot scaffolds it, edits the files on
5
+ * the Computer, runs `applet check` and `applet build` there, and only then
6
+ * publishes it. Until that publish lands there is nothing to run, so the
7
+ * canvas beside the conversation used to say "Not published yet" for the whole
8
+ * of it — true, unchanging, and no help to somebody watching. This module is
9
+ * the missing sentence: it reads what the client already knows and returns the
10
+ * one line that says where the work has got to.
11
+ *
12
+ * It is a projection, never an authority. Nothing here is stored, nothing here
13
+ * is asked of the backend, and every input is something the thread was already
14
+ * drawing. When the signals say nothing specific, the answer is the honest
15
+ * fallback rather than an invented step.
16
+ */
17
+ import type {
18
+ AppletBuildViewV1,
19
+ AppletSourceViewV1,
20
+ AppletSummaryV1,
21
+ } from "@frockbot/kernel-contracts";
22
+ import type { WebChatMessage, WebToolActivity } from "../shared.js";
23
+
24
+ /**
25
+ * Where the work has got to.
26
+ *
27
+ * The order is the order the Bot does them in, and the projection takes the
28
+ * furthest one it has evidence for. `unknown` is a draft with nothing said
29
+ * about it yet — the Bot has been asked, and the Turn that answers has not
30
+ * reached the Applet.
31
+ */
32
+ export type AppletProgressStageV1 =
33
+ | "unknown"
34
+ | "created"
35
+ | "writing"
36
+ | "checking"
37
+ | "building"
38
+ | "publishing"
39
+ | "published";
40
+
41
+ /** The tail of a check or a build, so a long log never fills the panel. */
42
+ export const APPLET_PROGRESS_OUTPUT_LINES_V1 = 12;
43
+ export const APPLET_PROGRESS_LINE_CHARACTERS_V1 = 200;
44
+ /** A failure is a sentence in the panel, not a wall of text. */
45
+ export const APPLET_PROGRESS_FAILURE_CHARACTERS_V1 = 400;
46
+
47
+ export interface AppletProgressV1 {
48
+ stage: AppletProgressStageV1;
49
+ /** The line the canvas and the phone both show. */
50
+ label: string;
51
+ /** True while the Turn doing this is still going. */
52
+ working: boolean;
53
+ /** True when this step finished and finished cleanly. */
54
+ done: boolean;
55
+ /** What went wrong, in the words of whatever refused. */
56
+ failure?: string;
57
+ /** The last lines the check or the build printed. */
58
+ output?: string[];
59
+ }
60
+
61
+ export interface AppletProgressInputV1 {
62
+ /** The focused Applet's directory entry, when there is one. */
63
+ applet?: AppletSummaryV1 | null;
64
+ /** The Applet's source, as the canvas already reads it. */
65
+ source?: AppletSourceViewV1;
66
+ /** The outcome the Applet authority recorded, when it has recorded one. */
67
+ build?: AppletBuildViewV1;
68
+ /** The newest Turn's tool activity, oldest first. */
69
+ tools?: readonly WebToolActivity[];
70
+ /** True while that Turn is still running. */
71
+ running?: boolean;
72
+ }
73
+
74
+ /**
75
+ * The words for each step, and for the step having finished.
76
+ *
77
+ * A check that has come back clean is worth its own sentence: "Checking the
78
+ * code" would keep saying a thing was happening after it stopped happening.
79
+ */
80
+ const LABELS: Record<AppletProgressStageV1, { doing: string; done: string }> = {
81
+ // Matches the Applets list, which says "Still being built" for a draft.
82
+ unknown: { doing: "Still being built", done: "Still being built" },
83
+ created: { doing: "Just getting started", done: "Just getting started" },
84
+ writing: { doing: "Writing the code", done: "Writing the code" },
85
+ checking: { doing: "Checking the code", done: "The code checks out" },
86
+ building: {
87
+ doing: "Putting it together",
88
+ done: "Built and ready to go live",
89
+ },
90
+ publishing: {
91
+ doing: "Getting it ready to open",
92
+ done: "Getting it ready to open",
93
+ },
94
+ published: { doing: "Ready to use", done: "Ready to use" },
95
+ };
96
+
97
+ const ORDER: AppletProgressStageV1[] = [
98
+ "unknown",
99
+ "created",
100
+ "writing",
101
+ "checking",
102
+ "building",
103
+ "publishing",
104
+ "published",
105
+ ];
106
+
107
+ function furthest(
108
+ left: AppletProgressStageV1,
109
+ right: AppletProgressStageV1,
110
+ ): AppletProgressStageV1 {
111
+ return ORDER.indexOf(right) > ORDER.indexOf(left) ? right : left;
112
+ }
113
+
114
+ /** The bare tool name, whether it arrived namespaced or native. */
115
+ function toolName(activity: WebToolActivity): string {
116
+ const slash = activity.name.lastIndexOf("/");
117
+ return slash < 0 ? activity.name : activity.name.slice(slash + 1);
118
+ }
119
+
120
+ /**
121
+ * Whether this tool call is about the Applet in the canvas.
122
+ *
123
+ * A dynamic call carries its parsed arguments, so a publish of some other
124
+ * Applet never moves this one's line. `applet_create` names no id — it is
125
+ * making one — so it counts for whichever Applet the Session then focuses,
126
+ * which is the one it just created.
127
+ */
128
+ function namesApplet(activity: WebToolActivity, appletId: string): boolean {
129
+ const input = activity.input;
130
+ if (!input || typeof input !== "object" || Array.isArray(input)) return true;
131
+ const named = (input as Record<string, unknown>).appletId;
132
+ return typeof named === "string" ? named === appletId : true;
133
+ }
134
+
135
+ function trimLine(line: string): string {
136
+ return line.length > APPLET_PROGRESS_LINE_CHARACTERS_V1
137
+ ? `${line.slice(0, APPLET_PROGRESS_LINE_CHARACTERS_V1 - 1)}…`
138
+ : line;
139
+ }
140
+
141
+ function tail(text: string): string[] {
142
+ const lines = text
143
+ .split("\n")
144
+ .map((line) => line.trimEnd())
145
+ .filter((line) => line.length > 0);
146
+ return lines.slice(-APPLET_PROGRESS_OUTPUT_LINES_V1).map(trimLine);
147
+ }
148
+
149
+ function sentence(text: string): string {
150
+ const trimmed = text.trim();
151
+ if (trimmed.length === 0) return "";
152
+ return trimmed.length > APPLET_PROGRESS_FAILURE_CHARACTERS_V1
153
+ ? `${trimmed.slice(0, APPLET_PROGRESS_FAILURE_CHARACTERS_V1 - 1)}…`
154
+ : trimmed;
155
+ }
156
+
157
+ /**
158
+ * Whether a shell command's output is the `applet` CLI reporting on itself.
159
+ *
160
+ * The client is never told what a `computer_exec` ran: the Turn projection
161
+ * carries the input of dynamic tool calls only, and `computer_exec` is a
162
+ * native tool. What it does carry is the result, and the CLI's output is a
163
+ * stated contract — `applet check:` on one line, and the three `dist/` paths a
164
+ * build writes — so recognising it is reading a published shape rather than
165
+ * guessing at a command. Anything else the Bot ran on the Computer looks like
166
+ * nothing here and is ignored, which is the right failure: no line rather than
167
+ * a wrong one.
168
+ */
169
+ export function appletCommandOutputV1(
170
+ text: string | undefined,
171
+ ): { command: "check" | "build"; output: string[] } | undefined {
172
+ if (!text) return undefined;
173
+ if (/^applet check:/m.test(text)) {
174
+ return { command: "check", output: tail(text) };
175
+ }
176
+ if (/dist\/manifest\.json\b/.test(text) && /dist\/server\.js\b/.test(text)) {
177
+ return { command: "build", output: tail(text) };
178
+ }
179
+ return undefined;
180
+ }
181
+
182
+ /** Whether the `applet check` output the CLI printed found errors. */
183
+ function checkFailed(text: string): boolean {
184
+ return /^applet check: \d+ error/m.test(text);
185
+ }
186
+
187
+ /**
188
+ * The one line about the focused Applet, and what sits under it.
189
+ *
190
+ * Returns `undefined` when there is no Applet in the canvas to say it about.
191
+ */
192
+ export function appletProgressV1(
193
+ input: AppletProgressInputV1,
194
+ ): AppletProgressV1 | undefined {
195
+ const applet = input.applet;
196
+ if (!applet) return undefined;
197
+
198
+ let stage: AppletProgressStageV1 = "unknown";
199
+ let failure: string | undefined;
200
+ let output: string[] | undefined;
201
+ let done = false;
202
+
203
+ // The directory entry is the settled fact: a generation is current, so the
204
+ // Applet runs. A Turn working on it now moves the line off this again.
205
+ if (applet.currentGenerationId) {
206
+ stage = "published";
207
+ done = true;
208
+ }
209
+
210
+ // What the Applet authority recorded, where it has recorded anything. It is
211
+ // read before the Turn's own evidence so a running Turn wins.
212
+ const recorded = input.build;
213
+ if (recorded && recorded.status !== "unknown") {
214
+ stage = furthest(
215
+ stage,
216
+ recorded.command === "build" ? "building" : "checking",
217
+ );
218
+ done = recorded.status === "passed";
219
+ if (recorded.status === "failed") {
220
+ failure = sentence(recorded.summary ?? "The last check did not pass.");
221
+ if (recorded.diagnostics && recorded.diagnostics.length > 0) {
222
+ output = recorded.diagnostics
223
+ .slice(-APPLET_PROGRESS_OUTPUT_LINES_V1)
224
+ .map(trimLine);
225
+ }
226
+ }
227
+ }
228
+
229
+ // Source on the Workspace means the Bot has written files, whether or not
230
+ // this Turn is the one that wrote them.
231
+ if ((input.source?.files.length ?? 0) > 0) stage = furthest(stage, "writing");
232
+
233
+ let working = false;
234
+ for (const activity of input.tools ?? []) {
235
+ const name = toolName(activity);
236
+ if (name === "computer_exec") {
237
+ const ran = appletCommandOutputV1(activity.text);
238
+ if (activity.status === "running") {
239
+ // A shell command in flight during a build is the Bot working on the
240
+ // Applet; which command it is only becomes knowable when it returns.
241
+ working = true;
242
+ done = false;
243
+ continue;
244
+ }
245
+ if (!ran) continue;
246
+ stage = furthest(
247
+ stage,
248
+ ran.command === "build" ? "building" : "checking",
249
+ );
250
+ output = ran.output;
251
+ const wrong =
252
+ activity.status === "failed" ||
253
+ (ran.command === "check" && checkFailed(activity.text ?? ""));
254
+ failure = wrong
255
+ ? ran.command === "build"
256
+ ? "Putting it together did not work."
257
+ : "The code has problems that need fixing."
258
+ : undefined;
259
+ done = !wrong;
260
+ continue;
261
+ }
262
+ if (!namesApplet(activity, applet.appletId)) continue;
263
+ if (name === "applet_create") {
264
+ stage = furthest(stage, "created");
265
+ if (activity.status === "running") {
266
+ working = true;
267
+ done = false;
268
+ } else if (activity.status === "failed") {
269
+ failure = sentence(activity.text ?? "This Applet could not be made.");
270
+ done = false;
271
+ } else done = true;
272
+ continue;
273
+ }
274
+ if (name === "applet_publish") {
275
+ if (activity.status === "running") {
276
+ stage = furthest(stage, "publishing");
277
+ working = true;
278
+ failure = undefined;
279
+ done = false;
280
+ continue;
281
+ }
282
+ if (activity.status === "failed") {
283
+ stage = furthest(stage, "publishing");
284
+ failure = sentence(
285
+ activity.text ?? "It could not be made ready to open.",
286
+ );
287
+ done = false;
288
+ continue;
289
+ }
290
+ stage = furthest(stage, "published");
291
+ failure = undefined;
292
+ done = true;
293
+ continue;
294
+ }
295
+ }
296
+
297
+ if (input.running) working = true;
298
+
299
+ return {
300
+ stage,
301
+ label: done ? LABELS[stage].done : LABELS[stage].doing,
302
+ working,
303
+ done,
304
+ ...(failure ? { failure } : {}),
305
+ ...(output && output.length > 0 ? { output } : {}),
306
+ };
307
+ }
308
+
309
+ /**
310
+ * The tool activity the line is read from: every Turn's, oldest first.
311
+ *
312
+ * Not just the Turn that is running. Building an Applet takes several Turns —
313
+ * the Bot writes, checks, fixes, builds, publishes, and the User says things
314
+ * in between — and the last thing that happened to the Applet is what a person
315
+ * wants to know, whether or not it happened in the Turn still open. The
316
+ * reducer takes the last relevant activity, so a settled failure stays on
317
+ * screen until something newer replaces it.
318
+ */
319
+ export function appletProgressToolsV1(
320
+ messages: readonly Pick<WebChatMessage, "role" | "tools">[],
321
+ ): WebToolActivity[] {
322
+ const tools: WebToolActivity[] = [];
323
+ for (const message of messages) {
324
+ if (message.role !== "assistant") continue;
325
+ tools.push(...message.tools);
326
+ }
327
+ return tools;
328
+ }
329
+
330
+ /**
331
+ * Whether the canvas should be showing the building view rather than the
332
+ * Applet. A published Applet with a Turn working on it keeps showing what it
333
+ * has: replacing a working Applet with a progress line takes something away.
334
+ */
335
+ export function appletIsBeingBuiltV1(
336
+ progress: AppletProgressV1 | undefined,
337
+ ): boolean {
338
+ return Boolean(progress && progress.stage !== "published");
339
+ }