@hachej/boring-agent 0.1.23 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,9 @@ import {
3
3
  WORKSPACE_AGENT_PLUGINS_RELOADED_EVENT,
4
4
  extractToolUiMetadata
5
5
  } from "../chunk-B5JECXMG.js";
6
+ import {
7
+ ErrorCode
8
+ } from "../chunk-FILARC5X.js";
6
9
  import {
7
10
  DebugDrawer,
8
11
  cn
@@ -256,6 +259,7 @@ import { DefaultChatTransport } from "ai";
256
259
  import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState3 } from "react";
257
260
  function useAgentChat(opts) {
258
261
  const { sessionId } = opts;
262
+ const hydrateMessages = opts.hydrateMessages ?? true;
259
263
  const optsRef = useRef4(opts);
260
264
  optsRef.current = opts;
261
265
  const transport = useMemo2(
@@ -273,7 +277,7 @@ function useAgentChat(opts) {
273
277
  const chat = useChat({
274
278
  id: sessionId,
275
279
  transport,
276
- resume: true,
280
+ resume: hydrateMessages,
277
281
  // Match AI SDK's documented React smoothing knob: render at most every
278
282
  // ~50ms while chunks stream instead of once per incoming chunk. This only
279
283
  // throttles AI SDK's own messages store; pi's custom data-pi projection
@@ -288,6 +292,10 @@ function useAgentChat(opts) {
288
292
  const [hydrated, setHydrated] = useState3(false);
289
293
  useEffect4(() => {
290
294
  if (!sessionId || !cacheKey) return;
295
+ if (!hydrateMessages) {
296
+ setHydrated(true);
297
+ return;
298
+ }
291
299
  let aborted = false;
292
300
  setHydrated(false);
293
301
  const loadFromCache = () => {
@@ -306,24 +314,30 @@ function useAgentChat(opts) {
306
314
  const request = fetchOpts ? fetch(messagesUrl, fetchOpts) : fetch(messagesUrl);
307
315
  request.then((res) => res.ok ? res.json() : null).then((payload) => {
308
316
  if (aborted) return;
317
+ const localTurnStarted = statusRef.current === "submitted" || statusRef.current === "streaming" || messagesRef.current.length > 0;
309
318
  const serverMessages = payload?.messages;
310
319
  if (Array.isArray(serverMessages) && serverMessages.length > 0) {
311
- setMessages(serverMessages);
320
+ if (!localTurnStarted) setMessages(serverMessages);
312
321
  return;
313
322
  }
314
- loadFromCache();
323
+ if (!localTurnStarted) loadFromCache();
315
324
  }).catch(() => {
316
325
  if (aborted) return;
317
- loadFromCache();
326
+ const localTurnStarted = statusRef.current === "submitted" || statusRef.current === "streaming" || messagesRef.current.length > 0;
327
+ if (!localTurnStarted) loadFromCache();
318
328
  }).finally(() => {
319
329
  if (!aborted) setHydrated(true);
320
330
  });
321
331
  return () => {
322
332
  aborted = true;
323
333
  };
324
- }, [sessionId, cacheKey, setMessages]);
334
+ }, [hydrateMessages, sessionId, cacheKey, setMessages]);
325
335
  const messages = chat.messages;
326
336
  const status = chat.status;
337
+ const messagesRef = useRef4(messages);
338
+ const statusRef = useRef4(status);
339
+ messagesRef.current = messages;
340
+ statusRef.current = status;
327
341
  useEffect4(() => {
328
342
  if (opts.persistMessages === false) return;
329
343
  if (!hydrated || !cacheKey) return;
@@ -444,6 +458,20 @@ function applyBufferedDeltas(items, deltas) {
444
458
  items
445
459
  );
446
460
  }
461
+ function messageVisibleText(message) {
462
+ return (message.parts ?? []).filter((part) => part.type === "text").map((part) => "text" in part && typeof part.text === "string" ? part.text : "").join("\n").trim();
463
+ }
464
+ function countExistingSdkUserTexts(messages) {
465
+ const counts = /* @__PURE__ */ new Map();
466
+ for (const message of messages) {
467
+ if (message.role !== "user") continue;
468
+ if ((message.parts ?? []).some((part) => typeof part.type === "string" && part.type.startsWith("data-pi-"))) continue;
469
+ const text = messageVisibleText(message);
470
+ if (!text) continue;
471
+ counts.set(text, (counts.get(text) ?? 0) + 1);
472
+ }
473
+ return counts;
474
+ }
447
475
  function rebuildPiMessagesFromDataParts(sourceMessages) {
448
476
  const dataParts = sourceMessages.flatMap((message) => message.parts ?? []).map(asPiDataPart).filter(Boolean);
449
477
  if (dataParts.length === 0) return [];
@@ -535,7 +563,16 @@ function mergeRebuiltPiMessages(existing, rebuilt) {
535
563
  if (rebuiltIds.has(message.id)) return false;
536
564
  return !(message.parts ?? []).some((part) => typeof part.type === "string" && part.type.startsWith("data-pi-"));
537
565
  });
538
- return [...preserved, ...rebuilt];
566
+ const sdkUserTextCounts = countExistingSdkUserTexts(preserved);
567
+ const rebuiltMissingFromSdk = rebuilt.filter((message) => {
568
+ if (message.role !== "user") return true;
569
+ const text = messageVisibleText(message);
570
+ const alreadyRepresentedCount = text ? sdkUserTextCounts.get(text) ?? 0 : 0;
571
+ if (alreadyRepresentedCount <= 0) return true;
572
+ sdkUserTextCounts.set(text, alreadyRepresentedCount - 1);
573
+ return false;
574
+ });
575
+ return [...preserved, ...rebuiltMissingFromSdk];
539
576
  }
540
577
  function usePiChatProjection({
541
578
  messages,
@@ -709,8 +746,7 @@ function usePiChatProjection({
709
746
  if (status !== "ready") return;
710
747
  if (prev !== "streaming" && prev !== "submitted") return;
711
748
  if (!sessionId || piMessages.length === 0) return;
712
- const canonicalMessages = rebuildPiMessagesFromDataParts(piMessages);
713
- const messagesToPersist = canonicalMessages.length > 0 ? mergeRebuiltPiMessages(piMessages, canonicalMessages) : piMessages;
749
+ const messagesToPersist = mergeRebuiltPiMessages(messages, piMessages);
714
750
  const stripped = messagesToPersist.map((msg) => ({
715
751
  ...msg,
716
752
  parts: msg.parts?.filter((part) => {
@@ -1760,6 +1796,38 @@ var ShimmerComponent = ({
1760
1796
  };
1761
1797
  var Shimmer = memo(ShimmerComponent);
1762
1798
 
1799
+ // src/front/workspaceReadinessStatus.ts
1800
+ var COPY = {
1801
+ "workspace-fs": "Files are still loading.",
1802
+ "sandbox-exec": "Sandbox is still waking.",
1803
+ "ui-bridge": "Workspace UI is still connecting."
1804
+ };
1805
+ var VALID_REQUIREMENTS = /* @__PURE__ */ new Set([
1806
+ "workspace-fs",
1807
+ "sandbox-exec",
1808
+ "ui-bridge"
1809
+ ]);
1810
+ function detailsFromOutput(output) {
1811
+ if (!output || typeof output !== "object") return null;
1812
+ const record = output;
1813
+ const details = record.details && typeof record.details === "object" ? record.details : record;
1814
+ return details;
1815
+ }
1816
+ function getWorkspaceNotReadyStatus(output) {
1817
+ const details = detailsFromOutput(output);
1818
+ if (!details) return null;
1819
+ if (details.code !== ErrorCode.enum.WORKSPACE_NOT_READY) return null;
1820
+ if (details.retryable !== true) return null;
1821
+ const requirement = details.requirement;
1822
+ if (typeof requirement !== "string" || !VALID_REQUIREMENTS.has(requirement)) return null;
1823
+ return {
1824
+ code: ErrorCode.enum.WORKSPACE_NOT_READY,
1825
+ retryable: true,
1826
+ requirement,
1827
+ message: COPY[requirement]
1828
+ };
1829
+ }
1830
+
1763
1831
  // src/front/primitives/tool-call-group.tsx
1764
1832
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1765
1833
  function isSettledState(state) {
@@ -1795,10 +1863,14 @@ var ToolCallGroup = memo2(({ tools, mergedToolRenderers }) => {
1795
1863
  if (!isToolUIPart(part)) return true;
1796
1864
  return isSettledState(part.state);
1797
1865
  });
1866
+ const workspaceNotReady = tools.map(({ part }) => {
1867
+ if (!isToolUIPart(part)) return null;
1868
+ return getWorkspaceNotReadyStatus(part.output);
1869
+ }).find(Boolean);
1798
1870
  const hasError = tools.some(({ part }) => {
1799
1871
  if (!isToolUIPart(part)) return false;
1800
1872
  return part.state === "output-error";
1801
- });
1873
+ }) && !workspaceNotReady;
1802
1874
  const [isOpen, setIsOpen] = useState9(false);
1803
1875
  const handleOpenChange = useCallback6((open) => setIsOpen(open), []);
1804
1876
  const title = useMemo5(() => buildTitle(tools, isSettled), [tools, isSettled]);
@@ -1831,7 +1903,7 @@ var ToolCallGroup = memo2(({ tools, mergedToolRenderers }) => {
1831
1903
  )
1832
1904
  }
1833
1905
  ),
1834
- !isSettled ? /* @__PURE__ */ jsx10(Shimmer, { as: "span", duration: 1.5, children: title }) : /* @__PURE__ */ jsx10("span", { children: title }),
1906
+ workspaceNotReady ? /* @__PURE__ */ jsx10("span", { children: workspaceNotReady.message }) : !isSettled ? /* @__PURE__ */ jsx10(Shimmer, { as: "span", duration: 1.5, children: title }) : /* @__PURE__ */ jsx10("span", { children: title }),
1835
1907
  /* @__PURE__ */ jsx10("span", { className: cn(
1836
1908
  "ml-1 shrink-0 rounded-sm border border-border/40 px-1 tabular-nums",
1837
1909
  "text-[10px] text-muted-foreground/50"
@@ -2498,6 +2570,17 @@ function PathLabel({ path }) {
2498
2570
  }
2499
2571
  );
2500
2572
  }
2573
+ function renderWorkspaceNotReady(part) {
2574
+ const status = getWorkspaceNotReadyStatus(part.output);
2575
+ if (!status) return null;
2576
+ return /* @__PURE__ */ jsxs13(Tool2, { children: [
2577
+ /* @__PURE__ */ jsx15(ToolHeader, { title: `${part.toolName} \xB7 ${status.message}`, ...toHeaderProps(part) }),
2578
+ /* @__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: [
2579
+ status.message,
2580
+ " Try again in a moment."
2581
+ ] }) })
2582
+ ] });
2583
+ }
2501
2584
  function pathTitle(prefix, path) {
2502
2585
  return /* @__PURE__ */ jsxs13("span", { className: "flex min-w-0 items-center gap-1.5", children: [
2503
2586
  /* @__PURE__ */ jsx15("span", { className: "text-muted-foreground", children: prefix }),
@@ -2506,6 +2589,8 @@ function pathTitle(prefix, path) {
2506
2589
  ] });
2507
2590
  }
2508
2591
  function renderBash2(part) {
2592
+ const readiness = renderWorkspaceNotReady(part);
2593
+ if (readiness) return readiness;
2509
2594
  const input = asRecord2(part.input);
2510
2595
  const output = asRecord2(part.output);
2511
2596
  const command = typeof input.command === "string" ? input.command : "";
@@ -2537,6 +2622,8 @@ function renderBash2(part) {
2537
2622
  ] });
2538
2623
  }
2539
2624
  function renderRead2(part) {
2625
+ const readiness = renderWorkspaceNotReady(part);
2626
+ if (readiness) return readiness;
2540
2627
  const input = asRecord2(part.input);
2541
2628
  const output = asRecord2(part.output);
2542
2629
  const path = typeof input.path === "string" ? input.path : "";
@@ -2554,6 +2641,8 @@ function renderRead2(part) {
2554
2641
  ] });
2555
2642
  }
2556
2643
  function renderWrite2(part) {
2644
+ const readiness = renderWorkspaceNotReady(part);
2645
+ if (readiness) return readiness;
2557
2646
  const input = asRecord2(part.input);
2558
2647
  const path = typeof input.path === "string" ? input.path : "";
2559
2648
  const content = typeof input.content === "string" ? input.content : "";
@@ -2616,6 +2705,8 @@ function renderWrite2(part) {
2616
2705
  ] });
2617
2706
  }
2618
2707
  function renderEdit2(part) {
2708
+ const readiness = renderWorkspaceNotReady(part);
2709
+ if (readiness) return readiness;
2619
2710
  const input = asRecord2(part.input);
2620
2711
  const path = typeof input.path === "string" ? input.path : "";
2621
2712
  const oldString = typeof input.oldString === "string" ? input.oldString : "";
@@ -2641,6 +2732,8 @@ function renderEdit2(part) {
2641
2732
  ] });
2642
2733
  }
2643
2734
  function renderSearchLike2(toolName, part) {
2735
+ const readiness = renderWorkspaceNotReady(part);
2736
+ if (readiness) return readiness;
2644
2737
  const input = asRecord2(part.input);
2645
2738
  const SearchLikeIcon = toolName === "find" || toolName === "grep" ? SearchIcon : FileTextIcon;
2646
2739
  const pattern = typeof input.pattern === "string" ? input.pattern : "";
@@ -2677,6 +2770,8 @@ function extractParamTokens(value, depth = 0) {
2677
2770
  return [String(value)];
2678
2771
  }
2679
2772
  function renderExecUi2(part) {
2773
+ const readiness = renderWorkspaceNotReady(part);
2774
+ if (readiness) return readiness;
2680
2775
  const input = asRecord2(part.input);
2681
2776
  const kind = typeof input.kind === "string" ? input.kind : "(empty)";
2682
2777
  const tokens = extractParamTokens(input.params);
@@ -2733,6 +2828,8 @@ function renderExecUi2(part) {
2733
2828
  ] });
2734
2829
  }
2735
2830
  function renderFallback2(part) {
2831
+ const readiness = renderWorkspaceNotReady(part);
2832
+ if (readiness) return readiness;
2736
2833
  return /* @__PURE__ */ jsxs13(Tool2, { children: [
2737
2834
  /* @__PURE__ */ jsx15(ToolHeader, { title: part.toolName, ...toHeaderProps(part) }),
2738
2835
  /* @__PURE__ */ jsxs13(ToolContent, { children: [
@@ -3678,23 +3775,25 @@ var PromptInput = ({
3678
3775
  return item;
3679
3776
  })
3680
3777
  );
3681
- const result = onSubmit({ files: convertedFiles, text }, event);
3682
- if (result instanceof Promise) {
3683
- try {
3684
- await result;
3685
- clear();
3686
- if (usingProvider) {
3687
- controller.textInput.clear();
3688
- }
3689
- } catch {
3690
- }
3691
- } else {
3692
- clear();
3778
+ const result = await onSubmit({ files: convertedFiles, text }, event);
3779
+ if (result === false) {
3693
3780
  if (usingProvider) {
3694
- controller.textInput.clear();
3781
+ controller.textInput.setInput(text);
3782
+ } else {
3783
+ const textInput = form.elements.namedItem("message");
3784
+ if (textInput) textInput.value = text;
3695
3785
  }
3786
+ return;
3787
+ }
3788
+ clear();
3789
+ if (usingProvider) {
3790
+ controller.textInput.clear();
3696
3791
  }
3697
3792
  } catch {
3793
+ if (!usingProvider) {
3794
+ const textInput = form.elements.namedItem("message");
3795
+ if (textInput && textInput.value === "") textInput.value = text;
3796
+ }
3698
3797
  }
3699
3798
  },
3700
3799
  [usingProvider, controller, files, onSubmit, clear]
@@ -4121,10 +4220,23 @@ function friendlyError(err) {
4121
4220
  detail: `${label} ${message?.toLowerCase() ?? "failed validation"}.`
4122
4221
  };
4123
4222
  }
4223
+ if (code3 === ErrorCode.enum.AGENT_RUNTIME_NOT_READY) {
4224
+ return {
4225
+ title: "Preparing workspace\u2026",
4226
+ code: code3
4227
+ };
4228
+ }
4229
+ if (code3 === ErrorCode.enum.RUNTIME_PROVISIONING_FAILED) {
4230
+ return {
4231
+ title: "Workspace setup failed.",
4232
+ detail: message ?? "Reload the workspace and try again.",
4233
+ code: code3
4234
+ };
4235
+ }
4124
4236
  if (code3 === "internal" || code3 === "internal_error") {
4125
- return { title: "The server hit an internal error.", detail: message };
4237
+ return { title: "The server hit an internal error.", detail: message, code: code3 };
4126
4238
  }
4127
- return { title: message ?? "Something went wrong.", detail: code3 };
4239
+ return { title: message ?? "Something went wrong.", detail: code3, code: code3 };
4128
4240
  } catch {
4129
4241
  return { title: raw };
4130
4242
  }
@@ -4429,7 +4541,8 @@ function encodeModelKey(sel) {
4429
4541
  // src/front/hooks/useChatModelSelection.ts
4430
4542
  function useChatModelSelection({
4431
4543
  defaultModel,
4432
- requestHeaders
4544
+ requestHeaders,
4545
+ enabled = true
4433
4546
  }) {
4434
4547
  const initialModelState = useMemo11(readStoredModelState, []);
4435
4548
  const [model, setModelState] = useState15(
@@ -4456,6 +4569,7 @@ function useChatModelSelection({
4456
4569
  setModelState(defaultModel);
4457
4570
  }, [defaultModel]);
4458
4571
  useEffect14(() => {
4572
+ if (!enabled) return;
4459
4573
  let aborted = false;
4460
4574
  fetch("/api/v1/agent/models", { headers: requestHeaders }).then((res) => res.ok ? res.json() : null).then((payload) => {
4461
4575
  if (aborted || !payload?.models) return;
@@ -4474,7 +4588,7 @@ function useChatModelSelection({
4474
4588
  return () => {
4475
4589
  aborted = true;
4476
4590
  };
4477
- }, [requestHeaders]);
4591
+ }, [enabled, requestHeaders]);
4478
4592
  useEffect14(() => {
4479
4593
  const onChange = (event) => {
4480
4594
  const next = parseModelSelection(event.detail);
@@ -4490,10 +4604,12 @@ function useChatModelSelection({
4490
4604
  import { useEffect as useEffect15, useState as useState16 } from "react";
4491
4605
  function useServerSkills({
4492
4606
  registry,
4493
- requestHeaders
4607
+ requestHeaders,
4608
+ enabled = true
4494
4609
  }) {
4495
4610
  const [skillsStamp, setSkillsStamp] = useState16(0);
4496
4611
  useEffect15(() => {
4612
+ if (!enabled) return;
4497
4613
  let aborted = false;
4498
4614
  fetch("/api/v1/agent/skills", { headers: requestHeaders }).then((res) => res.ok ? res.json() : null).then((payload) => {
4499
4615
  if (aborted || !payload?.skills) return;
@@ -4511,7 +4627,7 @@ function useServerSkills({
4511
4627
  return () => {
4512
4628
  aborted = true;
4513
4629
  };
4514
- }, [requestHeaders, registry]);
4630
+ }, [enabled, requestHeaders, registry]);
4515
4631
  return skillsStamp;
4516
4632
  }
4517
4633
 
@@ -4872,6 +4988,9 @@ function hasVisibleMessageContent(message) {
4872
4988
  return Boolean(reasoning?.text.trim());
4873
4989
  });
4874
4990
  }
4991
+ function messageText(message) {
4992
+ return (message.parts ?? []).filter(isTextPart).map((part) => part.text).join("\n").trim();
4993
+ }
4875
4994
  function hasPiVisibleDataParts(messages) {
4876
4995
  return messages.some(
4877
4996
  (message) => (message.parts ?? []).some((part) => {
@@ -4890,7 +5009,8 @@ function collectSdkRepresentedMessageIds(messages) {
4890
5009
  if (!hasVisibleMessageContent(message)) continue;
4891
5010
  represented.add(message.id);
4892
5011
  for (const part of message.parts ?? []) {
4893
- if (dataPartType(part) !== "data-pi-message-start") continue;
5012
+ const type = dataPartType(part);
5013
+ if (type !== "data-pi-message-start" && type !== "data-pi-message-end") continue;
4894
5014
  const data = part.data;
4895
5015
  if (typeof data?.messageId === "string") represented.add(data.messageId);
4896
5016
  }
@@ -4907,6 +5027,19 @@ function uniqueVisiblePiMessages(messages) {
4907
5027
  }
4908
5028
  return out;
4909
5029
  }
5030
+ function sdkVisibleMessagesWithoutPiOnlyAssistants(messages) {
5031
+ return messages.filter((message) => {
5032
+ if (!hasVisibleMessageContent(message)) return false;
5033
+ if (message.role !== "assistant") return true;
5034
+ const hasVisibleAssistantPart = (message.parts ?? []).some((part) => {
5035
+ if (isTextPart(part)) return !isBlankTextPart(part);
5036
+ if (isToolUIPart2(part)) return true;
5037
+ const reasoning = getReasoningPart(part);
5038
+ return Boolean(reasoning?.text.trim());
5039
+ });
5040
+ return hasVisibleAssistantPart;
5041
+ });
5042
+ }
4910
5043
  function getSettledPiAssistantIds(message) {
4911
5044
  const ids = [];
4912
5045
  for (const part of message.parts ?? []) {
@@ -4943,6 +5076,23 @@ function mergeSettledPiFallbacksInPlace(messages, piMessages) {
4943
5076
  }
4944
5077
  return out;
4945
5078
  }
5079
+ function composerNoticeForWarmup(status) {
5080
+ if (!status || status.status === "ready") return null;
5081
+ if (status.status === "failed") {
5082
+ return {
5083
+ title: "Workspace setup failed.",
5084
+ detail: status.message ?? "Reload the workspace and try again.",
5085
+ code: ErrorCode.enum.RUNTIME_PROVISIONING_FAILED
5086
+ };
5087
+ }
5088
+ return {
5089
+ title: "Preparing workspace\u2026",
5090
+ code: ErrorCode.enum.AGENT_RUNTIME_NOT_READY
5091
+ };
5092
+ }
5093
+ function isComposerRuntimeNotice(error) {
5094
+ return error?.code === ErrorCode.enum.AGENT_RUNTIME_NOT_READY || error?.code === ErrorCode.enum.RUNTIME_PROVISIONING_FAILED;
5095
+ }
4946
5096
  function getQueuedPiTail(messages, piMessages) {
4947
5097
  if (piMessages.length === 0) return [];
4948
5098
  const queuedIds = /* @__PURE__ */ new Set();
@@ -4997,8 +5147,19 @@ function ChatPanel(props) {
4997
5147
  defaultModel,
4998
5148
  onData,
4999
5149
  requestHeaders,
5150
+ hydrateMessages,
5000
5151
  onOpenArtifact,
5001
5152
  debug = false,
5153
+ initialDraft,
5154
+ autoSubmitInitialDraft = false,
5155
+ onDraftRestored,
5156
+ onAutoSubmitInitialDraftAccepted,
5157
+ onAutoSubmitInitialDraftSettled,
5158
+ onBeforeSubmit,
5159
+ serverResourcesEnabled = true,
5160
+ emptyPlacement = "default",
5161
+ composerPlaceholder,
5162
+ workspaceWarmupStatus,
5002
5163
  composerBlockers = [],
5003
5164
  onComposerStop,
5004
5165
  onComposerBlockerAction
@@ -5011,6 +5172,23 @@ function ChatPanel(props) {
5011
5172
  });
5012
5173
  const followUpDataHandlerRef = useRef13(() => {
5013
5174
  });
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);
5181
+ liveSessionIdRef.current = sessionId;
5182
+ activeAutoSubmitSessionRef.current = sessionId;
5183
+ const [acceptedAutoSubmittedDraft, setAcceptedAutoSubmittedDraft] = useState20(void 0);
5184
+ const [composerRuntimeNotice, setComposerRuntimeNotice] = useState20(null);
5185
+ useEffect18(() => {
5186
+ autoSubmittedDraftRef.current = void 0;
5187
+ autoSubmittingDraftRef.current = void 0;
5188
+ pendingAutoSubmitUnlockRef.current = void 0;
5189
+ pendingAutoSubmitSettleRef.current = void 0;
5190
+ setAcceptedAutoSubmittedDraft(void 0);
5191
+ }, [sessionId]);
5014
5192
  const {
5015
5193
  messages,
5016
5194
  sendMessage,
@@ -5027,7 +5205,8 @@ function ChatPanel(props) {
5027
5205
  onData?.(part);
5028
5206
  },
5029
5207
  requestHeaders,
5030
- persistMessages: capabilities.aiSdkOwnsHistory
5208
+ persistMessages: capabilities.aiSdkOwnsHistory,
5209
+ hydrateMessages
5031
5210
  });
5032
5211
  const { piMessages, handleData: handlePiData } = usePiChatProjection({
5033
5212
  messages,
@@ -5056,9 +5235,14 @@ function ChatPanel(props) {
5056
5235
  followUpDataHandlerRef.current = handleFollowUpData;
5057
5236
  }, [handleFollowUpData]);
5058
5237
  const mergedToolRenderers = mergeShadcnToolRenderers(toolRenderers);
5059
- const composerBlocked = composerBlockers.length > 0;
5238
+ const friendlyChatError = error ? friendlyError(error) : null;
5239
+ const runtimeErrorNotice = isComposerRuntimeNotice(friendlyChatError) ? friendlyChatError : null;
5240
+ const warmupNotice = composerNoticeForWarmup(workspaceWarmupStatus);
5241
+ const composerStatusNotice = composerRuntimeNotice ?? runtimeErrorNotice ?? warmupNotice;
5242
+ const workspaceWarmupBlocked = Boolean(warmupNotice);
5243
+ const composerBlocked = workspaceWarmupBlocked || composerBlockers.length > 0;
5060
5244
  const primaryComposerBlocker = composerBlockers[0];
5061
- const composerBlockerLabel = primaryComposerBlocker?.label ?? "Complete the pending workspace action to continue.";
5245
+ const composerBlockerLabel = workspaceWarmupBlocked ? warmupNotice?.title ?? "Preparing workspace\u2026" : primaryComposerBlocker?.label ?? "Complete the pending workspace action to continue.";
5062
5246
  const registry = useMemo12(
5063
5247
  () => {
5064
5248
  const effectiveBuiltins = hotReloadEnabled ? builtinCommands : builtinCommands.filter((cmd) => cmd.name !== "reload");
@@ -5066,11 +5250,12 @@ function ChatPanel(props) {
5066
5250
  },
5067
5251
  [extraCommands, hotReloadEnabled]
5068
5252
  );
5069
- const skillsStamp = useServerSkills({ registry, requestHeaders });
5253
+ const skillsStamp = useServerSkills({ registry, requestHeaders, enabled: serverResourcesEnabled });
5070
5254
  const allCommands = useMemo12(() => registry.list(), [registry, skillsStamp]);
5071
5255
  const { availableModels, model, setModel } = useChatModelSelection({
5072
5256
  defaultModel,
5073
- requestHeaders
5257
+ requestHeaders,
5258
+ enabled: serverResourcesEnabled
5074
5259
  });
5075
5260
  const { thinkingLevel, setThinkingLevel, showThoughts, setShowThoughts } = useThinkingSettings(thinkingControl);
5076
5261
  const { attachmentNotice, setAttachmentNotice } = useAttachmentNotice();
@@ -5169,23 +5354,49 @@ ${formatPluginReloadDiagnostics(diagnostics)}` : base;
5169
5354
  const waitingTail = projectedTailMessages.filter((message) => projectedStatusById.get(message.id) === "queued");
5170
5355
  const queuedPiTail = getQueuedPiTail(messages, piMessages);
5171
5356
  const sdkHasVisibleAssistant = hasStandardVisibleAssistantParts(messages);
5357
+ let baseMessages;
5172
5358
  if (sdkHasVisibleAssistant) {
5173
5359
  const mergedMessages = mergeSettledPiFallbacksInPlace(messages, piMessages);
5174
- return [
5360
+ baseMessages = [
5175
5361
  ...mergedMessages,
5176
5362
  ...coalesceAssistantToolFragments(queuedPiTail),
5177
5363
  ...waitingTail
5178
5364
  ];
5365
+ } else if (piMessages.length > 0 && queuedPiTail.length > 0) {
5366
+ baseMessages = [...coalesceAssistantToolFragments(piMessages), ...waitingTail];
5367
+ } else if (piMessages.length > 0 && status === "ready" && hasPiVisibleDataParts(messages)) {
5368
+ baseMessages = [
5369
+ ...sdkVisibleMessagesWithoutPiOnlyAssistants(messages),
5370
+ ...coalesceAssistantToolFragments(piMessages),
5371
+ ...waitingTail
5372
+ ];
5373
+ } else {
5374
+ baseMessages = [...messages, ...waitingTail];
5179
5375
  }
5180
- if (piMessages.length > 0 && queuedPiTail.length > 0) {
5181
- return [...coalesceAssistantToolFragments(piMessages), ...waitingTail];
5182
- }
5183
- if (piMessages.length > 0 && status === "ready" && hasPiVisibleDataParts(messages)) {
5184
- return [...coalesceAssistantToolFragments(piMessages), ...waitingTail];
5376
+ const autoSubmittedDraft = acceptedAutoSubmittedDraft?.trim();
5377
+ if (!autoSubmittedDraft) return baseMessages;
5378
+ const hasUserTurn = baseMessages.some((message) => message.role === "user" && messageText(message).includes(autoSubmittedDraft));
5379
+ if (hasUserTurn) return baseMessages;
5380
+ const syntheticUserMessage = {
5381
+ id: `auto-submitted-user:${sessionId}:${autoSubmittedDraft}`,
5382
+ role: "user",
5383
+ parts: [{ type: "text", text: autoSubmittedDraft }]
5384
+ };
5385
+ let insertAt = baseMessages.length;
5386
+ for (let index = baseMessages.length - 1; index >= 0; index -= 1) {
5387
+ if (baseMessages[index]?.role === "assistant") {
5388
+ insertAt = index;
5389
+ break;
5390
+ }
5185
5391
  }
5186
- return [...messages, ...waitingTail];
5187
- }, [messages, piMessages, projectedTailMessages, projectedStatusById, status]);
5188
- const renderMessages = displayMessages;
5392
+ return [
5393
+ ...baseMessages.slice(0, insertAt),
5394
+ syntheticUserMessage,
5395
+ ...baseMessages.slice(insertAt)
5396
+ ];
5397
+ }, [acceptedAutoSubmittedDraft, messages, piMessages, projectedTailMessages, projectedStatusById, sessionId, status]);
5398
+ const renderMessages = displayMessages.filter((message) => message.role !== "assistant" || hasVisibleMessageContent(message));
5399
+ const emptyHero = emptyPlacement === "hero" && renderMessages.length === 0;
5189
5400
  const handleStop = useCallback16(() => {
5190
5401
  onComposerStop?.();
5191
5402
  stopAndClearFollowUps();
@@ -5209,6 +5420,14 @@ ${formatPluginReloadDiagnostics(diagnostics)}` : base;
5209
5420
  [messages]
5210
5421
  );
5211
5422
  const textareaRef = useRef13(null);
5423
+ const promptInputController = useOptionalPromptInputController();
5424
+ const setComposerDraft = useCallback16((draft, focus = true) => {
5425
+ promptInputController?.textInput.setInput(draft);
5426
+ if (textareaRef.current) {
5427
+ textareaRef.current.value = draft;
5428
+ if (focus) textareaRef.current.focus();
5429
+ }
5430
+ }, [promptInputController]);
5212
5431
  const {
5213
5432
  mentionState,
5214
5433
  slashQuery,
@@ -5225,12 +5444,31 @@ ${formatPluginReloadDiagnostics(diagnostics)}` : base;
5225
5444
  textareaRef,
5226
5445
  disabled: mentionState !== null || slashQuery !== null
5227
5446
  });
5228
- async function handleSubmit({ text, files }) {
5229
- if (composerBlocked) return;
5447
+ const restoredDraftRef = useRef13(void 0);
5448
+ useEffect18(() => {
5449
+ if (initialDraft === void 0) return;
5450
+ if (restoredDraftRef.current === initialDraft) return;
5451
+ if (!promptInputController && !textareaRef.current) return;
5452
+ setComposerDraft(initialDraft, initialDraft.length > 0);
5453
+ restoredDraftRef.current = initialDraft;
5454
+ onDraftRestored?.();
5455
+ }, [initialDraft, onDraftRestored, promptInputController, setComposerDraft]);
5456
+ async function runBeforeSubmit(draft, files, source) {
5457
+ const result = await onBeforeSubmit?.(draft, { files, sessionId, source });
5458
+ return result !== false;
5459
+ }
5460
+ async function handleSubmit({ text, files }, source = "composer") {
5461
+ if (composerBlocked) {
5462
+ if (warmupNotice) setComposerRuntimeNotice(warmupNotice);
5463
+ return false;
5464
+ }
5230
5465
  const trimmed = text.trim();
5231
5466
  if (trimmed.length === 0 && (!files || files.length === 0)) {
5232
5467
  return;
5233
5468
  }
5469
+ setComposerRuntimeNotice(null);
5470
+ if (!await runBeforeSubmit(text, files ?? [], source)) return false;
5471
+ if (liveSessionIdRef.current !== sessionId) return false;
5234
5472
  const parsed = parseSlashCommand(text);
5235
5473
  if (parsed) {
5236
5474
  const cmd = registry.get(parsed.name);
@@ -5282,6 +5520,7 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5282
5520
  files: files ?? [],
5283
5521
  mentionedFiles
5284
5522
  });
5523
+ if (liveSessionIdRef.current !== sessionId) return false;
5285
5524
  clearMentionedFiles();
5286
5525
  if (isStreaming && files.length > 0) {
5287
5526
  setAttachmentNotice("Attachments can be sent after the current response finishes.");
@@ -5319,6 +5558,48 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5319
5558
  }
5320
5559
  );
5321
5560
  }
5561
+ useEffect18(() => {
5562
+ const pendingDraft = pendingAutoSubmitUnlockRef.current;
5563
+ if (!pendingDraft) return;
5564
+ const localTurnStarted = status === "submitted" || status === "streaming" || messages.some(
5565
+ (message) => message.role === "user" && message.parts.some((part) => isTextPart(part) && part.text.includes(pendingDraft))
5566
+ );
5567
+ if (!localTurnStarted) return;
5568
+ pendingAutoSubmitUnlockRef.current = void 0;
5569
+ onAutoSubmitInitialDraftAccepted?.();
5570
+ }, [messages, onAutoSubmitInitialDraftAccepted, status]);
5571
+ const prevAutoSubmitStatusRef = useRef13(status);
5572
+ useEffect18(() => {
5573
+ const prev = prevAutoSubmitStatusRef.current;
5574
+ prevAutoSubmitStatusRef.current = status;
5575
+ if (!pendingAutoSubmitSettleRef.current) return;
5576
+ if (status !== "ready") return;
5577
+ if (prev !== "submitted" && prev !== "streaming") return;
5578
+ pendingAutoSubmitSettleRef.current = void 0;
5579
+ onAutoSubmitInitialDraftSettled?.();
5580
+ }, [onAutoSubmitInitialDraftSettled, status]);
5581
+ useEffect18(() => {
5582
+ if (workspaceWarmupStatus?.status === "ready") setComposerRuntimeNotice(null);
5583
+ }, [workspaceWarmupStatus?.status]);
5584
+ useEffect18(() => {
5585
+ if (!autoSubmitInitialDraft) return;
5586
+ if (!initialDraft?.trim()) return;
5587
+ if (autoSubmittedDraftRef.current === initialDraft) return;
5588
+ if (autoSubmittingDraftRef.current === initialDraft) return;
5589
+ const targetSessionId = sessionId;
5590
+ autoSubmittingDraftRef.current = initialDraft;
5591
+ void (async () => {
5592
+ const result = await handleSubmit({ text: initialDraft, files: [] }, "composer");
5593
+ if (activeAutoSubmitSessionRef.current !== targetSessionId) return;
5594
+ autoSubmittingDraftRef.current = void 0;
5595
+ if (result === false) return;
5596
+ autoSubmittedDraftRef.current = initialDraft;
5597
+ pendingAutoSubmitUnlockRef.current = initialDraft;
5598
+ pendingAutoSubmitSettleRef.current = initialDraft;
5599
+ setAcceptedAutoSubmittedDraft(initialDraft);
5600
+ setComposerDraft("", false);
5601
+ })();
5602
+ }, [autoSubmitInitialDraft, handleSubmit, initialDraft, sessionId, setComposerDraft]);
5322
5603
  return /* @__PURE__ */ jsx25(ArtifactOpenProvider, { onOpenArtifact, children: /* @__PURE__ */ jsxs23(
5323
5604
  "div",
5324
5605
  {
@@ -5338,6 +5619,7 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5338
5619
  {
5339
5620
  className: cn(
5340
5621
  "flex h-full min-h-0 flex-col overflow-hidden",
5622
+ emptyHero && "justify-center",
5341
5623
  chrome && "mx-3 my-3 rounded-xl bg-[color:var(--surface-chat)] shadow-[0_1px_0_oklch(0_0_0/0.02),0_1px_2px_-1px_oklch(0_0_0/0.04),inset_0_0_0_1px_oklch(from_var(--border)_l_c_h/0.6)]"
5342
5624
  ),
5343
5625
  children: [
@@ -5367,7 +5649,7 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5367
5649
  /* @__PURE__ */ jsxs23(
5368
5650
  Conversation,
5369
5651
  {
5370
- className: "flex-1",
5652
+ className: emptyHero ? "max-h-[45vh] flex-none" : "flex-1",
5371
5653
  "aria-label": "Agent conversation",
5372
5654
  "aria-live": "polite",
5373
5655
  onScrollToBottomReady: (scrollToBottom) => {
@@ -5376,29 +5658,24 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5376
5658
  children: [
5377
5659
  /* @__PURE__ */ jsxs23(ConversationContent, { className: cn(
5378
5660
  "mx-auto flex w-full flex-col gap-6",
5379
- chrome ? "max-w-3xl px-6 py-8" : "max-w-[680px] px-4 py-4"
5661
+ chrome ? "max-w-3xl px-6 py-8" : "max-w-[680px] px-4 py-4",
5662
+ emptyHero && "py-4 text-center"
5380
5663
  ), children: [
5381
- displayMessages.length === 0 && /* @__PURE__ */ jsx25(
5664
+ renderMessages.length === 0 && /* @__PURE__ */ jsx25(
5382
5665
  ChatEmptyState,
5383
5666
  {
5384
5667
  eyebrow: emptyState?.eyebrow,
5385
5668
  title: emptyState?.title,
5386
5669
  description: emptyState?.description,
5387
5670
  suggestions,
5671
+ className: emptyHero ? "items-center text-center [&>p]:mx-auto" : void 0,
5388
5672
  onSelect: (s) => {
5389
5673
  const text = s.prompt ?? s.label;
5390
5674
  if (!text.trim()) return;
5391
- void sendMessage(
5392
- { text, files: [] },
5393
- {
5394
- body: {
5395
- sessionId,
5396
- message: text,
5397
- ...modelPayload(model),
5398
- attachments: []
5399
- }
5400
- }
5401
- );
5675
+ void (async () => {
5676
+ const submitted = await handleSubmit({ text, files: [] }, "suggestion");
5677
+ if (submitted === false) setComposerDraft(text);
5678
+ })();
5402
5679
  }
5403
5680
  }
5404
5681
  ),
@@ -5414,7 +5691,11 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5414
5691
  const key = `${message.id}-${index}`;
5415
5692
  if (!reasoningPart) {
5416
5693
  if (!isBlankTextPart(part)) {
5417
- items.push({ kind: "part", part, key });
5694
+ const previous2 = items[items.length - 1];
5695
+ const duplicateAdjacentAssistantText = role === "assistant" && isTextPart(part) && previous2?.kind === "part" && isTextPart(previous2.part) && previous2.part.text.trim() === part.text.trim();
5696
+ if (!duplicateAdjacentAssistantText) {
5697
+ items.push({ kind: "part", part, key });
5698
+ }
5418
5699
  }
5419
5700
  return items;
5420
5701
  }
@@ -5441,6 +5722,12 @@ ${reasoningPart.text}`;
5441
5722
  acc.push({ kind: "tool-group", tools: [{ part: item.part, key: item.key }], key: item.key });
5442
5723
  }
5443
5724
  } else if (item.kind === "part" && !isTextPart(item.part)) {
5725
+ } else if (item.kind === "part" && isTextPart(item.part)) {
5726
+ const prev = acc[acc.length - 1];
5727
+ const duplicateAdjacentAssistantText = role === "assistant" && prev?.kind === "part" && isTextPart(prev.part) && prev.part.text.trim() === item.part.text.trim();
5728
+ if (!duplicateAdjacentAssistantText) {
5729
+ acc.push(item);
5730
+ }
5444
5731
  } else {
5445
5732
  acc.push(item);
5446
5733
  }
@@ -5601,8 +5888,8 @@ ${reasoningPart.text}`;
5601
5888
  );
5602
5889
  }),
5603
5890
  (() => {
5604
- if (!error) return null;
5605
- const friendly = friendlyError(error);
5891
+ if (!friendlyChatError || isComposerRuntimeNotice(friendlyChatError)) return null;
5892
+ const friendly = friendlyChatError;
5606
5893
  return /* @__PURE__ */ jsx25(Message, { from: "assistant", className: "!max-w-full", children: /* @__PURE__ */ jsx25(MessageContent, { className: "rounded-xl border border-destructive/40 bg-destructive/10 px-4 py-3", children: /* @__PURE__ */ jsxs23("div", { role: "alert", className: "flex items-start gap-3 text-sm text-destructive", children: [
5607
5894
  /* @__PURE__ */ jsx25("div", { className: "mt-0.5 shrink-0", children: /* @__PURE__ */ jsxs23("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
5608
5895
  /* @__PURE__ */ jsx25("circle", { cx: "12", cy: "12", r: "10" }),
@@ -5669,7 +5956,26 @@ ${reasoningPart.text}`;
5669
5956
  )
5670
5957
  }
5671
5958
  ),
5672
- composerBlocked && /* @__PURE__ */ jsxs23(
5959
+ composerStatusNotice && /* @__PURE__ */ jsx25(
5960
+ "div",
5961
+ {
5962
+ "data-testid": "chat-composer-runtime-notice",
5963
+ role: "status",
5964
+ "aria-live": "polite",
5965
+ className: cn(
5966
+ "mx-auto mb-2 w-full max-w-3xl rounded-[var(--radius-md)] border border-accent/40 bg-[color:var(--accent-soft)]",
5967
+ "px-3 py-2 text-xs text-foreground"
5968
+ ),
5969
+ children: /* @__PURE__ */ jsxs23("div", { className: "flex items-start gap-2", children: [
5970
+ composerStatusNotice.code === ErrorCode.enum.AGENT_RUNTIME_NOT_READY ? /* @__PURE__ */ jsx25(Loader2, { "aria-hidden": "true", className: "mt-0.5 size-3.5 shrink-0 animate-spin text-muted-foreground" }) : /* @__PURE__ */ jsx25(AlertCircleIcon, { "aria-hidden": "true", className: "mt-0.5 size-3.5 shrink-0 text-destructive" }),
5971
+ /* @__PURE__ */ jsxs23("div", { className: "min-w-0", children: [
5972
+ /* @__PURE__ */ jsx25("div", { className: "font-medium", children: composerStatusNotice.title }),
5973
+ composerStatusNotice.detail && /* @__PURE__ */ jsx25("div", { className: "mt-0.5 text-muted-foreground", children: composerStatusNotice.detail })
5974
+ ] })
5975
+ ] })
5976
+ }
5977
+ ),
5978
+ composerBlocked && !workspaceWarmupBlocked && /* @__PURE__ */ jsxs23(
5673
5979
  "div",
5674
5980
  {
5675
5981
  role: "status",
@@ -5764,7 +6070,7 @@ ${reasoningPart.text}`;
5764
6070
  PromptInput,
5765
6071
  {
5766
6072
  "data-boring-state": status,
5767
- onSubmit: handleSubmit,
6073
+ onSubmit: (message) => handleSubmit(message),
5768
6074
  multiple: true,
5769
6075
  maxFiles: attachmentsDisabled ? 0 : 20,
5770
6076
  maxFileSize: 5 * 1024 * 1024,
@@ -5786,9 +6092,11 @@ ${reasoningPart.text}`;
5786
6092
  /* @__PURE__ */ jsx25(
5787
6093
  PromptInputTextarea,
5788
6094
  {
5789
- placeholder: composerBlocked ? composerBlockerLabel : "Ask anything\u2026",
6095
+ defaultValue: promptInputController ? void 0 : initialDraft,
6096
+ placeholder: composerBlocked ? composerBlockerLabel : composerPlaceholder ?? "Ask anything\u2026",
5790
6097
  disabled: composerBlocked,
5791
6098
  readOnly: composerBlocked,
6099
+ ref: textareaRef,
5792
6100
  onChange: handleComposerChange,
5793
6101
  onKeyDown: handleComposerKeyDown,
5794
6102
  className: cn(
@@ -6145,7 +6453,7 @@ function getAgentCommands(options = {}) {
6145
6453
  }
6146
6454
 
6147
6455
  // src/front/hooks/useSessions.ts
6148
- import { useState as useState21, useEffect as useEffect19, useCallback as useCallback17, useRef as useRef14 } from "react";
6456
+ import { useState as useState21, useEffect as useEffect19, useCallback as useCallback17, useMemo as useMemo13, useRef as useRef14 } from "react";
6149
6457
  var API_BASE = "/api/v1/agent/sessions";
6150
6458
  var STORAGE_KEY = "boring-agent:activeSessionId";
6151
6459
  function readPersistedId(storageKey) {
@@ -6162,6 +6470,9 @@ function persistId(storageKey, id) {
6162
6470
  } catch {
6163
6471
  }
6164
6472
  }
6473
+ function headersScopeKey(headers) {
6474
+ return JSON.stringify(Object.entries(headers ?? {}).sort(([a], [b]) => a.localeCompare(b)));
6475
+ }
6165
6476
  function requestInit(headers) {
6166
6477
  if (!headers || Object.keys(headers).length === 0) return void 0;
6167
6478
  return { headers };
@@ -6175,21 +6486,46 @@ async function fetchSessions(headers) {
6175
6486
  function useSessions(opts = {}) {
6176
6487
  const storageKey = opts.storageKey ?? STORAGE_KEY;
6177
6488
  const requestHeaders = opts.requestHeaders;
6489
+ const enabled = opts.enabled ?? true;
6490
+ const refreshKey = opts.refreshKey;
6491
+ const scopeKey = useMemo13(
6492
+ () => `${storageKey}
6493
+ ${headersScopeKey(requestHeaders)}`,
6494
+ [requestHeaders, storageKey]
6495
+ );
6178
6496
  const [sessions, setSessions] = useState21([]);
6179
6497
  const [activeSessionId, setActiveSessionId] = useState21(
6180
6498
  () => readPersistedId(storageKey)
6181
6499
  );
6182
6500
  const [loading, setLoading] = useState21(true);
6183
6501
  const [error, setError] = useState21();
6502
+ const [loaded, setLoaded] = useState21(false);
6184
6503
  const versionRef = useRef14(0);
6504
+ const loadedScopeRef = useRef14(scopeKey);
6185
6505
  const refresh = useCallback17(async () => {
6186
6506
  const v = ++versionRef.current;
6507
+ if (!enabled) {
6508
+ setSessions([]);
6509
+ setActiveSessionId(void 0);
6510
+ setError(void 0);
6511
+ setLoaded(false);
6512
+ setLoading(false);
6513
+ return;
6514
+ }
6515
+ setLoaded(false);
6516
+ setLoading(true);
6187
6517
  try {
6188
6518
  const data = await fetchSessions(requestHeaders);
6189
6519
  if (v === versionRef.current) {
6520
+ const replacingLoadedScope = loadedScopeRef.current !== scopeKey;
6521
+ loadedScopeRef.current = scopeKey;
6522
+ const persisted = readPersistedId(storageKey);
6523
+ setError(void 0);
6524
+ setLoaded(true);
6190
6525
  setSessions(data);
6191
6526
  setActiveSessionId((prev) => {
6192
- if (prev && data.some((session) => session.id === prev)) return prev;
6527
+ const preferred = replacingLoadedScope ? persisted : prev ?? persisted;
6528
+ if (preferred && data.some((session) => session.id === preferred)) return preferred;
6193
6529
  const next = data[0]?.id;
6194
6530
  persistId(storageKey, next);
6195
6531
  return next;
@@ -6198,16 +6534,32 @@ function useSessions(opts = {}) {
6198
6534
  }
6199
6535
  } catch (err) {
6200
6536
  if (v === versionRef.current) {
6537
+ const replacingLoadedScope = loadedScopeRef.current !== scopeKey;
6538
+ loadedScopeRef.current = scopeKey;
6539
+ if (replacingLoadedScope) {
6540
+ setSessions([]);
6541
+ setActiveSessionId(void 0);
6542
+ }
6543
+ setLoaded(true);
6201
6544
  setError(err instanceof Error ? err : new Error(String(err)));
6202
6545
  setLoading(false);
6203
6546
  }
6204
6547
  }
6205
- }, [requestHeaders, storageKey]);
6548
+ }, [enabled, requestHeaders, scopeKey, storageKey]);
6206
6549
  useEffect19(() => {
6550
+ if (!enabled) {
6551
+ setSessions([]);
6552
+ setActiveSessionId(void 0);
6553
+ setError(void 0);
6554
+ setLoaded(false);
6555
+ setLoading(false);
6556
+ return;
6557
+ }
6207
6558
  void refresh();
6208
- }, [refresh]);
6559
+ }, [enabled, refresh, refreshKey, scopeKey]);
6209
6560
  const create = useCallback17(
6210
6561
  async (init) => {
6562
+ if (!enabled) throw new Error("Sessions are disabled");
6211
6563
  const res = await fetch(API_BASE, {
6212
6564
  method: "POST",
6213
6565
  headers: { ...requestHeaders, "Content-Type": "application/json" },
@@ -6225,7 +6577,7 @@ function useSessions(opts = {}) {
6225
6577
  void refresh();
6226
6578
  return session;
6227
6579
  },
6228
- [refresh, requestHeaders, storageKey]
6580
+ [enabled, refresh, requestHeaders, storageKey]
6229
6581
  );
6230
6582
  const switchSession = useCallback17((id) => {
6231
6583
  setActiveSessionId(id);
@@ -6233,6 +6585,7 @@ function useSessions(opts = {}) {
6233
6585
  }, [storageKey]);
6234
6586
  const deleteSession = useCallback17(
6235
6587
  async (id) => {
6588
+ if (!enabled) throw new Error("Sessions are disabled");
6236
6589
  setSessions((prev) => prev.filter((s) => s.id !== id));
6237
6590
  setActiveSessionId((prev) => {
6238
6591
  if (prev === id) {
@@ -6256,13 +6609,16 @@ function useSessions(opts = {}) {
6256
6609
  }
6257
6610
  void refresh();
6258
6611
  },
6259
- [refresh, requestHeaders, storageKey]
6612
+ [enabled, refresh, requestHeaders, storageKey]
6260
6613
  );
6614
+ const scopeMatches = loadedScopeRef.current === scopeKey;
6615
+ const visibleSessions = enabled && scopeMatches ? sessions : [];
6616
+ const visibleActiveSessionId = enabled && scopeMatches ? activeSessionId : void 0;
6261
6617
  return {
6262
- sessions,
6263
- activeSession: sessions.find((s) => s.id === activeSessionId),
6264
- activeSessionId,
6265
- loading,
6618
+ sessions: visibleSessions,
6619
+ activeSession: visibleSessions.find((s) => s.id === visibleActiveSessionId),
6620
+ activeSessionId: visibleActiveSessionId,
6621
+ loading: enabled ? !scopeMatches || loading || !loaded : false,
6266
6622
  error,
6267
6623
  create,
6268
6624
  switch: switchSession,