@invergent/agent-chat-react 2.16.8 → 2.17.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/index.cjs CHANGED
@@ -215,6 +215,70 @@ function useAgentChatAdapterContext() {
215
215
  }
216
216
  return value;
217
217
  }
218
+ function useProjectThreads(adapter, projectId) {
219
+ const [rows, setRows] = react.useState({});
220
+ react.useEffect(() => {
221
+ setRows({});
222
+ if (!projectId || !adapter.listProjectThreads || !adapter.openProjectStream) return;
223
+ let closed = false;
224
+ let reading = false;
225
+ let wanted = /* @__PURE__ */ new Set();
226
+ const failed = /* @__PURE__ */ new Set();
227
+ const readOne = async (threadId) => {
228
+ if (threadId === null) {
229
+ const all = await adapter.listProjectThreads({ projectId });
230
+ if (!closed) setRows(Object.fromEntries(all.map((row2) => [row2.id, row2])));
231
+ return;
232
+ }
233
+ const [row] = await adapter.listProjectThreads({ projectId, threadId });
234
+ if (!closed) setRows(({ [threadId]: _gone, ...rest }) => row ? { ...rest, [threadId]: row } : rest);
235
+ };
236
+ const read = async () => {
237
+ if (reading) return;
238
+ reading = true;
239
+ while (wanted.size > 0 && !closed) {
240
+ const asked = wanted;
241
+ wanted = /* @__PURE__ */ new Set();
242
+ for (const threadId of asked.has(null) ? [null] : asked) {
243
+ if (closed) break;
244
+ try {
245
+ await readOne(threadId);
246
+ } catch {
247
+ failed.add(threadId);
248
+ }
249
+ }
250
+ }
251
+ reading = false;
252
+ };
253
+ const want = (threadId) => {
254
+ wanted.add(threadId);
255
+ for (const again of failed) wanted.add(again);
256
+ failed.clear();
257
+ void read();
258
+ };
259
+ const stream = adapter.openProjectStream({ projectId });
260
+ stream.onerror = () => {
261
+ if (closed) return;
262
+ closed = true;
263
+ setRows({});
264
+ };
265
+ stream.addEventListener("ready", () => want(null));
266
+ stream.addEventListener("change", (event) => {
267
+ let threadId = null;
268
+ try {
269
+ const named2 = JSON.parse(event.data).thread_id;
270
+ threadId = typeof named2 === "string" ? named2 : null;
271
+ } catch {
272
+ }
273
+ want(threadId);
274
+ });
275
+ return () => {
276
+ closed = true;
277
+ stream.close();
278
+ };
279
+ }, [adapter, projectId]);
280
+ return rows;
281
+ }
218
282
  var NAMED_KEYS = /* @__PURE__ */ new Set([
219
283
  "Enter",
220
284
  "Tab",
@@ -423,7 +487,7 @@ function BrowserShell({
423
487
  children: /* @__PURE__ */ jsxRuntime.jsx(Reload, {})
424
488
  }
425
489
  ),
426
- /* @__PURE__ */ jsxRuntime.jsx(
490
+ (onToggleControl || hasControl) && /* @__PURE__ */ jsxRuntime.jsx(
427
491
  "button",
428
492
  {
429
493
  type: "button",
@@ -947,11 +1011,26 @@ function ConfirmDialog({
947
1011
  }
948
1012
  );
949
1013
  }
1014
+ function ComputerBrowserPane({
1015
+ available,
1016
+ readOnly,
1017
+ draw
1018
+ }) {
1019
+ return /* @__PURE__ */ jsxRuntime.jsx(
1020
+ "div",
1021
+ {
1022
+ "data-testid": "browser-pane",
1023
+ className: "flex h-full min-h-0 flex-col items-center justify-center gap-3 bg-background p-6 text-center text-sm text-muted-foreground",
1024
+ children: draw ? draw({ available, readOnly }) : available ? "The browser is open on the chat's computer." : "The chat's computer has no supported browser."
1025
+ }
1026
+ );
1027
+ }
950
1028
  function BrowserPane({
951
1029
  sessionId,
952
1030
  state,
953
1031
  adapter,
954
- onClose
1032
+ onClose,
1033
+ readOnly = false
955
1034
  }) {
956
1035
  const [fullscreenOpen, setFullscreenOpen] = react.useState(false);
957
1036
  const [openFullscreenOnControl, setOpenFullscreenOnControl] = react.useState(false);
@@ -1008,8 +1087,8 @@ function BrowserPane({
1008
1087
  src: shellUrl,
1009
1088
  testId,
1010
1089
  hasControl: localControlActive,
1011
- onToggleControl: hasControlAdapter ? () => void control.toggleControl() : void 0,
1012
- onClose: onClose ? () => control.setConfirmOpen(true) : void 0,
1090
+ onToggleControl: hasControlAdapter && !readOnly ? () => void control.toggleControl() : void 0,
1091
+ onClose: onClose && !readOnly ? () => control.setConfirmOpen(true) : void 0,
1013
1092
  onMaximize: fullscreenOpen ? void 0 : () => setFullscreenOpen(true),
1014
1093
  onDisconnect: () => setLocalControlActive(false)
1015
1094
  }
@@ -4180,13 +4259,13 @@ function Textarea({ className, ...props }) {
4180
4259
  var MAX_REASON_LENGTH = 500;
4181
4260
  function ExpertToolBlock({ tc }) {
4182
4261
  const [expanded, setExpanded] = react.useState(false);
4183
- const { adapter, sessionId } = useAgentChatAdapterContext();
4262
+ const { adapter, sessionId, readOnly } = useAgentChatAdapterContext();
4184
4263
  const [pending, setPending] = react.useState(null);
4185
4264
  const [error, setError] = react.useState(null);
4186
4265
  const [reasonDraft, setReasonDraft] = react.useState(null);
4187
4266
  const rating = tc.expertFeedback?.rating ?? pending;
4188
4267
  const alreadyRated = tc.expertFeedback !== void 0;
4189
- const canRate = tc.expertResultEventId !== void 0 && sessionId !== null && adapter.submitExpertFeedback !== void 0 && tc.status !== "running";
4268
+ const canRate = !readOnly && tc.expertResultEventId !== void 0 && sessionId !== null && adapter.submitExpertFeedback !== void 0 && tc.status !== "running";
4190
4269
  const submit = async (next, reason) => {
4191
4270
  if (sessionId === null || tc.expertResultEventId === void 0 || adapter.submitExpertFeedback === void 0) return;
4192
4271
  setPending(next);
@@ -4548,11 +4627,12 @@ function ConversationalAsk({
4548
4627
  question,
4549
4628
  assistantContent
4550
4629
  }) {
4551
- const { adapter, sessionId } = useAgentChatAdapterContext();
4630
+ const { adapter, sessionId, readOnly } = useAgentChatAdapterContext();
4552
4631
  const [submitting, setSubmitting] = react.useState(false);
4553
4632
  const [error, setError] = react.useState(null);
4554
4633
  const answered = tc.askUserQuestionAnswers !== void 0;
4555
- const pending = !answered && tc.status === "running";
4634
+ const open = !answered && tc.status === "running";
4635
+ const pending = open && !readOnly;
4556
4636
  const choices = question.choices ?? [];
4557
4637
  const showPrompt = react.useMemo(
4558
4638
  () => !promptEchoedInContent(assistantContent, question.prompt),
@@ -4614,7 +4694,7 @@ function ConversationalAsk({
4614
4694
  }
4615
4695
  ),
4616
4696
  error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-destructive", children: error }),
4617
- !answered && !pending && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs italic text-muted-foreground/70", children: "No answer recorded." })
4697
+ !answered && !open && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs italic text-muted-foreground/70", children: "No answer recorded." })
4618
4698
  ] });
4619
4699
  }
4620
4700
  function QuickReply({
@@ -4648,14 +4728,14 @@ function BatchAsk({
4648
4728
  questions,
4649
4729
  viewMode
4650
4730
  }) {
4651
- const { adapter, sessionId } = useAgentChatAdapterContext();
4731
+ const { adapter, sessionId, readOnly } = useAgentChatAdapterContext();
4652
4732
  const [active, setActive] = react.useState(0);
4653
4733
  const [selections, setSelections] = react.useState(
4654
4734
  () => questions.map(emptySelection)
4655
4735
  );
4656
4736
  const [submitting, setSubmitting] = react.useState(false);
4657
4737
  const [error, setError] = react.useState(null);
4658
- const locked = tc.askUserQuestionAnswers !== void 0 || tc.status !== "running";
4738
+ const locked = readOnly || tc.askUserQuestionAnswers !== void 0 || tc.status !== "running";
4659
4739
  react.useEffect(() => {
4660
4740
  if (active >= questions.length && questions.length > 0) setActive(0);
4661
4741
  }, [active, questions.length]);
@@ -4955,7 +5035,7 @@ function BatchAskLocked({
4955
5035
  );
4956
5036
  const cancelled = !answers && tc.status !== "running";
4957
5037
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-md border border-border bg-muted/20 px-3 py-2 text-sm max-w-2xl", children: [
4958
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-1 text-xs text-muted-foreground", children: cancelled ? "Clarification cancelled" : "Clarification answered" }),
5038
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-1 text-xs text-muted-foreground", children: cancelled ? "Clarification cancelled" : answers ? "Clarification answered" : "Waiting for an answer" }),
4959
5039
  /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-1.5", children: questions.map((q, i) => {
4960
5040
  const a = byPrompt.get(q.prompt);
4961
5041
  return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "text-sm", children: [
@@ -6208,6 +6288,62 @@ function CommandItem({
6208
6288
  );
6209
6289
  }
6210
6290
 
6291
+ // src/components/ui/dropdown-menu.tsx
6292
+ init_utils();
6293
+ function DropdownMenu({
6294
+ ...props
6295
+ }) {
6296
+ return /* @__PURE__ */ jsxRuntime.jsx(radixUi.DropdownMenu.Root, { "data-slot": "dropdown-menu", ...props });
6297
+ }
6298
+ function DropdownMenuTrigger({
6299
+ ...props
6300
+ }) {
6301
+ return /* @__PURE__ */ jsxRuntime.jsx(
6302
+ radixUi.DropdownMenu.Trigger,
6303
+ {
6304
+ "data-slot": "dropdown-menu-trigger",
6305
+ ...props
6306
+ }
6307
+ );
6308
+ }
6309
+ function DropdownMenuContent({
6310
+ className,
6311
+ align = "start",
6312
+ sideOffset = 4,
6313
+ ...props
6314
+ }) {
6315
+ return /* @__PURE__ */ jsxRuntime.jsx(radixUi.DropdownMenu.Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(
6316
+ radixUi.DropdownMenu.Content,
6317
+ {
6318
+ "data-slot": "dropdown-menu-content",
6319
+ sideOffset,
6320
+ align,
6321
+ className: cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-48 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-none bg-popover p-1.5 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className),
6322
+ ...props
6323
+ }
6324
+ ) });
6325
+ }
6326
+ function DropdownMenuItem({
6327
+ className,
6328
+ inset,
6329
+ variant = "default",
6330
+ ...props
6331
+ }) {
6332
+ return /* @__PURE__ */ jsxRuntime.jsx(
6333
+ radixUi.DropdownMenu.Item,
6334
+ {
6335
+ "data-slot": "dropdown-menu-item",
6336
+ "data-inset": inset,
6337
+ "data-variant": variant,
6338
+ className: cn(
6339
+ "group/dropdown-menu-item relative flex cursor-default items-center gap-2.5 rounded-none px-3 py-2 text-xs font-medium tracking-wider uppercase outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-9.5 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 data-[variant=destructive]:*:[svg]:text-destructive",
6340
+ className
6341
+ ),
6342
+ ...props
6343
+ }
6344
+ );
6345
+ }
6346
+
6211
6347
  // src/components/ui/spinner.tsx
6212
6348
  init_utils();
6213
6349
  function Spinner({ className, ...props }) {
@@ -8489,12 +8625,12 @@ function BrandBeam({
8489
8625
  borderRadius,
8490
8626
  className
8491
8627
  }) {
8492
- const reducedMotion = react.useSyncExternalStore(
8628
+ const reducedMotion2 = react.useSyncExternalStore(
8493
8629
  subscribeReducedMotion,
8494
8630
  readReducedMotion,
8495
8631
  readReducedMotionServer
8496
8632
  );
8497
- const effectiveActive = active && !(reducedMotion && ROTATE_SIZES.has(size2));
8633
+ const effectiveActive = active && !(reducedMotion2 && ROTATE_SIZES.has(size2));
8498
8634
  return /* @__PURE__ */ jsxRuntime.jsx(
8499
8635
  BorderBeam,
8500
8636
  {
@@ -10921,7 +11057,9 @@ var MAX_CPS = 240;
10921
11057
  var DRAIN_SECONDS = 0.25;
10922
11058
  var SNAP_LAG = 800;
10923
11059
  var MAX_FRAME_DT = 0.1;
11060
+ var reducedMotion = () => typeof document !== "undefined" && document.documentElement.dataset.motion === "reduced";
10924
11061
  function useSmoothStream(target, isStreaming) {
11062
+ const instant = reducedMotion();
10925
11063
  const [displayedLen, setDisplayedLen] = react.useState(
10926
11064
  () => isStreaming ? 0 : target.length
10927
11065
  );
@@ -10930,7 +11068,7 @@ function useSmoothStream(target, isStreaming) {
10930
11068
  react.useEffect(() => {
10931
11069
  setDisplayedLen((cur) => cur > target.length ? target.length : cur);
10932
11070
  }, [target]);
10933
- const isActive = isStreaming || displayedLen < target.length;
11071
+ const isActive = !instant && (isStreaming || displayedLen < target.length);
10934
11072
  react.useEffect(() => {
10935
11073
  if (!isActive) return;
10936
11074
  let raf = 0;
@@ -10962,7 +11100,7 @@ function useSmoothStream(target, isStreaming) {
10962
11100
  raf = requestAnimationFrame(tick);
10963
11101
  return () => cancelAnimationFrame(raf);
10964
11102
  }, [isActive]);
10965
- return displayedLen >= target.length ? target : target.slice(0, displayedLen);
11103
+ return instant || displayedLen >= target.length ? target : target.slice(0, displayedLen);
10966
11104
  }
10967
11105
 
10968
11106
  // src/runtime/reasoning-tokens.ts
@@ -12045,6 +12183,304 @@ function ArtifactRow({
12045
12183
  }
12046
12184
  return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
12047
12185
  }
12186
+ var GROUP_LABEL = {
12187
+ waiting: "Waiting on you",
12188
+ working: "Working",
12189
+ idle: "Idle",
12190
+ resolved: "Resolved"
12191
+ };
12192
+ var STATE_LABEL = {
12193
+ working: "Working",
12194
+ reported: "Reported",
12195
+ failed: "Failed"
12196
+ };
12197
+ var FILES_SHOWN = 3;
12198
+ var MARKS = {
12199
+ landed: null,
12200
+ redoing: "being redone",
12201
+ not_merged: "not merged",
12202
+ undone: "undone"
12203
+ };
12204
+ var changedBy = (by) => by.kind === "you" ? "you" : by.kind === "thread" ? `the thread ${by.title}` : `the routine ${by.name}`;
12205
+ function named(paths) {
12206
+ const names = paths.slice(0, FILES_SHOWN);
12207
+ if (paths.length > FILES_SHOWN) names.push(`${paths.length - FILES_SHOWN} more files`);
12208
+ return names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
12209
+ }
12210
+ function undoLines(result) {
12211
+ const lines = result.applied.length > 0 ? [`Put back ${named(result.applied)}.`] : [];
12212
+ const picked = result.pickedUp ?? [];
12213
+ if (picked.length > 0) lines.push(`Your changes to ${named(picked)} were recorded first: each is a version in its History.`);
12214
+ for (const { path, by, pruned } of result.skipped.slice(0, FILES_SHOWN)) {
12215
+ lines.push(pruned ? `${path} could not be put back: its version from before is no longer kept.` : by === null ? `${path} was left as it is.` : `${path} was changed after this, by ${changedBy(by)}. It was left as it is. Restore an earlier version from its History.`);
12216
+ }
12217
+ const more = result.skipped.length - FILES_SHOWN;
12218
+ if (more === 1) lines.push("1 more file was left as it is: its History says who changed it.");
12219
+ if (more > 1) lines.push(`${more} more files were left as they are: each one's History says who changed it.`);
12220
+ return lines.length > 0 ? lines : ["Nothing to put back: the files are as they were."];
12221
+ }
12222
+ var ASIDE_WHAT = {
12223
+ copy: "its copy",
12224
+ repository: "its repository",
12225
+ place: "the earlier history at its folder's path, with its threads' copies"
12226
+ };
12227
+ function setAsideWhen(at) {
12228
+ const parts = new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(new Date(at));
12229
+ const part = (type) => parts.find((found) => found.type === type)?.value ?? "";
12230
+ return `${part("day")} ${part("month")}, ${part("hour")}:${part("minute")}`;
12231
+ }
12232
+ function setAsideLines(aside) {
12233
+ const each = (entries) => (Array.isArray(entries) ? entries : []).flatMap((entry) => {
12234
+ const { kind, at } = typeof entry === "object" && entry !== null ? entry : {};
12235
+ const moment = typeof at === "string" ? Date.parse(at) : NaN;
12236
+ const what = typeof kind === "string" && Object.hasOwn(ASIDE_WHAT, kind) ? ASIDE_WHAT[kind] : void 0;
12237
+ return what !== void 0 && !Number.isNaN(moment) ? [`${what} from ${setAsideWhen(moment)}`] : [];
12238
+ });
12239
+ if (!aside) return [];
12240
+ const count = Number.isSafeInteger(aside.snapshots) && aside.snapshots > 0 ? aside.snapshots : 0;
12241
+ const kept = [...each(aside.kept), ...count > 0 ? [`${count} snapshot${count > 1 ? "s" : ""}`] : []];
12242
+ const gone = each(aside.gone);
12243
+ return [
12244
+ ...kept.length > 0 ? [`Kept on its computer: ${kept.join(" \xB7 ")}`] : [],
12245
+ ...gone.length > 0 ? [`No longer kept: ${gone.join(" \xB7 ")}`] : []
12246
+ ];
12247
+ }
12248
+ function firstLine3(text) {
12249
+ return text?.split("\n").find((line) => line.trim())?.trim() ?? null;
12250
+ }
12251
+ function statusLineOf(report) {
12252
+ for (const line of report?.split("\n") ?? []) {
12253
+ if (/^\s*#{1,6}(\s|$)/.test(line)) continue;
12254
+ const plain = line.replace(/\*\*|__|`/g, "").trim();
12255
+ if (plain) return plain;
12256
+ }
12257
+ return null;
12258
+ }
12259
+ var cardStarts = /* @__PURE__ */ new WeakMap();
12260
+ var startListeners = /* @__PURE__ */ new Set();
12261
+ var startsChanged = 0;
12262
+ var drawnCards = /* @__PURE__ */ new Map();
12263
+ function setCardStart(adapter, card, start) {
12264
+ const starts = cardStarts.get(adapter) ?? /* @__PURE__ */ new Map();
12265
+ cardStarts.set(adapter, starts.set(card, start));
12266
+ startsChanged++;
12267
+ for (const listener of startListeners) listener();
12268
+ }
12269
+ function useCardStarts(adapter) {
12270
+ react.useSyncExternalStore(
12271
+ (listener) => {
12272
+ startListeners.add(listener);
12273
+ return () => {
12274
+ startListeners.delete(listener);
12275
+ };
12276
+ },
12277
+ () => startsChanged
12278
+ );
12279
+ return cardStarts.get(adapter) ?? /* @__PURE__ */ new Map();
12280
+ }
12281
+ function ThreadCards({ message }) {
12282
+ if (message.worker) return /* @__PURE__ */ jsxRuntime.jsx(WorkerCard, { worker: message.worker });
12283
+ if (message.proposal) return /* @__PURE__ */ jsxRuntime.jsx(ProposalCard, { proposal: message.proposal });
12284
+ return null;
12285
+ }
12286
+ function WorkerCard({ worker }) {
12287
+ const { adapter, projectId, onFileSelect, onOpenSession, threadRows } = useAgentChatAdapterContext();
12288
+ const undoKey = `undo:${worker.id}`;
12289
+ const undone = useCardStarts(adapter).get(undoKey);
12290
+ const live = threadRows?.[worker.id];
12291
+ const group = live?.group && Object.hasOwn(GROUP_LABEL, live.group) ? live.group : void 0;
12292
+ const status = live ? [group && GROUP_LABEL[group], live.progress && `${live.progress.done}/${live.progress.total}`].filter(Boolean).join(" \xB7 ") : STATE_LABEL[worker.state];
12293
+ const statusLine = live ? live.statusLine : statusLineOf(worker.report);
12294
+ const files = live ? Array.isArray(live.files) ? live.files : [] : worker.files;
12295
+ const unlisted = live && Number.isSafeInteger(live.moreFiles) && live.moreFiles > 0 ? live.moreFiles : 0;
12296
+ const more = Math.max(files.length - FILES_SHOWN, 0) + unlisted;
12297
+ const setAside = setAsideLines(live?.setAside);
12298
+ const name = worker.title ?? firstLine3(worker.goal);
12299
+ const view = worker.title !== null ? "View thread" : "View worker";
12300
+ const canUndo = !!live && !!projectId && !!adapter.undoProjectChanges;
12301
+ const undoingNow = undone?.state === "undoing";
12302
+ const undo = async (target) => {
12303
+ if (undoingNow) return;
12304
+ setCardStart(adapter, undoKey, { state: "undoing" });
12305
+ try {
12306
+ setCardStart(adapter, undoKey, { state: "undone", result: await adapter.undoProjectChanges({ projectId, target }) });
12307
+ } catch (error) {
12308
+ setCardStart(adapter, undoKey, { state: "not_undone", error: error instanceof Error ? error.message : "The change could not be undone." });
12309
+ }
12310
+ };
12311
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "worker-card", "data-group": group, className: "my-2 rounded-lg border border-border px-3 py-2 text-sm", children: [
12312
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
12313
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-foreground", title: name ?? void 0, children: /* @__PURE__ */ jsxRuntime.jsx("bdi", { children: name }) }),
12314
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-testid": "worker-card-status", className: "shrink-0 text-xs text-muted-foreground", children: status })
12315
+ ] }),
12316
+ statusLine && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-foreground/70", title: statusLine, children: /* @__PURE__ */ jsxRuntime.jsx("bdi", { children: statusLine }) }),
12317
+ files.length + unlisted > 0 && /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "mt-1 flex flex-wrap gap-x-3 text-xs", children: [
12318
+ files.slice(0, FILES_SHOWN).map((file) => {
12319
+ const landing = "landing" in file && file.landing && Object.hasOwn(MARKS, file.landing) ? file.landing : null;
12320
+ const mark = landing ? MARKS[landing] : null;
12321
+ return (
12322
+ // A long name is cut, its mark never.
12323
+ /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex min-w-0 max-w-full", title: file.label, children: [
12324
+ file.kind === "file" && onFileSelect ? /* @__PURE__ */ jsxRuntime.jsx(
12325
+ "button",
12326
+ {
12327
+ type: "button",
12328
+ className: "min-w-0 truncate underline",
12329
+ onClick: () => onFileSelect(file.ref),
12330
+ children: /* @__PURE__ */ jsxRuntime.jsx("bdi", { children: file.label })
12331
+ }
12332
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: /* @__PURE__ */ jsxRuntime.jsx("bdi", { children: file.label }) }),
12333
+ mark && /* @__PURE__ */ jsxRuntime.jsx("span", { "data-mark": landing, className: "shrink-0 whitespace-pre text-muted-foreground", children: ` \xB7 ${mark}` })
12334
+ ] }, `${file.kind}:${file.ref}`)
12335
+ );
12336
+ }),
12337
+ more > 0 && /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
12338
+ "+",
12339
+ more,
12340
+ " more"
12341
+ ] })
12342
+ ] }),
12343
+ setAside.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "worker-card-set-aside", className: "mt-1 text-xs text-muted-foreground", children: setAside.map((line, at) => /* @__PURE__ */ jsxRuntime.jsx("p", { children: line }, at)) }),
12344
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 flex items-center gap-1", children: [
12345
+ onOpenSession && /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "xs", variant: "ghost", "aria-label": name ? `${view} ${name}` : void 0, onClick: () => onOpenSession(worker.id), children: view }),
12346
+ canUndo && live.landingId && /* @__PURE__ */ jsxRuntime.jsx(
12347
+ Button,
12348
+ {
12349
+ size: "xs",
12350
+ variant: "ghost",
12351
+ disabled: undoingNow,
12352
+ "aria-label": name ? `Undo ${name}'s latest changes` : void 0,
12353
+ onClick: () => void undo({ landingId: live.landingId }),
12354
+ children: "Undo"
12355
+ }
12356
+ ),
12357
+ canUndo && /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu, { children: [
12358
+ /* @__PURE__ */ jsxRuntime.jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "xs", variant: "ghost", "aria-label": name ? `More for ${name}` : "More", children: "\u2026" }) }),
12359
+ /* @__PURE__ */ jsxRuntime.jsx(DropdownMenuContent, { children: /* @__PURE__ */ jsxRuntime.jsx(DropdownMenuItem, { disabled: undoingNow, onSelect: () => void undo({ threadId: worker.id }), children: "Undo this thread's changes" }) })
12360
+ ] })
12361
+ ] }),
12362
+ canUndo && /* @__PURE__ */ jsxRuntime.jsx("div", { role: "status", className: "text-xs text-foreground/70", children: undone?.state === "undone" && undoLines(undone.result).map((line, at) => /* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsx("bdi", { children: line }) }, at)) }),
12363
+ canUndo && undone?.state === "not_undone" && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "text-xs text-destructive", children: /* @__PURE__ */ jsxRuntime.jsx("bdi", { children: undone.error }) })
12364
+ ] });
12365
+ }
12366
+ function ProposalCard({ proposal }) {
12367
+ const { adapter, projectId, onOpenSession } = useAgentChatAdapterContext();
12368
+ const starts = useCardStarts(adapter);
12369
+ const startOf = (key) => starts.get(`${proposal.proposalId}:${key}`);
12370
+ const told = (key, start2) => setCardStart(adapter, `${proposal.proposalId}:${key}`, start2);
12371
+ const started = {};
12372
+ for (const { key } of proposal.threads) {
12373
+ const start2 = startOf(key);
12374
+ if (start2?.state === "started") started[key] = start2.threadId;
12375
+ }
12376
+ Object.assign(started, proposal.started);
12377
+ const startingAt = (key) => {
12378
+ const start2 = startOf(key);
12379
+ return start2?.state === "starting" ? start2.where : void 0;
12380
+ };
12381
+ const starting = (key) => startingAt(key) !== void 0;
12382
+ const canStart = !!projectId && !!adapter.startProposedThread;
12383
+ const canStartHere = !!projectId && !!adapter.startLocalThread;
12384
+ const viewButtons = react.useRef({});
12385
+ react.useEffect(() => {
12386
+ drawnCards.set(proposal.proposalId, (drawnCards.get(proposal.proposalId) ?? 0) + 1);
12387
+ return () => {
12388
+ const left = (drawnCards.get(proposal.proposalId) ?? 1) - 1;
12389
+ if (left > 0) drawnCards.set(proposal.proposalId, left);
12390
+ else drawnCards.delete(proposal.proposalId);
12391
+ };
12392
+ }, [proposal.proposalId]);
12393
+ react.useEffect(() => {
12394
+ for (const { key } of proposal.threads) {
12395
+ const start2 = startOf(key);
12396
+ if (start2?.state !== "started" || !start2.focus) continue;
12397
+ if (!document.activeElement || document.activeElement === document.body) viewButtons.current[key]?.focus();
12398
+ told(key, { ...start2, focus: false });
12399
+ }
12400
+ });
12401
+ const start = async (key, title) => {
12402
+ told(key, { state: "starting", where: title === void 0 ? "cloud" : "device" });
12403
+ try {
12404
+ const card = { projectId, proposalId: proposal.proposalId, key };
12405
+ const row = title === void 0 ? await adapter.startProposedThread(card) : await adapter.startLocalThread({ ...card, title });
12406
+ told(key, { state: "started", threadId: row.id, focus: drawnCards.has(proposal.proposalId) });
12407
+ } catch (error) {
12408
+ told(key, { state: "failed", error: error instanceof Error ? error.message : "The thread could not be started." });
12409
+ }
12410
+ };
12411
+ const startable = proposal.threads.filter(
12412
+ (thread) => thread.where === "cloud" && !started[thread.key] && !starting(thread.key)
12413
+ );
12414
+ const startAll = async (keys) => {
12415
+ for (const key of keys) told(key, { state: "starting", where: "cloud" });
12416
+ for (const key of keys) await start(key);
12417
+ };
12418
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "thread-proposal-card", className: "my-2 rounded-lg border border-border px-3 py-2 text-sm", children: [
12419
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
12420
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 font-medium text-foreground", children: "Proposed threads" }),
12421
+ canStart && startable.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "xs", onClick: () => void startAll(startable.map((thread) => thread.key)), children: "Start all" })
12422
+ ] }),
12423
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "mt-1 space-y-2", children: proposal.threads.map((thread) => {
12424
+ const threadId = started[thread.key];
12425
+ const tried = startOf(thread.key);
12426
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { "data-testid": "proposed-thread", children: [
12427
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "break-words font-medium text-foreground", children: /* @__PURE__ */ jsxRuntime.jsx("bdi", { children: thread.title }) }),
12428
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 break-words text-foreground/70", children: /* @__PURE__ */ jsxRuntime.jsx("bdi", { children: thread.goal }) }),
12429
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
12430
+ /* @__PURE__ */ jsxRuntime.jsx("span", { role: "status", children: threadId ? "Started" : "" }),
12431
+ threadId ? onOpenSession && /* @__PURE__ */ jsxRuntime.jsx(
12432
+ Button,
12433
+ {
12434
+ ref: (node) => {
12435
+ viewButtons.current[thread.key] = node;
12436
+ },
12437
+ size: "xs",
12438
+ variant: "ghost",
12439
+ "aria-label": `View thread ${thread.title}`,
12440
+ onClick: () => onOpenSession(threadId),
12441
+ children: "View thread"
12442
+ }
12443
+ ) : thread.where === "device" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
12444
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: canStartHere ? "Allow Surogate to work in a folder on your device" : "This works in a folder on your device, which needs Surogate Desktop" }),
12445
+ canStartHere && /* @__PURE__ */ jsxRuntime.jsx(
12446
+ Button,
12447
+ {
12448
+ size: "xs",
12449
+ variant: "outline",
12450
+ disabled: starting(thread.key),
12451
+ "aria-label": `${startingAt(thread.key) === "device" ? "Starting" : "Allow"} ${thread.title}`,
12452
+ onClick: () => void start(thread.key, thread.title),
12453
+ children: startingAt(thread.key) === "device" ? "Starting\u2026" : "Allow"
12454
+ }
12455
+ ),
12456
+ canStart && /* @__PURE__ */ jsxRuntime.jsx(
12457
+ Button,
12458
+ {
12459
+ size: "xs",
12460
+ variant: "ghost",
12461
+ disabled: starting(thread.key),
12462
+ "aria-label": startingAt(thread.key) === "cloud" ? `Starting ${thread.title}` : `Run ${thread.title} in the cloud instead`,
12463
+ onClick: () => void start(thread.key),
12464
+ children: startingAt(thread.key) === "cloud" ? "Starting\u2026" : "Run in the cloud instead"
12465
+ }
12466
+ )
12467
+ ] }) : canStart ? /* @__PURE__ */ jsxRuntime.jsx(
12468
+ Button,
12469
+ {
12470
+ size: "xs",
12471
+ variant: "outline",
12472
+ disabled: starting(thread.key),
12473
+ "aria-label": `${starting(thread.key) ? "Starting" : "Start"} ${thread.title}`,
12474
+ onClick: () => void start(thread.key),
12475
+ children: starting(thread.key) ? "Starting\u2026" : "Start"
12476
+ }
12477
+ ) : null
12478
+ ] }),
12479
+ !threadId && tried?.state === "failed" && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "text-xs text-destructive", children: tried.error })
12480
+ ] }, thread.key);
12481
+ }) })
12482
+ ] });
12483
+ }
12048
12484
 
12049
12485
  // src/components/chat/chat-thread.tsx
12050
12486
  init_utils();
@@ -12103,6 +12539,9 @@ function messageToEntries(msg, isLast, artifactFallbacks) {
12103
12539
  originatingSessionId
12104
12540
  }];
12105
12541
  }
12542
+ if (msg.systemKind === "worker" || msg.systemKind === "thread_proposal") {
12543
+ return [{ kind: "thread_card", key: msg.id, message: msg }];
12544
+ }
12106
12545
  if (msg.systemKind === "browser_marker" || msg.systemKind === "browser_marker_warning") {
12107
12546
  return [{
12108
12547
  kind: "browser_marker",
@@ -12334,6 +12773,9 @@ function OrphanSystemMarker({
12334
12773
  if (message.systemKind === "error" && message.errorInfo) {
12335
12774
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto my-2 w-full max-w-4xl", children: /* @__PURE__ */ jsxRuntime.jsx(ErrorMessage, { errorInfo: message.errorInfo, onRetry }) });
12336
12775
  }
12776
+ if (message.systemKind === "worker" || message.systemKind === "thread_proposal") {
12777
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto w-full max-w-4xl px-4", children: /* @__PURE__ */ jsxRuntime.jsx(ThreadCards, { message }) });
12778
+ }
12337
12779
  return null;
12338
12780
  }
12339
12781
  function CancelledToolRow({ tc }) {
@@ -12438,6 +12880,15 @@ function TimelineEntryItem({
12438
12880
  ) })
12439
12881
  ] });
12440
12882
  }
12883
+ if (entry.kind === "thread_card") {
12884
+ return /* @__PURE__ */ jsxRuntime.jsxs(TimelineItem, { step, children: [
12885
+ /* @__PURE__ */ jsxRuntime.jsxs(TimelineHeader, { children: [
12886
+ /* @__PURE__ */ jsxRuntime.jsx(TimelineSeparator, { style: { backgroundColor: "var(--color-border)" } }),
12887
+ /* @__PURE__ */ jsxRuntime.jsx(TimelineIndicator, { className: "size-2 border-none bg-foreground/30" })
12888
+ ] }),
12889
+ /* @__PURE__ */ jsxRuntime.jsx(TimelineContent, { children: /* @__PURE__ */ jsxRuntime.jsx(ThreadCards, { message: entry.message }) })
12890
+ ] });
12891
+ }
12441
12892
  if (entry.kind === "skill_invoked") {
12442
12893
  return /* @__PURE__ */ jsxRuntime.jsxs(TimelineItem, { step, children: [
12443
12894
  /* @__PURE__ */ jsxRuntime.jsxs(TimelineHeader, { children: [
@@ -13050,7 +13501,9 @@ function ChatThread({
13050
13501
  researchSources = [],
13051
13502
  hideTurnSummary = false,
13052
13503
  agentId,
13053
- onOpenIntegrations
13504
+ onOpenIntegrations,
13505
+ deviceWait = null,
13506
+ composerFooter
13054
13507
  }) {
13055
13508
  const groups = react.useMemo(() => groupMessages(messages), [messages]);
13056
13509
  const activeAssistantId = react.useMemo(() => {
@@ -13126,6 +13579,18 @@ function ChatThread({
13126
13579
  return null;
13127
13580
  }, [messages]);
13128
13581
  const composerArea = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-4xl px-3 sm:px-6 pb-3 sm:pb-5 pt-3", children: [
13582
+ /* @__PURE__ */ jsxRuntime.jsx(
13583
+ "div",
13584
+ {
13585
+ role: "status",
13586
+ "data-testid": "device-wait",
13587
+ className: deviceWait ? "mb-2 flex items-center gap-2 rounded-lg border border-line bg-card px-3 py-2 text-sm text-muted-foreground" : void 0,
13588
+ children: deviceWait && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
13589
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LaptopIcon, { className: "size-4 shrink-0", "aria-hidden": "true" }),
13590
+ deviceWait.reason === "copying" ? `Making this thread's copy of ${deviceWait.folder || "its folder"}\u2026` : `Waiting for ${deviceWait.deviceName || "your computer"}. The chat goes on once it is back online.`
13591
+ ] })
13592
+ }
13593
+ ),
13129
13594
  retryIndicator && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsxRuntime.jsx(RetryBanner, { indicator: retryIndicator }) }),
13130
13595
  researchSources.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsxRuntime.jsx(ResearchSourcesPanel, { sources: researchSources }) }),
13131
13596
  (paneCards?.browser || paneCards?.files) && // Layers sliding out from behind the prompt box: each card is
@@ -13210,6 +13675,7 @@ function ChatThread({
13210
13675
  compressEnabled
13211
13676
  }
13212
13677
  ),
13678
+ composerFooter,
13213
13679
  onOpenIntegrations && /* @__PURE__ */ jsxRuntime.jsx(
13214
13680
  IntegrationsBandSlot,
13215
13681
  {
@@ -13224,76 +13690,80 @@ function ChatThread({
13224
13690
  // input floating mid-screen with dead space under it, and the keyboard
13225
13691
  // then opens across that gap. There the composer keeps its usual place
13226
13692
  // at the bottom edge and the greeting takes the space above it.
13227
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col items-center justify-end overflow-hidden bg-background px-3 text-base md:justify-center", children: [
13693
+ // data-transcript: the host's transcript settings shape this, the composer with it.
13694
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-transcript": "", className: "flex flex-1 flex-col items-center justify-end overflow-hidden bg-background px-3 text-base md:justify-center", children: [
13228
13695
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex w-full flex-1 flex-col justify-center md:flex-none", children: /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "mb-6 text-center text-2xl font-bold text-foreground md:mb-8", children: "What should we do ?" }) }),
13229
13696
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full", children: composerArea })
13230
13697
  ] })
13231
13698
  );
13232
13699
  }
13233
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col overflow-hidden bg-background text-base", children: [
13234
- /* @__PURE__ */ jsxRuntime.jsxs(Conversation, { className: "relative flex-1 min-h-0", children: [
13235
- /* @__PURE__ */ jsxRuntime.jsx(ConversationContent, { className: "mx-auto w-full max-w-4xl", children: messages.length === 0 && isLoadingHistory ? /* @__PURE__ */ jsxRuntime.jsx(
13236
- ConversationEmptyState,
13237
- {
13238
- icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageSquareIcon, { className: "size-8 opacity-40" }),
13239
- title: "Loading conversation",
13240
- description: "Fetching the session history."
13241
- }
13242
- ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
13243
- groups.map((group) => {
13244
- if (group.role === "user") {
13245
- const msg = group.messages[0];
13246
- return /* @__PURE__ */ jsxRuntime.jsx(
13247
- ChatMessage,
13248
- {
13249
- message: msg,
13250
- onFileSelect
13251
- },
13252
- msg.id
13253
- );
13700
+ return (
13701
+ // data-transcript: the host's transcript settings shape this, the composer with it.
13702
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-transcript": "", className: "flex flex-1 flex-col overflow-hidden bg-background text-base", children: [
13703
+ /* @__PURE__ */ jsxRuntime.jsxs(Conversation, { className: "relative flex-1 min-h-0", children: [
13704
+ /* @__PURE__ */ jsxRuntime.jsx(ConversationContent, { className: "mx-auto w-full max-w-4xl", children: messages.length === 0 && isLoadingHistory ? /* @__PURE__ */ jsxRuntime.jsx(
13705
+ ConversationEmptyState,
13706
+ {
13707
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageSquareIcon, { className: "size-8 opacity-40" }),
13708
+ title: "Loading conversation",
13709
+ description: "Fetching the session history."
13254
13710
  }
13255
- if (group.role === "system") {
13256
- const msg = group.messages[0];
13257
- if (viewMode === "simple" && msg.systemKind === "skill_invoked") {
13258
- return null;
13711
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
13712
+ groups.map((group) => {
13713
+ if (group.role === "user") {
13714
+ const msg = group.messages[0];
13715
+ return /* @__PURE__ */ jsxRuntime.jsx(
13716
+ ChatMessage,
13717
+ {
13718
+ message: msg,
13719
+ onFileSelect
13720
+ },
13721
+ msg.id
13722
+ );
13723
+ }
13724
+ if (group.role === "system") {
13725
+ const msg = group.messages[0];
13726
+ if (viewMode === "simple" && msg.systemKind === "skill_invoked") {
13727
+ return null;
13728
+ }
13729
+ const groupRetry2 = msg.id === activeFailureId ? onRetry : void 0;
13730
+ return /* @__PURE__ */ jsxRuntime.jsx(
13731
+ OrphanSystemMarker,
13732
+ {
13733
+ message: msg,
13734
+ sessionId,
13735
+ onRetry: groupRetry2
13736
+ },
13737
+ msg.id
13738
+ );
13259
13739
  }
13260
- const groupRetry2 = msg.id === activeFailureId ? onRetry : void 0;
13740
+ const groupTail = group.messages[group.messages.length - 1];
13741
+ const groupRetry = groupTail.id === activeFailureId ? onRetry : void 0;
13261
13742
  return /* @__PURE__ */ jsxRuntime.jsx(
13262
- OrphanSystemMarker,
13743
+ AssistantGroup,
13263
13744
  {
13264
- message: msg,
13745
+ messages: group.messages,
13746
+ activeAssistantId,
13747
+ lastGlobalIndex: group.lastGlobalIndex,
13748
+ totalMessages: messages.length,
13749
+ isRunning,
13265
13750
  sessionId,
13266
- onRetry: groupRetry2
13751
+ artifactFallbacks,
13752
+ onFileSelect,
13753
+ onRetry: groupRetry,
13754
+ viewMode,
13755
+ hideTurnSummary
13267
13756
  },
13268
- msg.id
13757
+ group.messages[0].id
13269
13758
  );
13270
- }
13271
- const groupTail = group.messages[group.messages.length - 1];
13272
- const groupRetry = groupTail.id === activeFailureId ? onRetry : void 0;
13273
- return /* @__PURE__ */ jsxRuntime.jsx(
13274
- AssistantGroup,
13275
- {
13276
- messages: group.messages,
13277
- activeAssistantId,
13278
- lastGlobalIndex: group.lastGlobalIndex,
13279
- totalMessages: messages.length,
13280
- isRunning,
13281
- sessionId,
13282
- artifactFallbacks,
13283
- onFileSelect,
13284
- onRetry: groupRetry,
13285
- viewMode,
13286
- hideTurnSummary
13287
- },
13288
- group.messages[0].id
13289
- );
13290
- }),
13291
- showWorkingOnIt && /* @__PURE__ */ jsxRuntime.jsx(WorkingOnItIndicator, { activity: orbActivity })
13292
- ] }) }),
13293
- /* @__PURE__ */ jsxRuntime.jsx(ConversationScrollButton, {})
13294
- ] }),
13295
- composerArea
13296
- ] });
13759
+ }),
13760
+ showWorkingOnIt && /* @__PURE__ */ jsxRuntime.jsx(WorkingOnItIndicator, { activity: orbActivity })
13761
+ ] }) }),
13762
+ /* @__PURE__ */ jsxRuntime.jsx(ConversationScrollButton, {})
13763
+ ] }),
13764
+ composerArea
13765
+ ] })
13766
+ );
13297
13767
  }
13298
13768
  function IntegrationsBandSlot({
13299
13769
  agentId,
@@ -13323,6 +13793,7 @@ var WORKSPACE_MUTATING_TOOLS = /* @__PURE__ */ new Set([
13323
13793
  ]);
13324
13794
  var AGENT_CHAT_LISTENED_EVENTS = [
13325
13795
  "user.message",
13796
+ "coordinator.message",
13326
13797
  "llm.request",
13327
13798
  "llm.response",
13328
13799
  "llm.thinking",
@@ -13354,61 +13825,284 @@ var AGENT_CHAT_LISTENED_EVENTS = [
13354
13825
  "artifact.updated",
13355
13826
  "browser.provisioned",
13356
13827
  "browser.destroyed",
13828
+ "browser.unavailable",
13357
13829
  "browser.control_granted",
13358
13830
  "browser.control_returned",
13359
13831
  "ask_user_question.response",
13360
13832
  "iteration.summary",
13361
13833
  "turn.summary",
13362
- "loop.result"
13834
+ "loop.result",
13835
+ "device.waiting",
13836
+ "device.resumed",
13837
+ "worker.spawned",
13838
+ "worker.complete",
13839
+ "worker.failed",
13840
+ "thread.proposed"
13363
13841
  ];
13364
13842
  var AGENT_CHAT_LISTENED_EVENT_SET = new Set(
13365
13843
  AGENT_CHAT_LISTENED_EVENTS
13366
13844
  );
13367
13845
 
13368
- // src/runtime/reducer.ts
13369
- var EMPTY_TOKEN_USAGE = {
13370
- inputTokens: 0,
13371
- outputTokens: 0,
13372
- reasoningTokens: 0,
13373
- cachedInputTokens: 0,
13374
- totalTokens: 0,
13375
- contextWindow: 0,
13376
- model: ""
13377
- };
13378
- function createInitialAgentChatState(options = {}) {
13379
- return {
13380
- messages: [],
13381
- isRunning: false,
13382
- isLoadingHistory: options.isLoadingHistory ?? false,
13383
- tokenUsage: EMPTY_TOKEN_USAGE,
13384
- retryIndicator: null,
13385
- lastEventId: 0,
13386
- sessionDone: false,
13387
- hadDeltas: false,
13388
- terminal: false,
13389
- stopped: false,
13390
- workspaceRefreshKey: 0,
13391
- browser: null,
13392
- viewMode: options.viewMode ?? "simple",
13393
- researchSources: []
13394
- };
13846
+ // src/components/missions/mission-derive.ts
13847
+ var ACTIVE_MISSION_STATUSES = /* @__PURE__ */ new Set(["active", "paused"]);
13848
+ function isTerminalMissionStatus(status) {
13849
+ return !ACTIVE_MISSION_STATUSES.has(status);
13395
13850
  }
13396
- function applyAgentChatEvent(state, event) {
13397
- let nextState = {
13398
- ...state,
13399
- isLoadingHistory: false,
13400
- lastEventId: Math.max(state.lastEventId, event.eventId),
13401
- sessionDone: state.sessionDone || event.type === "session.done"
13851
+ function missionTaskBucket(status) {
13852
+ if (status === "done") return "done";
13853
+ if (status === "blocked") return "blocked";
13854
+ if (status === "failed" || status === "cancelled") {
13855
+ return "failed_or_cancelled";
13856
+ }
13857
+ return "in_flight";
13858
+ }
13859
+ function groupMissionTasksByBucket(tasks) {
13860
+ const buckets = {
13861
+ in_flight: [],
13862
+ done: [],
13863
+ blocked: [],
13864
+ failed_or_cancelled: []
13402
13865
  };
13403
- nextState = applyRetryIndicator(nextState, event);
13404
- if (state.stopped && isPostTerminalLiveEvent(event.type)) {
13405
- return nextState;
13866
+ for (const t of tasks) {
13867
+ buckets[missionTaskBucket(t.status)].push(t);
13406
13868
  }
13407
- switch (event.type) {
13408
- case "user.message":
13409
- return withMessages(
13410
- { ...nextState, terminal: false, stopped: false, isRunning: true },
13411
- applyUserMessage(nextState.messages, event.eventId, event.data)
13869
+ return buckets;
13870
+ }
13871
+ function deriveMissionWorkerActivityLabel(worker) {
13872
+ const kind = worker.latestEventKind;
13873
+ if (kind === "tool.call" && worker.latestEventSummary) {
13874
+ return `tool: ${truncate4(worker.latestEventSummary, 80)}`;
13875
+ }
13876
+ if (kind === "llm.response") {
13877
+ return "thinking";
13878
+ }
13879
+ if (kind === "tool.result") {
13880
+ return "received tool result";
13881
+ }
13882
+ if (kind && worker.latestEventSummary) {
13883
+ return `${kind}: ${truncate4(worker.latestEventSummary, 60)}`;
13884
+ }
13885
+ return worker.sessionStatus || "active";
13886
+ }
13887
+ function truncate4(s, max) {
13888
+ if (s.length <= max) return s;
13889
+ return `${s.slice(0, max - 1)}\u2026`;
13890
+ }
13891
+ var MISSION_EVENT_CATEGORIES = [
13892
+ "spawn",
13893
+ "output",
13894
+ "done",
13895
+ "verdict",
13896
+ "system"
13897
+ ];
13898
+ function missionEventCategory(type) {
13899
+ switch (type) {
13900
+ case "worker.spawned":
13901
+ return "spawn";
13902
+ case "iteration.summary":
13903
+ return "output";
13904
+ case "worker.complete":
13905
+ return "done";
13906
+ case "mission.evaluation.end":
13907
+ return "verdict";
13908
+ default:
13909
+ return "system";
13910
+ }
13911
+ }
13912
+ function dataString(data, key) {
13913
+ const v = data?.[key];
13914
+ return typeof v === "string" && v.trim().length > 0 ? v.trim() : null;
13915
+ }
13916
+ var CONTROL_MARKUP_RE = /<next_action\b[^>]*(?:\/>|>[\s\S]*?<\/next_action>)/gi;
13917
+ function stripMissionControlMarkup(text) {
13918
+ return text.replace(CONTROL_MARKUP_RE, "").trim();
13919
+ }
13920
+ function missionEventSummary(event) {
13921
+ return stripMissionControlMarkup(rawMissionEventSummary(event));
13922
+ }
13923
+ function rawMissionEventSummary(event) {
13924
+ const d = event.data ?? null;
13925
+ switch (event.type) {
13926
+ case "worker.spawned":
13927
+ return dataString(d, "goal") ?? "Task spawned";
13928
+ case "worker.complete":
13929
+ return dataString(d, "result") ?? "Worker complete";
13930
+ case "iteration.summary":
13931
+ return dataString(d, "summary") ?? "";
13932
+ case "mission.evaluation.end": {
13933
+ const result = dataString(d, "result");
13934
+ const feedback = dataString(d, "feedback") ?? dataString(d, "explanation");
13935
+ if (result && feedback) return `${result} \u2014 ${feedback}`;
13936
+ return result ?? feedback ?? "Evaluation finished";
13937
+ }
13938
+ case "task.blocked":
13939
+ return dataString(d, "reason") ?? "Task blocked";
13940
+ case "task.failed":
13941
+ return "Task failed";
13942
+ default: {
13943
+ const text = dataString(d, "summary") ?? dataString(d, "text") ?? dataString(d, "message") ?? dataString(d, "reason");
13944
+ if (text) return text;
13945
+ if (!d) return "";
13946
+ try {
13947
+ return truncate4(JSON.stringify(d), 160);
13948
+ } catch {
13949
+ return "";
13950
+ }
13951
+ }
13952
+ }
13953
+ }
13954
+ function missionEventTaskId(event, sessions) {
13955
+ return dataString(event.data ?? null, "task_id") ?? sessions[event.sessionId]?.taskId ?? null;
13956
+ }
13957
+ function missionEventActorLabel(event, sessions) {
13958
+ const meta = sessions[event.sessionId];
13959
+ if (!meta) return event.sessionId.slice(0, 8);
13960
+ if (meta.kind === "coordinator") return "orchestrator";
13961
+ return meta.agentDefName ?? "worker";
13962
+ }
13963
+ function missionTaskBlocks(tasks) {
13964
+ const blocks = {};
13965
+ for (const t of tasks) {
13966
+ for (const parent of t.parentIds) {
13967
+ (blocks[parent] ?? (blocks[parent] = [])).push(t.id);
13968
+ }
13969
+ }
13970
+ return blocks;
13971
+ }
13972
+ function missionTaskTitle(goal, max = 80) {
13973
+ const line = goal.split("\n").find((l) => l.trim().length > 0)?.trim() ?? goal.trim();
13974
+ return truncate4(line.replace(/\s+/g, " "), max);
13975
+ }
13976
+ var RAIL_GROUP_DEFS = [
13977
+ { key: "running", label: "Running", match: (s) => s === "running" },
13978
+ { key: "blocked", label: "Blocked", match: (s) => s === "blocked" },
13979
+ {
13980
+ key: "queued",
13981
+ label: "Queued",
13982
+ match: (s) => s === "ready" || s === "todo"
13983
+ },
13984
+ { key: "done", label: "Done", match: (s) => s === "done" },
13985
+ {
13986
+ key: "failed",
13987
+ label: "Failed / cancelled",
13988
+ match: (s) => s === "failed" || s === "cancelled"
13989
+ }
13990
+ ];
13991
+ function missionTaskRailGroups(tasks) {
13992
+ const grouped = /* @__PURE__ */ new Set();
13993
+ const groups = RAIL_GROUP_DEFS.map((def) => ({
13994
+ key: def.key,
13995
+ label: def.label,
13996
+ tasks: tasks.filter((t) => {
13997
+ const hit = def.match(t.status);
13998
+ if (hit) grouped.add(t.id);
13999
+ return hit;
14000
+ })
14001
+ }));
14002
+ const stray = tasks.filter((t) => !grouped.has(t.id));
14003
+ if (stray.length > 0) {
14004
+ const queued = groups.find((g) => g.key === "queued");
14005
+ if (queued) queued.tasks.push(...stray);
14006
+ }
14007
+ return groups.filter((g) => g.tasks.length > 0);
14008
+ }
14009
+ var TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
14010
+ function defaultSelectedMissionTaskId(tasks) {
14011
+ if (tasks.length === 0) return null;
14012
+ const ordered = missionTaskRailGroups(tasks).flatMap((g) => g.tasks);
14013
+ const firstLive = ordered.find(
14014
+ (t) => !TERMINAL_TASK_STATUSES.has(t.status)
14015
+ );
14016
+ return (firstLive ?? ordered[0] ?? tasks[0]).id;
14017
+ }
14018
+ function mergeMissionEvents(existing, incoming) {
14019
+ if (incoming.length === 0) return existing;
14020
+ const byId = /* @__PURE__ */ new Map();
14021
+ for (const e of existing) byId.set(e.id, e);
14022
+ for (const e of incoming) byId.set(e.id, e);
14023
+ return [...byId.values()].sort((a, b) => a.id - b.id);
14024
+ }
14025
+ function missionTaskStatusDotClass(status) {
14026
+ switch (status) {
14027
+ case "running":
14028
+ return "bg-primary";
14029
+ case "blocked":
14030
+ return "bg-amber-500";
14031
+ case "done":
14032
+ return "bg-emerald-500";
14033
+ case "failed":
14034
+ return "bg-destructive";
14035
+ case "cancelled":
14036
+ return "bg-muted-foreground";
14037
+ default:
14038
+ return "bg-foreground/30";
14039
+ }
14040
+ }
14041
+ function missionWorkerTaskCounts(worker, tasks) {
14042
+ if (worker.kind !== "task") return null;
14043
+ const mine = tasks.filter((t) => t.agentDefName === worker.agentDefName);
14044
+ return {
14045
+ completed: mine.filter((t) => t.status === "done").length,
14046
+ inFlight: mine.filter(
14047
+ (t) => t.status === "running" || t.status === "blocked"
14048
+ ).length
14049
+ };
14050
+ }
14051
+ function formatMissionTimestamp(iso) {
14052
+ if (!iso) return "";
14053
+ const parsed = new Date(iso);
14054
+ if (Number.isNaN(parsed.getTime())) return iso;
14055
+ return parsed.toLocaleTimeString();
14056
+ }
14057
+
14058
+ // src/runtime/reducer.ts
14059
+ var EMPTY_TOKEN_USAGE = {
14060
+ inputTokens: 0,
14061
+ outputTokens: 0,
14062
+ reasoningTokens: 0,
14063
+ cachedInputTokens: 0,
14064
+ totalTokens: 0,
14065
+ contextWindow: 0,
14066
+ model: ""
14067
+ };
14068
+ function createInitialAgentChatState(options = {}) {
14069
+ return {
14070
+ messages: [],
14071
+ isRunning: false,
14072
+ isLoadingHistory: options.isLoadingHistory ?? false,
14073
+ tokenUsage: EMPTY_TOKEN_USAGE,
14074
+ retryIndicator: null,
14075
+ lastEventId: 0,
14076
+ sessionDone: false,
14077
+ hadDeltas: false,
14078
+ terminal: false,
14079
+ stopped: false,
14080
+ workspaceRefreshKey: 0,
14081
+ browser: null,
14082
+ browserTabs: [],
14083
+ viewMode: options.viewMode ?? "simple",
14084
+ researchSources: [],
14085
+ deviceWait: null
14086
+ };
14087
+ }
14088
+ function applyAgentChatEvent(state, event) {
14089
+ let nextState = {
14090
+ ...state,
14091
+ isLoadingHistory: false,
14092
+ lastEventId: Math.max(state.lastEventId, event.eventId),
14093
+ sessionDone: state.sessionDone || event.type === "session.done"
14094
+ };
14095
+ nextState = applyRetryIndicator(nextState, event);
14096
+ if (state.stopped && isPostTerminalLiveEvent(event.type)) {
14097
+ return nextState;
14098
+ }
14099
+ switch (event.type) {
14100
+ case "user.message":
14101
+ // A project coordinator's follow-up to its thread: shown as the thread reads it, its header saying whose it is.
14102
+ case "coordinator.message":
14103
+ return withMessages(
14104
+ { ...nextState, terminal: false, stopped: false, isRunning: true },
14105
+ applyUserMessage(nextState.messages, event.eventId, event.data)
13412
14106
  );
13413
14107
  case "skill.invoked":
13414
14108
  return withMessages(nextState, [
@@ -13482,23 +14176,79 @@ function applyAgentChatEvent(state, event) {
13482
14176
  }
13483
14177
  return withMessages(nextState, [...nextState.messages, artifactMessage]);
13484
14178
  }
13485
- case "browser.provisioned":
13486
- return applyBrowserEvent(nextState, event, {
14179
+ case "browser.provisioned": {
14180
+ const tab = computerTab(event);
14181
+ if (tab === null) {
14182
+ return applyBrowserEvent(nextState, event, {
14183
+ status: "live",
14184
+ controlOwner: null
14185
+ });
14186
+ }
14187
+ const browserTabs = [...nextState.browserTabs.filter((open) => open !== tab), tab];
14188
+ if (nextState.browserTabs.length > 0) {
14189
+ const there = nextState.browser?.computer === true && nextState.browser.status !== "unavailable";
14190
+ return {
14191
+ ...nextState,
14192
+ browserTabs,
14193
+ browser: there ? nextState.browser : { status: "live", controlOwner: null, computer: true }
14194
+ };
14195
+ }
14196
+ return applyBrowserEvent({ ...nextState, browserTabs }, event, {
13487
14197
  status: "live",
13488
- controlOwner: null
14198
+ controlOwner: null,
14199
+ computer: true
13489
14200
  });
14201
+ }
14202
+ case "browser.unavailable": {
14203
+ const tab = computerTab(event);
14204
+ const browserTabs = tab === null ? [] : nextState.browserTabs.filter((open) => open !== tab);
14205
+ if (browserTabs.length > 0) return { ...nextState, browserTabs };
14206
+ const none = {
14207
+ ...nextState,
14208
+ browserTabs,
14209
+ browser: { status: "unavailable", controlOwner: null, computer: true }
14210
+ };
14211
+ if (saidLastOfBrowser(nextState.messages) === NO_BROWSER) return none;
14212
+ return withMessages(none, [...none.messages, browserMarker(event)]);
14213
+ }
14214
+ // A take-over on the user's computer is told as the cloud's is: the browser stays the computer's.
13490
14215
  case "browser.control_granted":
13491
14216
  return applyBrowserEvent(nextState, event, {
13492
14217
  status: "user-control",
13493
- controlOwner: stringValue2(event.data.owner_user_id) || null
14218
+ controlOwner: stringValue2(event.data.owner_user_id) || null,
14219
+ ...onComputer(event)
13494
14220
  });
13495
14221
  case "browser.control_returned":
13496
14222
  return applyBrowserEvent(nextState, event, {
13497
14223
  status: "live",
13498
- controlOwner: null
14224
+ controlOwner: null,
14225
+ ...onComputer(event)
13499
14226
  });
13500
- case "browser.destroyed":
13501
- return applyBrowserEvent(nextState, event, null);
14227
+ case "browser.destroyed": {
14228
+ const tab = computerTab(event);
14229
+ const browserTabs = tab === null ? [] : nextState.browserTabs.filter((open) => open !== tab);
14230
+ if (browserTabs.length > 0) return { ...nextState, browserTabs };
14231
+ return applyBrowserEvent({ ...nextState, browserTabs }, event, null);
14232
+ }
14233
+ case "device.waiting":
14234
+ return {
14235
+ ...nextState,
14236
+ deviceWait: {
14237
+ deviceId: stringValue2(event.data.device_id),
14238
+ deviceName: stringValue2(event.data.device_name),
14239
+ reason: stringValue2(event.data.reason) || "offline",
14240
+ folder: stringValue2(event.data.folder)
14241
+ }
14242
+ };
14243
+ case "device.resumed":
14244
+ return { ...nextState, deviceWait: null };
14245
+ case "worker.spawned":
14246
+ return withMessages(nextState, applyWorkerSpawned(nextState.messages, event));
14247
+ case "worker.complete":
14248
+ case "worker.failed":
14249
+ return withMessages(nextState, applyWorkerReport(nextState.messages, event));
14250
+ case "thread.proposed":
14251
+ return withMessages(nextState, [...nextState.messages, proposalMessage(event)]);
13502
14252
  case "llm.delta":
13503
14253
  return applyLlmDelta(nextState, event);
13504
14254
  case "llm.response":
@@ -13520,7 +14270,7 @@ function applyAgentChatEvent(state, event) {
13520
14270
  return collectResearchSource(withResult, toolName, event.data);
13521
14271
  }
13522
14272
  case "harness.wake":
13523
- return { ...nextState, terminal: false, isRunning: true };
14273
+ return { ...nextState, terminal: false, isRunning: true, deviceWait: null };
13524
14274
  case "llm.request":
13525
14275
  return {
13526
14276
  ...nextState,
@@ -13549,7 +14299,7 @@ function applyAgentChatEvent(state, event) {
13549
14299
  if (event.data.strategy === "clear") {
13550
14300
  return {
13551
14301
  ...nextState,
13552
- messages: [],
14302
+ messages: typedBehindTheClear(nextState.messages, event.data.answers),
13553
14303
  tokenUsage: EMPTY_TOKEN_USAGE
13554
14304
  };
13555
14305
  }
@@ -13641,6 +14391,17 @@ function applyRetryIndicator(state, event) {
13641
14391
  return state;
13642
14392
  }
13643
14393
  }
14394
+ function typedBehindTheClear(messages, answers) {
14395
+ if (typeof answers !== "number") return [];
14396
+ let first = messages.length;
14397
+ while (first > 0) {
14398
+ const message = messages[first - 1];
14399
+ const typedAt = /^evt-(\d+)$/.exec(message.id);
14400
+ if (message.role !== "user" || !typedAt || Number(typedAt[1]) <= answers) break;
14401
+ first -= 1;
14402
+ }
14403
+ return messages.slice(first);
14404
+ }
13644
14405
  function applyUserMessage(messages, eventId, data) {
13645
14406
  const content = stringValue2(data.content);
13646
14407
  const images = parseUserMessageImages(data.images);
@@ -13706,6 +14467,22 @@ function parseUserMessageAttachments(raw) {
13706
14467
  }
13707
14468
  return out.length > 0 ? out : void 0;
13708
14469
  }
14470
+ function onComputer(event) {
14471
+ return event.data.computer === true ? { computer: true } : {};
14472
+ }
14473
+ function computerTab(event) {
14474
+ return event.data.computer === true ? stringValue2(event.data.session_id) : null;
14475
+ }
14476
+ var NO_BROWSER = "No supported browser on the chat's computer.";
14477
+ function saidLastOfBrowser(messages) {
14478
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
14479
+ const message = messages[index];
14480
+ if (message.systemKind === "browser_marker" || message.systemKind === "browser_marker_warning") {
14481
+ return message.content;
14482
+ }
14483
+ }
14484
+ return void 0;
14485
+ }
13709
14486
  function applyBrowserEvent(state, event, browser) {
13710
14487
  return withMessages(
13711
14488
  { ...state, browser },
@@ -13729,6 +14506,10 @@ function browserMarker(event) {
13729
14506
  "browser.destroyed": {
13730
14507
  content: "Browser closed.",
13731
14508
  warning: false
14509
+ },
14510
+ "browser.unavailable": {
14511
+ content: NO_BROWSER,
14512
+ warning: true
13732
14513
  }
13733
14514
  };
13734
14515
  const label = labels[event.type] ?? {
@@ -13976,6 +14757,67 @@ function applyTurnSummary(state, event) {
13976
14757
  };
13977
14758
  return { ...state, messages };
13978
14759
  }
14760
+ function workerMessage(worker) {
14761
+ return {
14762
+ id: `worker-${worker.id}`,
14763
+ role: "system",
14764
+ content: worker.title ?? worker.goal,
14765
+ createdAt: /* @__PURE__ */ new Date(),
14766
+ status: "complete",
14767
+ systemKind: "worker",
14768
+ worker
14769
+ };
14770
+ }
14771
+ function applyWorkerSpawned(messages, event) {
14772
+ const id = stringValue2(event.data.worker_id);
14773
+ const proposalId = stringValue2(event.data.proposal_id);
14774
+ const key = stringValue2(event.data.key);
14775
+ const marked = proposalId && key ? messages.map((message) => message.proposal?.proposalId === proposalId ? { ...message, proposal: { ...message.proposal, started: { ...message.proposal.started, [key]: id } } } : message) : messages;
14776
+ if (marked.some((message) => message.worker?.id === id)) return marked;
14777
+ return [...marked, workerMessage({
14778
+ id,
14779
+ title: stringValue2(event.data.title) || null,
14780
+ goal: stringValue2(event.data.goal),
14781
+ state: "working",
14782
+ report: null,
14783
+ files: []
14784
+ })];
14785
+ }
14786
+ function applyWorkerReport(messages, event) {
14787
+ const id = stringValue2(event.data.worker_id);
14788
+ const failed = event.type === "worker.failed";
14789
+ const index = messages.findIndex((message) => message.worker?.id === id);
14790
+ if (index === -1) return messages;
14791
+ const worker = messages[index].worker;
14792
+ const reported = {
14793
+ ...worker,
14794
+ state: failed ? "failed" : "reported",
14795
+ // A mission worker's result can carry the loop's control markup, which never reaches the UI.
14796
+ report: stripMissionControlMarkup(stringValue2(failed ? event.data.error : event.data.result)) || null,
14797
+ // A card lists files, as a thread's row does: not the URLs and commands a turn names.
14798
+ files: failed ? worker.files : parseTurnArtifacts(event.data.files).filter((file) => file.kind === "file" || file.kind === "artifact")
14799
+ };
14800
+ const next = [...messages];
14801
+ next[index] = { ...next[index], worker: reported };
14802
+ return next;
14803
+ }
14804
+ function proposalMessage(event) {
14805
+ const threads = (Array.isArray(event.data.threads) ? event.data.threads : []).filter((thread) => !!thread && typeof thread === "object").map((thread) => ({
14806
+ key: stringValue2(thread.key),
14807
+ title: stringValue2(thread.title),
14808
+ goal: stringValue2(thread.goal),
14809
+ where: thread.where === "device" ? "device" : "cloud"
14810
+ }));
14811
+ return {
14812
+ id: `proposal-${stringValue2(event.data.proposal_id)}`,
14813
+ role: "system",
14814
+ content: threads.map((thread) => thread.title).join(", "),
14815
+ createdAt: /* @__PURE__ */ new Date(),
14816
+ status: "complete",
14817
+ systemKind: "thread_proposal",
14818
+ proposal: { proposalId: stringValue2(event.data.proposal_id), threads, started: {} }
14819
+ };
14820
+ }
13979
14821
  function parseTurnArtifacts(raw) {
13980
14822
  if (!Array.isArray(raw)) return [];
13981
14823
  const out = [];
@@ -14078,7 +14920,7 @@ function applyTerminalEvent(state, event) {
14078
14920
  status: event.type === "session.fail" ? "error" : "complete",
14079
14921
  errorInfo: errorInfo ?? doneMsg.errorInfo
14080
14922
  };
14081
- } else if (event.type === "session.fail" && errorInfo) {
14923
+ } else if (errorInfo) {
14082
14924
  messages.push({
14083
14925
  id: `error-${event.eventId}`,
14084
14926
  role: "system",
@@ -14097,7 +14939,7 @@ function applyTerminalEvent(state, event) {
14097
14939
  };
14098
14940
  }
14099
14941
  function buildErrorInfo(event) {
14100
- if (event.type !== "session.fail" || typeof event.data.error_category !== "string") {
14942
+ if (event.type !== "session.fail" && event.type !== "session.pause" || typeof event.data.error_category !== "string") {
14101
14943
  return void 0;
14102
14944
  }
14103
14945
  const detail = stringValue2(event.data.error_detail) || stringValue2(event.data.error);
@@ -14845,7 +15687,8 @@ function useAgentChatRuntime({
14845
15687
  ...prev,
14846
15688
  browser: browserState ? {
14847
15689
  status: browserState.status,
14848
- controlOwner: browserState.controlOwner ?? null
15690
+ controlOwner: browserState.controlOwner ?? null,
15691
+ ...browserState.computer ? { computer: true } : {}
14849
15692
  } : null
14850
15693
  }));
14851
15694
  }).catch(() => void 0);
@@ -17346,17 +18189,21 @@ function saveDoc(adapter, sessionId, doc) {
17346
18189
  file,
17347
18190
  directory: CANVAS_DIR
17348
18191
  });
18192
+ return true;
17349
18193
  } catch {
18194
+ return false;
17350
18195
  }
17351
18196
  })();
17352
18197
  inFlightSaves.set(sessionId, done);
17353
- return done.then(() => {
18198
+ return done.then((saved) => {
17354
18199
  if (inFlightSaves.get(sessionId) === done) {
17355
18200
  inFlightSaves.delete(sessionId);
17356
18201
  }
18202
+ return saved;
17357
18203
  });
17358
18204
  }
17359
18205
  function useDebouncedSave(adapter, sessionId, doc, delayMs = SAVE_DEBOUNCE_MS) {
18206
+ const [unsaved, setUnsaved] = react.useState(false);
17360
18207
  const latest = react.useRef(doc);
17361
18208
  latest.current = doc;
17362
18209
  const dirty = react.useRef(false);
@@ -17368,7 +18215,7 @@ function useDebouncedSave(adapter, sessionId, doc, delayMs = SAVE_DEBOUNCE_MS) {
17368
18215
  }
17369
18216
  dirty.current = true;
17370
18217
  const timer = setTimeout(() => {
17371
- void saveDoc(adapter, sessionId, latest.current);
18218
+ void saveDoc(adapter, sessionId, latest.current).then((saved) => setUnsaved(!saved));
17372
18219
  }, delayMs);
17373
18220
  return () => clearTimeout(timer);
17374
18221
  }, [adapter, sessionId, doc, delayMs]);
@@ -17378,6 +18225,7 @@ function useDebouncedSave(adapter, sessionId, doc, delayMs = SAVE_DEBOUNCE_MS) {
17378
18225
  void saveDoc(adapter, sessionId, latest.current);
17379
18226
  };
17380
18227
  }, [adapter, sessionId]);
18228
+ return unsaved;
17381
18229
  }
17382
18230
  function shouldReloadCanvas(previous, next, opts = {}) {
17383
18231
  if (!next) return false;
@@ -17689,7 +18537,7 @@ function WhiteboardSurface({
17689
18537
  return next;
17690
18538
  });
17691
18539
  }, [runtime.messages, canvasReady, resolveDraw]);
17692
- useDebouncedSave(adapter, canvasReady ? sessionId : null, doc);
18540
+ const unsaved = useDebouncedSave(adapter, canvasReady ? sessionId : null, doc);
17693
18541
  const hasAnswer = react.useMemo(
17694
18542
  () => runtime.messages.some((m) => m.role === "assistant"),
17695
18543
  [runtime.messages]
@@ -18302,6 +19150,7 @@ function WhiteboardSurface({
18302
19150
  /* @__PURE__ */ jsxRuntime.jsx(Spinner, { className: "size-4 shrink-0" }),
18303
19151
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: progress ?? (askMode === "deep" ? "Working it out\u2026" : "Thinking\u2026") })
18304
19152
  ] }) }) }) : null,
19153
+ unsaved ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute right-2 top-2 z-10", children: /* @__PURE__ */ jsxRuntime.jsx("span", { role: "status", className: "rounded-full border bg-background px-3 py-1 text-xs text-muted-foreground shadow-sm", children: "Your last change wasn't saved." }) }) : null,
18305
19154
  restoringVisible ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/10", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-3 rounded-full border bg-background px-6 py-3 text-sm font-medium text-muted-foreground shadow-md", children: [
18306
19155
  /* @__PURE__ */ jsxRuntime.jsx(Spinner, { className: "size-5" }),
18307
19156
  "Restoring board\u2026"
@@ -19045,7 +19894,38 @@ function FileTreeFile({
19045
19894
  }
19046
19895
  );
19047
19896
  }
19897
+ var NONE = {};
19898
+ var chats = /* @__PURE__ */ new Map();
19899
+ var listeners = /* @__PURE__ */ new Set();
19900
+ function changed() {
19901
+ for (const listener of listeners) listener();
19902
+ }
19903
+ function changeChatFiles(chat, changes) {
19904
+ chats.set(chat, { ...chats.get(chat), ...changes });
19905
+ changed();
19906
+ }
19907
+ function leaveChatFiles(chat) {
19908
+ const was = chats.get(chat);
19909
+ if (!was) return;
19910
+ const { entries: _tree, truncated: _cut, ...kept } = was;
19911
+ chats.set(chat, was.deleting ? kept : { ...kept, deleteTarget: null });
19912
+ changed();
19913
+ }
19914
+ function forgetChatFiles() {
19915
+ chats.clear();
19916
+ changed();
19917
+ }
19918
+ function subscribe2(listener) {
19919
+ listeners.add(listener);
19920
+ return () => {
19921
+ listeners.delete(listener);
19922
+ };
19923
+ }
19924
+ function useChatFiles(chat) {
19925
+ return react.useSyncExternalStore(subscribe2, () => chat === null ? NONE : chats.get(chat) ?? NONE);
19926
+ }
19048
19927
  var SKELETON_WIDTHS2 = [75, 60, 90, 65, 80, 70, 85, 55];
19928
+ var NO_ENTRIES = [];
19049
19929
  function collectExpandedPaths(entries, depth = 0) {
19050
19930
  const paths = [];
19051
19931
  for (const entry of entries) {
@@ -19159,43 +20039,69 @@ function WorkspacePanel({
19159
20039
  disabled = false
19160
20040
  }) {
19161
20041
  const fileInputRef = react.useRef(null);
19162
- const [entries, setEntries] = react.useState([]);
19163
- const [treeLoading, setTreeLoading] = react.useState(false);
19164
- const [treeError, setTreeError] = react.useState(null);
19165
- const [notice, setNotice] = react.useState(null);
19166
- const [uploading, setUploading] = react.useState(false);
19167
- const [deleteTarget, setDeleteTarget] = react.useState(null);
20042
+ const {
20043
+ entries = NO_ENTRIES,
20044
+ truncated = false,
20045
+ loading = false,
20046
+ error = null,
20047
+ waiting = null,
20048
+ notice = null,
20049
+ uploading = false,
20050
+ deleteTarget = null,
20051
+ deleting = false
20052
+ } = useChatFiles(sessionId);
19168
20053
  const [expandedPaths, setExpandedPaths] = react.useState(/* @__PURE__ */ new Set());
19169
20054
  const sessionIdRef = react.useRef(sessionId);
20055
+ const mountedRef = react.useRef(true);
20056
+ const readingRef = react.useRef(null);
19170
20057
  sessionIdRef.current = sessionId;
20058
+ const stopReading = react.useCallback(() => {
20059
+ const was = readingRef.current;
20060
+ readingRef.current = null;
20061
+ if (!was) return;
20062
+ was.controller.abort();
20063
+ changeChatFiles(was.chat, { loading: false, waiting: null });
20064
+ }, []);
20065
+ react.useEffect(() => {
20066
+ mountedRef.current = true;
20067
+ return () => {
20068
+ mountedRef.current = false;
20069
+ stopReading();
20070
+ };
20071
+ }, [stopReading]);
20072
+ react.useEffect(() => () => {
20073
+ if (sessionId) leaveChatFiles(sessionId);
20074
+ }, [sessionId]);
19171
20075
  const fetchTree = react.useCallback(async () => {
19172
- if (!sessionId) {
19173
- setEntries([]);
19174
- setExpandedPaths(/* @__PURE__ */ new Set());
19175
- setTreeLoading(false);
19176
- setTreeError(null);
19177
- return;
19178
- }
19179
- const requestedSessionId = sessionId;
19180
- setTreeLoading(true);
19181
- setTreeError(null);
20076
+ if (!mountedRef.current || sessionIdRef.current !== sessionId) return;
20077
+ stopReading();
20078
+ if (!sessionId) return;
20079
+ const chat = sessionId;
20080
+ const reading = { chat, controller: new AbortController() };
20081
+ readingRef.current = reading;
20082
+ const current = () => readingRef.current === reading;
20083
+ changeChatFiles(chat, { loading: true, waiting: null, error: null });
19182
20084
  try {
19183
20085
  const tree = await adapter.getWorkspaceTree({
19184
- sessionId: requestedSessionId
20086
+ sessionId: chat,
20087
+ signal: reading.controller.signal,
20088
+ onWaiting: (said) => {
20089
+ if (current()) changeChatFiles(chat, { waiting: said });
20090
+ }
19185
20091
  });
19186
- if (sessionIdRef.current !== requestedSessionId) return;
19187
- setEntries(tree.entries);
20092
+ if (!current()) return;
20093
+ changeChatFiles(chat, { entries: tree.entries, truncated: tree.truncated });
19188
20094
  setExpandedPaths(new Set(collectExpandedPaths(tree.entries)));
19189
- } catch (error) {
19190
- if (sessionIdRef.current !== requestedSessionId) return;
19191
- setEntries([]);
19192
- setTreeError(error.message);
20095
+ } catch (error2) {
20096
+ if (!current()) return;
20097
+ changeChatFiles(chat, { entries: [], error: error2.message });
19193
20098
  } finally {
19194
- if (sessionIdRef.current === requestedSessionId) {
19195
- setTreeLoading(false);
20099
+ if (current()) {
20100
+ readingRef.current = null;
20101
+ changeChatFiles(chat, { loading: false, waiting: null });
19196
20102
  }
19197
20103
  }
19198
- }, [adapter, sessionId]);
20104
+ }, [adapter, sessionId, stopReading]);
19199
20105
  react.useEffect(() => {
19200
20106
  void fetchTree();
19201
20107
  }, [fetchTree]);
@@ -19238,23 +20144,24 @@ function WorkspacePanel({
19238
20144
  const handleUpload = react.useCallback(
19239
20145
  async (files) => {
19240
20146
  if (disabled || !sessionId || files.length === 0) return;
19241
- setUploading(true);
19242
- setNotice(null);
20147
+ const chat = sessionId;
20148
+ changeChatFiles(chat, { uploading: true, notice: null });
19243
20149
  try {
19244
20150
  for (const uploadedFile of Array.from(files)) {
19245
20151
  await adapter.uploadWorkspaceFile({
19246
- sessionId,
19247
- file: uploadedFile
20152
+ sessionId: chat,
20153
+ file: uploadedFile,
20154
+ onWaiting: (said) => changeChatFiles(chat, { notice: said })
19248
20155
  });
19249
20156
  }
19250
- setNotice(
19251
- files.length === 1 ? `Uploaded ${files[0]?.name ?? "file"}` : `Uploaded ${files.length} files`
19252
- );
20157
+ changeChatFiles(chat, {
20158
+ notice: files.length === 1 ? `Uploaded ${files[0]?.name ?? "file"}` : `Uploaded ${files.length} files`
20159
+ });
19253
20160
  await fetchTree();
19254
- } catch (error) {
19255
- setNotice(error.message);
20161
+ } catch (error2) {
20162
+ changeChatFiles(chat, { notice: error2.message });
19256
20163
  } finally {
19257
- setUploading(false);
20164
+ changeChatFiles(chat, { uploading: false });
19258
20165
  if (fileInputRef.current) fileInputRef.current.value = "";
19259
20166
  }
19260
20167
  },
@@ -19263,17 +20170,23 @@ function WorkspacePanel({
19263
20170
  const handleDelete = react.useCallback(
19264
20171
  async (path) => {
19265
20172
  if (disabled || !sessionId) return;
20173
+ const chat = sessionId;
20174
+ changeChatFiles(chat, { deleting: true });
19266
20175
  try {
19267
- await adapter.deleteWorkspaceFile({ sessionId, path });
19268
- if (selectedPath === path) {
20176
+ await adapter.deleteWorkspaceFile({
20177
+ sessionId: chat,
20178
+ path,
20179
+ onWaiting: (said) => changeChatFiles(chat, { notice: said })
20180
+ });
20181
+ if (mountedRef.current && sessionIdRef.current === chat && selectedPath === path) {
19269
20182
  onSelectedPathChange(null);
19270
20183
  }
19271
- setNotice(`Deleted ${path.split("/").pop() ?? path}`);
20184
+ changeChatFiles(chat, { notice: `Deleted ${path.split("/").pop() ?? path}` });
19272
20185
  await fetchTree();
19273
- } catch (error) {
19274
- setNotice(error.message);
20186
+ } catch (error2) {
20187
+ changeChatFiles(chat, { notice: error2.message });
19275
20188
  } finally {
19276
- setDeleteTarget(null);
20189
+ changeChatFiles(chat, { deleteTarget: null, deleting: false });
19277
20190
  }
19278
20191
  },
19279
20192
  [
@@ -19331,12 +20244,12 @@ function WorkspacePanel({
19331
20244
  variant: "ghost",
19332
20245
  size: "icon-sm",
19333
20246
  onClick: () => void fetchTree(),
19334
- disabled: !sessionId || treeLoading,
20247
+ disabled: !sessionId || loading,
19335
20248
  "aria-label": "Refresh workspace",
19336
20249
  children: /* @__PURE__ */ jsxRuntime.jsx(
19337
20250
  lucideReact.RefreshCwIcon,
19338
20251
  {
19339
- className: cn("size-4", treeLoading && "animate-spin")
20252
+ className: cn("size-4", loading && "animate-spin")
19340
20253
  }
19341
20254
  )
19342
20255
  }
@@ -19344,9 +20257,26 @@ function WorkspacePanel({
19344
20257
  /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "bottom", children: "Refresh" })
19345
20258
  ] })
19346
20259
  ] }),
19347
- notice && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-line px-3 py-2 text-xs text-muted-foreground", children: notice }),
20260
+ notice && /* @__PURE__ */ jsxRuntime.jsx(
20261
+ "div",
20262
+ {
20263
+ "data-testid": "workspace-notice",
20264
+ className: "border-b border-line px-3 py-2 text-xs text-muted-foreground",
20265
+ children: notice
20266
+ }
20267
+ ),
20268
+ truncated && entries.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-line px-3 py-2 text-xs text-muted-foreground", children: "Some files are not shown." }),
19348
20269
  /* @__PURE__ */ jsxRuntime.jsx(ScrollArea, { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-1 py-1", children: [
19349
- treeLoading && entries.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1 p-2", children: Array.from({ length: 8 }).map((_, index) => /* @__PURE__ */ jsxRuntime.jsx(
20270
+ waiting && /* @__PURE__ */ jsxRuntime.jsx(
20271
+ "div",
20272
+ {
20273
+ role: "status",
20274
+ "data-testid": "tree-waiting",
20275
+ className: "px-3 py-2 text-sm text-muted-foreground",
20276
+ children: waiting
20277
+ }
20278
+ ),
20279
+ loading && !waiting && entries.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1 p-2", children: Array.from({ length: 8 }).map((_, index) => /* @__PURE__ */ jsxRuntime.jsx(
19350
20280
  Skeleton,
19351
20281
  {
19352
20282
  className: "h-5 rounded",
@@ -19356,11 +20286,11 @@ function WorkspacePanel({
19356
20286
  },
19357
20287
  index
19358
20288
  )) }),
19359
- treeError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 p-3 text-sm text-destructive", children: [
20289
+ error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 p-3 text-sm text-destructive", children: [
19360
20290
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircleIcon, { className: "mt-0.5 size-4 shrink-0" }),
19361
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: treeError })
20291
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: error })
19362
20292
  ] }),
19363
- !treeLoading && !treeError && entries.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-8 text-center text-sm text-faint", children: [
20293
+ !loading && !error && entries.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-8 text-center text-sm text-faint", children: [
19364
20294
  /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No workspace files" }),
19365
20295
  /* @__PURE__ */ jsxRuntime.jsxs(
19366
20296
  Button,
@@ -19389,7 +20319,9 @@ function WorkspacePanel({
19389
20319
  RenderEntries,
19390
20320
  {
19391
20321
  entries,
19392
- onDelete: setDeleteTarget,
20322
+ onDelete: (path) => {
20323
+ if (sessionId) changeChatFiles(sessionId, { deleteTarget: path });
20324
+ },
19393
20325
  downloadUrlFor: (path) => sessionId ? adapter.getWorkspaceDownloadUrl({ sessionId, path }) : "#"
19394
20326
  }
19395
20327
  )
@@ -19403,8 +20335,13 @@ function WorkspacePanel({
19403
20335
  title: "Delete file?",
19404
20336
  description: `This will permanently delete ${deleteTarget?.split("/").pop() ?? "this file"} from the workspace.`,
19405
20337
  confirmLabel: "Delete",
19406
- onConfirm: () => deleteTarget ? handleDelete(deleteTarget) : Promise.resolve(),
19407
- onCancel: () => setDeleteTarget(null)
20338
+ loading: deleting,
20339
+ onConfirm: () => {
20340
+ if (deleteTarget) void handleDelete(deleteTarget);
20341
+ },
20342
+ onCancel: () => {
20343
+ if (sessionId) changeChatFiles(sessionId, { deleteTarget: null });
20344
+ }
19408
20345
  }
19409
20346
  )
19410
20347
  ]
@@ -19417,18 +20354,10 @@ function ConfirmDialog2({
19417
20354
  title,
19418
20355
  description,
19419
20356
  confirmLabel,
20357
+ loading,
19420
20358
  onConfirm,
19421
20359
  onCancel
19422
20360
  }) {
19423
- const [loading, setLoading] = react.useState(false);
19424
- const handleConfirm = async () => {
19425
- setLoading(true);
19426
- try {
19427
- await onConfirm();
19428
- } finally {
19429
- setLoading(false);
19430
- }
19431
- };
19432
20361
  return /* @__PURE__ */ jsxRuntime.jsx(
19433
20362
  Dialog,
19434
20363
  {
@@ -19448,7 +20377,7 @@ function ConfirmDialog2({
19448
20377
  {
19449
20378
  variant: "destructive",
19450
20379
  disabled: loading,
19451
- onClick: handleConfirm,
20380
+ onClick: onConfirm,
19452
20381
  children: [
19453
20382
  loading && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2Icon, { className: "mr-1.5 size-3.5 animate-spin" }),
19454
20383
  confirmLabel
@@ -19465,6 +20394,10 @@ function ConfirmDialog2({
19465
20394
  init_utils();
19466
20395
 
19467
20396
  // src/lib/sessions.ts
20397
+ function sessionComputer(config) {
20398
+ const execution = config?.execution;
20399
+ return execution?.kind === "device" && typeof execution.device_name === "string" ? execution.device_name : null;
20400
+ }
19468
20401
  function isScheduledRunSession(session) {
19469
20402
  return Boolean(
19470
20403
  session?.channel === "scheduled" || session?.config?.scheduled_session_id
@@ -19525,7 +20458,11 @@ function AgentChat({
19525
20458
  goalsEnabled = true,
19526
20459
  compressEnabled = true,
19527
20460
  onOpenIntegrations,
19528
- onOpenBilling
20461
+ onOpenBilling,
20462
+ composerFooter,
20463
+ firstMessage,
20464
+ onFirstMessageSent,
20465
+ computerBrowser
19529
20466
  }) {
19530
20467
  const [workspacePath, setWorkspacePath] = react.useState(null);
19531
20468
  const [previewPath, setPreviewPath] = react.useState(null);
@@ -19538,6 +20475,9 @@ function AgentChat({
19538
20475
  sessionId,
19539
20476
  onSessionChange
19540
20477
  });
20478
+ const sessionConfig = runtime.session?.config;
20479
+ const projectId = sessionConfig?.workstream_role === "coordinator" && typeof sessionConfig.workstream_id === "string" ? sessionConfig.workstream_id : null;
20480
+ const threadRows = useProjectThreads(adapter, projectId);
19541
20481
  react.useEffect(() => {
19542
20482
  setOpenPane(null);
19543
20483
  setFilesOpen(false);
@@ -19565,13 +20505,14 @@ function AgentChat({
19565
20505
  const browserState = runtime.state.browser;
19566
20506
  const browserAvailable = browserState !== null && browserState.status !== "closed" && !!sessionId;
19567
20507
  const browserVisible = browserAvailable && openPane === "browser";
20508
+ const browserOnComputer = browserState?.computer === true;
19568
20509
  const workspaceAvailable = !!sessionId;
19569
20510
  const filePreviewVisible = workspaceAvailable && previewPath !== null && openPane === "file";
19570
20511
  const rightStackVisible = browserVisible || filePreviewVisible;
19571
20512
  const browserPreview = useBrowserPreview({
19572
20513
  adapter,
19573
20514
  sessionId,
19574
- enabled: browserAvailable
20515
+ enabled: browserAvailable && !browserOnComputer
19575
20516
  });
19576
20517
  const browserRunning = browserAvailable;
19577
20518
  react.useEffect(() => {
@@ -19582,6 +20523,46 @@ function AgentChat({
19582
20523
  react.useEffect(() => {
19583
20524
  onMessagesChange?.(runtime.messages);
19584
20525
  }, [onMessagesChange, runtime.messages]);
20526
+ const firstSent = react.useRef(null);
20527
+ const first = react.useRef({ message: firstMessage ?? null, told: onFirstMessageSent });
20528
+ react.useEffect(() => {
20529
+ first.current = { message: firstMessage ?? null, told: onFirstMessageSent };
20530
+ });
20531
+ const send = runtime.send;
20532
+ react.useEffect(() => {
20533
+ if (!firstMessage || firstSent.current === firstMessage.id) return;
20534
+ if (sessionId !== null) {
20535
+ firstSent.current = firstMessage.id;
20536
+ first.current.told?.(firstMessage.id, "Another chat opened before this one was made, so nothing was sent.");
20537
+ return;
20538
+ }
20539
+ if (effectiveDisabled) return;
20540
+ let live = true;
20541
+ queueMicrotask(() => {
20542
+ if (!live || firstSent.current === firstMessage.id) return;
20543
+ firstSent.current = firstMessage.id;
20544
+ send(firstMessage.text).then(
20545
+ () => first.current.told?.(firstMessage.id, null),
20546
+ (error) => first.current.told?.(firstMessage.id, error instanceof Error ? error.message : String(error))
20547
+ );
20548
+ });
20549
+ return () => {
20550
+ live = false;
20551
+ };
20552
+ }, [firstMessage, sessionId, effectiveDisabled, send]);
20553
+ const mounted = react.useRef(false);
20554
+ react.useEffect(() => {
20555
+ mounted.current = true;
20556
+ return () => {
20557
+ mounted.current = false;
20558
+ queueMicrotask(() => {
20559
+ const { message, told } = first.current;
20560
+ if (mounted.current || !message || firstSent.current === message.id) return;
20561
+ firstSent.current = message.id;
20562
+ told?.(message.id, "The page left the new chat before it was made, so nothing was sent.");
20563
+ });
20564
+ };
20565
+ }, []);
19585
20566
  const handleOpenFilePreview = react.useCallback((path) => {
19586
20567
  setWorkspacePath(path);
19587
20568
  setPreviewPath(path);
@@ -19621,7 +20602,11 @@ function AgentChat({
19621
20602
  adapter,
19622
20603
  sessionId,
19623
20604
  onFileSelect: handleFileSelect,
19624
- onOpenBilling
20605
+ onOpenBilling,
20606
+ onOpenSession: onSessionChange,
20607
+ projectId,
20608
+ threadRows,
20609
+ readOnly: disabled === true
19625
20610
  },
19626
20611
  children: /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { children: /* @__PURE__ */ jsxRuntime.jsxs(
19627
20612
  "section",
@@ -19703,7 +20688,7 @@ function AgentChat({
19703
20688
  isLoadingHistory: runtime.isLoadingHistory,
19704
20689
  onSend: (content, images, attachments) => runtime.send(content, images, attachments),
19705
20690
  onStop: () => runtime.stop(),
19706
- onRetry: runtime.retry,
20691
+ onRetry: disabled ? void 0 : runtime.retry,
19707
20692
  onFileSelect: handleFileSelect,
19708
20693
  disabled: effectiveDisabled,
19709
20694
  disabledReason,
@@ -19721,8 +20706,9 @@ function AgentChat({
19721
20706
  canShowBrowser: browserAvailable,
19722
20707
  canShowWorkspace: workspaceAvailable,
19723
20708
  paneCards: {
19724
- browser: browserRunning ? {
19725
- subtitle: browserState?.controlOwner ? `${browserState.controlOwner} has control` : void 0,
20709
+ // A chat the host only reads offers its browser no card: the card is the way to take it over.
20710
+ browser: browserRunning && !disabled ? {
20711
+ subtitle: browserOnComputer ? browserState?.status === "unavailable" ? "No supported browser on its computer" : "On the chat's computer" : browserState?.controlOwner ? `${browserState.controlOwner} has control` : void 0,
19726
20712
  thumbnail: browserPreview,
19727
20713
  // Show and hide on the same card, like the Files
19728
20714
  // accordion header and the composer's Tools item.
@@ -19758,7 +20744,9 @@ function AgentChat({
19758
20744
  researchSources: runtime.researchSources,
19759
20745
  hideTurnSummary,
19760
20746
  agentId,
19761
- onOpenIntegrations
20747
+ onOpenIntegrations,
20748
+ deviceWait: runtime.state.deviceWait,
20749
+ composerFooter
19762
20750
  }
19763
20751
  )
19764
20752
  }
@@ -19784,13 +20772,21 @@ function AgentChat({
19784
20772
  "data-testid": "browser-panel",
19785
20773
  "data-mobile-view": activeMobilePane,
19786
20774
  className: "min-h-0 h-full w-full overflow-hidden",
19787
- children: /* @__PURE__ */ jsxRuntime.jsx(
20775
+ children: browserOnComputer ? /* @__PURE__ */ jsxRuntime.jsx(
20776
+ ComputerBrowserPane,
20777
+ {
20778
+ available: browserState.status !== "unavailable",
20779
+ readOnly: disabled === true,
20780
+ draw: computerBrowser
20781
+ }
20782
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
19788
20783
  BrowserPane,
19789
20784
  {
19790
20785
  sessionId,
19791
20786
  state: browserState,
19792
20787
  adapter,
19793
- onClose: () => setOpenPane(null)
20788
+ onClose: () => setOpenPane(null),
20789
+ readOnly: disabled === true
19794
20790
  }
19795
20791
  )
19796
20792
  }
@@ -19895,9 +20891,9 @@ var FetchSseEventStream = class {
19895
20891
  queueMicrotask(() => void this.start());
19896
20892
  }
19897
20893
  addEventListener(type, listener) {
19898
- const listeners = this.listeners.get(type) ?? /* @__PURE__ */ new Set();
19899
- listeners.add(listener);
19900
- this.listeners.set(type, listeners);
20894
+ const listeners2 = this.listeners.get(type) ?? /* @__PURE__ */ new Set();
20895
+ listeners2.add(listener);
20896
+ this.listeners.set(type, listeners2);
19901
20897
  }
19902
20898
  close() {
19903
20899
  this.watchdog.stop();
@@ -19974,13 +20970,13 @@ var FetchSseEventStream = class {
19974
20970
  this.dataLines = [];
19975
20971
  return;
19976
20972
  }
19977
- const listeners = this.listeners.get(this.eventType);
20973
+ const listeners2 = this.listeners.get(this.eventType);
19978
20974
  const event = {
19979
20975
  data: this.dataLines.join("\n"),
19980
20976
  lastEventId: this.eventId
19981
20977
  };
19982
- if (listeners) {
19983
- for (const listener of listeners) {
20978
+ if (listeners2) {
20979
+ for (const listener of listeners2) {
19984
20980
  listener(event);
19985
20981
  }
19986
20982
  }
@@ -21383,218 +22379,6 @@ function CardContent({ className, ...props }) {
21383
22379
  }
21384
22380
  );
21385
22381
  }
21386
-
21387
- // src/components/missions/mission-derive.ts
21388
- var ACTIVE_MISSION_STATUSES = /* @__PURE__ */ new Set(["active", "paused"]);
21389
- function isTerminalMissionStatus(status) {
21390
- return !ACTIVE_MISSION_STATUSES.has(status);
21391
- }
21392
- function missionTaskBucket(status) {
21393
- if (status === "done") return "done";
21394
- if (status === "blocked") return "blocked";
21395
- if (status === "failed" || status === "cancelled") {
21396
- return "failed_or_cancelled";
21397
- }
21398
- return "in_flight";
21399
- }
21400
- function groupMissionTasksByBucket(tasks) {
21401
- const buckets = {
21402
- in_flight: [],
21403
- done: [],
21404
- blocked: [],
21405
- failed_or_cancelled: []
21406
- };
21407
- for (const t of tasks) {
21408
- buckets[missionTaskBucket(t.status)].push(t);
21409
- }
21410
- return buckets;
21411
- }
21412
- function deriveMissionWorkerActivityLabel(worker) {
21413
- const kind = worker.latestEventKind;
21414
- if (kind === "tool.call" && worker.latestEventSummary) {
21415
- return `tool: ${truncate4(worker.latestEventSummary, 80)}`;
21416
- }
21417
- if (kind === "llm.response") {
21418
- return "thinking";
21419
- }
21420
- if (kind === "tool.result") {
21421
- return "received tool result";
21422
- }
21423
- if (kind && worker.latestEventSummary) {
21424
- return `${kind}: ${truncate4(worker.latestEventSummary, 60)}`;
21425
- }
21426
- return worker.sessionStatus || "active";
21427
- }
21428
- function truncate4(s, max) {
21429
- if (s.length <= max) return s;
21430
- return `${s.slice(0, max - 1)}\u2026`;
21431
- }
21432
- var MISSION_EVENT_CATEGORIES = [
21433
- "spawn",
21434
- "output",
21435
- "done",
21436
- "verdict",
21437
- "system"
21438
- ];
21439
- function missionEventCategory(type) {
21440
- switch (type) {
21441
- case "worker.spawned":
21442
- return "spawn";
21443
- case "iteration.summary":
21444
- return "output";
21445
- case "worker.complete":
21446
- return "done";
21447
- case "mission.evaluation.end":
21448
- return "verdict";
21449
- default:
21450
- return "system";
21451
- }
21452
- }
21453
- function dataString(data, key) {
21454
- const v = data?.[key];
21455
- return typeof v === "string" && v.trim().length > 0 ? v.trim() : null;
21456
- }
21457
- var CONTROL_MARKUP_RE = /<next_action\b[^>]*(?:\/>|>[\s\S]*?<\/next_action>)/gi;
21458
- function stripMissionControlMarkup(text) {
21459
- return text.replace(CONTROL_MARKUP_RE, "").trim();
21460
- }
21461
- function missionEventSummary(event) {
21462
- return stripMissionControlMarkup(rawMissionEventSummary(event));
21463
- }
21464
- function rawMissionEventSummary(event) {
21465
- const d = event.data ?? null;
21466
- switch (event.type) {
21467
- case "worker.spawned":
21468
- return dataString(d, "goal") ?? "Task spawned";
21469
- case "worker.complete":
21470
- return dataString(d, "result") ?? "Worker complete";
21471
- case "iteration.summary":
21472
- return dataString(d, "summary") ?? "";
21473
- case "mission.evaluation.end": {
21474
- const result = dataString(d, "result");
21475
- const feedback = dataString(d, "feedback") ?? dataString(d, "explanation");
21476
- if (result && feedback) return `${result} \u2014 ${feedback}`;
21477
- return result ?? feedback ?? "Evaluation finished";
21478
- }
21479
- case "task.blocked":
21480
- return dataString(d, "reason") ?? "Task blocked";
21481
- case "task.failed":
21482
- return "Task failed";
21483
- default: {
21484
- const text = dataString(d, "summary") ?? dataString(d, "text") ?? dataString(d, "message") ?? dataString(d, "reason");
21485
- if (text) return text;
21486
- if (!d) return "";
21487
- try {
21488
- return truncate4(JSON.stringify(d), 160);
21489
- } catch {
21490
- return "";
21491
- }
21492
- }
21493
- }
21494
- }
21495
- function missionEventTaskId(event, sessions) {
21496
- return dataString(event.data ?? null, "task_id") ?? sessions[event.sessionId]?.taskId ?? null;
21497
- }
21498
- function missionEventActorLabel(event, sessions) {
21499
- const meta = sessions[event.sessionId];
21500
- if (!meta) return event.sessionId.slice(0, 8);
21501
- if (meta.kind === "coordinator") return "orchestrator";
21502
- return meta.agentDefName ?? "worker";
21503
- }
21504
- function missionTaskBlocks(tasks) {
21505
- const blocks = {};
21506
- for (const t of tasks) {
21507
- for (const parent of t.parentIds) {
21508
- (blocks[parent] ?? (blocks[parent] = [])).push(t.id);
21509
- }
21510
- }
21511
- return blocks;
21512
- }
21513
- function missionTaskTitle(goal, max = 80) {
21514
- const line = goal.split("\n").find((l) => l.trim().length > 0)?.trim() ?? goal.trim();
21515
- return truncate4(line.replace(/\s+/g, " "), max);
21516
- }
21517
- var RAIL_GROUP_DEFS = [
21518
- { key: "running", label: "Running", match: (s) => s === "running" },
21519
- { key: "blocked", label: "Blocked", match: (s) => s === "blocked" },
21520
- {
21521
- key: "queued",
21522
- label: "Queued",
21523
- match: (s) => s === "ready" || s === "todo"
21524
- },
21525
- { key: "done", label: "Done", match: (s) => s === "done" },
21526
- {
21527
- key: "failed",
21528
- label: "Failed / cancelled",
21529
- match: (s) => s === "failed" || s === "cancelled"
21530
- }
21531
- ];
21532
- function missionTaskRailGroups(tasks) {
21533
- const grouped = /* @__PURE__ */ new Set();
21534
- const groups = RAIL_GROUP_DEFS.map((def) => ({
21535
- key: def.key,
21536
- label: def.label,
21537
- tasks: tasks.filter((t) => {
21538
- const hit = def.match(t.status);
21539
- if (hit) grouped.add(t.id);
21540
- return hit;
21541
- })
21542
- }));
21543
- const stray = tasks.filter((t) => !grouped.has(t.id));
21544
- if (stray.length > 0) {
21545
- const queued = groups.find((g) => g.key === "queued");
21546
- if (queued) queued.tasks.push(...stray);
21547
- }
21548
- return groups.filter((g) => g.tasks.length > 0);
21549
- }
21550
- var TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
21551
- function defaultSelectedMissionTaskId(tasks) {
21552
- if (tasks.length === 0) return null;
21553
- const ordered = missionTaskRailGroups(tasks).flatMap((g) => g.tasks);
21554
- const firstLive = ordered.find(
21555
- (t) => !TERMINAL_TASK_STATUSES.has(t.status)
21556
- );
21557
- return (firstLive ?? ordered[0] ?? tasks[0]).id;
21558
- }
21559
- function mergeMissionEvents(existing, incoming) {
21560
- if (incoming.length === 0) return existing;
21561
- const byId = /* @__PURE__ */ new Map();
21562
- for (const e of existing) byId.set(e.id, e);
21563
- for (const e of incoming) byId.set(e.id, e);
21564
- return [...byId.values()].sort((a, b) => a.id - b.id);
21565
- }
21566
- function missionTaskStatusDotClass(status) {
21567
- switch (status) {
21568
- case "running":
21569
- return "bg-primary";
21570
- case "blocked":
21571
- return "bg-amber-500";
21572
- case "done":
21573
- return "bg-emerald-500";
21574
- case "failed":
21575
- return "bg-destructive";
21576
- case "cancelled":
21577
- return "bg-muted-foreground";
21578
- default:
21579
- return "bg-foreground/30";
21580
- }
21581
- }
21582
- function missionWorkerTaskCounts(worker, tasks) {
21583
- if (worker.kind !== "task") return null;
21584
- const mine = tasks.filter((t) => t.agentDefName === worker.agentDefName);
21585
- return {
21586
- completed: mine.filter((t) => t.status === "done").length,
21587
- inFlight: mine.filter(
21588
- (t) => t.status === "running" || t.status === "blocked"
21589
- ).length
21590
- };
21591
- }
21592
- function formatMissionTimestamp(iso) {
21593
- if (!iso) return "";
21594
- const parsed = new Date(iso);
21595
- if (Number.isNaN(parsed.getTime())) return iso;
21596
- return parsed.toLocaleTimeString();
21597
- }
21598
22382
  var CATEGORY_BADGE_CLASS = {
21599
22383
  spawn: "border-primary/30 bg-primary/10 text-primary",
21600
22384
  output: "border-foreground/20 bg-foreground/5 text-foreground/80",
@@ -22247,7 +23031,7 @@ function MissionWorkersTab({
22247
23031
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "py-8 text-center text-sm text-muted-foreground/60", children: "No workers attached to this mission right now." });
22248
23032
  }
22249
23033
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: workers.map((w) => /* @__PURE__ */ jsxRuntime.jsx(
22250
- WorkerCard,
23034
+ WorkerCard2,
22251
23035
  {
22252
23036
  worker: w,
22253
23037
  tasks,
@@ -22256,7 +23040,7 @@ function MissionWorkersTab({
22256
23040
  w.workerSessionId
22257
23041
  )) });
22258
23042
  }
22259
- function WorkerCard({
23043
+ function WorkerCard2({
22260
23044
  worker,
22261
23045
  tasks,
22262
23046
  onOpenTranscript
@@ -23444,6 +24228,7 @@ function sessionToTreeNode(session) {
23444
24228
  model: session.model,
23445
24229
  messageCount: session.messageCount,
23446
24230
  toolCallCount: session.toolCallCount,
24231
+ computer: sessionComputer(session.config),
23447
24232
  createdAt: session.createdAt ?? timestamp,
23448
24233
  updatedAt: session.updatedAt ?? timestamp
23449
24234
  };
@@ -23470,13 +24255,13 @@ function mergeTreeNodes(groups) {
23470
24255
  }
23471
24256
  function collectDeletedSessionIds(nodes, deletedSessionId) {
23472
24257
  const deletedIds = /* @__PURE__ */ new Set([deletedSessionId]);
23473
- let changed = true;
23474
- while (changed) {
23475
- changed = false;
24258
+ let changed2 = true;
24259
+ while (changed2) {
24260
+ changed2 = false;
23476
24261
  for (const node of nodes) {
23477
24262
  if (node.parentId && deletedIds.has(node.parentId) && !deletedIds.has(node.id)) {
23478
24263
  deletedIds.add(node.id);
23479
- changed = true;
24264
+ changed2 = true;
23480
24265
  }
23481
24266
  }
23482
24267
  }
@@ -23572,7 +24357,15 @@ function TreeNodeRow({
23572
24357
  ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-4 h-4 shrink-0" }),
23573
24358
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
23574
24359
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm truncate", children: title }),
23575
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-faint truncate", children: subtitle })
24360
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 text-xs text-faint min-w-0", children: [
24361
+ entry.computer && // A chat on a folder of the user's computer, in every list of chats, the browser's too.
24362
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-testid": "session-computer", title: `On ${entry.computer}`, className: "inline-flex min-w-0 max-w-[50%] items-center gap-1", children: [
24363
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LaptopIcon, { className: "w-3 h-3 shrink-0", "aria-hidden": "true" }),
24364
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: entry.computer })
24365
+ ] }),
24366
+ entry.computer && subtitle && /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\xB7" }),
24367
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: subtitle })
24368
+ ] })
23576
24369
  ] }),
23577
24370
  isRunning && canStop && isChildSession && /* @__PURE__ */ jsxRuntime.jsx(
23578
24371
  "button",
@@ -23669,9 +24462,10 @@ function SessionTreePanel({
23669
24462
  }
23670
24463
  const sessionList = listResult.status === "fulfilled" ? listResult.value : null;
23671
24464
  const sessionTree = treeResult.status === "fulfilled" ? treeResult.value : null;
24465
+ const project = sessionTree?.nodes.some((node) => node.parentId === null && node.runKind === "project");
23672
24466
  let nextNodes = mergeTreeNodes([
23673
24467
  sessionList?.sessions.map(sessionToTreeNode) ?? [],
23674
- sessionTree?.nodes ?? []
24468
+ project ? [] : sessionTree?.nodes ?? []
23675
24469
  ]);
23676
24470
  if (pendingDeleteIds.current.size > 0) {
23677
24471
  const stillReturned = /* @__PURE__ */ new Set();
@@ -23851,6 +24645,7 @@ exports.WhiteboardSurface = WhiteboardSurface;
23851
24645
  exports.defaultSelectedMissionTaskId = defaultSelectedMissionTaskId;
23852
24646
  exports.deriveMissionWorkerActivityLabel = deriveMissionWorkerActivityLabel;
23853
24647
  exports.deriveOrbActivity = deriveOrbActivity;
24648
+ exports.forgetChatFiles = forgetChatFiles;
23854
24649
  exports.formatMissionTimestamp = formatMissionTimestamp;
23855
24650
  exports.groupMissionTasksByBucket = groupMissionTasksByBucket;
23856
24651
  exports.isHiddenSimpleTool = isHiddenSimpleTool;
@@ -23868,6 +24663,7 @@ exports.missionTaskStatusDotClass = missionTaskStatusDotClass;
23868
24663
  exports.missionTaskTitle = missionTaskTitle;
23869
24664
  exports.missionWorkerTaskCounts = missionWorkerTaskCounts;
23870
24665
  exports.persistChatViewMode = persistChatViewMode;
24666
+ exports.sessionComputer = sessionComputer;
23871
24667
  exports.splitCitations = splitCitations;
23872
24668
  exports.stripMissionControlMarkup = stripMissionControlMarkup;
23873
24669
  exports.toolOrbState = toolOrbState;