@hachej/boring-agent 0.1.30 → 0.1.32

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.
@@ -3,13 +3,19 @@ import {
3
3
  WORKSPACE_AGENT_PLUGINS_RELOADED_EVENT,
4
4
  extractToolUiMetadata
5
5
  } from "../chunk-B5JECXMG.js";
6
+ import {
7
+ sanitizeUiMessage,
8
+ sanitizeUiMessages,
9
+ uiMessageContentKey
10
+ } from "../chunk-IDRC4SFU.js";
6
11
  import {
7
12
  ErrorCode
8
13
  } from "../chunk-UBWQ5LT4.js";
9
14
  import {
10
15
  DebugDrawer,
11
- cn
12
- } from "../chunk-MMJA3QON.js";
16
+ cn,
17
+ copyTextToClipboard
18
+ } from "../chunk-UTUXVT2K.js";
13
19
 
14
20
  // src/front/upload/uploadFile.ts
15
21
  function readAsDataUrl(file) {
@@ -50,7 +56,7 @@ async function uploadFile(file, opts = {}) {
50
56
  // src/front/ChatPanel.tsx
51
57
  import { isToolUIPart as isToolUIPart2 } from "ai";
52
58
  import { motion as motion2 } from "motion/react";
53
- import { lazy, Suspense, useCallback as useCallback16, useEffect as useEffect18, useMemo as useMemo12, useRef as useRef13, useState as useState20 } from "react";
59
+ import { lazy, Suspense, useCallback as useCallback17, useEffect as useEffect18, useMemo as useMemo12, useRef as useRef14, useState as useState20 } from "react";
54
60
 
55
61
  // src/front/primitives/mention-picker.tsx
56
62
  import { useEffect as useEffect2, useRef as useRef2, useState, useCallback } from "react";
@@ -256,12 +262,164 @@ function SlashCommandPicker({ query, commands, onSelect, onDismiss }) {
256
262
  // src/front/hooks/useAgentChat.ts
257
263
  import { useChat } from "@ai-sdk/react";
258
264
  import { DefaultChatTransport } from "ai";
259
- import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState3 } from "react";
265
+ import { useCallback as useCallback3, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState3 } from "react";
266
+ function mergeMessages(base, tail, opts) {
267
+ const seenIds = /* @__PURE__ */ new Set();
268
+ const seenContent = /* @__PURE__ */ new Set();
269
+ const stableContent = /* @__PURE__ */ new Set();
270
+ const pendingUserContent = /* @__PURE__ */ new Map();
271
+ const merged = [];
272
+ for (const rawMessage of [...base, ...tail]) {
273
+ const message = sanitizeUiMessage(rawMessage);
274
+ const id = typeof message.id === "string" ? message.id : void 0;
275
+ const contentKey = uiMessageContentKey(message);
276
+ const pendingUser = id?.startsWith("pending-user:") === true;
277
+ if (id) {
278
+ if (seenIds.has(id)) continue;
279
+ if (pendingUser && (seenContent.has(contentKey) || opts?.dedupePendingAgainstStable !== false && stableContent.has(contentKey))) continue;
280
+ if (!pendingUser) {
281
+ const pendingIndex = pendingUserContent.get(contentKey);
282
+ if (pendingIndex !== void 0) {
283
+ merged[pendingIndex] = message;
284
+ seenIds.add(id);
285
+ seenContent.add(contentKey);
286
+ pendingUserContent.delete(contentKey);
287
+ continue;
288
+ }
289
+ }
290
+ seenIds.add(id);
291
+ if (pendingUser) {
292
+ pendingUserContent.set(contentKey, merged.length);
293
+ } else {
294
+ stableContent.add(contentKey);
295
+ }
296
+ } else {
297
+ if (seenContent.has(contentKey)) continue;
298
+ seenContent.add(contentKey);
299
+ }
300
+ merged.push(message);
301
+ }
302
+ return sanitizeUiMessages(merged);
303
+ }
304
+ function sameMessageOrder(a, b) {
305
+ if (a.length !== b.length) return false;
306
+ return a.every((message, index) => {
307
+ const other = b[index];
308
+ return message.id === other?.id && JSON.stringify(message.parts ?? []) === JSON.stringify(other?.parts ?? []);
309
+ });
310
+ }
311
+ function sameMessageIdentityOrContent(a, b) {
312
+ if (!a || !b) return false;
313
+ if (typeof a.id === "string" && typeof b.id === "string" && a.id === b.id) return true;
314
+ return uiMessageContentKey(a) === uiMessageContentKey(b);
315
+ }
316
+ function mergeHydratedMessages(serverMessages, cachedMessages) {
317
+ if (serverMessages.length === 0) return mergeMessages([], cachedMessages);
318
+ if (cachedMessages.length === 0) return mergeMessages([], serverMessages);
319
+ let commonPrefix = 0;
320
+ while (commonPrefix < serverMessages.length && commonPrefix < cachedMessages.length && sameMessageIdentityOrContent(serverMessages[commonPrefix], cachedMessages[commonPrefix])) {
321
+ commonPrefix += 1;
322
+ }
323
+ if (commonPrefix > 0 && commonPrefix < cachedMessages.length && commonPrefix < serverMessages.length) {
324
+ return mergeMessages(serverMessages.slice(0, commonPrefix), [
325
+ ...cachedMessages.slice(commonPrefix),
326
+ ...serverMessages.slice(commonPrefix)
327
+ ]);
328
+ }
329
+ const firstCached = cachedMessages[0];
330
+ const serverHasUser = serverMessages.some((message) => message.role === "user");
331
+ if (commonPrefix === 0 && !serverHasUser && firstCached?.role === "user" && typeof firstCached.id === "string" && firstCached.id.startsWith("pending-user:") && serverMessages[0]?.role === "assistant") {
332
+ return mergeMessages([], [...cachedMessages, ...serverMessages]);
333
+ }
334
+ return mergeMessages(serverMessages, cachedMessages);
335
+ }
336
+ function messagesLookSettled(messages) {
337
+ const last = messages[messages.length - 1];
338
+ if (last?.role !== "assistant") return false;
339
+ return !last.parts?.some((part) => {
340
+ const candidate = part;
341
+ return typeof candidate.type === "string" && candidate.type.startsWith("tool-") && candidate.state !== "output-available" && candidate.state !== "output-error" && candidate.state !== "output-denied" && candidate.state !== "approval-responded";
342
+ });
343
+ }
344
+ function readCachedMessages(cacheKey) {
345
+ try {
346
+ const cached = globalThis.localStorage?.getItem(cacheKey);
347
+ if (!cached) return [];
348
+ const parsed = JSON.parse(cached);
349
+ return Array.isArray(parsed) ? parsed : [];
350
+ } catch {
351
+ return [];
352
+ }
353
+ }
354
+ function messagesNeedResume(cachedMessages) {
355
+ const last = cachedMessages[cachedMessages.length - 1];
356
+ if (!last) return false;
357
+ if (last.role === "user") return true;
358
+ return Boolean(last.parts?.some((part) => {
359
+ const candidate = part;
360
+ if (typeof candidate.type === "string" && candidate.type.startsWith("tool-")) {
361
+ return candidate.state !== "output-available" && candidate.state !== "output-error" && candidate.state !== "output-denied" && candidate.state !== "approval-responded";
362
+ }
363
+ return false;
364
+ }));
365
+ }
366
+ function cachedMessagesNeedResume(cacheKey) {
367
+ return messagesNeedResume(readCachedMessages(cacheKey));
368
+ }
369
+ function cachedStatusNeedsResume(statusKey) {
370
+ try {
371
+ return globalThis.localStorage?.getItem(statusKey) === "active";
372
+ } catch {
373
+ return false;
374
+ }
375
+ }
376
+ function createClientTurnId() {
377
+ return `turn:${Date.now()}:${Math.random().toString(36).slice(2)}`;
378
+ }
379
+ function turnIdFromDataPart(part) {
380
+ const candidate = part;
381
+ if (candidate?.type !== "data-turn-start") return null;
382
+ return typeof candidate.data?.turnId === "string" && candidate.data.turnId.length > 0 ? candidate.data.turnId : null;
383
+ }
384
+ function optimisticUserMessageFromSendArgs(args) {
385
+ const input = args[0];
386
+ const text = typeof input?.text === "string" ? input.text : "";
387
+ if (!text.trim()) return null;
388
+ return {
389
+ id: `pending-user:${Date.now()}:${Math.random().toString(36).slice(2)}`,
390
+ role: "user",
391
+ parts: [{ type: "text", text }]
392
+ };
393
+ }
394
+ function writeCachedMessages(cacheKey, messages) {
395
+ try {
396
+ globalThis.localStorage?.setItem(cacheKey, JSON.stringify(messages));
397
+ } catch {
398
+ }
399
+ }
400
+ function clearCachedMessages(cacheKey) {
401
+ try {
402
+ globalThis.localStorage?.removeItem?.(cacheKey);
403
+ } catch {
404
+ }
405
+ }
260
406
  function useAgentChat(opts) {
261
407
  const { sessionId } = opts;
262
408
  const hydrateMessages = opts.hydrateMessages ?? true;
263
409
  const optsRef = useRef4(opts);
264
410
  optsRef.current = opts;
411
+ const cacheKey = sessionId ? `boring-agent:messages:${sessionId}` : null;
412
+ const statusKey = sessionId ? `boring-agent:status:${sessionId}` : null;
413
+ const [settledResumeKey, setSettledResumeKey] = useState3(null);
414
+ const [localTurnActive, setLocalTurnActive] = useState3(false);
415
+ const localTurnVersionRef = useRef4(0);
416
+ const shouldResume = useMemo2(
417
+ () => hydrateMessages && Boolean(
418
+ cacheKey && !localTurnActive && settledResumeKey !== cacheKey && (cachedMessagesNeedResume(cacheKey) || (statusKey ? cachedStatusNeedsResume(statusKey) : false))
419
+ ),
420
+ [cacheKey, hydrateMessages, localTurnActive, settledResumeKey, statusKey]
421
+ );
422
+ const activeTurnIdRef = useRef4(null);
265
423
  const transport = useMemo2(
266
424
  () => new DefaultChatTransport({
267
425
  api: "/api/v1/agent/chat",
@@ -269,7 +427,8 @@ function useAgentChat(opts) {
269
427
  body: () => ({
270
428
  sessionId: optsRef.current.sessionId,
271
429
  model: optsRef.current.model,
272
- thinkingLevel: optsRef.current.thinkingLevel
430
+ thinkingLevel: optsRef.current.thinkingLevel,
431
+ ...activeTurnIdRef.current ? { clientTurnId: activeTurnIdRef.current } : {}
273
432
  })
274
433
  }),
275
434
  [sessionId]
@@ -277,75 +436,166 @@ function useAgentChat(opts) {
277
436
  const chat = useChat({
278
437
  id: sessionId,
279
438
  transport,
280
- resume: hydrateMessages,
439
+ resume: shouldResume,
281
440
  // Match AI SDK's documented React smoothing knob: render at most every
282
441
  // ~50ms while chunks stream instead of once per incoming chunk. This only
283
442
  // throttles AI SDK's own messages store; pi's custom data-pi projection
284
443
  // does its own matching delta batching in usePiChatProjection.
285
444
  experimental_throttle: 50,
286
445
  onData: (part) => {
446
+ const turnId = turnIdFromDataPart(part);
447
+ if (turnId) activeTurnIdRef.current = turnId;
287
448
  optsRef.current.onData?.(part);
288
449
  }
289
450
  });
451
+ const rawMessages = chat.messages;
452
+ const messages = useMemo2(() => mergeMessages([], rawMessages), [rawMessages]);
453
+ const messagesRef = useRef4(messages);
454
+ messagesRef.current = messages;
455
+ const rawStop = chat.stop;
456
+ const stop = useCallback3(() => {
457
+ rawStop();
458
+ if (!sessionId) return;
459
+ const turnId = activeTurnIdRef.current;
460
+ const suffix = turnId ? `?turnId=${encodeURIComponent(turnId)}` : "";
461
+ fetch(`/api/v1/agent/chat/${encodeURIComponent(sessionId)}/turn${suffix}`, {
462
+ method: "DELETE",
463
+ headers: optsRef.current.requestHeaders
464
+ }).catch(() => {
465
+ });
466
+ }, [rawStop, sessionId]);
467
+ useEffect4(() => {
468
+ setLocalTurnActive(false);
469
+ }, [sessionId]);
470
+ const sendMessage = useCallback3((...args) => {
471
+ activeTurnIdRef.current = createClientTurnId();
472
+ localTurnVersionRef.current += 1;
473
+ setLocalTurnActive(true);
474
+ setSettledResumeKey((current) => current === cacheKey ? null : current);
475
+ if (cacheKey) {
476
+ const optimisticUser = optimisticUserMessageFromSendArgs(args);
477
+ if (optimisticUser) {
478
+ const cachedMessages = readCachedMessages(cacheKey);
479
+ const base = messagesRef.current.length > 0 ? messagesRef.current : cachedMessages;
480
+ const next = mergeMessages(base, [optimisticUser], { dedupePendingAgainstStable: false });
481
+ writeCachedMessages(cacheKey, next);
482
+ }
483
+ }
484
+ if (statusKey) {
485
+ try {
486
+ globalThis.localStorage?.setItem(statusKey, "active");
487
+ } catch {
488
+ }
489
+ }
490
+ return chat.sendMessage(...args);
491
+ }, [cacheKey, chat.sendMessage, statusKey]);
290
492
  const setMessages = chat.setMessages;
291
- const cacheKey = sessionId ? `boring-agent:messages:${sessionId}` : null;
292
- const [hydrated, setHydrated] = useState3(false);
493
+ const [hydratedKey, setHydratedKey] = useState3(null);
494
+ const hydrated = !hydrateMessages || !sessionId || !cacheKey || hydratedKey === cacheKey;
293
495
  useEffect4(() => {
294
496
  if (!sessionId || !cacheKey) return;
295
497
  if (!hydrateMessages) {
296
- setHydrated(true);
498
+ setHydratedKey(cacheKey);
297
499
  return;
298
500
  }
299
501
  let aborted = false;
300
- setHydrated(false);
301
- const loadFromCache = () => {
302
- try {
303
- const cached = globalThis.localStorage?.getItem(cacheKey);
304
- if (!cached) return;
305
- const parsed = JSON.parse(cached);
306
- if (Array.isArray(parsed) && parsed.length > 0) {
307
- setMessages(parsed);
502
+ setHydratedKey(null);
503
+ const hydrateLocalTurnVersion = localTurnVersionRef.current;
504
+ const hydrateMerged = (serverMessages, cachedMessages) => {
505
+ const current = messagesRef.current;
506
+ const fromServerAndCache = mergeHydratedMessages(serverMessages, cachedMessages);
507
+ const next = current.length > 0 ? mergeMessages(fromServerAndCache, current) : fromServerAndCache;
508
+ if (next.length > 0) {
509
+ setMessages(next);
510
+ const serverLatest = serverMessages[serverMessages.length - 1];
511
+ const cachedLatest = cachedMessages[cachedMessages.length - 1];
512
+ const serverCoversCachedTail = !messagesNeedResume(cachedMessages) || serverLatest && cachedLatest && sameMessageIdentityOrContent(serverLatest, cachedLatest) || serverMessages.length >= cachedMessages.length;
513
+ const authoritativeSettledServerTail = Boolean(
514
+ serverLatest && serverCoversCachedTail && messagesLookSettled(serverMessages)
515
+ );
516
+ if (authoritativeSettledServerTail) {
517
+ setSettledResumeKey(cacheKey);
518
+ if (localTurnVersionRef.current === hydrateLocalTurnVersion) rawStop();
519
+ clearCachedMessages(cacheKey);
520
+ if (statusKey) {
521
+ try {
522
+ globalThis.localStorage?.setItem(statusKey, "ready");
523
+ } catch {
524
+ }
525
+ }
308
526
  }
309
- } catch {
310
527
  }
311
528
  };
529
+ const loadFromCache = () => {
530
+ const cachedMessages = readCachedMessages(cacheKey);
531
+ if (cachedMessages.length > 0) hydrateMerged([], cachedMessages);
532
+ };
312
533
  const fetchOpts = optsRef.current.requestHeaders ? { headers: optsRef.current.requestHeaders } : void 0;
313
534
  const messagesUrl = `/api/v1/agent/chat/${encodeURIComponent(sessionId)}/messages`;
314
535
  const request = fetchOpts ? fetch(messagesUrl, fetchOpts) : fetch(messagesUrl);
315
536
  request.then((res) => res.ok ? res.json() : null).then((payload) => {
316
537
  if (aborted) return;
317
- const localTurnStarted = statusRef.current === "submitted" || statusRef.current === "streaming" || messagesRef.current.length > 0;
318
538
  const serverMessages = payload?.messages;
539
+ const cachedMessages = readCachedMessages(cacheKey);
319
540
  if (Array.isArray(serverMessages) && serverMessages.length > 0) {
320
- if (!localTurnStarted) setMessages(serverMessages);
541
+ hydrateMerged(serverMessages, cachedMessages);
321
542
  return;
322
543
  }
323
- if (!localTurnStarted) loadFromCache();
544
+ if (cachedMessages.length > 0) hydrateMerged([], cachedMessages);
324
545
  }).catch(() => {
325
546
  if (aborted) return;
326
- const localTurnStarted = statusRef.current === "submitted" || statusRef.current === "streaming" || messagesRef.current.length > 0;
327
- if (!localTurnStarted) loadFromCache();
547
+ loadFromCache();
328
548
  }).finally(() => {
329
- if (!aborted) setHydrated(true);
549
+ if (!aborted) setHydratedKey(cacheKey);
330
550
  });
331
551
  return () => {
332
552
  aborted = true;
333
553
  };
334
- }, [hydrateMessages, sessionId, cacheKey, setMessages]);
335
- const messages = chat.messages;
336
- const status = chat.status;
337
- const messagesRef = useRef4(messages);
338
- const statusRef = useRef4(status);
339
- messagesRef.current = messages;
340
- statusRef.current = status;
554
+ }, [hydrateMessages, sessionId, cacheKey, setMessages, statusKey, rawStop]);
555
+ const rawStatus = chat.status;
556
+ const rawStatusActive = rawStatus === "submitted" || rawStatus === "streaming";
557
+ const knownActiveTurn = localTurnActive || shouldResume;
558
+ const hydratingWithoutMessages = hydrateMessages && Boolean(sessionId) && !hydrated && messages.length === 0;
559
+ const status = hydratingWithoutMessages ? "ready" : rawStatusActive && !knownActiveTurn ? "ready" : knownActiveTurn && rawStatus === "ready" ? "submitted" : rawStatus;
560
+ const prevRawStatusRef = useRef4(rawStatus);
341
561
  useEffect4(() => {
342
- if (opts.persistMessages === false) return;
343
- if (!hydrated || !cacheKey) return;
344
- if (messages.length === 0) return;
562
+ const prev = prevRawStatusRef.current;
563
+ prevRawStatusRef.current = rawStatus;
564
+ const prevActive = prev === "submitted" || prev === "streaming";
565
+ if (prevActive && (rawStatus === "ready" || rawStatus === "error")) {
566
+ setLocalTurnActive(false);
567
+ activeTurnIdRef.current = null;
568
+ if (cacheKey) setSettledResumeKey(cacheKey);
569
+ if (statusKey) {
570
+ try {
571
+ globalThis.localStorage?.setItem(statusKey, "ready");
572
+ } catch {
573
+ }
574
+ }
575
+ if (cacheKey && shouldResume) clearCachedMessages(cacheKey);
576
+ }
577
+ }, [rawStatus, sessionId, cacheKey, statusKey, shouldResume]);
578
+ useEffect4(() => {
579
+ if (!statusKey) return;
345
580
  try {
346
- globalThis.localStorage?.setItem(cacheKey, JSON.stringify(messages));
581
+ if (knownActiveTurn) {
582
+ globalThis.localStorage?.setItem(statusKey, "active");
583
+ } else if (rawStatus === "ready" || rawStatus === "error" || rawStatusActive) {
584
+ globalThis.localStorage?.setItem(statusKey, "ready");
585
+ }
347
586
  } catch {
348
587
  }
588
+ }, [knownActiveTurn, rawStatus, rawStatusActive, statusKey]);
589
+ useEffect4(() => {
590
+ if (messages.length === 0) return;
591
+ const deduped = mergeMessages([], rawMessages);
592
+ if (!sameMessageOrder(rawMessages, deduped)) setMessages(deduped);
593
+ }, [rawMessages, messages.length, setMessages]);
594
+ useEffect4(() => {
595
+ if (opts.persistMessages === false) return;
596
+ if (!hydrated || !cacheKey) return;
597
+ if (messages.length === 0) return;
598
+ writeCachedMessages(cacheKey, mergeMessages([], messages));
349
599
  }, [opts.persistMessages, hydrated, cacheKey, messages]);
350
600
  const SETTLED_STATES = /* @__PURE__ */ new Set([
351
601
  "output-available",
@@ -388,7 +638,8 @@ function useAgentChat(opts) {
388
638
  if (prev !== "streaming" && prev !== "submitted") return;
389
639
  if (!sessionId || messages.length === 0) return;
390
640
  const url = `/api/v1/agent/chat/${encodeURIComponent(sessionId)}/messages`;
391
- const stripped = messages.map((msg) => ({
641
+ const persistedMessages = mergeMessages([], messages);
642
+ const stripped = persistedMessages.map((msg) => ({
392
643
  ...msg,
393
644
  parts: msg.parts?.map((part) => {
394
645
  const p = part;
@@ -408,11 +659,11 @@ function useAgentChat(opts) {
408
659
  }).catch(() => {
409
660
  });
410
661
  }, [opts.persistMessages, sessionId, status, messages]);
411
- return chat;
662
+ return { ...chat, messages, sendMessage, stop, status, hydrated, hydratingMessages: hydrateMessages && Boolean(sessionId) && !hydrated };
412
663
  }
413
664
 
414
665
  // src/front/pi/piChatProjection.ts
415
- import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
666
+ import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
416
667
  function asPiDataPart(part) {
417
668
  const typed = part;
418
669
  if (typeof typed.type !== "string" || !typed.type.startsWith("data-pi-")) return null;
@@ -584,12 +835,12 @@ function usePiChatProjection({
584
835
  const piMessagesRef = useRef5([]);
585
836
  const bufferedDeltaRef = useRef5(/* @__PURE__ */ new Map());
586
837
  const flushTimerRef = useRef5(null);
587
- const updatePiMessages = useCallback3((updater) => {
838
+ const updatePiMessages = useCallback4((updater) => {
588
839
  const next = updater(piMessagesRef.current);
589
840
  piMessagesRef.current = next;
590
841
  setPiMessages(next);
591
842
  }, []);
592
- const flushBufferedDeltas = useCallback3(() => {
843
+ const flushBufferedDeltas = useCallback4(() => {
593
844
  if (flushTimerRef.current) {
594
845
  clearTimeout(flushTimerRef.current);
595
846
  flushTimerRef.current = null;
@@ -599,7 +850,7 @@ function usePiChatProjection({
599
850
  bufferedDeltaRef.current.clear();
600
851
  updatePiMessages((items) => applyBufferedDeltas(items, deltas));
601
852
  }, [updatePiMessages]);
602
- const queueBufferedDelta = useCallback3((entry) => {
853
+ const queueBufferedDelta = useCallback4((entry) => {
603
854
  const key = `${entry.kind}:${entry.messageId}:${entry.partId}`;
604
855
  const existing = bufferedDeltaRef.current.get(key);
605
856
  bufferedDeltaRef.current.set(key, existing ? { ...existing, delta: `${existing.delta}${entry.delta}` } : entry);
@@ -624,7 +875,7 @@ function usePiChatProjection({
624
875
  piMessagesRef.current = [];
625
876
  setPiMessages([]);
626
877
  }, [sessionId]);
627
- const handleData = useCallback3((part) => {
878
+ const handleData = useCallback4((part) => {
628
879
  const typed = part;
629
880
  const data = typed.data ?? {};
630
881
  const piMessageId = typeof data.messageId === "string" ? data.messageId : void 0;
@@ -774,11 +1025,18 @@ function usePiChatProjection({
774
1025
  }
775
1026
 
776
1027
  // src/front/pi/piNativeFollowUpQueue.ts
777
- import { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState5 } from "react";
1028
+ import { useCallback as useCallback5, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState5 } from "react";
778
1029
  var STORAGE_FOLLOWUP_SEQ_PREFIX = "boring-agent:followup-seq:";
1030
+ var STORAGE_FOLLOWUP_QUEUE_PREFIX = "boring-agent:followup-queue:";
779
1031
  var MAX_FOLLOWUP_POST_ATTEMPTS = 5;
780
- function nextStoredFollowUpSeq(sessionId) {
781
- const key = `${STORAGE_FOLLOWUP_SEQ_PREFIX}${sessionId}`;
1032
+ function workspaceScopeFromHeaders(headers) {
1033
+ return headers?.["x-boring-workspace-id"] ?? headers?.["X-Boring-Workspace-Id"] ?? "global";
1034
+ }
1035
+ function scopedStorageKey(prefix, storageScope, sessionId) {
1036
+ return `${prefix}${encodeURIComponent(storageScope)}:${encodeURIComponent(sessionId)}`;
1037
+ }
1038
+ function nextStoredFollowUpSeq(storageScope, sessionId) {
1039
+ const key = scopedStorageKey(STORAGE_FOLLOWUP_SEQ_PREFIX, storageScope, sessionId);
782
1040
  try {
783
1041
  const current = Number(globalThis.localStorage?.getItem(key) ?? "0");
784
1042
  const next = Number.isFinite(current) ? current + 1 : 1;
@@ -788,16 +1046,57 @@ function nextStoredFollowUpSeq(sessionId) {
788
1046
  return Date.now();
789
1047
  }
790
1048
  }
1049
+ function followUpQueueStorageKey(storageScope, sessionId) {
1050
+ return scopedStorageKey(STORAGE_FOLLOWUP_QUEUE_PREFIX, storageScope, sessionId);
1051
+ }
1052
+ function isPendingFollowUp(value, sessionId) {
1053
+ const item = value;
1054
+ return Boolean(
1055
+ item && item.sessionId === sessionId && typeof item.id === "string" && typeof item.text === "string" && typeof item.serverMessage === "string" && typeof item.clientNonce === "string" && typeof item.clientSeq === "number" && typeof item.postAttempts === "number" && Array.isArray(item.files) && Array.isArray(item.attachments) && typeof item.posted === "boolean" && typeof item.consumed === "boolean"
1056
+ );
1057
+ }
1058
+ function pendingToProjected(item) {
1059
+ return {
1060
+ id: item.id,
1061
+ role: "user",
1062
+ text: item.text,
1063
+ files: item.files,
1064
+ status: item.consumed ? "done" : "queued"
1065
+ };
1066
+ }
1067
+ function readStoredFollowUps(storageScope, sessionId) {
1068
+ try {
1069
+ const raw = globalThis.localStorage?.getItem(followUpQueueStorageKey(storageScope, sessionId));
1070
+ if (!raw) return [];
1071
+ const parsed = JSON.parse(raw);
1072
+ if (!Array.isArray(parsed)) return [];
1073
+ return parsed.filter((item) => isPendingFollowUp(item, sessionId));
1074
+ } catch {
1075
+ return [];
1076
+ }
1077
+ }
1078
+ function writeStoredFollowUps(storageScope, sessionId, items) {
1079
+ try {
1080
+ const scoped = items.filter((item) => item.sessionId === sessionId && !item.consumed);
1081
+ const storage = globalThis.localStorage;
1082
+ if (!storage) return;
1083
+ const key = followUpQueueStorageKey(storageScope, sessionId);
1084
+ if (scoped.length === 0) storage.removeItem(key);
1085
+ else storage.setItem(key, JSON.stringify(scoped));
1086
+ } catch {
1087
+ }
1088
+ }
791
1089
  function usePiNativeFollowUpQueue({
792
1090
  sessionId,
793
1091
  status,
794
1092
  requestHeaders,
795
1093
  stop
796
1094
  }) {
797
- const [pendingMessages, setPendingMessages] = useState5([]);
798
- const pendingMessagesRef = useRef6([]);
799
- const [projectedFollowUps, setProjectedFollowUps] = useState5([]);
800
- const projectedFollowUpsRef = useRef6([]);
1095
+ const storageScope = workspaceScopeFromHeaders(requestHeaders);
1096
+ const [pendingMessages, setPendingMessages] = useState5(() => readStoredFollowUps(storageScope, sessionId));
1097
+ const pendingMessagesRef = useRef6(pendingMessages);
1098
+ const [projectedFollowUps, setProjectedFollowUps] = useState5(() => pendingMessages.map(pendingToProjected));
1099
+ const projectedFollowUpsRef = useRef6(projectedFollowUps);
801
1100
  const activeProjectedAssistantRef = useRef6(null);
802
1101
  const lastConsumedProjectedUserRef = useRef6(null);
803
1102
  const followUpPostTimerRef = useRef6(null);
@@ -805,34 +1104,50 @@ function usePiNativeFollowUpQueue({
805
1104
  const postPendingFollowUpsRef = useRef6(() => {
806
1105
  });
807
1106
  const consumedFollowUpRef = useRef6(null);
808
- const updatePendingMessages = useCallback4((updater) => {
1107
+ const activeStorageRef = useRef6({ storageScope, sessionId });
1108
+ activeStorageRef.current = { storageScope, sessionId };
1109
+ const updatePendingMessages = useCallback5((updater) => {
809
1110
  const next = updater(pendingMessagesRef.current);
810
1111
  pendingMessagesRef.current = next;
1112
+ writeStoredFollowUps(storageScope, sessionId, next);
811
1113
  setPendingMessages(next);
812
- }, []);
813
- const updateProjectedFollowUps = useCallback4((updater) => {
1114
+ }, [storageScope, sessionId]);
1115
+ const updateProjectedFollowUps = useCallback5((updater) => {
814
1116
  const next = updater(projectedFollowUpsRef.current);
815
1117
  projectedFollowUpsRef.current = next;
816
1118
  setProjectedFollowUps(next);
817
1119
  }, []);
818
- const clearLocal = useCallback4(() => {
1120
+ const clearRuntimeState = useCallback5(() => {
819
1121
  consumedFollowUpRef.current = null;
820
- pendingMessagesRef.current = [];
821
- projectedFollowUpsRef.current = [];
822
1122
  activeProjectedAssistantRef.current = null;
823
1123
  lastConsumedProjectedUserRef.current = null;
824
1124
  if (followUpPostTimerRef.current) clearTimeout(followUpPostTimerRef.current);
825
1125
  followUpPostTimerRef.current = null;
826
1126
  followUpPostInFlightRef.current = false;
1127
+ }, []);
1128
+ const loadLocalForScope = useCallback5((nextStorageScope, nextSessionId) => {
1129
+ clearRuntimeState();
1130
+ const restored = readStoredFollowUps(nextStorageScope, nextSessionId);
1131
+ const projected = restored.map(pendingToProjected);
1132
+ pendingMessagesRef.current = restored;
1133
+ projectedFollowUpsRef.current = projected;
1134
+ setPendingMessages(restored);
1135
+ setProjectedFollowUps(projected);
1136
+ }, [clearRuntimeState]);
1137
+ const clearLocal = useCallback5(() => {
1138
+ clearRuntimeState();
1139
+ pendingMessagesRef.current = [];
1140
+ projectedFollowUpsRef.current = [];
1141
+ writeStoredFollowUps(storageScope, sessionId, []);
827
1142
  setPendingMessages([]);
828
1143
  setProjectedFollowUps([]);
829
- }, []);
830
- const previousSessionIdRef = useRef6(sessionId);
1144
+ }, [clearRuntimeState, storageScope, sessionId]);
1145
+ const previousStorageRef = useRef6({ storageScope, sessionId });
831
1146
  useEffect6(() => {
832
- if (previousSessionIdRef.current === sessionId) return;
833
- previousSessionIdRef.current = sessionId;
834
- clearLocal();
835
- }, [sessionId, clearLocal]);
1147
+ if (previousStorageRef.current.storageScope === storageScope && previousStorageRef.current.sessionId === sessionId) return;
1148
+ previousStorageRef.current = { storageScope, sessionId };
1149
+ loadLocalForScope(storageScope, sessionId);
1150
+ }, [storageScope, sessionId, loadLocalForScope]);
836
1151
  useEffect6(() => {
837
1152
  return () => {
838
1153
  if (followUpPostTimerRef.current) {
@@ -841,18 +1156,22 @@ function usePiNativeFollowUpQueue({
841
1156
  }
842
1157
  };
843
1158
  }, []);
844
- const postPendingFollowUps = useCallback4(() => {
1159
+ const postPendingFollowUps = useCallback5(() => {
845
1160
  if (followUpPostInFlightRef.current) return;
1161
+ const postStorageScope = storageScope;
1162
+ const postSessionId = sessionId;
1163
+ const isActiveScope = () => activeStorageRef.current.storageScope === postStorageScope && activeStorageRef.current.sessionId === postSessionId;
1164
+ if (!isActiveScope()) return;
846
1165
  followUpPostInFlightRef.current = true;
847
1166
  void (async () => {
848
1167
  let shouldContinue = true;
849
1168
  try {
850
1169
  while (true) {
851
- const pending = pendingMessagesRef.current.find((item) => item.sessionId === sessionId && !item.posted);
1170
+ if (!isActiveScope()) return;
1171
+ const pending = pendingMessagesRef.current.find((item) => item.sessionId === postSessionId && !item.posted);
852
1172
  if (!pending) return;
853
- updatePendingMessages((items) => items.map((item) => item.id === pending.id ? { ...item, posted: true } : item));
854
1173
  try {
855
- const res = await fetch(`/api/v1/agent/chat/${encodeURIComponent(sessionId)}/followup`, {
1174
+ const res = await fetch(`/api/v1/agent/chat/${encodeURIComponent(postSessionId)}/followup`, {
856
1175
  method: "POST",
857
1176
  headers: {
858
1177
  "Content-Type": "application/json",
@@ -867,8 +1186,11 @@ function usePiNativeFollowUpQueue({
867
1186
  })
868
1187
  });
869
1188
  if (!res.ok) throw new Error(`follow-up rejected: ${res.status}`);
1189
+ if (!isActiveScope()) return;
1190
+ updatePendingMessages((items) => items.map((item) => item.id === pending.id ? { ...item, posted: true } : item));
870
1191
  } catch {
871
1192
  shouldContinue = false;
1193
+ if (!isActiveScope()) return;
872
1194
  const nextAttempts = pending.postAttempts + 1;
873
1195
  updatePendingMessages((items) => items.map((item) => item.id === pending.id ? { ...item, posted: false, postAttempts: nextAttempts } : item));
874
1196
  if (nextAttempts < MAX_FOLLOWUP_POST_ATTEMPTS) {
@@ -882,15 +1204,17 @@ function usePiNativeFollowUpQueue({
882
1204
  }
883
1205
  }
884
1206
  } finally {
885
- followUpPostInFlightRef.current = false;
886
- if (shouldContinue && pendingMessagesRef.current.some((item) => item.sessionId === sessionId && !item.posted)) {
887
- postPendingFollowUps();
1207
+ if (isActiveScope()) {
1208
+ followUpPostInFlightRef.current = false;
1209
+ if (shouldContinue && pendingMessagesRef.current.some((item) => item.sessionId === postSessionId && !item.posted)) {
1210
+ postPendingFollowUps();
1211
+ }
888
1212
  }
889
1213
  }
890
1214
  })();
891
- }, [sessionId, requestHeaders, updatePendingMessages]);
1215
+ }, [storageScope, sessionId, requestHeaders, updatePendingMessages]);
892
1216
  postPendingFollowUpsRef.current = postPendingFollowUps;
893
- const queueFollowUp = useCallback4((input) => {
1217
+ const queueFollowUp = useCallback5((input) => {
894
1218
  const nextPending = {
895
1219
  id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`,
896
1220
  sessionId,
@@ -901,7 +1225,7 @@ function usePiNativeFollowUpQueue({
901
1225
  posted: false,
902
1226
  consumed: false,
903
1227
  clientNonce: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}-nonce`,
904
- clientSeq: nextStoredFollowUpSeq(sessionId),
1228
+ clientSeq: nextStoredFollowUpSeq(storageScope, sessionId),
905
1229
  postAttempts: 0
906
1230
  };
907
1231
  updatePendingMessages((items) => [...items, nextPending]);
@@ -920,8 +1244,8 @@ function usePiNativeFollowUpQueue({
920
1244
  postPendingFollowUps();
921
1245
  }, 1e3);
922
1246
  }
923
- }, [sessionId, status, postPendingFollowUps, updatePendingMessages, updateProjectedFollowUps]);
924
- const handleData = useCallback4((part) => {
1247
+ }, [storageScope, sessionId, status, postPendingFollowUps, updatePendingMessages, updateProjectedFollowUps]);
1248
+ const handleData = useCallback5((part) => {
925
1249
  const typed = part;
926
1250
  if (typed.type === "data-followup-consumed") {
927
1251
  const serverText = typeof typed.data?.text === "string" ? typed.data.text : "";
@@ -984,7 +1308,7 @@ function usePiNativeFollowUpQueue({
984
1308
  lastConsumedProjectedUserRef.current = null;
985
1309
  setProjectedFollowUps([]);
986
1310
  }, [status, updatePendingMessages]);
987
- const deleteFollowUp = useCallback4((id) => {
1311
+ const deleteFollowUp = useCallback5((id) => {
988
1312
  const pending = pendingMessagesRef.current.find((item) => item.id === id);
989
1313
  updatePendingMessages((items) => items.filter((item) => item.id !== id));
990
1314
  updateProjectedFollowUps((items) => items.filter((item) => item.id !== id));
@@ -998,7 +1322,7 @@ function usePiNativeFollowUpQueue({
998
1322
  }).catch(() => {
999
1323
  });
1000
1324
  }, [sessionId, requestHeaders, updatePendingMessages, updateProjectedFollowUps]);
1001
- const clearFollowUps = useCallback4(() => {
1325
+ const clearFollowUps = useCallback5(() => {
1002
1326
  clearLocal();
1003
1327
  fetch(`/api/v1/agent/chat/${encodeURIComponent(sessionId)}/followup`, {
1004
1328
  method: "DELETE",
@@ -1006,7 +1330,7 @@ function usePiNativeFollowUpQueue({
1006
1330
  }).catch(() => {
1007
1331
  });
1008
1332
  }, [clearLocal, sessionId, requestHeaders]);
1009
- const stopAndClearFollowUps = useCallback4(() => {
1333
+ const stopAndClearFollowUps = useCallback5(() => {
1010
1334
  stop();
1011
1335
  clearFollowUps();
1012
1336
  }, [stop, clearFollowUps]);
@@ -1103,21 +1427,27 @@ function createCommandRegistry(initial) {
1103
1427
  }
1104
1428
 
1105
1429
  // src/front/composer/PluginUpdateStatus.tsx
1106
- import { useEffect as useEffect7 } from "react";
1430
+ import { useEffect as useEffect7, useRef as useRef7 } from "react";
1107
1431
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1108
1432
  function PluginUpdateStatus({
1109
1433
  state,
1110
1434
  onDismiss,
1111
1435
  onRetry,
1112
- successAutoDismissMs = 4e3
1436
+ successAutoDismissMs = 1400,
1437
+ maxWidthClassName = "max-w-3xl"
1113
1438
  }) {
1439
+ const onDismissRef = useRef7(onDismiss);
1440
+ useEffect7(() => {
1441
+ onDismissRef.current = onDismiss;
1442
+ }, [onDismiss]);
1443
+ const successDismissKey = state?.kind === "success" ? `${state.reloaded}:${(state.restartWarnings?.length ?? 0) > 0 || (state.diagnostics?.length ?? 0) > 0}` : null;
1114
1444
  useEffect7(() => {
1115
1445
  if (!state || state.kind !== "success" || successAutoDismissMs <= 0) return;
1116
- const hasDetails = (state.restartWarnings?.length ?? 0) > 0 || (state.diagnostics?.length ?? 0) > 0 || (state.frontEvents?.length ?? 0) > 0;
1117
- if (hasDetails) return;
1118
- const timeout = window.setTimeout(onDismiss, successAutoDismissMs);
1446
+ const hasWarningsOrDiagnostics = (state.restartWarnings?.length ?? 0) > 0 || (state.diagnostics?.length ?? 0) > 0;
1447
+ if (hasWarningsOrDiagnostics) return;
1448
+ const timeout = window.setTimeout(() => onDismissRef.current(), successAutoDismissMs);
1119
1449
  return () => window.clearTimeout(timeout);
1120
- }, [state, onDismiss, successAutoDismissMs]);
1450
+ }, [state?.kind, successDismissKey, successAutoDismissMs]);
1121
1451
  if (!state) return null;
1122
1452
  if (state.kind === "running") {
1123
1453
  return /* @__PURE__ */ jsxs3(
@@ -1127,8 +1457,9 @@ function PluginUpdateStatus({
1127
1457
  role: "status",
1128
1458
  "aria-live": "polite",
1129
1459
  className: cn(
1130
- "mx-auto mb-2 w-full max-w-3xl rounded-[var(--radius-md)] border border-accent/30 bg-[color:var(--accent-soft)]",
1131
- "px-3 py-2 text-xs text-foreground flex items-center gap-2"
1460
+ "mx-auto mb-2 w-full rounded-[var(--radius-md)] border border-accent/30 bg-[color:var(--accent-soft)]",
1461
+ "px-3 py-2 text-xs text-foreground flex items-center gap-2",
1462
+ maxWidthClassName
1132
1463
  ),
1133
1464
  children: [
1134
1465
  /* @__PURE__ */ jsx3("span", { className: "inline-block h-2 w-2 animate-pulse rounded-full bg-accent", "aria-hidden": "true" }),
@@ -1141,6 +1472,9 @@ function PluginUpdateStatus({
1141
1472
  const warnings = state.restartWarnings ?? [];
1142
1473
  const diagnostics = state.diagnostics ?? [];
1143
1474
  const frontEvents = state.frontEvents ?? [];
1475
+ const hasWarningsOrDiagnostics = warnings.length > 0 || diagnostics.length > 0;
1476
+ const title = state.reloaded ? hasWarningsOrDiagnostics ? "Reload finished with warnings" : "Reload complete" : "Reload queued";
1477
+ const detail = state.reloaded ? hasWarningsOrDiagnostics ? "Some plugin changes need attention." : frontEvents.length > 0 ? `${frontEvents.length} plugin module${frontEvents.length === 1 ? "" : "s"} refreshed. Changes are live.` : "Changes are live." : void 0;
1144
1478
  return /* @__PURE__ */ jsxs3(
1145
1479
  "div",
1146
1480
  {
@@ -1148,21 +1482,25 @@ function PluginUpdateStatus({
1148
1482
  role: "status",
1149
1483
  "aria-live": "polite",
1150
1484
  className: cn(
1151
- "mx-auto mb-2 w-full max-w-3xl rounded-[var(--radius-md)] border border-[oklch(0.78_0.13_148)]/40 bg-[oklch(0.95_0.05_148/0.3)]",
1152
- "px-3 py-2 text-xs text-foreground"
1485
+ "mx-auto mb-2 w-full rounded-[var(--radius-md)] border border-[oklch(0.78_0.13_148)]/35 bg-[oklch(0.95_0.05_148/0.28)]",
1486
+ "px-3 py-2 text-xs text-foreground shadow-sm",
1487
+ maxWidthClassName
1153
1488
  ),
1154
1489
  children: [
1155
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
1156
- /* @__PURE__ */ jsx3("span", { className: "text-[oklch(0.45_0.13_148)]", "aria-hidden": "true", children: "\u2713" }),
1157
- /* @__PURE__ */ jsx3("span", { className: "flex-1", children: state.reloaded ? warnings.length > 0 || diagnostics.length > 0 ? "Plugins updated with warnings." : frontEvents.length > 0 ? "Plugins updated. Browser modules reloaded." : "Plugins updated." : "Plugins will reload on the next message." }),
1490
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-start gap-2", children: [
1491
+ /* @__PURE__ */ jsx3("span", { className: "mt-0.5 text-[oklch(0.45_0.13_148)]", "aria-hidden": "true", children: "\u2713" }),
1492
+ /* @__PURE__ */ jsxs3("span", { className: "min-w-0 flex-1", children: [
1493
+ /* @__PURE__ */ jsx3("span", { className: "block font-medium leading-5", children: title }),
1494
+ detail ? /* @__PURE__ */ jsx3("span", { className: "block leading-4 text-muted-foreground", children: detail }) : null
1495
+ ] }),
1158
1496
  /* @__PURE__ */ jsx3(
1159
1497
  "button",
1160
1498
  {
1161
1499
  type: "button",
1162
1500
  onClick: onDismiss,
1163
- className: "rounded border border-transparent px-2 py-0.5 text-[11px] font-medium text-muted-foreground hover:bg-muted hover:text-foreground",
1501
+ className: "-mr-1 rounded border border-transparent px-1.5 py-0.5 text-[13px] leading-none text-muted-foreground hover:bg-muted hover:text-foreground",
1164
1502
  "aria-label": "Dismiss plugin update status",
1165
- children: "Dismiss"
1503
+ children: "\xD7"
1166
1504
  }
1167
1505
  )
1168
1506
  ] }),
@@ -1192,31 +1530,6 @@ function PluginUpdateStatus({
1192
1530
  ]
1193
1531
  }
1194
1532
  ) : null,
1195
- frontEvents.length > 0 ? /* @__PURE__ */ jsxs3(
1196
- "div",
1197
- {
1198
- "data-boring-plugin-update-front-events": "",
1199
- className: cn(
1200
- "mt-2 rounded border border-[oklch(0.78_0.13_148)]/35 bg-[oklch(0.95_0.05_148/0.25)]",
1201
- "px-2 py-1.5 text-[11px] text-foreground"
1202
- ),
1203
- children: [
1204
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1.5 font-medium text-[oklch(0.45_0.13_148)]", children: [
1205
- /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "\u2713" }),
1206
- /* @__PURE__ */ jsxs3("span", { children: [
1207
- "Browser plugin modules updated (",
1208
- frontEvents.length,
1209
- ")"
1210
- ] })
1211
- ] }),
1212
- /* @__PURE__ */ jsx3("ul", { className: "mt-1 ml-4 list-disc text-foreground/85", children: frontEvents.map((event, index) => /* @__PURE__ */ jsxs3("li", { children: [
1213
- /* @__PURE__ */ jsx3("code", { className: "font-mono text-[10.5px]", children: event.pluginId ?? event.source ?? "plugin" }),
1214
- " \u2014 ",
1215
- event.message ?? "front module updated"
1216
- ] }, `${event.pluginId ?? event.source ?? "plugin"}-${index}`)) })
1217
- ]
1218
- }
1219
- ) : null,
1220
1533
  warnings.length > 0 ? /* @__PURE__ */ jsxs3(
1221
1534
  "div",
1222
1535
  {
@@ -1255,8 +1568,9 @@ function PluginUpdateStatus({
1255
1568
  role: "status",
1256
1569
  "aria-live": "polite",
1257
1570
  className: cn(
1258
- "mx-auto mb-2 w-full max-w-3xl rounded-[var(--radius-md)] border border-destructive/40 bg-destructive/10",
1259
- "px-3 py-2 text-xs text-foreground"
1571
+ "mx-auto mb-2 w-full rounded-[var(--radius-md)] border border-destructive/40 bg-destructive/10",
1572
+ "px-3 py-2 text-xs text-foreground",
1573
+ maxWidthClassName
1260
1574
  ),
1261
1575
  children: [
1262
1576
  /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
@@ -1291,7 +1605,7 @@ function PluginUpdateStatus({
1291
1605
  // src/front/primitives/tool-call-group.tsx
1292
1606
  import { getToolName, isToolUIPart } from "ai";
1293
1607
  import { ChevronDownIcon } from "lucide-react";
1294
- import { memo as memo2, useCallback as useCallback6, useMemo as useMemo5, useState as useState9 } from "react";
1608
+ import { memo as memo2, useCallback as useCallback7, useMemo as useMemo5, useState as useState9 } from "react";
1295
1609
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@hachej/boring-ui-kit";
1296
1610
 
1297
1611
  // src/front/bareToolRenderers/DiffView.tsx
@@ -1347,7 +1661,7 @@ function DiffView({ oldString, newString, path, replaceAll }) {
1347
1661
  }
1348
1662
 
1349
1663
  // src/front/barePrimitives/Tool.tsx
1350
- import { useEffect as useEffect8, useRef as useRef7, useState as useState7 } from "react";
1664
+ import { useEffect as useEffect8, useRef as useRef8, useState as useState7 } from "react";
1351
1665
  import { Button as Button2 } from "@hachej/boring-ui-kit";
1352
1666
  import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1353
1667
  function stateLabel(state) {
@@ -1386,7 +1700,7 @@ function Tool({
1386
1700
  const isRunning = state === "input-streaming" || state === "input-available";
1387
1701
  const isComplete = state === "output-available" || state === "output-error" || state === "output-denied";
1388
1702
  const [elapsedSec, setElapsedSec] = useState7(0);
1389
- const startRef = useRef7(0);
1703
+ const startRef = useRef8(0);
1390
1704
  useEffect8(() => {
1391
1705
  if (!isRunning) {
1392
1706
  setElapsedSec(0);
@@ -1486,7 +1800,7 @@ function Terminal({
1486
1800
  }
1487
1801
 
1488
1802
  // src/front/barePrimitives/CodeBlock.tsx
1489
- import { useCallback as useCallback5, useState as useState8 } from "react";
1803
+ import { useCallback as useCallback6, useState as useState8 } from "react";
1490
1804
  import { Button as Button3 } from "@hachej/boring-ui-kit";
1491
1805
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1492
1806
  function CodeBlock({
@@ -1498,8 +1812,9 @@ function CodeBlock({
1498
1812
  copyable = true
1499
1813
  }) {
1500
1814
  const [copied, setCopied] = useState8(false);
1501
- const handleCopy = useCallback5(() => {
1502
- navigator.clipboard.writeText(code3).then(() => {
1815
+ const handleCopy = useCallback6(() => {
1816
+ void copyTextToClipboard(code3).then((ok) => {
1817
+ if (!ok) return;
1503
1818
  setCopied(true);
1504
1819
  setTimeout(() => setCopied(false), 2e3);
1505
1820
  });
@@ -1824,7 +2139,7 @@ function getWorkspaceNotReadyStatus(output) {
1824
2139
  code: ErrorCode.enum.WORKSPACE_NOT_READY,
1825
2140
  retryable: true,
1826
2141
  requirement,
1827
- message: COPY[requirement]
2142
+ message: COPY[requirement] ?? "Workspace is still preparing."
1828
2143
  };
1829
2144
  }
1830
2145
 
@@ -1872,7 +2187,7 @@ var ToolCallGroup = memo2(({ tools, mergedToolRenderers }) => {
1872
2187
  return part.state === "output-error";
1873
2188
  }) && !workspaceNotReady;
1874
2189
  const [isOpen, setIsOpen] = useState9(false);
1875
- const handleOpenChange = useCallback6((open) => setIsOpen(open), []);
2190
+ const handleOpenChange = useCallback7((open) => setIsOpen(open), []);
1876
2191
  const title = useMemo5(() => buildTitle(tools, isSettled), [tools, isSettled]);
1877
2192
  return /* @__PURE__ */ jsxs9(Collapsible, { open: isOpen, onOpenChange: handleOpenChange, className: "not-prose my-1.5", children: [
1878
2193
  /* @__PURE__ */ jsxs9(
@@ -1951,11 +2266,11 @@ import { CheckIcon, CopyIcon } from "lucide-react";
1951
2266
  import {
1952
2267
  createContext,
1953
2268
  memo as memo3,
1954
- useCallback as useCallback7,
2269
+ useCallback as useCallback8,
1955
2270
  useContext,
1956
2271
  useEffect as useEffect9,
1957
2272
  useMemo as useMemo6,
1958
- useRef as useRef8,
2273
+ useRef as useRef9,
1959
2274
  useState as useState10
1960
2275
  } from "react";
1961
2276
  import { createHighlighter } from "shiki";
@@ -2195,7 +2510,7 @@ var CodeBlockContent = ({
2195
2510
  [code3, lang, rawTokens]
2196
2511
  );
2197
2512
  const [asyncTokens, setAsyncTokens] = useState10(null);
2198
- const asyncKeyRef = useRef8({ code: code3, language });
2513
+ const asyncKeyRef = useRef9({ code: code3, language });
2199
2514
  if (asyncKeyRef.current.code !== code3 || asyncKeyRef.current.language !== language) {
2200
2515
  asyncKeyRef.current = { code: code3, language };
2201
2516
  setAsyncTokens(null);
@@ -2244,26 +2559,27 @@ var CodeBlockCopyButton = ({
2244
2559
  ...props
2245
2560
  }) => {
2246
2561
  const [isCopied, setIsCopied] = useState10(false);
2247
- const timeoutRef = useRef8(0);
2562
+ const timeoutRef = useRef9(0);
2248
2563
  const { code: code3 } = useContext(CodeBlockContext);
2249
- const copyToClipboard = useCallback7(async () => {
2250
- if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
2564
+ const copyToClipboard = useCallback8(async () => {
2565
+ if (typeof window === "undefined") {
2251
2566
  onError?.(new Error("Clipboard API not available"));
2252
2567
  return;
2253
2568
  }
2254
- try {
2255
- if (!isCopied) {
2256
- await navigator.clipboard.writeText(code3);
2257
- setIsCopied(true);
2258
- onCopy?.();
2259
- timeoutRef.current = window.setTimeout(
2260
- () => setIsCopied(false),
2261
- timeout
2262
- );
2263
- }
2264
- } catch (error) {
2265
- onError?.(error);
2569
+ if (isCopied) {
2570
+ return;
2266
2571
  }
2572
+ const copied = await copyTextToClipboard(code3);
2573
+ if (!copied) {
2574
+ onError?.(new Error("Clipboard API not available"));
2575
+ return;
2576
+ }
2577
+ setIsCopied(true);
2578
+ onCopy?.();
2579
+ timeoutRef.current = window.setTimeout(
2580
+ () => setIsCopied(false),
2581
+ timeout
2582
+ );
2267
2583
  }, [code3, onCopy, onError, timeout, isCopied]);
2268
2584
  useEffect9(
2269
2585
  () => () => {
@@ -2507,6 +2823,39 @@ var ArtifactContent = ({
2507
2823
 
2508
2824
  // src/front/toolRenderers.tsx
2509
2825
  import { CopyIcon as CopyIcon2, DownloadIcon } from "lucide-react";
2826
+
2827
+ // src/front/runtimeReadinessStatus.ts
2828
+ var COPY2 = {
2829
+ "runtime-dependencies": "Runtime dependencies are still installing.",
2830
+ "runtime:python": "Python runtime dependencies are still installing.",
2831
+ "runtime:node": "Node runtime dependencies are still installing."
2832
+ };
2833
+ function isRuntimeRequirement(requirement) {
2834
+ return requirement === "runtime-dependencies" || requirement.startsWith("runtime:");
2835
+ }
2836
+ function detailsFromOutput2(output) {
2837
+ if (!output || typeof output !== "object") return null;
2838
+ const record = output;
2839
+ const details = record.details && typeof record.details === "object" ? record.details : record;
2840
+ return details;
2841
+ }
2842
+ function getRuntimeReadinessStatus(output) {
2843
+ const details = detailsFromOutput2(output);
2844
+ if (!details) return null;
2845
+ if (details.code !== ErrorCode.enum.AGENT_RUNTIME_NOT_READY && details.code !== ErrorCode.enum.RUNTIME_PROVISIONING_FAILED) return null;
2846
+ const requirement = details.requirement;
2847
+ if (typeof requirement !== "string" || !isRuntimeRequirement(requirement)) return null;
2848
+ const failed = details.code === ErrorCode.enum.RUNTIME_PROVISIONING_FAILED || details.state === "failed";
2849
+ return {
2850
+ code: details.code,
2851
+ retryable: details.retryable === true,
2852
+ requirement,
2853
+ state: typeof details.state === "string" ? details.state : void 0,
2854
+ message: failed ? "Runtime setup failed. Retry or reload the workspace." : COPY2[requirement] ?? "Runtime dependencies are still installing."
2855
+ };
2856
+ }
2857
+
2858
+ // src/front/toolRenderers.tsx
2510
2859
  import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
2511
2860
  function asRecord2(value) {
2512
2861
  return typeof value === "object" && value !== null ? value : {};
@@ -2581,6 +2930,22 @@ function renderWorkspaceNotReady(part) {
2581
2930
  ] }) })
2582
2931
  ] });
2583
2932
  }
2933
+ function renderRuntimeNotReady(part) {
2934
+ const status = getRuntimeReadinessStatus(part.output);
2935
+ if (!status) return null;
2936
+ const suffix = status.code === ErrorCode.enum.RUNTIME_PROVISIONING_FAILED ? "Retry or reload the workspace." : "This is retryable; try again shortly.";
2937
+ return /* @__PURE__ */ jsxs13(Tool2, { children: [
2938
+ /* @__PURE__ */ jsx15(ToolHeader, { title: `${part.toolName} \xB7 ${status.message} ${suffix}`, ...toHeaderProps(part) }),
2939
+ /* @__PURE__ */ jsx15(ToolContent, { children: /* @__PURE__ */ jsxs13("div", { className: "rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-sm text-muted-foreground", children: [
2940
+ status.message,
2941
+ " ",
2942
+ suffix
2943
+ ] }) })
2944
+ ] });
2945
+ }
2946
+ function renderReadinessBlock(part) {
2947
+ return renderWorkspaceNotReady(part) ?? renderRuntimeNotReady(part);
2948
+ }
2584
2949
  function pathTitle(prefix, path) {
2585
2950
  return /* @__PURE__ */ jsxs13("span", { className: "flex min-w-0 items-center gap-1.5", children: [
2586
2951
  /* @__PURE__ */ jsx15("span", { className: "text-muted-foreground", children: prefix }),
@@ -2589,7 +2954,7 @@ function pathTitle(prefix, path) {
2589
2954
  ] });
2590
2955
  }
2591
2956
  function renderBash2(part) {
2592
- const readiness = renderWorkspaceNotReady(part);
2957
+ const readiness = renderReadinessBlock(part);
2593
2958
  if (readiness) return readiness;
2594
2959
  const input = asRecord2(part.input);
2595
2960
  const output = asRecord2(part.output);
@@ -2622,7 +2987,7 @@ function renderBash2(part) {
2622
2987
  ] });
2623
2988
  }
2624
2989
  function renderRead2(part) {
2625
- const readiness = renderWorkspaceNotReady(part);
2990
+ const readiness = renderReadinessBlock(part);
2626
2991
  if (readiness) return readiness;
2627
2992
  const input = asRecord2(part.input);
2628
2993
  const output = asRecord2(part.output);
@@ -2641,7 +3006,7 @@ function renderRead2(part) {
2641
3006
  ] });
2642
3007
  }
2643
3008
  function renderWrite2(part) {
2644
- const readiness = renderWorkspaceNotReady(part);
3009
+ const readiness = renderReadinessBlock(part);
2645
3010
  if (readiness) return readiness;
2646
3011
  const input = asRecord2(part.input);
2647
3012
  const path = typeof input.path === "string" ? input.path : "";
@@ -2669,10 +3034,7 @@ function renderWrite2(part) {
2669
3034
  tooltip: "Copy contents",
2670
3035
  label: "Copy",
2671
3036
  onClick: () => {
2672
- if (typeof navigator !== "undefined" && navigator.clipboard) {
2673
- navigator.clipboard.writeText(content).catch(() => {
2674
- });
2675
- }
3037
+ void copyTextToClipboard(content);
2676
3038
  }
2677
3039
  }
2678
3040
  ),
@@ -2705,7 +3067,7 @@ function renderWrite2(part) {
2705
3067
  ] });
2706
3068
  }
2707
3069
  function renderEdit2(part) {
2708
- const readiness = renderWorkspaceNotReady(part);
3070
+ const readiness = renderReadinessBlock(part);
2709
3071
  if (readiness) return readiness;
2710
3072
  const input = asRecord2(part.input);
2711
3073
  const path = typeof input.path === "string" ? input.path : "";
@@ -2732,7 +3094,7 @@ function renderEdit2(part) {
2732
3094
  ] });
2733
3095
  }
2734
3096
  function renderSearchLike2(toolName, part) {
2735
- const readiness = renderWorkspaceNotReady(part);
3097
+ const readiness = renderReadinessBlock(part);
2736
3098
  if (readiness) return readiness;
2737
3099
  const input = asRecord2(part.input);
2738
3100
  const SearchLikeIcon = toolName === "find" || toolName === "grep" ? SearchIcon : FileTextIcon;
@@ -2770,7 +3132,7 @@ function extractParamTokens(value, depth = 0) {
2770
3132
  return [String(value)];
2771
3133
  }
2772
3134
  function renderExecUi2(part) {
2773
- const readiness = renderWorkspaceNotReady(part);
3135
+ const readiness = renderReadinessBlock(part);
2774
3136
  if (readiness) return readiness;
2775
3137
  const input = asRecord2(part.input);
2776
3138
  const kind = typeof input.kind === "string" ? input.kind : "(empty)";
@@ -2828,7 +3190,7 @@ function renderExecUi2(part) {
2828
3190
  ] });
2829
3191
  }
2830
3192
  function renderFallback2(part) {
2831
- const readiness = renderWorkspaceNotReady(part);
3193
+ const readiness = renderReadinessBlock(part);
2832
3194
  if (readiness) return readiness;
2833
3195
  return /* @__PURE__ */ jsxs13(Tool2, { children: [
2834
3196
  /* @__PURE__ */ jsx15(ToolHeader, { title: part.toolName, ...toHeaderProps(part) }),
@@ -2861,7 +3223,7 @@ function mergeShadcnToolRenderers(overrides) {
2861
3223
  // src/front/primitives/conversation.tsx
2862
3224
  import { Button as Button6 } from "@hachej/boring-ui-kit";
2863
3225
  import { ArrowDownIcon, DownloadIcon as DownloadIcon2 } from "lucide-react";
2864
- import { useCallback as useCallback8, useEffect as useEffect10 } from "react";
3226
+ import { useCallback as useCallback9, useEffect as useEffect10 } from "react";
2865
3227
  import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
2866
3228
  import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
2867
3229
  var ConversationScrollController = ({
@@ -2909,7 +3271,7 @@ var ConversationScrollButton = ({
2909
3271
  ...props
2910
3272
  }) => {
2911
3273
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
2912
- const handleScrollToBottom = useCallback8(() => {
3274
+ const handleScrollToBottom = useCallback9(() => {
2913
3275
  scrollToBottom();
2914
3276
  }, [scrollToBottom]);
2915
3277
  return !isAtBottom && /* @__PURE__ */ jsx16(
@@ -2973,17 +3335,24 @@ function ChatEmptyState({
2973
3335
  "div",
2974
3336
  {
2975
3337
  "data-boring-agent-part": "empty-state",
3338
+ style: {
3339
+ ["--chat-empty-eyebrow-color"]: "var(--muted-foreground)",
3340
+ ["--chat-empty-title-color"]: "var(--foreground)",
3341
+ ["--chat-empty-description-color"]: "var(--muted-foreground)",
3342
+ ["--chat-empty-card-hover"]: "var(--accent-soft)",
3343
+ ["--chat-empty-accent"]: "var(--accent)"
3344
+ },
2976
3345
  className: cn(
2977
3346
  "mx-auto flex w-full max-w-[640px] flex-col px-2 pt-12 pb-4",
2978
3347
  className
2979
3348
  ),
2980
3349
  children: [
2981
- eyebrow && /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 text-[10.5px] font-medium uppercase tracking-[0.16em] text-muted-foreground dark:text-zinc-300", children: [
2982
- /* @__PURE__ */ jsx17("span", { className: "inline-block h-px w-4 bg-[color:var(--accent)]", "aria-hidden": "true" }),
3350
+ eyebrow && /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 text-[10.5px] font-medium uppercase tracking-[0.16em] text-[color:var(--chat-empty-eyebrow-color)]", children: [
3351
+ /* @__PURE__ */ jsx17("span", { className: "inline-block h-px w-4 bg-[color:var(--chat-empty-accent)]", "aria-hidden": "true" }),
2983
3352
  eyebrow
2984
3353
  ] }),
2985
- title && /* @__PURE__ */ jsx17("h3", { className: "mt-3 text-[34px] font-medium leading-[1.05] tracking-[-0.02em] text-foreground dark:text-zinc-50", children: title }),
2986
- description && /* @__PURE__ */ jsx17("p", { className: "mt-3 max-w-[440px] text-[14px] leading-relaxed text-muted-foreground dark:text-zinc-300", children: description }),
3354
+ title && /* @__PURE__ */ jsx17("h3", { className: "mt-3 text-[34px] font-medium leading-[1.05] tracking-[-0.02em] text-[color:var(--chat-empty-title-color)]", children: title }),
3355
+ description && /* @__PURE__ */ jsx17("p", { className: "mt-3 max-w-[440px] text-[14px] leading-relaxed text-[color:var(--chat-empty-description-color)]", children: description }),
2987
3356
  suggestions.length > 0 && /* @__PURE__ */ jsx17(
2988
3357
  "div",
2989
3358
  {
@@ -3004,12 +3373,12 @@ function ChatEmptyState({
3004
3373
  }
3005
3374
  onSelect?.(suggestion);
3006
3375
  },
3007
- className: "group h-auto justify-start gap-3 rounded-none bg-background px-4 py-3.5 text-left hover:bg-[color:var(--accent-soft)] focus-visible:bg-[color:var(--accent-soft)]",
3376
+ className: "group h-auto justify-start gap-3 rounded-none bg-background px-4 py-3.5 text-left hover:bg-[color:var(--chat-empty-card-hover)] focus-visible:bg-[color:var(--chat-empty-card-hover)]",
3008
3377
  children: [
3009
3378
  Icon && /* @__PURE__ */ jsx17(
3010
3379
  Icon,
3011
3380
  {
3012
- className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground transition-colors group-hover:text-[color:var(--accent)]",
3381
+ className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground transition-colors group-hover:text-[color:var(--chat-empty-accent)]",
3013
3382
  strokeWidth: 1.75
3014
3383
  }
3015
3384
  ),
@@ -3022,7 +3391,7 @@ function ChatEmptyState({
3022
3391
  {
3023
3392
  className: cn(
3024
3393
  "mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground opacity-0 transition-all duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]",
3025
- "group-hover:translate-x-0.5 group-hover:-translate-y-0.5 group-hover:opacity-100 group-hover:text-[color:var(--accent)]"
3394
+ "group-hover:translate-x-0.5 group-hover:-translate-y-0.5 group-hover:opacity-100 group-hover:text-[color:var(--chat-empty-accent)]"
3026
3395
  ),
3027
3396
  strokeWidth: 2
3028
3397
  }
@@ -3056,7 +3425,7 @@ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
3056
3425
  import {
3057
3426
  createContext as createContext3,
3058
3427
  memo as memo4,
3059
- useCallback as useCallback9,
3428
+ useCallback as useCallback10,
3060
3429
  useContext as useContext3,
3061
3430
  useEffect as useEffect11,
3062
3431
  useMemo as useMemo7,
@@ -3218,11 +3587,11 @@ import { BrainIcon, ChevronDownIcon as ChevronDownIcon4 } from "lucide-react";
3218
3587
  import {
3219
3588
  createContext as createContext4,
3220
3589
  memo as memo5,
3221
- useCallback as useCallback10,
3590
+ useCallback as useCallback11,
3222
3591
  useContext as useContext4,
3223
3592
  useEffect as useEffect12,
3224
3593
  useMemo as useMemo8,
3225
- useRef as useRef9,
3594
+ useRef as useRef10,
3226
3595
  useState as useState12
3227
3596
  } from "react";
3228
3597
  import { Streamdown as Streamdown2 } from "streamdown";
@@ -3259,9 +3628,9 @@ var Reasoning = memo5(
3259
3628
  defaultProp: void 0,
3260
3629
  prop: durationProp
3261
3630
  });
3262
- const hasEverStreamedRef = useRef9(isStreaming);
3631
+ const hasEverStreamedRef = useRef10(isStreaming);
3263
3632
  const [hasAutoClosed, setHasAutoClosed] = useState12(false);
3264
- const startTimeRef = useRef9(null);
3633
+ const startTimeRef = useRef10(null);
3265
3634
  useEffect12(() => {
3266
3635
  if (isStreaming) {
3267
3636
  hasEverStreamedRef.current = true;
@@ -3287,7 +3656,7 @@ var Reasoning = memo5(
3287
3656
  return () => clearTimeout(timer);
3288
3657
  }
3289
3658
  }, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);
3290
- const handleOpenChange = useCallback10(
3659
+ const handleOpenChange = useCallback11(
3291
3660
  (newOpen) => {
3292
3661
  setIsOpen(newOpen);
3293
3662
  },
@@ -3408,7 +3777,7 @@ import {
3408
3777
  XIcon as XIcon2
3409
3778
  } from "lucide-react";
3410
3779
  import { nanoid } from "nanoid";
3411
- import { useCallback as useCallback11, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef10, useState as useState13 } from "react";
3780
+ import { useCallback as useCallback12, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef11, useState as useState13 } from "react";
3412
3781
 
3413
3782
  // src/front/browserFiles.ts
3414
3783
  var convertBlobUrlToDataUrl = async (url) => {
@@ -3490,10 +3859,10 @@ var PromptInput = ({
3490
3859
  }) => {
3491
3860
  const controller = useOptionalPromptInputController();
3492
3861
  const usingProvider = !!controller;
3493
- const inputRef = useRef10(null);
3494
- const formRef = useRef10(null);
3862
+ const inputRef = useRef11(null);
3863
+ const formRef = useRef11(null);
3495
3864
  const [items, setItems] = useState13([]);
3496
- const setFileUrlLocal = useCallback11((id, url, status) => {
3865
+ const setFileUrlLocal = useCallback12((id, url, status) => {
3497
3866
  setItems((prev) => prev.map((f) => {
3498
3867
  if (f.id !== id) return f;
3499
3868
  if (f.url !== url && f.url.startsWith("blob:")) URL.revokeObjectURL(f.url);
@@ -3502,14 +3871,14 @@ var PromptInput = ({
3502
3871
  }, []);
3503
3872
  const files = usingProvider ? controller.attachments.files : items;
3504
3873
  const [referencedSources, setReferencedSources] = useState13([]);
3505
- const filesRef = useRef10(files);
3874
+ const filesRef = useRef11(files);
3506
3875
  useEffect13(() => {
3507
3876
  filesRef.current = files;
3508
3877
  }, [files]);
3509
- const openFileDialogLocal = useCallback11(() => {
3878
+ const openFileDialogLocal = useCallback12(() => {
3510
3879
  inputRef.current?.click();
3511
3880
  }, []);
3512
- const matchesAccept = useCallback11(
3881
+ const matchesAccept = useCallback12(
3513
3882
  (f) => {
3514
3883
  if (!accept || accept.trim() === "") {
3515
3884
  return true;
@@ -3525,7 +3894,7 @@ var PromptInput = ({
3525
3894
  },
3526
3895
  [accept]
3527
3896
  );
3528
- const addLocal = useCallback11(
3897
+ const addLocal = useCallback12(
3529
3898
  (fileList) => {
3530
3899
  const incoming = [...fileList];
3531
3900
  const accepted = incoming.filter((f) => matchesAccept(f));
@@ -3572,7 +3941,7 @@ var PromptInput = ({
3572
3941
  },
3573
3942
  [matchesAccept, maxFiles, maxFileSize, onError, items.length, onUploadFile, setFileUrlLocal]
3574
3943
  );
3575
- const removeLocal = useCallback11(
3944
+ const removeLocal = useCallback12(
3576
3945
  (id) => setItems((prev) => {
3577
3946
  const found = prev.find((file) => file.id === id);
3578
3947
  if (found?.url.startsWith("blob:")) {
@@ -3582,7 +3951,7 @@ var PromptInput = ({
3582
3951
  }),
3583
3952
  []
3584
3953
  );
3585
- const addWithProviderValidation = useCallback11(
3954
+ const addWithProviderValidation = useCallback12(
3586
3955
  (fileList) => {
3587
3956
  const incoming = [...fileList];
3588
3957
  const accepted = incoming.filter((f) => matchesAccept(f));
@@ -3617,7 +3986,7 @@ var PromptInput = ({
3617
3986
  },
3618
3987
  [matchesAccept, maxFileSize, maxFiles, onError, files.length, controller]
3619
3988
  );
3620
- const clearAttachments = useCallback11(
3989
+ const clearAttachments = useCallback12(
3621
3990
  () => usingProvider ? controller?.attachments.clear() : setItems((prev) => {
3622
3991
  for (const file of prev) {
3623
3992
  if (file.url.startsWith("blob:")) {
@@ -3628,14 +3997,14 @@ var PromptInput = ({
3628
3997
  }),
3629
3998
  [usingProvider, controller]
3630
3999
  );
3631
- const clearReferencedSources = useCallback11(
4000
+ const clearReferencedSources = useCallback12(
3632
4001
  () => setReferencedSources([]),
3633
4002
  []
3634
4003
  );
3635
4004
  const add = usingProvider ? addWithProviderValidation : addLocal;
3636
4005
  const remove = usingProvider ? controller.attachments.remove : removeLocal;
3637
4006
  const openFileDialog = usingProvider ? controller.attachments.openFileDialog : openFileDialogLocal;
3638
- const clear = useCallback11(() => {
4007
+ const clear = useCallback12(() => {
3639
4008
  clearAttachments();
3640
4009
  clearReferencedSources();
3641
4010
  }, [clearAttachments, clearReferencedSources]);
@@ -3714,7 +4083,7 @@ var PromptInput = ({
3714
4083
  },
3715
4084
  [usingProvider]
3716
4085
  );
3717
- const handleChange = useCallback11(
4086
+ const handleChange = useCallback12(
3718
4087
  (event) => {
3719
4088
  if (event.currentTarget.files) {
3720
4089
  add(event.currentTarget.files);
@@ -3753,7 +4122,7 @@ var PromptInput = ({
3753
4122
  }),
3754
4123
  [referencedSources, clearReferencedSources]
3755
4124
  );
3756
- const handleSubmit = useCallback11(
4125
+ const handleSubmit = useCallback12(
3757
4126
  async (event) => {
3758
4127
  event.preventDefault();
3759
4128
  const form = event.currentTarget;
@@ -3837,7 +4206,7 @@ var PromptInputTextarea = ({
3837
4206
  const controller = useOptionalPromptInputController();
3838
4207
  const attachments = usePromptInputAttachments();
3839
4208
  const [isComposing, setIsComposing] = useState13(false);
3840
- const handleKeyDown = useCallback11(
4209
+ const handleKeyDown = useCallback12(
3841
4210
  (e) => {
3842
4211
  onKeyDown?.(e);
3843
4212
  if (e.defaultPrevented) {
@@ -3870,7 +4239,7 @@ var PromptInputTextarea = ({
3870
4239
  },
3871
4240
  [onKeyDown, isComposing, attachments]
3872
4241
  );
3873
- const handlePaste = useCallback11(
4242
+ const handlePaste = useCallback12(
3874
4243
  (event) => {
3875
4244
  const items = event.clipboardData?.items;
3876
4245
  if (!items) {
@@ -3892,8 +4261,8 @@ var PromptInputTextarea = ({
3892
4261
  },
3893
4262
  [attachments]
3894
4263
  );
3895
- const handleCompositionEnd = useCallback11(() => setIsComposing(false), []);
3896
- const handleCompositionStart = useCallback11(() => setIsComposing(true), []);
4264
+ const handleCompositionEnd = useCallback12(() => setIsComposing(false), []);
4265
+ const handleCompositionStart = useCallback12(() => setIsComposing(true), []);
3897
4266
  const controlledProps = controller ? {
3898
4267
  onChange: (e) => {
3899
4268
  controller.textInput.setInput(e.currentTarget.value);
@@ -3938,7 +4307,7 @@ var PromptInputSubmit = ({
3938
4307
  } else if (status === "error") {
3939
4308
  Icon = /* @__PURE__ */ jsx21(XIcon2, { className: "size-4" });
3940
4309
  }
3941
- const handleClick = useCallback11(
4310
+ const handleClick = useCallback12(
3942
4311
  (e) => {
3943
4312
  if (isGenerating && onStop) {
3944
4313
  e.preventDefault();
@@ -3975,7 +4344,7 @@ import {
3975
4344
  VideoIcon,
3976
4345
  XIcon as XIcon3
3977
4346
  } from "lucide-react";
3978
- import { createContext as createContext6, useCallback as useCallback12, useContext as useContext6, useMemo as useMemo10 } from "react";
4347
+ import { createContext as createContext6, useCallback as useCallback13, useContext as useContext6, useMemo as useMemo10 } from "react";
3979
4348
  import { jsx as jsx22, jsxs as jsxs20 } from "react/jsx-runtime";
3980
4349
  var mediaCategoryIcons = {
3981
4350
  audio: Music2Icon,
@@ -4152,7 +4521,7 @@ var AttachmentRemove = ({
4152
4521
  ...props
4153
4522
  }) => {
4154
4523
  const { onRemove, variant } = useAttachmentContext();
4155
- const handleClick = useCallback12(
4524
+ const handleClick = useCallback13(
4156
4525
  (e) => {
4157
4526
  e.stopPropagation();
4158
4527
  onRemove?.();
@@ -4195,7 +4564,7 @@ var AttachmentRemove = ({
4195
4564
  };
4196
4565
 
4197
4566
  // src/front/ChatPanel.tsx
4198
- import { PaperclipIcon as PaperclipIcon2, CopyIcon as CopyIcon3, CheckIcon as CheckIcon3, RefreshCwIcon, Loader2, AlertCircleIcon, XIcon as XIcon4 } from "lucide-react";
4567
+ import { PaperclipIcon as PaperclipIcon2, CopyIcon as CopyIcon3, CheckIcon as CheckIcon3, RefreshCwIcon, Loader2, AlertCircleIcon, XIcon as XIcon4, SquareArrowOutUpRight } from "lucide-react";
4199
4568
  import {
4200
4569
  Button as Button10,
4201
4570
  IconButton as IconButton2
@@ -4269,7 +4638,7 @@ function getReasoningPart(part) {
4269
4638
  }
4270
4639
 
4271
4640
  // src/front/useComposerHistory.ts
4272
- import { useCallback as useCallback13, useRef as useRef11 } from "react";
4641
+ import { useCallback as useCallback14, useRef as useRef12 } from "react";
4273
4642
  var HISTORY_RESET_IGNORED_KEYS = ["Shift", "Meta", "Control", "Alt", "CapsLock"];
4274
4643
  function setTextareaValue(textarea, value) {
4275
4644
  const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
@@ -4281,9 +4650,9 @@ function useComposerHistory({
4281
4650
  textareaRef,
4282
4651
  disabled
4283
4652
  }) {
4284
- const historyIdxRef = useRef11(-1);
4285
- const draftRef = useRef11("");
4286
- return useCallback13((e) => {
4653
+ const historyIdxRef = useRef12(-1);
4654
+ const draftRef = useRef12("");
4655
+ return useCallback14((e) => {
4287
4656
  const ta = e.currentTarget;
4288
4657
  textareaRef.current = ta;
4289
4658
  if (disabled) return;
@@ -4312,7 +4681,7 @@ function useComposerHistory({
4312
4681
  }
4313
4682
 
4314
4683
  // src/front/useComposerPickers.ts
4315
- import { useCallback as useCallback14, useState as useState14 } from "react";
4684
+ import { useCallback as useCallback15, useState as useState14 } from "react";
4316
4685
  function setTextareaValue2(textarea, value) {
4317
4686
  const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
4318
4687
  setter?.call(textarea, value);
@@ -4324,7 +4693,7 @@ function useComposerPickers({
4324
4693
  const [mentionState, setMentionState] = useState14(null);
4325
4694
  const [slashQuery, setSlashQuery] = useState14(null);
4326
4695
  const [mentionedFiles, setMentionedFiles] = useState14([]);
4327
- const handleComposerChange = useCallback14((e) => {
4696
+ const handleComposerChange = useCallback15((e) => {
4328
4697
  const ta = e.currentTarget;
4329
4698
  textareaRef.current = ta;
4330
4699
  const cursor = ta.selectionStart ?? ta.value.length;
@@ -4338,7 +4707,7 @@ function useComposerPickers({
4338
4707
  setMentionState(detectMention(ta.value, cursor));
4339
4708
  }
4340
4709
  }, [textareaRef]);
4341
- const selectMention = useCallback14((path) => {
4710
+ const selectMention = useCallback15((path) => {
4342
4711
  const ta = textareaRef.current;
4343
4712
  if (!ta || !mentionState) return;
4344
4713
  const { anchorStart, anchorEnd } = mentionState;
@@ -4351,7 +4720,7 @@ function useComposerPickers({
4351
4720
  setMentionState(null);
4352
4721
  setMentionedFiles((prev) => prev.includes(path) ? prev : [...prev, path]);
4353
4722
  }, [mentionState, textareaRef]);
4354
- const selectSlashCommand = useCallback14((name) => {
4723
+ const selectSlashCommand = useCallback15((name) => {
4355
4724
  const ta = textareaRef.current;
4356
4725
  if (!ta) return;
4357
4726
  const newValue = `/${name} `;
@@ -4360,13 +4729,13 @@ function useComposerPickers({
4360
4729
  ta.focus();
4361
4730
  setSlashQuery(null);
4362
4731
  }, [textareaRef]);
4363
- const clearMentionedFiles = useCallback14(() => {
4732
+ const clearMentionedFiles = useCallback15(() => {
4364
4733
  setMentionedFiles([]);
4365
4734
  }, []);
4366
- const dismissMention = useCallback14(() => {
4735
+ const dismissMention = useCallback15(() => {
4367
4736
  setMentionState(null);
4368
4737
  }, []);
4369
- const dismissSlash = useCallback14(() => {
4738
+ const dismissSlash = useCallback15(() => {
4370
4739
  setSlashQuery(null);
4371
4740
  }, []);
4372
4741
  return {
@@ -4437,7 +4806,7 @@ ${content}
4437
4806
  }
4438
4807
 
4439
4808
  // src/front/hooks/useChatModelSelection.ts
4440
- import { useCallback as useCallback15, useEffect as useEffect14, useMemo as useMemo11, useRef as useRef12, useState as useState15 } from "react";
4809
+ import { useCallback as useCallback16, useEffect as useEffect14, useMemo as useMemo11, useRef as useRef13, useState as useState15 } from "react";
4441
4810
 
4442
4811
  // src/front/chatPanelSettings.ts
4443
4812
  var STORAGE_MODEL_KEY = "boring-agent:composer:model";
@@ -4551,11 +4920,11 @@ function useChatModelSelection({
4551
4920
  const [userSelectedModel, setUserSelectedModel] = useState15(
4552
4921
  () => initialModelState.userSelected
4553
4922
  );
4554
- const userSelectedModelRef = useRef12(userSelectedModel);
4923
+ const userSelectedModelRef = useRef13(userSelectedModel);
4555
4924
  useEffect14(() => {
4556
4925
  userSelectedModelRef.current = userSelectedModel;
4557
4926
  }, [userSelectedModel]);
4558
- const setModel = useCallback15((next) => {
4927
+ const setModel = useCallback16((next) => {
4559
4928
  setUserSelectedModel(true);
4560
4929
  setModelState(next);
4561
4930
  }, []);
@@ -4961,7 +5330,7 @@ function KbdHints() {
4961
5330
 
4962
5331
  // src/front/ChatPanel.tsx
4963
5332
  import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
4964
- var DebugDrawer2 = lazy(() => import("../DebugDrawer-MCYZ4AWZ.js").then((m) => ({ default: m.DebugDrawer })));
5333
+ var DebugDrawer2 = lazy(() => import("../DebugDrawer-PX2PHXJH.js").then((m) => ({ default: m.DebugDrawer })));
4965
5334
  function hasToolPart(message) {
4966
5335
  return (message.parts ?? []).some((part) => isToolUIPart2(part));
4967
5336
  }
@@ -5090,6 +5459,18 @@ function composerNoticeForWarmup(status) {
5090
5459
  code: ErrorCode.enum.AGENT_RUNTIME_NOT_READY
5091
5460
  };
5092
5461
  }
5462
+ function composerNoticeForRuntimeDependencies(status) {
5463
+ const runtime = status?.runtimeDependencies;
5464
+ if (!runtime || runtime.state === "ready") return null;
5465
+ if (runtime.state === "failed") {
5466
+ return {
5467
+ title: "Runtime tools failed to prepare.",
5468
+ detail: runtime.message ?? "Chat still works, but dependency-backed tools may be unavailable.",
5469
+ code: ErrorCode.enum.RUNTIME_PROVISIONING_FAILED
5470
+ };
5471
+ }
5472
+ return null;
5473
+ }
5093
5474
  function isComposerRuntimeNotice(error) {
5094
5475
  return error?.code === ErrorCode.enum.AGENT_RUNTIME_NOT_READY || error?.code === ErrorCode.enum.RUNTIME_PROVISIONING_FAILED;
5095
5476
  }
@@ -5166,18 +5547,18 @@ function ChatPanel(props) {
5166
5547
  } = props;
5167
5548
  const [debugWidth, setDebugWidth] = useState20(440);
5168
5549
  const capabilities = PI_AGENT_RUNTIME_CAPABILITIES;
5169
- const scrollToBottomRef = useRef13(() => {
5550
+ const scrollToBottomRef = useRef14(() => {
5170
5551
  });
5171
- const piDataHandlerRef = useRef13(() => {
5552
+ const piDataHandlerRef = useRef14(() => {
5172
5553
  });
5173
- const followUpDataHandlerRef = useRef13(() => {
5554
+ const followUpDataHandlerRef = useRef14(() => {
5174
5555
  });
5175
- const autoSubmittedDraftRef = useRef13(void 0);
5176
- const autoSubmittingDraftRef = useRef13(void 0);
5177
- const pendingAutoSubmitUnlockRef = useRef13(void 0);
5178
- const pendingAutoSubmitSettleRef = useRef13(void 0);
5179
- const activeAutoSubmitSessionRef = useRef13(sessionId);
5180
- const liveSessionIdRef = useRef13(sessionId);
5556
+ const autoSubmittedDraftRef = useRef14(void 0);
5557
+ const autoSubmittingDraftRef = useRef14(void 0);
5558
+ const pendingAutoSubmitUnlockRef = useRef14(void 0);
5559
+ const pendingAutoSubmitSettleRef = useRef14(void 0);
5560
+ const activeAutoSubmitSessionRef = useRef14(sessionId);
5561
+ const liveSessionIdRef = useRef14(sessionId);
5181
5562
  liveSessionIdRef.current = sessionId;
5182
5563
  activeAutoSubmitSessionRef.current = sessionId;
5183
5564
  const [acceptedAutoSubmittedDraft, setAcceptedAutoSubmittedDraft] = useState20(void 0);
@@ -5196,7 +5577,8 @@ function ChatPanel(props) {
5196
5577
  status,
5197
5578
  error,
5198
5579
  stop,
5199
- clearError
5580
+ clearError,
5581
+ hydratingMessages
5200
5582
  } = useAgentChat({
5201
5583
  sessionId,
5202
5584
  onData: (part) => {
@@ -5238,11 +5620,15 @@ function ChatPanel(props) {
5238
5620
  const friendlyChatError = error ? friendlyError(error) : null;
5239
5621
  const runtimeErrorNotice = isComposerRuntimeNotice(friendlyChatError) ? friendlyChatError : null;
5240
5622
  const warmupNotice = composerNoticeForWarmup(workspaceWarmupStatus);
5241
- const composerStatusNotice = composerRuntimeNotice ?? runtimeErrorNotice ?? warmupNotice;
5623
+ const runtimeDependenciesNotice = composerNoticeForRuntimeDependencies(workspaceWarmupStatus);
5624
+ const composerStatusNotice = composerRuntimeNotice ?? runtimeErrorNotice ?? warmupNotice ?? runtimeDependenciesNotice;
5242
5625
  const workspaceWarmupBlocked = Boolean(warmupNotice);
5243
- const composerBlocked = workspaceWarmupBlocked || composerBlockers.length > 0;
5626
+ const hostComposerBlocked = composerBlockers.length > 0;
5627
+ const composerBlocked = workspaceWarmupBlocked || hostComposerBlocked;
5244
5628
  const primaryComposerBlocker = composerBlockers[0];
5245
5629
  const composerBlockerLabel = workspaceWarmupBlocked ? warmupNotice?.title ?? "Preparing workspace\u2026" : primaryComposerBlocker?.label ?? "Complete the pending workspace action to continue.";
5630
+ const composerSubmitStatus = hostComposerBlocked && !workspaceWarmupBlocked ? "streaming" : status;
5631
+ const activityBadgeLabel = hostComposerBlocked && !workspaceWarmupBlocked ? "Waiting for your answer\u2026" : "Working\u2026";
5246
5632
  const registry = useMemo12(
5247
5633
  () => {
5248
5634
  const effectiveBuiltins = hotReloadEnabled ? builtinCommands : builtinCommands.filter((cmd) => cmd.name !== "reload");
@@ -5259,7 +5645,7 @@ function ChatPanel(props) {
5259
5645
  });
5260
5646
  const { thinkingLevel, setThinkingLevel, showThoughts, setShowThoughts } = useThinkingSettings(thinkingControl);
5261
5647
  const { attachmentNotice, setAttachmentNotice } = useAttachmentNotice();
5262
- const callPluginReload = useCallback16(async () => {
5648
+ const callPluginReload = useCallback17(async () => {
5263
5649
  const res = await fetch("/api/v1/agent/reload", {
5264
5650
  method: "POST",
5265
5651
  headers: { ...requestHeaders ?? {}, "content-type": "application/json" },
@@ -5279,7 +5665,7 @@ function ChatPanel(props) {
5279
5665
  ...payload.diagnostics && payload.diagnostics.length > 0 ? { diagnostics: payload.diagnostics } : {}
5280
5666
  };
5281
5667
  }, [requestHeaders, sessionId]);
5282
- const reloadAgentPlugins = useCallback16(async () => {
5668
+ const reloadAgentPlugins = useCallback17(async () => {
5283
5669
  try {
5284
5670
  const { reloaded, diagnostics } = await callPluginReload();
5285
5671
  const base = reloaded ? "Agent plugins reloaded." : "Agent plugins will reload on the next message.";
@@ -5292,7 +5678,7 @@ ${formatPluginReloadDiagnostics(diagnostics)}` : base;
5292
5678
  }
5293
5679
  }, [callPluginReload]);
5294
5680
  const [pluginUpdateState, setPluginUpdateState] = useState20(null);
5295
- const activeSessionRef = useRef13(sessionId);
5681
+ const activeSessionRef = useRef14(sessionId);
5296
5682
  useEffect18(() => {
5297
5683
  activeSessionRef.current = sessionId;
5298
5684
  setPluginUpdateState(null);
@@ -5323,7 +5709,7 @@ ${formatPluginReloadDiagnostics(diagnostics)}` : base;
5323
5709
  window.addEventListener(WORKSPACE_AGENT_PLUGINS_RELOADED_EVENT, onBrowserPluginReload);
5324
5710
  return () => window.removeEventListener(WORKSPACE_AGENT_PLUGINS_RELOADED_EVENT, onBrowserPluginReload);
5325
5711
  }, []);
5326
- const runPluginUpdate = useCallback16(async () => {
5712
+ const runPluginUpdate = useCallback17(async () => {
5327
5713
  const capturedSession = activeSessionRef.current;
5328
5714
  setPluginUpdateState({ kind: "running" });
5329
5715
  try {
@@ -5347,7 +5733,7 @@ ${formatPluginReloadDiagnostics(diagnostics)}` : base;
5347
5733
  return `Plugin update failed: ${message}`;
5348
5734
  }
5349
5735
  }, [callPluginReload]);
5350
- const dismissPluginUpdate = useCallback16(() => setPluginUpdateState(null), []);
5736
+ const dismissPluginUpdate = useCallback17(() => setPluginUpdateState(null), []);
5351
5737
  const isStreaming = status === "submitted" || status === "streaming";
5352
5738
  const attachmentsDisabled = isStreaming || pendingMessages.length > 0;
5353
5739
  const displayMessages = useMemo12(() => {
@@ -5396,32 +5782,36 @@ ${formatPluginReloadDiagnostics(diagnostics)}` : base;
5396
5782
  ];
5397
5783
  }, [acceptedAutoSubmittedDraft, messages, piMessages, projectedTailMessages, projectedStatusById, sessionId, status]);
5398
5784
  const renderMessages = displayMessages.filter((message) => message.role !== "assistant" || hasVisibleMessageContent(message));
5399
- const emptyHero = emptyPlacement === "hero" && renderMessages.length === 0;
5400
- const handleStop = useCallback16(() => {
5785
+ const emptyHero = emptyPlacement === "hero" && renderMessages.length === 0 && !hydratingMessages;
5786
+ const handleStop = useCallback17(() => {
5401
5787
  onComposerStop?.();
5788
+ if (hostComposerBlocked && !workspaceWarmupBlocked) return;
5402
5789
  stopAndClearFollowUps();
5403
- }, [onComposerStop, stopAndClearFollowUps]);
5404
- const handleInterrupt = useCallback16(() => {
5790
+ }, [hostComposerBlocked, onComposerStop, stopAndClearFollowUps, workspaceWarmupBlocked]);
5791
+ const handleInterrupt = useCallback17(() => {
5405
5792
  stop();
5406
5793
  }, [stop]);
5407
5794
  useEffect18(() => {
5408
5795
  const onKeyDown = (e) => {
5409
5796
  if (e.key !== "Escape" || !isStreaming) return;
5410
- const tag = e.target?.tagName;
5797
+ const target = e.target instanceof HTMLElement ? e.target : null;
5798
+ const tag = target?.tagName;
5411
5799
  if (tag === "INPUT" || tag === "TEXTAREA") return;
5800
+ if (target?.closest('[role="dialog"], [role="menu"], [role="listbox"]')) return;
5412
5801
  e.preventDefault();
5802
+ e.stopPropagation();
5413
5803
  handleInterrupt();
5414
5804
  };
5415
- window.addEventListener("keydown", onKeyDown);
5416
- return () => window.removeEventListener("keydown", onKeyDown);
5805
+ window.addEventListener("keydown", onKeyDown, { capture: true });
5806
+ return () => window.removeEventListener("keydown", onKeyDown, { capture: true });
5417
5807
  }, [isStreaming, handleInterrupt]);
5418
5808
  const userHistory = useMemo12(
5419
5809
  () => messages.filter((m) => m.role === "user").map((m) => m.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n").trim()).filter(Boolean),
5420
5810
  [messages]
5421
5811
  );
5422
- const textareaRef = useRef13(null);
5812
+ const textareaRef = useRef14(null);
5423
5813
  const promptInputController = useOptionalPromptInputController();
5424
- const setComposerDraft = useCallback16((draft, focus = true) => {
5814
+ const setComposerDraft = useCallback17((draft, focus = true) => {
5425
5815
  promptInputController?.textInput.setInput(draft);
5426
5816
  if (textareaRef.current) {
5427
5817
  textareaRef.current.value = draft;
@@ -5444,7 +5834,7 @@ ${formatPluginReloadDiagnostics(diagnostics)}` : base;
5444
5834
  textareaRef,
5445
5835
  disabled: mentionState !== null || slashQuery !== null
5446
5836
  });
5447
- const restoredDraftRef = useRef13(void 0);
5837
+ const restoredDraftRef = useRef14(void 0);
5448
5838
  useEffect18(() => {
5449
5839
  if (initialDraft === void 0) return;
5450
5840
  if (restoredDraftRef.current === initialDraft) return;
@@ -5473,6 +5863,7 @@ ${formatPluginReloadDiagnostics(diagnostics)}` : base;
5473
5863
  if (parsed) {
5474
5864
  const cmd = registry.get(parsed.name);
5475
5865
  if (cmd?.kind === "skill") {
5866
+ dismissSlash();
5476
5867
  const skillMessage = parsed.args ? `skill: ${parsed.name}
5477
5868
 
5478
5869
  ${parsed.args}` : `skill: ${parsed.name}`;
@@ -5484,6 +5875,7 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5484
5875
  return;
5485
5876
  }
5486
5877
  if (cmd) {
5878
+ dismissSlash();
5487
5879
  const ctx = {
5488
5880
  sessionId,
5489
5881
  clearMessages: () => setMessages([]),
@@ -5568,7 +5960,7 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5568
5960
  pendingAutoSubmitUnlockRef.current = void 0;
5569
5961
  onAutoSubmitInitialDraftAccepted?.();
5570
5962
  }, [messages, onAutoSubmitInitialDraftAccepted, status]);
5571
- const prevAutoSubmitStatusRef = useRef13(status);
5963
+ const prevAutoSubmitStatusRef = useRef14(status);
5572
5964
  useEffect18(() => {
5573
5965
  const prev = prevAutoSubmitStatusRef.current;
5574
5966
  prevAutoSubmitStatusRef.current = status;
@@ -5661,7 +6053,11 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5661
6053
  chrome ? "max-w-3xl px-6 py-8" : "max-w-[680px] px-4 py-4",
5662
6054
  emptyHero && "py-4 text-center"
5663
6055
  ), children: [
5664
- renderMessages.length === 0 && /* @__PURE__ */ jsx25(
6056
+ renderMessages.length === 0 && hydratingMessages && /* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-2 rounded-xl border border-border bg-card/70 px-4 py-3 text-sm text-muted-foreground", children: [
6057
+ /* @__PURE__ */ jsx25(Loader2, { className: "h-4 w-4 animate-spin" }),
6058
+ "Loading chat history\u2026"
6059
+ ] }),
6060
+ renderMessages.length === 0 && !hydratingMessages && /* @__PURE__ */ jsx25(
5665
6061
  ChatEmptyState,
5666
6062
  {
5667
6063
  eyebrow: emptyState?.eyebrow,
@@ -5923,13 +6319,12 @@ ${reasoningPart.text}`;
5923
6319
  }
5924
6320
  ),
5925
6321
  /* @__PURE__ */ jsxs23("div", { className: cn(chrome ? "px-4 pb-4 pt-2 sm:px-6 sm:pb-5" : "px-3 pb-3 pt-1"), children: [
5926
- /* @__PURE__ */ jsx25(
6322
+ (isStreaming || hostComposerBlocked && !workspaceWarmupBlocked) && /* @__PURE__ */ jsx25(
5927
6323
  "div",
5928
6324
  {
5929
6325
  className: cn(
5930
- "mx-auto w-full overflow-hidden transition-all duration-300",
5931
- chrome ? "max-w-3xl" : "max-w-[680px]",
5932
- isStreaming ? "mb-2 max-h-8" : "max-h-0"
6326
+ "mx-auto mb-2 w-full overflow-hidden transition-all duration-300 max-h-8",
6327
+ chrome ? "max-w-3xl" : "max-w-[680px]"
5933
6328
  ),
5934
6329
  children: /* @__PURE__ */ jsxs23(
5935
6330
  "div",
@@ -5950,7 +6345,7 @@ ${reasoningPart.text}`;
5950
6345
  transition: { duration: 1.2, repeat: Infinity, ease: "easeInOut" }
5951
6346
  }
5952
6347
  ),
5953
- /* @__PURE__ */ jsx25("span", { children: "Working\u2026" })
6348
+ /* @__PURE__ */ jsx25("span", { children: activityBadgeLabel })
5954
6349
  ]
5955
6350
  }
5956
6351
  )
@@ -5975,28 +6370,34 @@ ${reasoningPart.text}`;
5975
6370
  ] })
5976
6371
  }
5977
6372
  ),
5978
- composerBlocked && !workspaceWarmupBlocked && /* @__PURE__ */ jsxs23(
6373
+ composerBlocked && !workspaceWarmupBlocked && /* @__PURE__ */ jsx25(
5979
6374
  "div",
5980
6375
  {
5981
6376
  role: "status",
5982
6377
  "aria-live": "polite",
5983
6378
  className: cn(
5984
- "mx-auto mb-2 w-full max-w-3xl rounded-[var(--radius-md)] border border-primary/30 bg-primary/10",
5985
- "px-3 py-2 text-xs text-foreground"
6379
+ "mx-auto mb-2 w-full rounded-[var(--radius-md)] border border-primary/30 bg-primary/10",
6380
+ "px-3 py-2 text-xs text-foreground",
6381
+ chrome ? "max-w-3xl" : "max-w-[680px]"
5986
6382
  ),
5987
- children: [
5988
- /* @__PURE__ */ jsx25("span", { children: composerBlockerLabel }),
5989
- primaryComposerBlocker?.actions?.map((action) => /* @__PURE__ */ jsx25(
5990
- "button",
5991
- {
5992
- type: "button",
5993
- className: "ml-2 rounded border border-primary/30 px-2 py-0.5 text-[11px] font-medium hover:bg-primary/10",
5994
- onClick: () => onComposerBlockerAction?.(primaryComposerBlocker, action.id),
5995
- children: action.label
5996
- },
5997
- action.id
5998
- ))
5999
- ]
6383
+ children: /* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-2", children: [
6384
+ /* @__PURE__ */ jsx25("span", { className: "min-w-0 flex-1 truncate", children: composerBlockerLabel }),
6385
+ primaryComposerBlocker?.actions?.map((action) => {
6386
+ const ActionIcon = action.id === "open" ? SquareArrowOutUpRight : action.id === "cancel" ? XIcon4 : null;
6387
+ return /* @__PURE__ */ jsx25(
6388
+ "button",
6389
+ {
6390
+ type: "button",
6391
+ "aria-label": action.label,
6392
+ title: action.label,
6393
+ className: "inline-flex size-6 shrink-0 items-center justify-center rounded border border-primary/30 text-primary hover:bg-primary/10",
6394
+ onClick: () => onComposerBlockerAction?.(primaryComposerBlocker, action.id),
6395
+ children: ActionIcon ? /* @__PURE__ */ jsx25(ActionIcon, { "aria-hidden": "true", className: "size-3.5" }) : /* @__PURE__ */ jsx25("span", { className: "px-1 text-[11px] font-medium", children: action.label })
6396
+ },
6397
+ action.id
6398
+ );
6399
+ })
6400
+ ] })
6000
6401
  }
6001
6402
  ),
6002
6403
  hotReloadEnabled && /* @__PURE__ */ jsx25(
@@ -6004,7 +6405,8 @@ ${reasoningPart.text}`;
6004
6405
  {
6005
6406
  state: pluginUpdateState,
6006
6407
  onDismiss: dismissPluginUpdate,
6007
- onRetry: runPluginUpdate
6408
+ onRetry: runPluginUpdate,
6409
+ maxWidthClassName: chrome ? "max-w-3xl" : "max-w-[680px]"
6008
6410
  }
6009
6411
  ),
6010
6412
  attachmentNotice && /* @__PURE__ */ jsx25(
@@ -6149,9 +6551,9 @@ ${reasoningPart.text}`;
6149
6551
  /* @__PURE__ */ jsx25(
6150
6552
  PromptInputSubmit,
6151
6553
  {
6152
- status,
6554
+ status: composerSubmitStatus,
6153
6555
  onStop: handleStop,
6154
- disabled: composerBlocked && !isStreaming,
6556
+ disabled: workspaceWarmupBlocked,
6155
6557
  className: cn(
6156
6558
  // Primary action. Uses the warm accent (not `primary`,
6157
6559
  // which is a neutral foreground tone) — this is the one
@@ -6366,30 +6768,8 @@ function MessageActionsBar({
6366
6768
  };
6367
6769
  const handleCopy = async () => {
6368
6770
  if (!visible) return;
6369
- if (typeof navigator !== "undefined" && navigator.clipboard && window.isSecureContext) {
6370
- try {
6371
- await navigator.clipboard.writeText(text);
6372
- markCopied();
6373
- return;
6374
- } catch {
6375
- }
6376
- }
6377
- if (typeof document === "undefined") return;
6378
- const ta = document.createElement("textarea");
6379
- ta.value = text;
6380
- ta.setAttribute("readonly", "");
6381
- ta.style.position = "fixed";
6382
- ta.style.opacity = "0";
6383
- ta.style.pointerEvents = "none";
6384
- document.body.appendChild(ta);
6385
- ta.select();
6386
- try {
6387
- const ok = document.execCommand("copy");
6388
- if (ok) markCopied();
6389
- } catch {
6390
- } finally {
6391
- document.body.removeChild(ta);
6392
- }
6771
+ const ok = await copyTextToClipboard(text);
6772
+ if (ok) markCopied();
6393
6773
  };
6394
6774
  const iconActionBtnClass = cn(
6395
6775
  "inline-flex h-6 w-6 items-center justify-center rounded-[var(--radius-sm)]",
@@ -6453,7 +6833,7 @@ function getAgentCommands(options = {}) {
6453
6833
  }
6454
6834
 
6455
6835
  // src/front/hooks/useSessions.ts
6456
- import { useState as useState21, useEffect as useEffect19, useCallback as useCallback17, useMemo as useMemo13, useRef as useRef14 } from "react";
6836
+ import { useState as useState21, useEffect as useEffect19, useCallback as useCallback18, useMemo as useMemo13, useRef as useRef15 } from "react";
6457
6837
  var API_BASE = "/api/v1/agent/sessions";
6458
6838
  var STORAGE_KEY = "boring-agent:activeSessionId";
6459
6839
  function readPersistedId(storageKey) {
@@ -6477,9 +6857,23 @@ function requestInit(headers) {
6477
6857
  if (!headers || Object.keys(headers).length === 0) return void 0;
6478
6858
  return { headers };
6479
6859
  }
6860
+ var SessionsPreparingError = class extends Error {
6861
+ constructor() {
6862
+ super("Agent runtime is still preparing");
6863
+ this.name = "SessionsPreparingError";
6864
+ }
6865
+ };
6866
+ var MAX_SESSIONS_RETRIES = 8;
6867
+ function retryDelayMs(attempt) {
6868
+ return Math.min(250 * 2 ** attempt, 2e3);
6869
+ }
6870
+ function delay(ms) {
6871
+ return new Promise((resolve) => setTimeout(resolve, ms));
6872
+ }
6480
6873
  async function fetchSessions(headers) {
6481
6874
  const init = requestInit(headers);
6482
6875
  const res = init ? await fetch(API_BASE, init) : await fetch(API_BASE);
6876
+ if (res.status === 503) throw new SessionsPreparingError();
6483
6877
  if (!res.ok) throw new Error(`Failed to load sessions: ${res.status}`);
6484
6878
  return res.json();
6485
6879
  }
@@ -6488,6 +6882,7 @@ function useSessions(opts = {}) {
6488
6882
  const requestHeaders = opts.requestHeaders;
6489
6883
  const enabled = opts.enabled ?? true;
6490
6884
  const refreshKey = opts.refreshKey;
6885
+ const initialActiveSessionId = opts.initialActiveSessionId;
6491
6886
  const scopeKey = useMemo13(
6492
6887
  () => `${storageKey}
6493
6888
  ${headersScopeKey(requestHeaders)}`,
@@ -6495,15 +6890,24 @@ ${headersScopeKey(requestHeaders)}`,
6495
6890
  );
6496
6891
  const [sessions, setSessions] = useState21([]);
6497
6892
  const [activeSessionId, setActiveSessionId] = useState21(
6498
- () => readPersistedId(storageKey)
6893
+ () => initialActiveSessionId ?? readPersistedId(storageKey)
6499
6894
  );
6500
6895
  const [loading, setLoading] = useState21(true);
6501
6896
  const [error, setError] = useState21();
6502
6897
  const [loaded, setLoaded] = useState21(false);
6503
- const versionRef = useRef14(0);
6504
- const loadedScopeRef = useRef14(scopeKey);
6505
- const refresh = useCallback17(async () => {
6898
+ const versionRef = useRef15(0);
6899
+ const loadedScopeRef = useRef15(scopeKey);
6900
+ const pendingCreatedSessionsRef = useRef15(/* @__PURE__ */ new Map());
6901
+ const pendingCreatedScopeRef = useRef15(scopeKey);
6902
+ const mountedRef = useRef15(true);
6903
+ function ensurePendingCreatedScope() {
6904
+ if (pendingCreatedScopeRef.current === scopeKey) return;
6905
+ pendingCreatedScopeRef.current = scopeKey;
6906
+ pendingCreatedSessionsRef.current.clear();
6907
+ }
6908
+ const refresh = useCallback18(async () => {
6506
6909
  const v = ++versionRef.current;
6910
+ const isCurrent = () => v === versionRef.current && mountedRef.current;
6507
6911
  if (!enabled) {
6508
6912
  setSessions([]);
6509
6913
  setActiveSessionId(void 0);
@@ -6515,25 +6919,45 @@ ${headersScopeKey(requestHeaders)}`,
6515
6919
  setLoaded(false);
6516
6920
  setLoading(true);
6517
6921
  try {
6518
- const data = await fetchSessions(requestHeaders);
6519
- if (v === versionRef.current) {
6922
+ let data;
6923
+ for (let attempt = 0; ; attempt++) {
6924
+ try {
6925
+ data = await fetchSessions(requestHeaders);
6926
+ break;
6927
+ } catch (err) {
6928
+ const retryable = err instanceof SessionsPreparingError && attempt < MAX_SESSIONS_RETRIES;
6929
+ if (!retryable) throw err;
6930
+ if (!isCurrent()) return;
6931
+ await delay(retryDelayMs(attempt));
6932
+ if (!isCurrent()) return;
6933
+ }
6934
+ }
6935
+ if (isCurrent() && data) {
6936
+ ensurePendingCreatedScope();
6937
+ const pendingCreatedSessions = pendingCreatedSessionsRef.current;
6938
+ for (const session of data) pendingCreatedSessions.delete(session.id);
6939
+ const serverIds = new Set(data.map((session) => session.id));
6940
+ const mergedData = pendingCreatedSessions.size > 0 ? [
6941
+ ...Array.from(pendingCreatedSessions.values()).filter((session) => !serverIds.has(session.id)),
6942
+ ...data
6943
+ ] : data;
6520
6944
  const replacingLoadedScope = loadedScopeRef.current !== scopeKey;
6521
6945
  loadedScopeRef.current = scopeKey;
6522
- const persisted = readPersistedId(storageKey);
6946
+ const persisted = initialActiveSessionId ?? readPersistedId(storageKey);
6523
6947
  setError(void 0);
6524
6948
  setLoaded(true);
6525
- setSessions(data);
6949
+ setSessions(mergedData);
6526
6950
  setActiveSessionId((prev) => {
6527
6951
  const preferred = replacingLoadedScope ? persisted : prev ?? persisted;
6528
- if (preferred && data.some((session) => session.id === preferred)) return preferred;
6529
- const next = data[0]?.id;
6952
+ if (preferred && mergedData.some((session) => session.id === preferred)) return preferred;
6953
+ const next = mergedData[0]?.id;
6530
6954
  persistId(storageKey, next);
6531
6955
  return next;
6532
6956
  });
6533
6957
  setLoading(false);
6534
6958
  }
6535
6959
  } catch (err) {
6536
- if (v === versionRef.current) {
6960
+ if (isCurrent()) {
6537
6961
  const replacingLoadedScope = loadedScopeRef.current !== scopeKey;
6538
6962
  loadedScopeRef.current = scopeKey;
6539
6963
  if (replacingLoadedScope) {
@@ -6545,8 +6969,9 @@ ${headersScopeKey(requestHeaders)}`,
6545
6969
  setLoading(false);
6546
6970
  }
6547
6971
  }
6548
- }, [enabled, requestHeaders, scopeKey, storageKey]);
6972
+ }, [enabled, initialActiveSessionId, requestHeaders, scopeKey, storageKey]);
6549
6973
  useEffect19(() => {
6974
+ mountedRef.current = true;
6550
6975
  if (!enabled) {
6551
6976
  setSessions([]);
6552
6977
  setActiveSessionId(void 0);
@@ -6556,8 +6981,11 @@ ${headersScopeKey(requestHeaders)}`,
6556
6981
  return;
6557
6982
  }
6558
6983
  void refresh();
6984
+ return () => {
6985
+ mountedRef.current = false;
6986
+ };
6559
6987
  }, [enabled, refresh, refreshKey, scopeKey]);
6560
- const create = useCallback17(
6988
+ const create = useCallback18(
6561
6989
  async (init) => {
6562
6990
  if (!enabled) throw new Error("Sessions are disabled");
6563
6991
  const res = await fetch(API_BASE, {
@@ -6571,21 +6999,25 @@ ${headersScopeKey(requestHeaders)}`,
6571
6999
  throw err;
6572
7000
  }
6573
7001
  const session = await res.json();
6574
- setSessions((prev) => [session, ...prev]);
7002
+ ensurePendingCreatedScope();
7003
+ pendingCreatedSessionsRef.current.set(session.id, session);
7004
+ setSessions((prev) => [session, ...prev.filter((existing) => existing.id !== session.id)]);
6575
7005
  setActiveSessionId(session.id);
6576
7006
  persistId(storageKey, session.id);
6577
7007
  void refresh();
6578
7008
  return session;
6579
7009
  },
6580
- [enabled, refresh, requestHeaders, storageKey]
7010
+ [enabled, refresh, requestHeaders, scopeKey, storageKey]
6581
7011
  );
6582
- const switchSession = useCallback17((id) => {
7012
+ const switchSession = useCallback18((id) => {
6583
7013
  setActiveSessionId(id);
6584
7014
  persistId(storageKey, id);
6585
7015
  }, [storageKey]);
6586
- const deleteSession = useCallback17(
7016
+ const deleteSession = useCallback18(
6587
7017
  async (id) => {
6588
7018
  if (!enabled) throw new Error("Sessions are disabled");
7019
+ ensurePendingCreatedScope();
7020
+ pendingCreatedSessionsRef.current.delete(id);
6589
7021
  setSessions((prev) => prev.filter((s) => s.id !== id));
6590
7022
  setActiveSessionId((prev) => {
6591
7023
  if (prev === id) {
@@ -6609,7 +7041,7 @@ ${headersScopeKey(requestHeaders)}`,
6609
7041
  }
6610
7042
  void refresh();
6611
7043
  },
6612
- [enabled, refresh, requestHeaders, storageKey]
7044
+ [enabled, refresh, requestHeaders, scopeKey, storageKey]
6613
7045
  );
6614
7046
  const scopeMatches = loadedScopeRef.current === scopeKey;
6615
7047
  const visibleSessions = enabled && scopeMatches ? sessions : [];