@hachej/boring-agent 0.1.22 → 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
 
@@ -4551,7 +4667,7 @@ function useAttachmentNotice(timeoutMs = 4e3) {
4551
4667
 
4552
4668
  // src/front/chatPanelComposerControls.tsx
4553
4669
  import { useState as useState19 } from "react";
4554
- import { BotIcon, BrainIcon as BrainIcon2, CheckIcon as CheckIcon2, ChevronDownIcon as ChevronDownIcon5, EyeIcon, EyeOffIcon } from "lucide-react";
4670
+ import { CheckIcon as CheckIcon2, ChevronDownIcon as ChevronDownIcon5, EyeIcon, EyeOffIcon } from "lucide-react";
4555
4671
  import {
4556
4672
  Command as Command2,
4557
4673
  CommandEmpty as CommandEmpty2,
@@ -4603,6 +4719,33 @@ var THINKING_LEVEL_LABELS = {
4603
4719
  medium: "Med",
4604
4720
  high: "High"
4605
4721
  };
4722
+ function ThinkingLevelGlyph({ level }) {
4723
+ const lit = level === "off" ? 0 : level === "low" ? 1 : level === "medium" ? 2 : 3;
4724
+ return /* @__PURE__ */ jsx23(
4725
+ "svg",
4726
+ {
4727
+ "aria-hidden": "true",
4728
+ width: "14",
4729
+ height: "14",
4730
+ viewBox: "0 0 14 14",
4731
+ fill: "none",
4732
+ className: "shrink-0",
4733
+ children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx23(
4734
+ "rect",
4735
+ {
4736
+ x: 2 + i * 4,
4737
+ y: 10 - i * 2,
4738
+ width: "2",
4739
+ height: 3 + i * 2,
4740
+ rx: "0.5",
4741
+ fill: "currentColor",
4742
+ opacity: i < lit ? 1 : 0.25
4743
+ },
4744
+ i
4745
+ ))
4746
+ }
4747
+ );
4748
+ }
4606
4749
  function ModelSelect({
4607
4750
  value,
4608
4751
  onChange,
@@ -4641,13 +4784,16 @@ function ModelSelect({
4641
4784
  "aria-label": "Model",
4642
4785
  className: cn(
4643
4786
  composerActionClass,
4644
- "w-auto max-w-[min(52vw,200px)] px-2 text-xs font-medium",
4645
- open && "bg-muted/60 text-foreground"
4787
+ // Model is the only piece of state the composer carries between
4788
+ // turns — give it a status-pill shape so it reads as data, not
4789
+ // another tertiary control. The label is the signal; no icon
4790
+ // (the bot/AI glyph was decoration, not information).
4791
+ "w-auto max-w-[min(52vw,260px)] gap-1 rounded-full bg-muted/40 px-2.5 text-[12px] font-medium text-foreground/85 hover:bg-muted/70",
4792
+ open && "bg-muted/70 text-foreground"
4646
4793
  ),
4647
4794
  children: [
4648
- /* @__PURE__ */ jsx23(BotIcon, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }),
4649
4795
  /* @__PURE__ */ jsx23("span", { className: "min-w-0 truncate", children: triggerLabel }),
4650
- /* @__PURE__ */ jsx23(ChevronDownIcon5, { className: "h-3 w-3 shrink-0 text-muted-foreground/60", "aria-hidden": "true" })
4796
+ /* @__PURE__ */ jsx23(ChevronDownIcon5, { className: "h-3 w-3 shrink-0 text-muted-foreground/50", "aria-hidden": "true" })
4651
4797
  ]
4652
4798
  }
4653
4799
  ) }),
@@ -4657,7 +4803,7 @@ function ModelSelect({
4657
4803
  align: "start",
4658
4804
  sideOffset: 6,
4659
4805
  "data-boring-agent": "",
4660
- className: "w-[min(90vw,260px)] rounded-xl border-border/60 bg-popover p-1 shadow-xl",
4806
+ className: "w-[min(92vw,340px)] rounded-xl border-border/60 bg-popover p-1 shadow-xl",
4661
4807
  children: /* @__PURE__ */ jsxs21(Command2, { children: [
4662
4808
  menuOptions.length > 8 && /* @__PURE__ */ jsx23(
4663
4809
  CommandInput2,
@@ -4686,7 +4832,13 @@ function ModelSelect({
4686
4832
  },
4687
4833
  className: cn(
4688
4834
  "flex items-center gap-2 rounded-md px-2 py-1.5 text-[13px]",
4689
- key === currentKey && "bg-accent text-accent-foreground"
4835
+ // Soften cmdk's full-saturation `data-[selected=true]:bg-accent`
4836
+ // (keyboard-cursor focus) to a subtle warm tint so the
4837
+ // active row doesn't read as a saturated tile in dark mode.
4838
+ "data-[selected=true]:bg-[color:oklch(from_var(--accent)_l_c_h/0.15)] data-[selected=true]:text-foreground",
4839
+ // The user's CURRENTLY-SELECTED model gets a neutral wash +
4840
+ // the CheckIcon (accent is reserved for primary CTA / focus).
4841
+ key === currentKey && "bg-foreground/[0.06] text-foreground"
4690
4842
  ),
4691
4843
  children: [
4692
4844
  /* @__PURE__ */ jsx23(
@@ -4694,12 +4846,12 @@ function ModelSelect({
4694
4846
  {
4695
4847
  className: cn(
4696
4848
  "h-3.5 w-3.5 shrink-0",
4697
- key === currentKey ? "opacity-100" : "opacity-0"
4849
+ key === currentKey ? "text-[color:var(--accent)] opacity-100" : "opacity-0"
4698
4850
  )
4699
4851
  }
4700
4852
  ),
4701
4853
  /* @__PURE__ */ jsx23("span", { className: "truncate", children: label }),
4702
- /* @__PURE__ */ jsx23("span", { className: "ml-auto shrink-0 text-[10px] text-muted-foreground/70", children: m.id })
4854
+ /* @__PURE__ */ jsx23("span", { className: "ml-auto shrink-0 text-[10px] text-muted-foreground/60", children: m.id })
4703
4855
  ]
4704
4856
  },
4705
4857
  key
@@ -4733,12 +4885,17 @@ function ThinkingSelect({
4733
4885
  {
4734
4886
  "data-boring-agent-part": "thinking-select",
4735
4887
  "data-boring-state": disabled ? "disabled" : void 0,
4736
- className: cn(composerActionClass, "w-8 px-0"),
4737
- "aria-label": "Thinking level",
4888
+ className: cn(
4889
+ composerActionClass,
4890
+ "w-8 px-0 [&>span[aria-hidden]]:hidden",
4891
+ value !== "off" && "text-foreground"
4892
+ ),
4893
+ "aria-label": `Thinking level: ${THINKING_LEVEL_LABELS[value]}`,
4894
+ title: `Thinking: ${THINKING_LEVEL_LABELS[value]}`,
4738
4895
  "data-testid": "thinking-select",
4739
4896
  children: [
4740
4897
  THINKING_LEVELS.map((level) => /* @__PURE__ */ jsx23("span", { "data-value": level, hidden: true }, level)),
4741
- /* @__PURE__ */ jsx23(BrainIcon2, { className: "h-3.5 w-3.5" })
4898
+ /* @__PURE__ */ jsx23(ThinkingLevelGlyph, { level: value })
4742
4899
  ]
4743
4900
  }
4744
4901
  ),
@@ -4772,25 +4929,26 @@ function ThoughtVisibilityButton({
4772
4929
  variant: "ghost",
4773
4930
  size: "icon-sm",
4774
4931
  onClick: onToggle,
4775
- className: cn(composerActionClass, "w-8"),
4932
+ className: cn(composerActionClass, "w-8", visible && "text-foreground"),
4776
4933
  "aria-pressed": visible,
4777
4934
  "aria-label": visible ? "Hide thoughts" : "Show thoughts",
4778
4935
  title: visible ? "Hide thoughts" : "Show thoughts",
4779
- children: /* @__PURE__ */ jsx23(Icon, { className: "h-3.5 w-3.5" })
4936
+ children: /* @__PURE__ */ jsx23(Icon, { className: "h-3.5 w-3.5", strokeWidth: 1.75 })
4780
4937
  }
4781
4938
  );
4782
4939
  }
4783
4940
 
4784
4941
  // src/front/chatPanelKbdHints.tsx
4942
+ import { Kbd } from "@hachej/boring-ui-kit";
4785
4943
  import { jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
4786
4944
  function KbdHints() {
4787
4945
  return /* @__PURE__ */ jsxs22(
4788
- "kbd",
4946
+ Kbd,
4789
4947
  {
4790
4948
  "aria-hidden": "true",
4949
+ title: "Shift + Enter for newline",
4791
4950
  className: cn(
4792
- "hidden h-5 items-center gap-0.5 rounded-[var(--radius-sm)] border border-border/50",
4793
- "bg-background/50 px-1.5 font-mono text-[10px] text-muted-foreground/40",
4951
+ "hidden gap-0.5 border-border/60 bg-muted/40 leading-none shadow-none",
4794
4952
  "sm:inline-flex"
4795
4953
  ),
4796
4954
  children: [
@@ -4830,6 +4988,9 @@ function hasVisibleMessageContent(message) {
4830
4988
  return Boolean(reasoning?.text.trim());
4831
4989
  });
4832
4990
  }
4991
+ function messageText(message) {
4992
+ return (message.parts ?? []).filter(isTextPart).map((part) => part.text).join("\n").trim();
4993
+ }
4833
4994
  function hasPiVisibleDataParts(messages) {
4834
4995
  return messages.some(
4835
4996
  (message) => (message.parts ?? []).some((part) => {
@@ -4848,7 +5009,8 @@ function collectSdkRepresentedMessageIds(messages) {
4848
5009
  if (!hasVisibleMessageContent(message)) continue;
4849
5010
  represented.add(message.id);
4850
5011
  for (const part of message.parts ?? []) {
4851
- 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;
4852
5014
  const data = part.data;
4853
5015
  if (typeof data?.messageId === "string") represented.add(data.messageId);
4854
5016
  }
@@ -4865,6 +5027,19 @@ function uniqueVisiblePiMessages(messages) {
4865
5027
  }
4866
5028
  return out;
4867
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
+ }
4868
5043
  function getSettledPiAssistantIds(message) {
4869
5044
  const ids = [];
4870
5045
  for (const part of message.parts ?? []) {
@@ -4901,6 +5076,23 @@ function mergeSettledPiFallbacksInPlace(messages, piMessages) {
4901
5076
  }
4902
5077
  return out;
4903
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
+ }
4904
5096
  function getQueuedPiTail(messages, piMessages) {
4905
5097
  if (piMessages.length === 0) return [];
4906
5098
  const queuedIds = /* @__PURE__ */ new Set();
@@ -4955,8 +5147,19 @@ function ChatPanel(props) {
4955
5147
  defaultModel,
4956
5148
  onData,
4957
5149
  requestHeaders,
5150
+ hydrateMessages,
4958
5151
  onOpenArtifact,
4959
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,
4960
5163
  composerBlockers = [],
4961
5164
  onComposerStop,
4962
5165
  onComposerBlockerAction
@@ -4969,6 +5172,23 @@ function ChatPanel(props) {
4969
5172
  });
4970
5173
  const followUpDataHandlerRef = useRef13(() => {
4971
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]);
4972
5192
  const {
4973
5193
  messages,
4974
5194
  sendMessage,
@@ -4985,7 +5205,8 @@ function ChatPanel(props) {
4985
5205
  onData?.(part);
4986
5206
  },
4987
5207
  requestHeaders,
4988
- persistMessages: capabilities.aiSdkOwnsHistory
5208
+ persistMessages: capabilities.aiSdkOwnsHistory,
5209
+ hydrateMessages
4989
5210
  });
4990
5211
  const { piMessages, handleData: handlePiData } = usePiChatProjection({
4991
5212
  messages,
@@ -5014,9 +5235,14 @@ function ChatPanel(props) {
5014
5235
  followUpDataHandlerRef.current = handleFollowUpData;
5015
5236
  }, [handleFollowUpData]);
5016
5237
  const mergedToolRenderers = mergeShadcnToolRenderers(toolRenderers);
5017
- 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;
5018
5244
  const primaryComposerBlocker = composerBlockers[0];
5019
- 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.";
5020
5246
  const registry = useMemo12(
5021
5247
  () => {
5022
5248
  const effectiveBuiltins = hotReloadEnabled ? builtinCommands : builtinCommands.filter((cmd) => cmd.name !== "reload");
@@ -5024,11 +5250,12 @@ function ChatPanel(props) {
5024
5250
  },
5025
5251
  [extraCommands, hotReloadEnabled]
5026
5252
  );
5027
- const skillsStamp = useServerSkills({ registry, requestHeaders });
5253
+ const skillsStamp = useServerSkills({ registry, requestHeaders, enabled: serverResourcesEnabled });
5028
5254
  const allCommands = useMemo12(() => registry.list(), [registry, skillsStamp]);
5029
5255
  const { availableModels, model, setModel } = useChatModelSelection({
5030
5256
  defaultModel,
5031
- requestHeaders
5257
+ requestHeaders,
5258
+ enabled: serverResourcesEnabled
5032
5259
  });
5033
5260
  const { thinkingLevel, setThinkingLevel, showThoughts, setShowThoughts } = useThinkingSettings(thinkingControl);
5034
5261
  const { attachmentNotice, setAttachmentNotice } = useAttachmentNotice();
@@ -5127,23 +5354,49 @@ ${formatPluginReloadDiagnostics(diagnostics)}` : base;
5127
5354
  const waitingTail = projectedTailMessages.filter((message) => projectedStatusById.get(message.id) === "queued");
5128
5355
  const queuedPiTail = getQueuedPiTail(messages, piMessages);
5129
5356
  const sdkHasVisibleAssistant = hasStandardVisibleAssistantParts(messages);
5357
+ let baseMessages;
5130
5358
  if (sdkHasVisibleAssistant) {
5131
5359
  const mergedMessages = mergeSettledPiFallbacksInPlace(messages, piMessages);
5132
- return [
5360
+ baseMessages = [
5133
5361
  ...mergedMessages,
5134
5362
  ...coalesceAssistantToolFragments(queuedPiTail),
5135
5363
  ...waitingTail
5136
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];
5137
5375
  }
5138
- if (piMessages.length > 0 && queuedPiTail.length > 0) {
5139
- return [...coalesceAssistantToolFragments(piMessages), ...waitingTail];
5140
- }
5141
- if (piMessages.length > 0 && status === "ready" && hasPiVisibleDataParts(messages)) {
5142
- 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
+ }
5143
5391
  }
5144
- return [...messages, ...waitingTail];
5145
- }, [messages, piMessages, projectedTailMessages, projectedStatusById, status]);
5146
- 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;
5147
5400
  const handleStop = useCallback16(() => {
5148
5401
  onComposerStop?.();
5149
5402
  stopAndClearFollowUps();
@@ -5167,6 +5420,14 @@ ${formatPluginReloadDiagnostics(diagnostics)}` : base;
5167
5420
  [messages]
5168
5421
  );
5169
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]);
5170
5431
  const {
5171
5432
  mentionState,
5172
5433
  slashQuery,
@@ -5183,12 +5444,31 @@ ${formatPluginReloadDiagnostics(diagnostics)}` : base;
5183
5444
  textareaRef,
5184
5445
  disabled: mentionState !== null || slashQuery !== null
5185
5446
  });
5186
- async function handleSubmit({ text, files }) {
5187
- 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
+ }
5188
5465
  const trimmed = text.trim();
5189
5466
  if (trimmed.length === 0 && (!files || files.length === 0)) {
5190
5467
  return;
5191
5468
  }
5469
+ setComposerRuntimeNotice(null);
5470
+ if (!await runBeforeSubmit(text, files ?? [], source)) return false;
5471
+ if (liveSessionIdRef.current !== sessionId) return false;
5192
5472
  const parsed = parseSlashCommand(text);
5193
5473
  if (parsed) {
5194
5474
  const cmd = registry.get(parsed.name);
@@ -5240,6 +5520,7 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5240
5520
  files: files ?? [],
5241
5521
  mentionedFiles
5242
5522
  });
5523
+ if (liveSessionIdRef.current !== sessionId) return false;
5243
5524
  clearMentionedFiles();
5244
5525
  if (isStreaming && files.length > 0) {
5245
5526
  setAttachmentNotice("Attachments can be sent after the current response finishes.");
@@ -5277,6 +5558,48 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5277
5558
  }
5278
5559
  );
5279
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]);
5280
5603
  return /* @__PURE__ */ jsx25(ArtifactOpenProvider, { onOpenArtifact, children: /* @__PURE__ */ jsxs23(
5281
5604
  "div",
5282
5605
  {
@@ -5296,6 +5619,7 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5296
5619
  {
5297
5620
  className: cn(
5298
5621
  "flex h-full min-h-0 flex-col overflow-hidden",
5622
+ emptyHero && "justify-center",
5299
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)]"
5300
5624
  ),
5301
5625
  children: [
@@ -5325,7 +5649,7 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5325
5649
  /* @__PURE__ */ jsxs23(
5326
5650
  Conversation,
5327
5651
  {
5328
- className: "flex-1",
5652
+ className: emptyHero ? "max-h-[45vh] flex-none" : "flex-1",
5329
5653
  "aria-label": "Agent conversation",
5330
5654
  "aria-live": "polite",
5331
5655
  onScrollToBottomReady: (scrollToBottom) => {
@@ -5334,29 +5658,24 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5334
5658
  children: [
5335
5659
  /* @__PURE__ */ jsxs23(ConversationContent, { className: cn(
5336
5660
  "mx-auto flex w-full flex-col gap-6",
5337
- 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"
5338
5663
  ), children: [
5339
- displayMessages.length === 0 && /* @__PURE__ */ jsx25(
5664
+ renderMessages.length === 0 && /* @__PURE__ */ jsx25(
5340
5665
  ChatEmptyState,
5341
5666
  {
5342
5667
  eyebrow: emptyState?.eyebrow,
5343
5668
  title: emptyState?.title,
5344
5669
  description: emptyState?.description,
5345
5670
  suggestions,
5671
+ className: emptyHero ? "items-center text-center [&>p]:mx-auto" : void 0,
5346
5672
  onSelect: (s) => {
5347
5673
  const text = s.prompt ?? s.label;
5348
5674
  if (!text.trim()) return;
5349
- void sendMessage(
5350
- { text, files: [] },
5351
- {
5352
- body: {
5353
- sessionId,
5354
- message: text,
5355
- ...modelPayload(model),
5356
- attachments: []
5357
- }
5358
- }
5359
- );
5675
+ void (async () => {
5676
+ const submitted = await handleSubmit({ text, files: [] }, "suggestion");
5677
+ if (submitted === false) setComposerDraft(text);
5678
+ })();
5360
5679
  }
5361
5680
  }
5362
5681
  ),
@@ -5372,7 +5691,11 @@ ${parsed.args}` : `skill: ${parsed.name}`;
5372
5691
  const key = `${message.id}-${index}`;
5373
5692
  if (!reasoningPart) {
5374
5693
  if (!isBlankTextPart(part)) {
5375
- 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
+ }
5376
5699
  }
5377
5700
  return items;
5378
5701
  }
@@ -5399,6 +5722,12 @@ ${reasoningPart.text}`;
5399
5722
  acc.push({ kind: "tool-group", tools: [{ part: item.part, key: item.key }], key: item.key });
5400
5723
  }
5401
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
+ }
5402
5731
  } else {
5403
5732
  acc.push(item);
5404
5733
  }
@@ -5559,8 +5888,8 @@ ${reasoningPart.text}`;
5559
5888
  );
5560
5889
  }),
5561
5890
  (() => {
5562
- if (!error) return null;
5563
- const friendly = friendlyError(error);
5891
+ if (!friendlyChatError || isComposerRuntimeNotice(friendlyChatError)) return null;
5892
+ const friendly = friendlyChatError;
5564
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: [
5565
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: [
5566
5895
  /* @__PURE__ */ jsx25("circle", { cx: "12", cy: "12", r: "10" }),
@@ -5627,7 +5956,26 @@ ${reasoningPart.text}`;
5627
5956
  )
5628
5957
  }
5629
5958
  ),
5630
- 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(
5631
5979
  "div",
5632
5980
  {
5633
5981
  role: "status",
@@ -5722,7 +6070,7 @@ ${reasoningPart.text}`;
5722
6070
  PromptInput,
5723
6071
  {
5724
6072
  "data-boring-state": status,
5725
- onSubmit: handleSubmit,
6073
+ onSubmit: (message) => handleSubmit(message),
5726
6074
  multiple: true,
5727
6075
  maxFiles: attachmentsDisabled ? 0 : 20,
5728
6076
  maxFileSize: 5 * 1024 * 1024,
@@ -5744,14 +6092,16 @@ ${reasoningPart.text}`;
5744
6092
  /* @__PURE__ */ jsx25(
5745
6093
  PromptInputTextarea,
5746
6094
  {
5747
- placeholder: composerBlocked ? composerBlockerLabel : "Ask anything\u2026",
6095
+ defaultValue: promptInputController ? void 0 : initialDraft,
6096
+ placeholder: composerBlocked ? composerBlockerLabel : composerPlaceholder ?? "Ask anything\u2026",
5748
6097
  disabled: composerBlocked,
5749
6098
  readOnly: composerBlocked,
6099
+ ref: textareaRef,
5750
6100
  onChange: handleComposerChange,
5751
6101
  onKeyDown: handleComposerKeyDown,
5752
6102
  className: cn(
5753
- "min-h-[48px] resize-none border-0 bg-transparent shadow-none",
5754
- "px-4 py-3 text-[13px] leading-[1.55] placeholder:text-muted-foreground/50",
6103
+ "min-h-11 resize-none border-0 bg-transparent shadow-none",
6104
+ "px-4 py-2.5 text-[13px] leading-[1.55] placeholder:text-muted-foreground/45",
5755
6105
  "focus-visible:ring-0 focus-visible:ring-offset-0"
5756
6106
  )
5757
6107
  }
@@ -5809,7 +6159,7 @@ ${reasoningPart.text}`;
5809
6159
  // to earn the real estate. Becomes a Stop affordance
5810
6160
  // (square icon + aria-label="Stop") while the turn
5811
6161
  // streams.
5812
- "h-8 w-8 shrink-0 rounded-[var(--radius-lg)]",
6162
+ "h-8 w-8 shrink-0 rounded-full",
5813
6163
  "bg-[color:var(--accent)] text-[color:var(--accent-foreground)]",
5814
6164
  "transition-all duration-150 ease-[cubic-bezier(0.22,1,0.36,1)]",
5815
6165
  "hover:shadow-[0_0_0_3px_oklch(from_var(--accent)_l_c_h/0.30)] hover:brightness-110 hover:scale-[1.04]",
@@ -5863,7 +6213,7 @@ function AttachmentButton({ disabled }) {
5863
6213
  className: cn(composerActionClass, "w-8"),
5864
6214
  "aria-label": "Attach files",
5865
6215
  title: disabled ? "Attachments are available after the current response finishes." : "Attach files",
5866
- children: /* @__PURE__ */ jsx25(PaperclipIcon2, { className: "h-4 w-4" })
6216
+ children: /* @__PURE__ */ jsx25(PaperclipIcon2, { className: "h-3.5 w-3.5", strokeWidth: 1.75 })
5867
6217
  }
5868
6218
  );
5869
6219
  }
@@ -6103,7 +6453,7 @@ function getAgentCommands(options = {}) {
6103
6453
  }
6104
6454
 
6105
6455
  // src/front/hooks/useSessions.ts
6106
- 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";
6107
6457
  var API_BASE = "/api/v1/agent/sessions";
6108
6458
  var STORAGE_KEY = "boring-agent:activeSessionId";
6109
6459
  function readPersistedId(storageKey) {
@@ -6120,6 +6470,9 @@ function persistId(storageKey, id) {
6120
6470
  } catch {
6121
6471
  }
6122
6472
  }
6473
+ function headersScopeKey(headers) {
6474
+ return JSON.stringify(Object.entries(headers ?? {}).sort(([a], [b]) => a.localeCompare(b)));
6475
+ }
6123
6476
  function requestInit(headers) {
6124
6477
  if (!headers || Object.keys(headers).length === 0) return void 0;
6125
6478
  return { headers };
@@ -6133,21 +6486,46 @@ async function fetchSessions(headers) {
6133
6486
  function useSessions(opts = {}) {
6134
6487
  const storageKey = opts.storageKey ?? STORAGE_KEY;
6135
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
+ );
6136
6496
  const [sessions, setSessions] = useState21([]);
6137
6497
  const [activeSessionId, setActiveSessionId] = useState21(
6138
6498
  () => readPersistedId(storageKey)
6139
6499
  );
6140
6500
  const [loading, setLoading] = useState21(true);
6141
6501
  const [error, setError] = useState21();
6502
+ const [loaded, setLoaded] = useState21(false);
6142
6503
  const versionRef = useRef14(0);
6504
+ const loadedScopeRef = useRef14(scopeKey);
6143
6505
  const refresh = useCallback17(async () => {
6144
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);
6145
6517
  try {
6146
6518
  const data = await fetchSessions(requestHeaders);
6147
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);
6148
6525
  setSessions(data);
6149
6526
  setActiveSessionId((prev) => {
6150
- 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;
6151
6529
  const next = data[0]?.id;
6152
6530
  persistId(storageKey, next);
6153
6531
  return next;
@@ -6156,16 +6534,32 @@ function useSessions(opts = {}) {
6156
6534
  }
6157
6535
  } catch (err) {
6158
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);
6159
6544
  setError(err instanceof Error ? err : new Error(String(err)));
6160
6545
  setLoading(false);
6161
6546
  }
6162
6547
  }
6163
- }, [requestHeaders, storageKey]);
6548
+ }, [enabled, requestHeaders, scopeKey, storageKey]);
6164
6549
  useEffect19(() => {
6550
+ if (!enabled) {
6551
+ setSessions([]);
6552
+ setActiveSessionId(void 0);
6553
+ setError(void 0);
6554
+ setLoaded(false);
6555
+ setLoading(false);
6556
+ return;
6557
+ }
6165
6558
  void refresh();
6166
- }, [refresh]);
6559
+ }, [enabled, refresh, refreshKey, scopeKey]);
6167
6560
  const create = useCallback17(
6168
6561
  async (init) => {
6562
+ if (!enabled) throw new Error("Sessions are disabled");
6169
6563
  const res = await fetch(API_BASE, {
6170
6564
  method: "POST",
6171
6565
  headers: { ...requestHeaders, "Content-Type": "application/json" },
@@ -6183,7 +6577,7 @@ function useSessions(opts = {}) {
6183
6577
  void refresh();
6184
6578
  return session;
6185
6579
  },
6186
- [refresh, requestHeaders, storageKey]
6580
+ [enabled, refresh, requestHeaders, storageKey]
6187
6581
  );
6188
6582
  const switchSession = useCallback17((id) => {
6189
6583
  setActiveSessionId(id);
@@ -6191,6 +6585,7 @@ function useSessions(opts = {}) {
6191
6585
  }, [storageKey]);
6192
6586
  const deleteSession = useCallback17(
6193
6587
  async (id) => {
6588
+ if (!enabled) throw new Error("Sessions are disabled");
6194
6589
  setSessions((prev) => prev.filter((s) => s.id !== id));
6195
6590
  setActiveSessionId((prev) => {
6196
6591
  if (prev === id) {
@@ -6214,13 +6609,16 @@ function useSessions(opts = {}) {
6214
6609
  }
6215
6610
  void refresh();
6216
6611
  },
6217
- [refresh, requestHeaders, storageKey]
6612
+ [enabled, refresh, requestHeaders, storageKey]
6218
6613
  );
6614
+ const scopeMatches = loadedScopeRef.current === scopeKey;
6615
+ const visibleSessions = enabled && scopeMatches ? sessions : [];
6616
+ const visibleActiveSessionId = enabled && scopeMatches ? activeSessionId : void 0;
6219
6617
  return {
6220
- sessions,
6221
- activeSession: sessions.find((s) => s.id === activeSessionId),
6222
- activeSessionId,
6223
- loading,
6618
+ sessions: visibleSessions,
6619
+ activeSession: visibleSessions.find((s) => s.id === visibleActiveSessionId),
6620
+ activeSessionId: visibleActiveSessionId,
6621
+ loading: enabled ? !scopeMatches || loading || !loaded : false,
6224
6622
  error,
6225
6623
  create,
6226
6624
  switch: switchSession,