@camox/api-contract 0.51.0 → 0.52.1

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/dist/threads.d.ts CHANGED
@@ -8,6 +8,32 @@ import { AgentEvent } from "@earendil-works/pi-durable";
8
8
  export declare const EDITOR_MESSAGE_ENTRY = "camox.editor-message";
9
9
  /** The kind of the transcript entry recording that an editor stopped a run, with them as data. */
10
10
  export declare const RUN_STOPPED_ENTRY = "camox.run-stopped";
11
+ /**
12
+ * The kind of the transcript entry recording where the build of the code a turn changed stands,
13
+ * as the turn ends, with a `buildStatusDataSchema` as data: it started, or it failed and the
14
+ * agent is fixing it. The model never sees it.
15
+ */
16
+ export declare const BUILD_STATUS_ENTRY = "camox.build-status";
17
+ /**
18
+ * The kind of the transcript entry starting Camox's own turn, which reconciles the draft's code
19
+ * with Live's when merging it clashed, with a `reconcileDataSchema` as data: the short message
20
+ * editors see. The model gets Camox's instructions instead.
21
+ */
22
+ export declare const RECONCILE_ENTRY = "camox.reconcile";
23
+ export declare const reconcileDataSchema: z.ZodObject<{
24
+ message: z.ZodString;
25
+ }, z.core.$strip>;
26
+ export declare const buildStatusSchema: z.ZodEnum<{
27
+ building: "building";
28
+ fixing: "fixing";
29
+ }>;
30
+ export type BuildStatus = z.infer<typeof buildStatusSchema>;
31
+ export declare const buildStatusDataSchema: z.ZodObject<{
32
+ status: z.ZodEnum<{
33
+ building: "building";
34
+ fixing: "fixing";
35
+ }>;
36
+ }, z.core.$strip>;
11
37
  /** Who sent a message in a thread. */
12
38
  export declare const threadSenderSchema: z.ZodObject<{
13
39
  id: z.ZodString;
@@ -131,6 +157,12 @@ export type ThreadMessage = {
131
157
  sender: ThreadSender;
132
158
  text: string;
133
159
  } |
160
+ /** Camox's own message, starting a turn it began itself: why it did. */
161
+ {
162
+ id: string;
163
+ from: "camox";
164
+ text: string;
165
+ } |
134
166
  /** The agent's final message of a turn, in Markdown. */
135
167
  {
136
168
  id: string;
@@ -167,6 +199,11 @@ export type ThreadView = LiveThreadHandoff & {
167
199
  messages: readonly ThreadMessage[];
168
200
  /** Whether a run is going: the agent is working on a turn, and stopping would end it. */
169
201
  running: boolean;
202
+ /**
203
+ * While a run is going, where the build of the code its turn changed stands: the end of the
204
+ * turn is building it, or it failed and the agent is fixing it.
205
+ */
206
+ status?: BuildStatus;
170
207
  };
171
208
  export declare const EMPTY_THREAD_VIEW: ThreadView;
172
209
  /**
@@ -186,6 +223,11 @@ type ThreadStep = {
186
223
  type: "editor";
187
224
  sender: ThreadSender;
188
225
  text: string;
226
+ } |
227
+ /** Camox starting a turn of its own, saying why. */
228
+ {
229
+ type: "camox";
230
+ text: string;
189
231
  } | {
190
232
  type: "answer";
191
233
  text: string;
@@ -200,6 +242,18 @@ type ThreadStep = {
200
242
  } | {
201
243
  type: "stopped";
202
244
  sender: ThreadSender;
245
+ } |
246
+ /** Where the build of the turn's code stands, as the turn ends. */
247
+ {
248
+ type: "build";
249
+ status: BuildStatus;
250
+ } |
251
+ /**
252
+ * The end of the turn sent the agent back to work, after its answer: the answer wasn't the
253
+ * turn's last, and what the agent was told is never shown.
254
+ */
255
+ {
256
+ type: "continued";
203
257
  });
204
258
  /** Studio → thread. */
205
259
  export declare const threadClientMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -259,7 +313,8 @@ export type ThreadServerMessage =
259
313
  */
260
314
  export declare const THREAD_ENDED_CLOSE_CODE = 4000;
261
315
  /**
262
- * What the studio shows for a thread. A turn starts with an editor's message, and messages sent
316
+ * What the studio shows for a thread. A turn starts with an editor's message, or Camox's own
317
+ * when it began the turn itself, and messages sent
263
318
  * while it runs join it. It ends with the agent's final message; text written beside tool calls
264
319
  * is never shown. A stopped turn shows who stopped it instead, and whatever its run wrote after
265
320
  * the stop is left out. A turn that ended without either shows a failed line in its place: its
package/dist/threads.js CHANGED
@@ -8,6 +8,21 @@ import { z } from "zod";
8
8
  const EDITOR_MESSAGE_ENTRY = "camox.editor-message";
9
9
  /** The kind of the transcript entry recording that an editor stopped a run, with them as data. */
10
10
  const RUN_STOPPED_ENTRY = "camox.run-stopped";
11
+ /**
12
+ * The kind of the transcript entry recording where the build of the code a turn changed stands,
13
+ * as the turn ends, with a `buildStatusDataSchema` as data: it started, or it failed and the
14
+ * agent is fixing it. The model never sees it.
15
+ */
16
+ const BUILD_STATUS_ENTRY = "camox.build-status";
17
+ /**
18
+ * The kind of the transcript entry starting Camox's own turn, which reconciles the draft's code
19
+ * with Live's when merging it clashed, with a `reconcileDataSchema` as data: the short message
20
+ * editors see. The model gets Camox's instructions instead.
21
+ */
22
+ const RECONCILE_ENTRY = "camox.reconcile";
23
+ const reconcileDataSchema = z.object({ message: z.string() });
24
+ const buildStatusSchema = z.enum(["building", "fixing"]);
25
+ const buildStatusDataSchema = z.object({ status: buildStatusSchema });
11
26
  /** Who sent a message in a thread. */
12
27
  const threadSenderSchema = z.object({
13
28
  id: z.string(),
@@ -131,14 +146,36 @@ function threadStepOf(entry) {
131
146
  type: "stopped",
132
147
  sender
133
148
  };
149
+ if (entry.kind === "camox.build-status") {
150
+ const build = buildStatusDataSchema.safeParse(entry.data).data;
151
+ return build && {
152
+ id,
153
+ type: "build",
154
+ status: build.status
155
+ };
156
+ }
134
157
  const message = entry.model?.find((candidate) => candidate.role !== "system");
135
158
  if (!message) return void 0;
136
- if (entry.kind === "camox.editor-message" && message.role === "user" && sender) return {
137
- id,
138
- type: "editor",
139
- sender,
140
- text: textOf(message.content)
141
- };
159
+ if (message.role === "user") {
160
+ if (entry.kind === "camox.editor-message") return sender && {
161
+ id,
162
+ type: "editor",
163
+ sender,
164
+ text: textOf(message.content)
165
+ };
166
+ if (entry.kind === "camox.reconcile") {
167
+ const reconcile = reconcileDataSchema.safeParse(entry.data).data;
168
+ return reconcile && {
169
+ id,
170
+ type: "camox",
171
+ text: reconcile.message
172
+ };
173
+ }
174
+ return {
175
+ id,
176
+ type: "continued"
177
+ };
178
+ }
142
179
  if (message.role === "assistant") return assistantStep(id, message);
143
180
  if (message.role === "toolResult") return {
144
181
  id,
@@ -146,7 +183,8 @@ function threadStepOf(entry) {
146
183
  };
147
184
  }
148
185
  /**
149
- * What the studio shows for a thread. A turn starts with an editor's message, and messages sent
186
+ * What the studio shows for a thread. A turn starts with an editor's message, or Camox's own
187
+ * when it began the turn itself, and messages sent
150
188
  * while it runs join it. It ends with the agent's final message; text written beside tool calls
151
189
  * is never shown. A stopped turn shows who stopped it instead, and whatever its run wrote after
152
190
  * the stop is left out. A turn that ended without either shows a failed line in its place: its
@@ -157,10 +195,14 @@ function threadViewOf({ steps, running, ...drafts }) {
157
195
  const messages = [];
158
196
  let waiting;
159
197
  let unfinished = false;
198
+ let status;
199
+ let answered;
160
200
  /** Ends the turn waiting for the agent's message, if any. */
161
201
  const endTurn = () => {
162
202
  waiting = void 0;
163
203
  unfinished = false;
204
+ status = void 0;
205
+ answered = void 0;
164
206
  };
165
207
  /** Ends the turn waiting for the agent's message with a failed line, if any. */
166
208
  const fail = () => {
@@ -181,6 +223,15 @@ function threadViewOf({ steps, running, ...drafts }) {
181
223
  });
182
224
  waiting = step.id;
183
225
  break;
226
+ case "camox":
227
+ if (unfinished) fail();
228
+ messages.push({
229
+ id: step.id,
230
+ from: "camox",
231
+ text: step.text
232
+ });
233
+ waiting = step.id;
234
+ break;
184
235
  case "stopped":
185
236
  if (waiting === void 0) break;
186
237
  messages.push({
@@ -197,7 +248,24 @@ function threadViewOf({ steps, running, ...drafts }) {
197
248
  from: "agent",
198
249
  text: step.text
199
250
  });
200
- endTurn();
251
+ {
252
+ const before = {
253
+ waiting,
254
+ status
255
+ };
256
+ endTurn();
257
+ answered = before;
258
+ }
259
+ break;
260
+ case "continued":
261
+ if (!answered) break;
262
+ if (messages.at(-1)?.from === "agent") messages.pop();
263
+ waiting = answered.waiting;
264
+ status = answered.status === "fixing" ? "fixing" : void 0;
265
+ answered = void 0;
266
+ break;
267
+ case "build":
268
+ status = step.status;
201
269
  break;
202
270
  case "tools":
203
271
  unfinished = false;
@@ -208,7 +276,8 @@ function threadViewOf({ steps, running, ...drafts }) {
208
276
  return {
209
277
  ...drafts,
210
278
  messages,
211
- running
279
+ running,
280
+ ...running && status && { status }
212
281
  };
213
282
  }
214
283
  const append = (state, entry) => {
@@ -260,4 +329,4 @@ function reduceThreadFrame(state, frame) {
260
329
  }
261
330
 
262
331
  //#endregion
263
- export { EDITOR_MESSAGE_ENTRY, EMPTY_THREAD_STATE, EMPTY_THREAD_VIEW, RUN_STOPPED_ENTRY, THREAD_ENDED_CLOSE_CODE, editorMessageDataSchema, isSender, reduceThreadEvents, reduceThreadFrame, reduceThreadState, startedDraftSchema, threadClientMessageSchema, threadSenderSchema, threadViewOf, touchKindSchema, touchSchema, touchedBatchSchema };
332
+ export { BUILD_STATUS_ENTRY, EDITOR_MESSAGE_ENTRY, EMPTY_THREAD_STATE, EMPTY_THREAD_VIEW, RECONCILE_ENTRY, RUN_STOPPED_ENTRY, THREAD_ENDED_CLOSE_CODE, buildStatusDataSchema, buildStatusSchema, editorMessageDataSchema, isSender, reconcileDataSchema, reduceThreadEvents, reduceThreadFrame, reduceThreadState, startedDraftSchema, threadClientMessageSchema, threadSenderSchema, threadViewOf, touchKindSchema, touchSchema, touchedBatchSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camox/api-contract",
3
- "version": "0.51.0",
3
+ "version": "0.52.1",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "src",
package/src/drafts.ts CHANGED
@@ -9,6 +9,22 @@ export const DRAFT_HEADER = "x-camox-draft";
9
9
  */
10
10
  export const LOCALE_HEADER = "x-camox-locale";
11
11
 
12
+ /**
13
+ * Request header the Worker a publish with code uploaded sends on its public reads, naming
14
+ * the draft it publishes (spec #560, section 13). While that draft is in its publishing state, a
15
+ * read naming it sees Live overlaid with the draft, with no session, so the new code serves the
16
+ * content it was previewed with from its first request. Once the draft has ended, or if it isn't
17
+ * publishing, the read is Live's. A request naming a draft with `DRAFT_HEADER` ignores it.
18
+ */
19
+ export const PUBLISHING_DRAFT_HEADER = "x-camox-publishing-draft";
20
+
21
+ /**
22
+ * The Worker variable carrying the publishing draft's ID, which the publish sets on the Worker it
23
+ * uploads (apps/api/src/domains/publishes/scripts.ts). The website's Worker sends it as
24
+ * `PUBLISHING_DRAFT_HEADER`.
25
+ */
26
+ export const PUBLISHING_DRAFT_VAR = "CAMOX_PUBLISHING_DRAFT";
27
+
12
28
  /**
13
29
  * Request header asking the API to create the draft named by `DRAFT_HEADER` if it doesn't
14
30
  * exist yet. Its value is `"1"`.
@@ -56,6 +72,31 @@ export const DRAFT_ENDED_ERROR_CODE = "DRAFT_ENDED";
56
72
  */
57
73
  export const CONFLICTS_CHANGED_ERROR = "CONFLICTS_CHANGED";
58
74
 
75
+ /**
76
+ * oRPC error code for a publish of a draft with code whose revision isn't the one the review
77
+ * showed: the agent left a newer one, or the draft is already publishing. The caller reviews
78
+ * the new preview.
79
+ */
80
+ export const REVISION_CHANGED_ERROR = "REVISION_CHANGED";
81
+
82
+ /**
83
+ * Why a publish with code went back to the review instead of landing: the agent left a revision
84
+ * after the one the review showed, Live's conflicts changed, the draft got a blocker, Live's code
85
+ * moved and the draft's merge of it clashed (the "doesn't merge with Live's" blocker, which
86
+ * Camox's turn fixes), Live's code moved and the draft took too long to merge it, or the publish
87
+ * failed on Camox's side.
88
+ */
89
+ export const PUBLISH_ABORT_REASONS = [
90
+ "revisionChanged",
91
+ "conflictsChanged",
92
+ "blocked",
93
+ "codeDoesNotMerge",
94
+ "liveCodeMoved",
95
+ "failed",
96
+ ] as const;
97
+
98
+ export type PublishAbortReason = (typeof PUBLISH_ABORT_REASONS)[number];
99
+
59
100
  /** oRPC error code for a publish refused because the draft still has blockers. */
60
101
  export const PUBLISH_BLOCKED_ERROR = "PUBLISH_BLOCKED";
61
102
 
@@ -68,12 +109,13 @@ export const TRANSLATIONS_PENDING_ERROR = "TRANSLATIONS_PENDING";
68
109
  /**
69
110
  * Why the publish tool published nothing of a draft with changes, and returned a fresh preview to
70
111
  * review again, by the error code of each: Live changed since the preview, the draft has blockers,
71
- * or translations are still running.
112
+ * translations are still running, or the draft's code changed since the preview.
72
113
  */
73
114
  export const NOT_PUBLISHED_CODES = {
74
115
  conflictsChanged: CONFLICTS_CHANGED_ERROR,
75
116
  blocked: PUBLISH_BLOCKED_ERROR,
76
117
  translating: TRANSLATIONS_PENDING_ERROR,
118
+ revisionChanged: REVISION_CHANGED_ERROR,
77
119
  } as const;
78
120
 
79
121
  export type NotPublishedReason = keyof typeof NOT_PUBLISHED_CODES;
package/src/query-keys.ts CHANGED
@@ -92,5 +92,11 @@ export type InvalidationMessage = {
92
92
  /** A draft was published or discarded: its viewers go back to Live. */
93
93
  export type DraftEndedMessage = { type: "draftEnded" } & EndedDraft;
94
94
 
95
+ /** A draft with code has a new revision, built with Camox's `sdkVersion`. */
96
+ export type RevisionAnnouncement = { draftId: string; revisionId: string; sdkVersion: string };
97
+
98
+ /** A draft's new revision: its viewers' frames load it. */
99
+ export type RevisionMessage = { type: "revision" } & RevisionAnnouncement;
100
+
95
101
  /** Everything the project room broadcasts. */
96
- export type ProjectRoomMessage = InvalidationMessage | DraftEndedMessage;
102
+ export type ProjectRoomMessage = InvalidationMessage | DraftEndedMessage | RevisionMessage;
package/src/threads.ts CHANGED
@@ -15,6 +15,27 @@ export const EDITOR_MESSAGE_ENTRY = "camox.editor-message";
15
15
  /** The kind of the transcript entry recording that an editor stopped a run, with them as data. */
16
16
  export const RUN_STOPPED_ENTRY = "camox.run-stopped";
17
17
 
18
+ /**
19
+ * The kind of the transcript entry recording where the build of the code a turn changed stands,
20
+ * as the turn ends, with a `buildStatusDataSchema` as data: it started, or it failed and the
21
+ * agent is fixing it. The model never sees it.
22
+ */
23
+ export const BUILD_STATUS_ENTRY = "camox.build-status";
24
+
25
+ /**
26
+ * The kind of the transcript entry starting Camox's own turn, which reconciles the draft's code
27
+ * with Live's when merging it clashed, with a `reconcileDataSchema` as data: the short message
28
+ * editors see. The model gets Camox's instructions instead.
29
+ */
30
+ export const RECONCILE_ENTRY = "camox.reconcile";
31
+
32
+ export const reconcileDataSchema = z.object({ message: z.string() });
33
+
34
+ export const buildStatusSchema = z.enum(["building", "fixing"]);
35
+ export type BuildStatus = z.infer<typeof buildStatusSchema>;
36
+
37
+ export const buildStatusDataSchema = z.object({ status: buildStatusSchema });
38
+
18
39
  /** Who sent a message in a thread. */
19
40
  export const threadSenderSchema = z.object({
20
41
  id: z.string(),
@@ -88,6 +109,8 @@ export type TouchedBatch = z.infer<typeof touchedBatchSchema>;
88
109
  /** A message the studio shows in a thread. */
89
110
  export type ThreadMessage =
90
111
  | { id: string; from: "editor"; sender: ThreadSender; text: string }
112
+ /** Camox's own message, starting a turn it began itself: why it did. */
113
+ | { id: string; from: "camox"; text: string }
91
114
  /** The agent's final message of a turn, in Markdown. */
92
115
  | { id: string; from: "agent"; text: string }
93
116
  /**
@@ -115,6 +138,11 @@ export type ThreadView = LiveThreadHandoff & {
115
138
  messages: readonly ThreadMessage[];
116
139
  /** Whether a run is going: the agent is working on a turn, and stopping would end it. */
117
140
  running: boolean;
141
+ /**
142
+ * While a run is going, where the build of the code its turn changed stands: the end of the
143
+ * turn is building it, or it failed and the agent is fixing it.
144
+ */
145
+ status?: BuildStatus;
118
146
  };
119
147
 
120
148
  export const EMPTY_THREAD_VIEW: ThreadView = { messages: [], running: false };
@@ -131,12 +159,21 @@ export const EMPTY_THREAD_STATE: ThreadState = { steps: [], running: false };
131
159
  /** One transcript entry, as far as the studio's view is concerned. */
132
160
  type ThreadStep = { id: string } & (
133
161
  | { type: "editor"; sender: ThreadSender; text: string }
162
+ /** Camox starting a turn of its own, saying why. */
163
+ | { type: "camox"; text: string }
134
164
  | { type: "answer"; text: string }
135
165
  /** A round of tool calls, or their results: the run goes on after it. */
136
166
  | { type: "tools" }
137
167
  /** A model call that ended without an answer: failed, aborted, or cut short. */
138
168
  | { type: "unfinished" }
139
169
  | { type: "stopped"; sender: ThreadSender }
170
+ /** Where the build of the turn's code stands, as the turn ends. */
171
+ | { type: "build"; status: BuildStatus }
172
+ /**
173
+ * The end of the turn sent the agent back to work, after its answer: the answer wasn't the
174
+ * turn's last, and what the agent was told is never shown.
175
+ */
176
+ | { type: "continued" }
140
177
  );
141
178
 
142
179
  /** Studio → thread. */
@@ -211,10 +248,23 @@ function threadStepOf(entry: EntryRecord): ThreadStep | undefined {
211
248
  const id = String(entry.id);
212
249
  const sender = senderOf(entry.data);
213
250
  if (entry.kind === RUN_STOPPED_ENTRY && sender) return { id, type: "stopped", sender };
251
+ if (entry.kind === BUILD_STATUS_ENTRY) {
252
+ const build = buildStatusDataSchema.safeParse(entry.data).data;
253
+ return build && { id, type: "build", status: build.status };
254
+ }
214
255
  const message = entry.model?.find((candidate) => candidate.role !== "system");
215
256
  if (!message) return undefined;
216
- if (entry.kind === EDITOR_MESSAGE_ENTRY && message.role === "user" && sender) {
217
- return { id, type: "editor", sender, text: textOf(message.content) };
257
+ if (message.role === "user") {
258
+ if (entry.kind === EDITOR_MESSAGE_ENTRY) {
259
+ return sender && { id, type: "editor", sender, text: textOf(message.content) };
260
+ }
261
+ if (entry.kind === RECONCILE_ENTRY) {
262
+ const reconcile = reconcileDataSchema.safeParse(entry.data).data;
263
+ return reconcile && { id, type: "camox", text: reconcile.message };
264
+ }
265
+ // Editors' messages have a kind of their own: a user message without it is Pi continuing a
266
+ // run as the end of a turn asked.
267
+ return { id, type: "continued" };
218
268
  }
219
269
  if (message.role === "assistant") return assistantStep(id, message);
220
270
  if (message.role === "toolResult") return { id, type: "tools" };
@@ -222,7 +272,8 @@ function threadStepOf(entry: EntryRecord): ThreadStep | undefined {
222
272
  }
223
273
 
224
274
  /**
225
- * What the studio shows for a thread. A turn starts with an editor's message, and messages sent
275
+ * What the studio shows for a thread. A turn starts with an editor's message, or Camox's own
276
+ * when it began the turn itself, and messages sent
226
277
  * while it runs join it. It ends with the agent's final message; text written beside tool calls
227
278
  * is never shown. A stopped turn shows who stopped it instead, and whatever its run wrote after
228
279
  * the stop is left out. A turn that ended without either shows a failed line in its place: its
@@ -234,10 +285,16 @@ export function threadViewOf({ steps, running, ...drafts }: ThreadState): Thread
234
285
  // The latest editor's message of a turn still waiting for the agent's message.
235
286
  let waiting: string | undefined;
236
287
  let unfinished = false;
288
+ let status: BuildStatus | undefined;
289
+ // The turn the agent's last answer ended, and the status before it: the end of the turn may
290
+ // send the agent back to work after it.
291
+ let answered: { waiting: string | undefined; status: BuildStatus | undefined } | undefined;
237
292
  /** Ends the turn waiting for the agent's message, if any. */
238
293
  const endTurn = () => {
239
294
  waiting = undefined;
240
295
  unfinished = false;
296
+ status = undefined;
297
+ answered = undefined;
241
298
  };
242
299
  /** Ends the turn waiting for the agent's message with a failed line, if any. */
243
300
  const fail = () => {
@@ -255,6 +312,11 @@ export function threadViewOf({ steps, running, ...drafts }: ThreadState): Thread
255
312
  messages.push({ id: step.id, from: "editor", sender: step.sender, text: step.text });
256
313
  waiting = step.id;
257
314
  break;
315
+ case "camox":
316
+ if (unfinished) fail();
317
+ messages.push({ id: step.id, from: "camox", text: step.text });
318
+ waiting = step.id;
319
+ break;
258
320
  case "stopped":
259
321
  if (waiting === undefined) break;
260
322
  messages.push({ id: step.id, from: "stopped", sender: step.sender });
@@ -264,7 +326,23 @@ export function threadViewOf({ steps, running, ...drafts }: ThreadState): Thread
264
326
  // A stopped turn's run may still answer before it ends.
265
327
  if (waiting === undefined && messages.at(-1)?.from === "stopped") break;
266
328
  messages.push({ id: step.id, from: "agent", text: step.text });
267
- endTurn();
329
+ {
330
+ const before = { waiting, status };
331
+ endTurn();
332
+ answered = before;
333
+ }
334
+ break;
335
+ case "continued":
336
+ // The answer before wasn't the turn's last. Once the end of the turn gives up fixing,
337
+ // the agent explains without a status.
338
+ if (!answered) break;
339
+ if (messages.at(-1)?.from === "agent") messages.pop();
340
+ waiting = answered.waiting;
341
+ status = answered.status === "fixing" ? "fixing" : undefined;
342
+ answered = undefined;
343
+ break;
344
+ case "build":
345
+ status = step.status;
268
346
  break;
269
347
  case "tools":
270
348
  unfinished = false;
@@ -275,7 +353,7 @@ export function threadViewOf({ steps, running, ...drafts }: ThreadState): Thread
275
353
  }
276
354
  }
277
355
  if (!running) fail();
278
- return { ...drafts, messages, running };
356
+ return { ...drafts, messages, running, ...(running && status && { status }) };
279
357
  }
280
358
 
281
359
  const append = (state: ThreadState, entry: EntryRecord | undefined): ThreadState => {