@hachej/boring-agent 0.1.94 → 0.1.96

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.
Files changed (45) hide show
  1. package/dist/DebugDrawer-2TO5SE2D.js +7 -0
  2. package/dist/{chunk-NITBKFG6.js → chunk-3OEDBRKY.js} +93 -58
  3. package/dist/{chunk-BOIV2I3N.js → chunk-46HJK2ES.js} +31 -16
  4. package/dist/{chunk-AQBXNPMD.js → chunk-4IQ4HHBG.js} +4 -0
  5. package/dist/{chunk-OFQ7J6SI.js → chunk-LSO2AO6Q.js} +57 -1
  6. package/dist/{chunk-N4AATPH7.js → chunk-NSE2RHU3.js} +8973 -7731
  7. package/dist/{chunk-2GBHYJVC.js → chunk-VXF5DWS6.js} +34 -0
  8. package/dist/{chunk-2TMS2RU2.js → chunk-WC5KBGP2.js} +518 -165
  9. package/dist/chunk-YGRQ4Q5P.js +20 -0
  10. package/dist/core/index.d.ts +5 -5
  11. package/dist/core/index.js +604 -7
  12. package/dist/{createHarness-UCD74XXK.js → createHarness-PW3A2MRB.js} +3 -3
  13. package/dist/{errors-G6T6S7PQ.d.ts → errors-cNC4HMpV.d.ts} +14 -7
  14. package/dist/eval/index.d.ts +5 -3
  15. package/dist/eval/index.js +31 -23
  16. package/dist/file-search-QxnanNjO.d.ts +87 -0
  17. package/dist/front/index.d.ts +87 -17
  18. package/dist/front/index.js +1232 -690
  19. package/dist/front/styles.css +1228 -2167
  20. package/dist/{harness-Cc48o6uS.d.ts → harness-DS8jLKIx.d.ts} +4 -2
  21. package/dist/{lease-CusB-uLg.d.ts → lease-D8WC6pQl.d.ts} +10 -89
  22. package/dist/{middleware-CzW8n7B7.d.ts → middleware-Cq_cYHjJ.d.ts} +1 -1
  23. package/dist/piChatEvent-B3ehh9V0.d.ts +384 -0
  24. package/dist/{piChatSessionService-C0KbcGw9.d.ts → piChatSessionService-xnv8rcdh.d.ts} +1 -1
  25. package/dist/{sandbox-DAD8cWQR.d.ts → sandbox-KQiEdz4W.d.ts} +12 -1
  26. package/dist/server/agent-host/testing/compositionRouteProof.d.ts +8 -0
  27. package/dist/server/agent-host/testing/compositionRouteProof.js +78 -0
  28. package/dist/server/agent-host/testing/gatewayConformance.d.ts +6 -6
  29. package/dist/server/agent-host/testing/gatewayConformance.js +17 -9
  30. package/dist/server/index.d.ts +570 -370
  31. package/dist/server/index.js +69 -17
  32. package/dist/server/worker/index.d.ts +2 -2
  33. package/dist/server/worker/index.js +9 -9
  34. package/dist/shared/index.d.ts +212 -56
  35. package/dist/shared/index.js +43 -7
  36. package/dist/{types-C-Eroi__.d.ts → types-D_SD-7_l.d.ts} +304 -87
  37. package/docs/AGENT_GATEWAY_V0.md +6 -3
  38. package/docs/AGENT_PACKAGES.md +80 -0
  39. package/docs/ERROR_CODES.md +58 -0
  40. package/docs/README.md +1 -0
  41. package/docs/runtime.md +19 -2
  42. package/package.json +34 -29
  43. package/dist/DebugDrawer-RYEBQ6CO.js +0 -6
  44. package/dist/chunk-EOMY2HDT.js +0 -619
  45. package/dist/piChatEvent-BsMp3MSo.d.ts +0 -368
@@ -1,9 +1,14 @@
1
1
  import {
2
2
  DebugDrawer,
3
+ agentResourceUrl,
3
4
  cn,
4
- copyTextToClipboard
5
- } from "../chunk-BOIV2I3N.js";
5
+ copyTextToClipboard,
6
+ createRequestId,
7
+ withStorageScope
8
+ } from "../chunk-46HJK2ES.js";
6
9
  import {
10
+ AGENT_GATEWAY_ERROR_CODES,
11
+ AgentGatewayErrorCode,
7
12
  CommandReceiptSchema,
8
13
  FollowUpReceiptSchema,
9
14
  PiChatSnapshotSchema,
@@ -15,10 +20,11 @@ import {
15
20
  WORKSPACE_COMMAND_NOTIFY_EVENT,
16
21
  extractToolUiMetadata,
17
22
  sanitizeToolUiMetadata
18
- } from "../chunk-OFQ7J6SI.js";
23
+ } from "../chunk-LSO2AO6Q.js";
24
+ import "../chunk-YGRQ4Q5P.js";
19
25
  import {
20
26
  ErrorCode
21
- } from "../chunk-2GBHYJVC.js";
27
+ } from "../chunk-VXF5DWS6.js";
22
28
 
23
29
  // src/front/upload/uploadFile.ts
24
30
  function readAsDataUrl(file) {
@@ -140,12 +146,73 @@ var defaultChatSuggestions = [
140
146
  prompt: "Help me refactor a function \u2014 clean it up without changing behavior."
141
147
  }
142
148
  ];
149
+ function ChatSuggestionGrid({ suggestions, onSelect, className }) {
150
+ if (suggestions.length === 0) return null;
151
+ return /* @__PURE__ */ jsx2(
152
+ "div",
153
+ {
154
+ "data-boring-agent-part": "suggestion-grid",
155
+ style: {
156
+ ["--chat-empty-card-hover"]: "var(--accent-soft)",
157
+ ["--chat-empty-accent"]: "var(--accent)"
158
+ },
159
+ className: cn(
160
+ "grid w-full grid-cols-1 gap-px overflow-hidden rounded-xl bg-border/70 ring-1 ring-border/70 @[480px]:grid-cols-2",
161
+ className
162
+ ),
163
+ children: suggestions.map((suggestion) => {
164
+ const Icon = suggestion.icon;
165
+ return /* @__PURE__ */ jsxs(
166
+ Button,
167
+ {
168
+ type: "button",
169
+ variant: "ghost",
170
+ "data-boring-agent-part": "suggestion-card",
171
+ onClick: () => {
172
+ if (suggestion.onSelect) {
173
+ suggestion.onSelect();
174
+ return;
175
+ }
176
+ onSelect?.(suggestion);
177
+ },
178
+ className: "group h-auto justify-start gap-3 rounded-none bg-background px-4 py-3.5 text-left hover:bg-[color:var(--chat-empty-card-hover)] focus-visible:bg-[color:var(--chat-empty-card-hover)]",
179
+ children: [
180
+ Icon && /* @__PURE__ */ jsx2(
181
+ Icon,
182
+ {
183
+ className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground transition-colors group-hover:text-[color:var(--chat-empty-accent)]",
184
+ strokeWidth: 1.75
185
+ }
186
+ ),
187
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
188
+ /* @__PURE__ */ jsx2("span", { className: "text-[13px] font-medium text-foreground", children: suggestion.label }),
189
+ suggestion.hint && /* @__PURE__ */ jsx2("span", { className: "text-[12px] text-muted-foreground", children: suggestion.hint })
190
+ ] }),
191
+ /* @__PURE__ */ jsx2(
192
+ ArrowUpRight,
193
+ {
194
+ className: cn(
195
+ "mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground opacity-0 transition-all duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]",
196
+ "group-hover:translate-x-0.5 group-hover:-translate-y-0.5 group-hover:opacity-100 group-hover:text-[color:var(--chat-empty-accent)]"
197
+ ),
198
+ strokeWidth: 2
199
+ }
200
+ )
201
+ ]
202
+ },
203
+ suggestion.label
204
+ );
205
+ })
206
+ }
207
+ );
208
+ }
143
209
  function ChatEmptyState({
144
210
  eyebrow = "New session",
145
- title = "What are we building?",
211
+ title = "What should we work on?",
146
212
  description = "Ask a question, open a file from the workbench, or start from a template.",
147
213
  suggestions = defaultChatSuggestions,
148
214
  onSelect,
215
+ hero = false,
149
216
  footer,
150
217
  className
151
218
  }) {
@@ -164,65 +231,20 @@ function ChatEmptyState({
164
231
  // @container lets the hero scale to the pane it lives in, not the
165
232
  // viewport — split chat panes get the compact variant.
166
233
  "@container mx-auto flex w-full max-w-[640px] flex-col px-2 pt-12 pb-4",
234
+ hero && "items-center py-0 text-center",
167
235
  className
168
236
  ),
169
237
  children: [
170
- eyebrow && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-[10.5px] font-medium uppercase tracking-[0.16em] text-[color:var(--chat-empty-eyebrow-color)]", children: [
238
+ !hero && eyebrow && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-[10.5px] font-medium uppercase tracking-[0.16em] text-[color:var(--chat-empty-eyebrow-color)]", children: [
171
239
  /* @__PURE__ */ jsx2("span", { className: "inline-block h-px w-4 bg-[color:var(--chat-empty-accent)]", "aria-hidden": "true" }),
172
240
  eyebrow
173
241
  ] }),
174
- title && /* @__PURE__ */ jsx2("h3", { className: "mt-3 text-[24px] font-medium leading-[1.05] tracking-[-0.02em] text-[color:var(--chat-empty-title-color)] @[480px]:text-[34px]", children: title }),
175
- description && /* @__PURE__ */ jsx2("p", { className: "mt-3 hidden max-w-[440px] text-[14px] leading-relaxed text-[color:var(--chat-empty-description-color)] @[420px]:block", children: description }),
176
- suggestions.length > 0 && /* @__PURE__ */ jsx2(
177
- "div",
178
- {
179
- "data-boring-agent-part": "suggestion-grid",
180
- className: "mt-8 grid w-full grid-cols-1 gap-px overflow-hidden rounded-xl bg-border/70 ring-1 ring-border/70 @[480px]:grid-cols-2",
181
- children: suggestions.map((suggestion) => {
182
- const Icon = suggestion.icon;
183
- return /* @__PURE__ */ jsxs(
184
- Button,
185
- {
186
- type: "button",
187
- variant: "ghost",
188
- "data-boring-agent-part": "suggestion-card",
189
- onClick: () => {
190
- if (suggestion.onSelect) {
191
- suggestion.onSelect();
192
- return;
193
- }
194
- onSelect?.(suggestion);
195
- },
196
- className: "group h-auto justify-start gap-3 rounded-none bg-background px-4 py-3.5 text-left hover:bg-[color:var(--chat-empty-card-hover)] focus-visible:bg-[color:var(--chat-empty-card-hover)]",
197
- children: [
198
- Icon && /* @__PURE__ */ jsx2(
199
- Icon,
200
- {
201
- className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground transition-colors group-hover:text-[color:var(--chat-empty-accent)]",
202
- strokeWidth: 1.75
203
- }
204
- ),
205
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
206
- /* @__PURE__ */ jsx2("span", { className: "text-[13px] font-medium text-foreground", children: suggestion.label }),
207
- suggestion.hint && /* @__PURE__ */ jsx2("span", { className: "text-[12px] text-muted-foreground", children: suggestion.hint })
208
- ] }),
209
- /* @__PURE__ */ jsx2(
210
- ArrowUpRight,
211
- {
212
- className: cn(
213
- "mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground opacity-0 transition-all duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]",
214
- "group-hover:translate-x-0.5 group-hover:-translate-y-0.5 group-hover:opacity-100 group-hover:text-[color:var(--chat-empty-accent)]"
215
- ),
216
- strokeWidth: 2
217
- }
218
- )
219
- ]
220
- },
221
- suggestion.label
222
- );
223
- })
224
- }
225
- ),
242
+ title && /* @__PURE__ */ jsx2("h3", { className: cn(
243
+ "text-[24px] font-medium leading-[1.05] tracking-[-0.02em] text-[color:var(--chat-empty-title-color)] @[480px]:text-[34px]",
244
+ hero ? "m-0" : "mt-3"
245
+ ), children: title }),
246
+ !hero && description && /* @__PURE__ */ jsx2("p", { className: "mt-3 hidden max-w-[440px] text-[14px] leading-relaxed text-[color:var(--chat-empty-description-color)] @[420px]:block", children: description }),
247
+ /* @__PURE__ */ jsx2(ChatSuggestionGrid, { suggestions, onSelect, className: hero ? "mt-4" : "mt-8" }),
226
248
  footer
227
249
  ]
228
250
  }
@@ -1996,14 +2018,16 @@ function MentionPicker({
1996
2018
  setActiveIdx(0);
1997
2019
  const ctrl = new AbortController();
1998
2020
  const runFetch = fetchImpl ?? globalThis.fetch.bind(globalThis);
1999
- const headers = scopedHeaders(requestHeaders, storageScope);
2021
+ const headers = withStorageScope(requestHeaders, storageScope);
2000
2022
  const base = apiBaseUrl.replace(/\/$/, "");
2001
2023
  const t = setTimeout(() => {
2002
2024
  runFetch(`${base}/api/v1/files/search?q=${encodeURIComponent(mentionSearchGlob(mention.query))}&limit=8`, {
2003
2025
  signal: ctrl.signal,
2004
2026
  ...headers ? { headers } : {}
2005
2027
  }).then((r) => r.ok ? r.json() : null).then((data) => {
2006
- if (data?.results) setResults(data.results.slice(0, 8));
2028
+ if (data?.resources) {
2029
+ setResults(data.resources.filter((resource) => resource.filesystem === "user").map((resource) => resource.path).slice(0, 8));
2030
+ }
2007
2031
  }).catch(() => {
2008
2032
  });
2009
2033
  }, 120);
@@ -2054,15 +2078,6 @@ function detectMention(value, cursorPos) {
2054
2078
  const atIdx = before.lastIndexOf("@");
2055
2079
  return { query: match[2], anchorStart: atIdx, anchorEnd: cursorPos };
2056
2080
  }
2057
- function scopedHeaders(requestHeaders, storageScope) {
2058
- const headers = {};
2059
- for (const [key, value] of Object.entries(requestHeaders ?? {})) {
2060
- if (value !== void 0) headers[key] = value;
2061
- }
2062
- const hasStorageScope = Object.keys(headers).some((key) => key.toLowerCase() === "x-boring-storage-scope");
2063
- if (storageScope && !hasStorageScope) headers["x-boring-storage-scope"] = storageScope;
2064
- return Object.keys(headers).length > 0 ? headers : void 0;
2065
- }
2066
2081
 
2067
2082
  // src/front/useComposerPickers.ts
2068
2083
  function setTextareaValue2(textarea, value) {
@@ -2583,6 +2598,7 @@ function resolveStorage(storage) {
2583
2598
 
2584
2599
  // src/front/hooks/useChatModelSelection.ts
2585
2600
  function useChatModelSelection({
2601
+ agentTypeId,
2586
2602
  apiBaseUrl,
2587
2603
  defaultModel,
2588
2604
  fetch: fetchImpl,
@@ -2604,14 +2620,16 @@ function useChatModelSelection({
2604
2620
  userSelectedModelRef.current = userSelectedModel;
2605
2621
  }, [userSelectedModel]);
2606
2622
  const setModel = useCallback6((next) => {
2607
- userSelectedModelRef.current = next !== null;
2608
- setUserSelectedModel(next !== null);
2609
- setModelState(next);
2610
- writePiComposerModelSelection(next, { storageScope, storage });
2623
+ const normalized = next === null ? null : parseModelSelection(next);
2624
+ userSelectedModelRef.current = normalized !== null;
2625
+ setUserSelectedModel(normalized !== null);
2626
+ setModelState(normalized);
2627
+ writePiComposerModelSelection(normalized, { storageScope, storage });
2611
2628
  }, [storage, storageScope]);
2612
2629
  const discoveryKey = useMemo2(
2613
2630
  () => JSON.stringify({
2614
2631
  apiBaseUrl: apiBaseUrl ?? "",
2632
+ agentTypeId,
2615
2633
  headers: Object.entries(requestHeaders ?? {}).sort(([a], [b]) => a.localeCompare(b)),
2616
2634
  storageScope: storageScope ?? ""
2617
2635
  }),
@@ -2645,8 +2663,8 @@ function useChatModelSelection({
2645
2663
  let aborted = false;
2646
2664
  setLoaded(false);
2647
2665
  const nextFetch = fetchImpl ?? globalThis.fetch.bind(globalThis);
2648
- nextFetch(agentResourceUrl(apiBaseUrl, "/api/v1/agent/models"), {
2649
- headers: scopedHeaders2(requestHeaders, storageScope)
2666
+ nextFetch(agentResourceUrl(apiBaseUrl, `/api/v1/agents/${encodeURIComponent(agentTypeId)}/models`), {
2667
+ headers: withStorageScope(requestHeaders, storageScope)
2650
2668
  }).then((res) => res.ok ? res.json() : null).then((payload) => {
2651
2669
  if (aborted) return;
2652
2670
  if (!payload?.models) {
@@ -2686,7 +2704,7 @@ function useChatModelSelection({
2686
2704
  return () => {
2687
2705
  aborted = true;
2688
2706
  };
2689
- }, [apiBaseUrl, discoveryKey, enabled, fetchImpl, requestHeaders, storage, storageScope]);
2707
+ }, [agentTypeId, apiBaseUrl, discoveryKey, enabled, fetchImpl, requestHeaders, storage, storageScope]);
2690
2708
  useEffect5(() => {
2691
2709
  const onChange = (event) => {
2692
2710
  const next = parseModelSelection(event.detail);
@@ -2700,17 +2718,6 @@ function useChatModelSelection({
2700
2718
  const currentModel = currentDiscoveryLoaded ? model : null;
2701
2719
  return { availableModels: currentAvailableModels, loaded: currentDiscoveryLoaded, model: currentModel, setModel };
2702
2720
  }
2703
- function agentResourceUrl(apiBaseUrl, path) {
2704
- const base = apiBaseUrl?.replace(/\/$/, "") ?? "";
2705
- return `${base}${path}`;
2706
- }
2707
- function scopedHeaders2(headers, storageScope) {
2708
- if (!headers && !storageScope) return void 0;
2709
- const result = { ...headers ?? {} };
2710
- const hasStorageScope = Object.keys(result).some((key) => key.toLowerCase() === "x-boring-storage-scope");
2711
- if (storageScope && !hasStorageScope) result["x-boring-storage-scope"] = storageScope;
2712
- return result;
2713
- }
2714
2721
 
2715
2722
  // src/front/hooks/useServerCommands.ts
2716
2723
  import { useEffect as useEffect6, useRef as useRef7, useState as useState8 } from "react";
@@ -2719,21 +2726,21 @@ function serverCommandErrorMessage(body, fallback) {
2719
2726
  if (body.error && typeof body.error === "object" && typeof body.error.message === "string") return body.error.message;
2720
2727
  return fallback;
2721
2728
  }
2722
- function toSlashCommand(command, getSessionId, apiBaseUrl, requestHeaders, fetchImpl) {
2729
+ function toSlashCommand(command, identity, isCurrentIdentity, apiBaseUrl, requestHeaders, fetchImpl) {
2723
2730
  return {
2724
2731
  name: command.name,
2725
2732
  description: command.description ?? "",
2726
2733
  source: command.source,
2727
2734
  ...command.sourcePlugin ? { sourcePlugin: command.sourcePlugin } : {},
2728
2735
  handler: async (args) => {
2736
+ if (!isCurrentIdentity(identity.key)) return;
2729
2737
  const base = apiBaseUrl?.replace(/\/$/, "") ?? "";
2730
- const params = new URLSearchParams({ sessionId: getSessionId() });
2731
- const url = `${base}/api/v1/agent/commands/execute?${params.toString()}`;
2738
+ const url = `${base}/api/v1/agents/${encodeURIComponent(identity.agentTypeId)}/commands/execute`;
2732
2739
  try {
2733
2740
  const res = await fetchImpl(url, {
2734
2741
  method: "POST",
2735
2742
  headers: { ...requestHeaders ?? {}, "content-type": "application/json" },
2736
- body: JSON.stringify({ name: command.name, args })
2743
+ body: JSON.stringify({ requestId: createRequestId("command"), sessionId: identity.sessionId, name: command.name, args })
2737
2744
  });
2738
2745
  if (!res.ok) {
2739
2746
  const body = await res.json().catch(() => ({}));
@@ -2754,6 +2761,7 @@ function toSlashCommand(command, getSessionId, apiBaseUrl, requestHeaders, fetch
2754
2761
  };
2755
2762
  }
2756
2763
  function useServerCommands({
2764
+ agentTypeId,
2757
2765
  registry,
2758
2766
  requestHeaders,
2759
2767
  sessionId,
@@ -2764,41 +2772,62 @@ function useServerCommands({
2764
2772
  refreshKey = 0
2765
2773
  }) {
2766
2774
  const [stamp, setStamp] = useState8(0);
2767
- const registeredNamesRef = useRef7(/* @__PURE__ */ new Set());
2768
- const sessionIdRef = useRef7(sessionId);
2769
- useEffect6(() => {
2770
- sessionIdRef.current = sessionId;
2771
- }, [sessionId]);
2775
+ const registrationsRef = useRef7({ registry, identity: void 0, names: /* @__PURE__ */ new Set() });
2776
+ const canonicalSessionId = sessionId.trim() || void 0;
2777
+ const identityKey = canonicalSessionId ? `${agentTypeId}\0${canonicalSessionId}` : void 0;
2778
+ const identityKeyRef = useRef7(identityKey);
2779
+ identityKeyRef.current = identityKey;
2780
+ useEffect6(() => () => {
2781
+ const registrations = registrationsRef.current;
2782
+ for (const name of registrations.names) registrations.registry.unregister(name);
2783
+ registrationsRef.current = { registry: registrations.registry, identity: void 0, names: /* @__PURE__ */ new Set() };
2784
+ }, []);
2772
2785
  useEffect6(() => {
2773
2786
  const clearRegistered = () => {
2774
- if (registeredNamesRef.current.size === 0) return false;
2775
- for (const name of registeredNamesRef.current) registry.unregister(name);
2776
- registeredNamesRef.current = /* @__PURE__ */ new Set();
2777
- return true;
2787
+ const registrations = registrationsRef.current;
2788
+ for (const name of registrations.names) registrations.registry.unregister(name);
2789
+ const changed = registrations.names.size > 0;
2790
+ registrationsRef.current = { registry, identity: void 0, names: /* @__PURE__ */ new Set() };
2791
+ return changed;
2778
2792
  };
2779
- if (!enabled) {
2793
+ if (registrationsRef.current.registry !== registry) {
2794
+ if (clearRegistered()) setStamp((n) => n + 1);
2795
+ }
2796
+ if (!enabled || !canonicalSessionId || !identityKey) {
2780
2797
  if (clearRegistered()) setStamp((n) => n + 1);
2781
2798
  return;
2782
2799
  }
2800
+ if (registrationsRef.current.identity !== identityKey) {
2801
+ if (clearRegistered()) setStamp((n) => n + 1);
2802
+ }
2783
2803
  let aborted = false;
2784
2804
  const nextFetch = fetchImpl ?? globalThis.fetch.bind(globalThis);
2785
2805
  const base = apiBaseUrl?.replace(/\/$/, "") ?? "";
2786
- const url = `${base}/api/v1/agent/commands?sessionId=${encodeURIComponent(sessionIdRef.current)}`;
2787
- const headers = scopedHeaders3(requestHeaders, storageScope);
2806
+ const url = `${base}/api/v1/agents/${encodeURIComponent(agentTypeId)}/commands?sessionId=${encodeURIComponent(canonicalSessionId)}`;
2807
+ const headers = withStorageScope(requestHeaders, storageScope);
2788
2808
  nextFetch(url, { headers }).then(async (res) => {
2789
2809
  if (!res.ok) throw new Error(`commands request failed (${res.status})`);
2790
2810
  return await res.json();
2791
2811
  }).then((payload) => {
2792
2812
  if (aborted) return;
2793
2813
  const removed = clearRegistered();
2814
+ const registeredNames = /* @__PURE__ */ new Set();
2794
2815
  let added = false;
2795
2816
  for (const serverCommand of payload.commands ?? []) {
2796
- const command = toSlashCommand(serverCommand, () => sessionIdRef.current, apiBaseUrl, headers, nextFetch);
2817
+ const command = toSlashCommand(
2818
+ serverCommand,
2819
+ { key: identityKey, sessionId: canonicalSessionId, agentTypeId },
2820
+ (key) => identityKeyRef.current === key,
2821
+ apiBaseUrl,
2822
+ headers,
2823
+ nextFetch
2824
+ );
2797
2825
  if (registry.get(command.name)) continue;
2798
2826
  registry.register(command);
2799
- registeredNamesRef.current.add(command.name);
2827
+ registeredNames.add(command.name);
2800
2828
  added = true;
2801
2829
  }
2830
+ registrationsRef.current = { registry, identity: identityKey, names: registeredNames };
2802
2831
  if (removed || added) setStamp((n) => n + 1);
2803
2832
  }).catch(() => {
2804
2833
  if (aborted) return;
@@ -2807,16 +2836,9 @@ function useServerCommands({
2807
2836
  return () => {
2808
2837
  aborted = true;
2809
2838
  };
2810
- }, [apiBaseUrl, enabled, fetchImpl, refreshKey, requestHeaders, registry, storageScope]);
2839
+ }, [agentTypeId, apiBaseUrl, canonicalSessionId, enabled, fetchImpl, identityKey, refreshKey, requestHeaders, registry, storageScope]);
2811
2840
  return stamp;
2812
2841
  }
2813
- function scopedHeaders3(headers, storageScope) {
2814
- if (!headers && !storageScope) return void 0;
2815
- const result = { ...headers ?? {} };
2816
- const hasScope = Object.keys(result).some((k) => k.toLowerCase() === "x-boring-storage-scope");
2817
- if (storageScope && !hasScope) result["x-boring-storage-scope"] = storageScope;
2818
- return result;
2819
- }
2820
2842
 
2821
2843
  // src/front/hooks/useAttachmentNotice.ts
2822
2844
  import { useEffect as useEffect7, useState as useState9 } from "react";
@@ -3788,7 +3810,8 @@ function syncCursor(state, cursor) {
3788
3810
  return { ...state, lastSeq: cursor, hydrated: true, needsResync: void 0 };
3789
3811
  }
3790
3812
  function hydrateFromSnapshot(state, snapshot, options = {}) {
3791
- if (snapshot.seq < state.lastSeq && !options.allowSeqRewind) return state;
3813
+ const hasUnhydratedLiveEvents = state.turnId !== void 0 || state.streamingMessage !== void 0 || state.committedMessages.length > 0 || state.status === "streaming" || state.status === "aborting";
3814
+ if (snapshot.seq < state.lastSeq && !options.allowSeqRewind && (state.hydrated || hasUnhydratedLiveEvents)) return state;
3792
3815
  const queue = { followUps: enrichQueueWithKnownMetadata(snapshot.queue.followUps, state.optimisticOutbox, state.queue.followUps) };
3793
3816
  const snapshotMessages = normalizeSnapshotMessages(snapshot.messages);
3794
3817
  const preserveLocalHistory = shouldPreserveLocalCommittedHistory(state, snapshot, snapshotMessages, options);
@@ -4451,11 +4474,12 @@ function parsePiChatReplayRangeError(status, body) {
4451
4474
  }
4452
4475
  function buildPiChatEventsUrl({
4453
4476
  apiBaseUrl = "",
4477
+ agentTypeId,
4454
4478
  sessionId,
4455
4479
  cursor
4456
4480
  }) {
4457
4481
  const base = apiBaseUrl.replace(/\/$/, "");
4458
- return `${base}/api/v1/agent/pi-chat/${encodeURIComponent(sessionId)}/events?cursor=${encodeURIComponent(String(cursor))}`;
4482
+ return `${base}/api/v1/agents/${encodeURIComponent(agentTypeId)}/sessions/${encodeURIComponent(sessionId)}/events?cursor=${encodeURIComponent(String(cursor))}`;
4459
4483
  }
4460
4484
  function calculateJitteredBackoffDelayMs({
4461
4485
  attempt,
@@ -4532,6 +4556,7 @@ var RemotePiSession = class {
4532
4556
  streamRunId = 0;
4533
4557
  reconnectAttempt = 0;
4534
4558
  started = false;
4559
+ suspended = false;
4535
4560
  disposed = false;
4536
4561
  streamAbortController;
4537
4562
  reconnectTimer;
@@ -4565,6 +4590,7 @@ var RemotePiSession = class {
4565
4590
  streamingMessageCount: state.streamingMessage ? 1 : 0
4566
4591
  },
4567
4592
  disposed: this.disposed,
4593
+ suspended: this.suspended,
4568
4594
  generation: this.generation,
4569
4595
  streamRunId: this.streamRunId,
4570
4596
  reconnectAttempt: this.reconnectAttempt,
@@ -4576,7 +4602,7 @@ var RemotePiSession = class {
4576
4602
  return this.store.subscribe(listener);
4577
4603
  }
4578
4604
  start(cursor) {
4579
- if (this.disposed || this.started) return Promise.resolve();
4605
+ if (this.disposed || this.suspended || this.started) return Promise.resolve();
4580
4606
  this.started = true;
4581
4607
  const generation = this.generation;
4582
4608
  if (cursor === void 0) {
@@ -4591,8 +4617,8 @@ var RemotePiSession = class {
4591
4617
  if (!this.disposed) {
4592
4618
  this.store.dispatch({ type: "optimistic-user-message", message: toOptimisticUserMessage(payload) }, { flush: true });
4593
4619
  }
4594
- if (!this.started) await this.start(this.store.getState().lastSeq);
4595
- else this.ensureReconnectScheduled();
4620
+ if (!this.started && !this.suspended) await this.start(this.store.getState().lastSeq);
4621
+ else if (!this.suspended) this.ensureReconnectScheduled();
4596
4622
  try {
4597
4623
  const receipt = await this.postCommand("/prompt", payload, PromptReceiptSchema);
4598
4624
  return receipt;
@@ -4605,8 +4631,8 @@ var RemotePiSession = class {
4605
4631
  if (!this.disposed) {
4606
4632
  this.store.dispatch({ type: "optimistic-user-message", message: toOptimisticUserMessage(payload) }, { flush: true });
4607
4633
  }
4608
- if (!this.started) await this.start(this.store.getState().lastSeq);
4609
- else this.ensureReconnectScheduled();
4634
+ if (!this.started && !this.suspended) await this.start(this.store.getState().lastSeq);
4635
+ else if (!this.suspended) this.ensureReconnectScheduled();
4610
4636
  try {
4611
4637
  const receipt = await this.postCommand("/followup", payload, FollowUpReceiptSchema);
4612
4638
  return receipt;
@@ -4632,6 +4658,28 @@ var RemotePiSession = class {
4632
4658
  }
4633
4659
  return receipt;
4634
4660
  }
4661
+ suspendStream() {
4662
+ if (this.disposed || this.suspended) return;
4663
+ this.suspended = true;
4664
+ this.started = false;
4665
+ this.streamRunId += 1;
4666
+ this.clearReconnectTimer();
4667
+ this.abortEventStream();
4668
+ this.store.dispatch({ type: "connection-state", state: "suspended" }, { flush: true });
4669
+ }
4670
+ resumeStream() {
4671
+ if (this.disposed || !this.suspended && this.started) return;
4672
+ this.suspended = false;
4673
+ this.started = true;
4674
+ const generation = this.generation;
4675
+ const state = this.store.getState();
4676
+ if (!state.hydrated) {
4677
+ void this.hydrateAndConnect(generation);
4678
+ return;
4679
+ }
4680
+ this.store.dispatch({ type: "connection-state", state: "connecting" }, { flush: true });
4681
+ void this.connectEvents(state.lastSeq, generation);
4682
+ }
4635
4683
  dispose() {
4636
4684
  if (this.disposed) return;
4637
4685
  this.disposed = true;
@@ -4645,33 +4693,34 @@ var RemotePiSession = class {
4645
4693
  this.store.dispose();
4646
4694
  }
4647
4695
  async hydrateAndConnect(generation, options = {}) {
4648
- if (!this.isGenerationActive(generation)) return;
4696
+ const hydrationRunId = this.streamRunId;
4697
+ if (!this.isHydrationActive(generation, hydrationRunId)) return;
4649
4698
  this.clearReconnectTimer();
4650
4699
  this.abortEventStream();
4651
4700
  this.store.dispatch({ type: "connection-state", state: this.store.getState().hydrated ? "reconnecting" : "connecting" }, { flush: true });
4652
4701
  try {
4653
4702
  const headers = await this.requestHeaders();
4654
- if (!this.isGenerationActive(generation)) return;
4703
+ if (!this.isHydrationActive(generation, hydrationRunId)) return;
4655
4704
  const responseBody = await this.fetchJson(this.stateUrl(), { method: "GET", headers });
4656
- if (!this.isGenerationActive(generation)) return;
4657
- const raw = this.options.agentTypeId ? addressedSnapshot(responseBody) : responseBody;
4705
+ if (!this.isHydrationActive(generation, hydrationRunId)) return;
4706
+ const raw = addressedSnapshot(responseBody);
4658
4707
  this.recordLargeStateWarning(raw);
4659
4708
  if (readProtocolVersion(raw) !== SUPPORTED_PROTOCOL_VERSION) {
4660
4709
  this.dispatchProtocolError(`Unsupported Pi chat protocol version: ${String(readProtocolVersion(raw) ?? "missing")}`);
4661
4710
  return;
4662
4711
  }
4663
4712
  const snapshot = PiChatSnapshotSchema.parse(raw);
4664
- if (!this.isGenerationActive(generation)) return;
4713
+ if (!this.isHydrationActive(generation, hydrationRunId)) return;
4665
4714
  this.store.dispatch({ type: "hydrate", snapshot, allowSeqRewind: options?.allowSeqRewind }, { flush: true });
4666
4715
  this.connectEvents(snapshot.seq, generation);
4667
4716
  } catch (error) {
4668
- if (!this.isGenerationActive(generation) || isAbortError(error)) return;
4717
+ if (!this.isHydrationActive(generation, hydrationRunId) || isAbortError(error)) return;
4669
4718
  this.dispatchProtocolError(errorMessage(error, "Failed to hydrate Pi chat session state."));
4670
4719
  this.scheduleReconnect(generation);
4671
4720
  }
4672
4721
  }
4673
4722
  connectEvents(cursor, generation) {
4674
- if (!this.isGenerationActive(generation)) return Promise.resolve();
4723
+ if (!this.isStreamLifecycleActive(generation)) return Promise.resolve();
4675
4724
  this.clearReconnectTimer();
4676
4725
  this.abortEventStream();
4677
4726
  const runId = ++this.streamRunId;
@@ -4785,20 +4834,20 @@ var RemotePiSession = class {
4785
4834
  }
4786
4835
  }
4787
4836
  rehydrateAfterStreamReset(generation, options = {}) {
4788
- if (!this.isGenerationActive(generation)) return;
4837
+ if (!this.isStreamLifecycleActive(generation)) return;
4789
4838
  this.streamRunId += 1;
4790
4839
  this.abortEventStream();
4791
4840
  void this.hydrateAndConnect(generation, options);
4792
4841
  }
4793
4842
  ensureReconnectScheduled() {
4794
- if (!this.isGenerationActive(this.generation)) return;
4843
+ if (!this.isStreamLifecycleActive(this.generation)) return;
4795
4844
  const state = this.store.getState();
4796
4845
  if (state.connection.state === "connected" || state.connection.state === "connecting") return;
4797
4846
  if (this.reconnectTimer !== void 0) return;
4798
4847
  void this.hydrateAndConnect(this.generation);
4799
4848
  }
4800
4849
  scheduleReconnect(generation) {
4801
- if (!this.isGenerationActive(generation)) return;
4850
+ if (!this.isStreamLifecycleActive(generation)) return;
4802
4851
  this.clearReconnectTimer();
4803
4852
  this.store.dispatch({ type: "connection-state", state: "reconnecting" }, { flush: true });
4804
4853
  const attempt = this.reconnectAttempt++;
@@ -4812,7 +4861,7 @@ var RemotePiSession = class {
4812
4861
  clearTimeoutFn: this.clearTimeoutFn,
4813
4862
  reconnect: () => {
4814
4863
  this.reconnectTimer = void 0;
4815
- if (!this.isGenerationActive(generation)) return;
4864
+ if (!this.isStreamLifecycleActive(generation)) return;
4816
4865
  const state = this.store.getState();
4817
4866
  if (state.hydrated) this.connectEvents(state.lastSeq, generation);
4818
4867
  else void this.hydrateAndConnect(generation);
@@ -4835,7 +4884,7 @@ var RemotePiSession = class {
4835
4884
  return schema.parse(this.addressedCommandReceipt(path, raw));
4836
4885
  }
4837
4886
  addressedCommandReceipt(path, receipt) {
4838
- if (!this.options.agentTypeId || path !== "/followup" || typeof receipt !== "object" || receipt === null) {
4887
+ if (path !== "/followup" || typeof receipt !== "object" || receipt === null) {
4839
4888
  return receipt;
4840
4889
  }
4841
4890
  const record = receipt;
@@ -4881,19 +4930,18 @@ var RemotePiSession = class {
4881
4930
  return this.sessionUrl("/state");
4882
4931
  }
4883
4932
  eventsUrl(cursor) {
4884
- if (!this.options.agentTypeId) {
4885
- return buildPiChatEventsUrl({ apiBaseUrl: this.apiBaseUrl, sessionId: this.options.sessionId, cursor });
4886
- }
4887
- return `${this.sessionUrl("/events")}?cursor=${encodeURIComponent(String(cursor))}`;
4933
+ return buildPiChatEventsUrl({
4934
+ apiBaseUrl: this.apiBaseUrl,
4935
+ agentTypeId: this.options.agentTypeId,
4936
+ sessionId: this.options.sessionId,
4937
+ cursor
4938
+ });
4888
4939
  }
4889
4940
  sessionUrl(path) {
4890
- if (this.options.agentTypeId) {
4891
- return `${this.apiBaseUrl}/api/v1/agents/${encodeURIComponent(this.options.agentTypeId)}/sessions/${encodeURIComponent(this.options.sessionId)}${path}`;
4892
- }
4893
- return `${this.apiBaseUrl}/api/v1/agent/pi-chat/${encodeURIComponent(this.options.sessionId)}${path}`;
4941
+ return `${this.apiBaseUrl}/api/v1/agents/${encodeURIComponent(this.options.agentTypeId)}/sessions/${encodeURIComponent(this.options.sessionId)}${path}`;
4894
4942
  }
4895
4943
  addressedCommandPayload(path, payload) {
4896
- if (!this.options.agentTypeId || typeof payload !== "object" || payload === null) return payload;
4944
+ if (typeof payload !== "object" || payload === null) return payload;
4897
4945
  const record = payload;
4898
4946
  if (typeof record.requestId === "string" && record.requestId.length > 0) return payload;
4899
4947
  if (path === "/prompt" && typeof record.clientNonce === "string" && typeof record.message === "string") {
@@ -4956,8 +5004,14 @@ var RemotePiSession = class {
4956
5004
  isGenerationActive(generation) {
4957
5005
  return !this.disposed && generation === this.generation;
4958
5006
  }
5007
+ isStreamLifecycleActive(generation) {
5008
+ return !this.suspended && this.isGenerationActive(generation);
5009
+ }
5010
+ isHydrationActive(generation, runId) {
5011
+ return this.isStreamLifecycleActive(generation) && runId === this.streamRunId;
5012
+ }
4959
5013
  isStreamActive(generation, runId) {
4960
- return this.isGenerationActive(generation) && runId === this.streamRunId;
5014
+ return this.isStreamLifecycleActive(generation) && runId === this.streamRunId;
4961
5015
  }
4962
5016
  };
4963
5017
  function addressedSnapshot(value) {
@@ -5084,13 +5138,23 @@ function clearPageUnloading() {
5084
5138
  pageUnloading = false;
5085
5139
  }
5086
5140
 
5087
- // src/front/chat/session/activeSessionStorage.ts
5141
+ // src/front/chat/session/sessionSelectionStorage.ts
5088
5142
  var ACTIVE_SESSION_KEY_PREFIX = "boring-agent:v2";
5089
5143
  var ACTIVE_SESSION_KEY_SUFFIX = "activeSessionId";
5144
+ var BOOT_RESUME_SESSION_KEY_SUFFIX = "bootResumeSessionId";
5090
5145
  var DEFAULT_STORAGE_SCOPE2 = "default";
5091
5146
  function activeSessionStorageKey(storageScope) {
5092
- const scope = storageScope && storageScope.length > 0 ? storageScope : DEFAULT_STORAGE_SCOPE2;
5093
- return `${ACTIVE_SESSION_KEY_PREFIX}:${scope}:${ACTIVE_SESSION_KEY_SUFFIX}`;
5147
+ return scopedSessionStorageKey(storageScope, ACTIVE_SESSION_KEY_SUFFIX);
5148
+ }
5149
+ function bootResumeSessionStorageKey(source) {
5150
+ const scope = typeof source === "object" && source !== null ? encodeURIComponent(JSON.stringify([
5151
+ source.apiBaseUrl?.replace(/\/$/, "") ?? "",
5152
+ source.sessionsApiPath,
5153
+ source.agentTypeId ?? "",
5154
+ source.workspaceId ?? "",
5155
+ source.storageScope ?? DEFAULT_STORAGE_SCOPE2
5156
+ ])) : source;
5157
+ return scopedSessionStorageKey(scope, BOOT_RESUME_SESSION_KEY_SUFFIX);
5094
5158
  }
5095
5159
  function readActiveSessionId(options = {}) {
5096
5160
  const storage = resolveStorage2(options.storage);
@@ -5111,8 +5175,28 @@ function writeActiveSessionId(sessionId, options = {}) {
5111
5175
  } catch {
5112
5176
  }
5113
5177
  }
5114
- function clearActiveSessionId(options = {}) {
5115
- writeActiveSessionId(void 0, options);
5178
+ function readBootResumeSessionId(options = {}) {
5179
+ const storage = resolveBootStorage(options.storage);
5180
+ if (!storage) return void 0;
5181
+ try {
5182
+ return storage.getItem(bootResumeSessionStorageKey(options.bootResumeSource ?? options.storageScope)) ?? void 0;
5183
+ } catch {
5184
+ return void 0;
5185
+ }
5186
+ }
5187
+ function writeBootResumeSessionId(sessionId, options = {}) {
5188
+ const storage = resolveBootStorage(options.storage);
5189
+ if (!storage) return;
5190
+ try {
5191
+ const key = bootResumeSessionStorageKey(options.bootResumeSource ?? options.storageScope);
5192
+ if (sessionId === void 0 || sessionId.length === 0) storage.removeItem(key);
5193
+ else storage.setItem(key, sessionId);
5194
+ } catch {
5195
+ }
5196
+ }
5197
+ function scopedSessionStorageKey(storageScope, suffix) {
5198
+ const scope = storageScope && storageScope.length > 0 ? storageScope : DEFAULT_STORAGE_SCOPE2;
5199
+ return `${ACTIVE_SESSION_KEY_PREFIX}:${scope}:${suffix}`;
5116
5200
  }
5117
5201
  function resolveStorage2(storage) {
5118
5202
  if (storage) return storage;
@@ -5122,29 +5206,43 @@ function resolveStorage2(storage) {
5122
5206
  return void 0;
5123
5207
  }
5124
5208
  }
5209
+ function resolveBootStorage(storage) {
5210
+ if (storage) return storage;
5211
+ try {
5212
+ return globalThis.sessionStorage;
5213
+ } catch {
5214
+ return void 0;
5215
+ }
5216
+ }
5125
5217
 
5126
5218
  // src/front/chat/session/usePiSessions.ts
5127
- import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo4, useRef as useRef8, useState as useState10 } from "react";
5128
- var DEFAULT_SESSIONS_API_PATH = "/api/v1/agent/pi-chat/sessions";
5219
+ import { useCallback as useCallback7, useEffect as useEffect8, useLayoutEffect, useMemo as useMemo4, useRef as useRef8, useState as useState10 } from "react";
5129
5220
  var SESSION_PAGE_SIZE = 50;
5130
5221
  var DEFAULT_MAX_RETRIES = 60;
5131
5222
  var DEFAULT_RETRY_BASE_MS = 250;
5132
5223
  var DEFAULT_RETRY_MAX_MS = 2e3;
5224
+ var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect8 : useLayoutEffect;
5133
5225
  var SessionsPreparingError = class extends Error {
5134
5226
  constructor() {
5135
5227
  super("Agent runtime is still preparing");
5136
5228
  this.name = "SessionsPreparingError";
5137
5229
  }
5138
5230
  };
5231
+ var StaleSessionsSourceError = class extends Error {
5232
+ constructor() {
5233
+ super("Pi sessions source is stale");
5234
+ this.name = "StaleSessionsSourceError";
5235
+ }
5236
+ };
5139
5237
  function isNetworkFetchError(error) {
5140
5238
  return error instanceof TypeError;
5141
5239
  }
5142
- function usePiSessions(options = {}) {
5240
+ function usePiSessions(options) {
5143
5241
  const enabled = options.enabled ?? true;
5144
5242
  const apiBaseUrl = options.apiBaseUrl?.replace(/\/$/, "") ?? "";
5145
- const addressed = typeof options.agentTypeId === "string" && options.agentTypeId.length > 0;
5146
- const sessionsApiPath = options.sessionsApiPath ?? (addressed ? `/api/v1/agents/${encodeURIComponent(options.agentTypeId)}/sessions` : DEFAULT_SESSIONS_API_PATH);
5243
+ const sessionsApiPath = options.sessionsApiPath ?? `/api/v1/agents/${encodeURIComponent(options.agentTypeId)}/sessions`;
5147
5244
  const storageScope = options.storageScope ?? "default";
5245
+ const activeSessionStorageScope = options.activeSessionStorageScope ?? storageScope;
5148
5246
  const fetchImpl = useMemo4(() => options.fetch ?? globalThis.fetch.bind(globalThis), [options.fetch]);
5149
5247
  const createRemoteSession = options.createRemoteSession ?? createRemotePiSession;
5150
5248
  const connectActiveSession = options.connectActiveSession ?? true;
@@ -5152,12 +5250,30 @@ function usePiSessions(options = {}) {
5152
5250
  const retryBaseMs = options.retry?.baseMs ?? DEFAULT_RETRY_BASE_MS;
5153
5251
  const retryMaxMs = options.retry?.maxMs ?? DEFAULT_RETRY_MAX_MS;
5154
5252
  const headersKey = useMemo4(() => headersScopeKey(options.requestHeaders, storageScope), [options.requestHeaders, storageScope]);
5155
- const normalizedHeaders = useMemo4(() => buildRequestHeaders(options.requestHeaders, storageScope), [headersKey, storageScope]);
5156
- const requestScopeKey = useMemo4(() => requestScopeIdentity(apiBaseUrl, sessionsApiPath, storageScope, headersKey), [apiBaseUrl, headersKey, sessionsApiPath, storageScope]);
5157
- const dataSourceKey = useMemo4(() => dataSourceIdentity(apiBaseUrl, sessionsApiPath, storageScope), [apiBaseUrl, sessionsApiPath, storageScope]);
5253
+ const normalizedHeaders = useMemo4(() => withStorageScope(options.requestHeaders, storageScope) ?? {}, [headersKey, storageScope]);
5254
+ const requestScopeKey = useMemo4(
5255
+ () => requestScopeIdentity(apiBaseUrl, sessionsApiPath, options.agentTypeId, storageScope, headersKey, options.workspaceId, options.sourceIdentity),
5256
+ [apiBaseUrl, headersKey, options.agentTypeId, options.sourceIdentity, options.workspaceId, sessionsApiPath, storageScope]
5257
+ );
5258
+ const dataSourceKey = useMemo4(
5259
+ () => dataSourceIdentity(apiBaseUrl, sessionsApiPath, options.agentTypeId, storageScope, options.workspaceId, options.sourceIdentity),
5260
+ [apiBaseUrl, options.agentTypeId, options.sourceIdentity, options.workspaceId, sessionsApiPath, storageScope]
5261
+ );
5262
+ const bootResumeSource = useMemo4(() => ({
5263
+ apiBaseUrl,
5264
+ sessionsApiPath,
5265
+ agentTypeId: options.agentTypeId,
5266
+ workspaceId: options.workspaceId,
5267
+ storageScope
5268
+ }), [apiBaseUrl, options.agentTypeId, options.workspaceId, sessionsApiPath, storageScope]);
5158
5269
  const [sessions, setSessions] = useState10([]);
5159
5270
  const [dataStorageScope, setDataStorageScope] = useState10(storageScope);
5160
- const [activeSessionId, setActiveSessionId] = useState10(() => options.initialActiveSessionId ?? readActiveSessionId({ storageScope, storage: options.storage }));
5271
+ const [activeSessionId, setActiveSessionId] = useState10(void 0);
5272
+ const [resumeSessionState, setResumeSessionState] = useState10(() => ({
5273
+ dataSourceKey,
5274
+ id: readBootResumeSessionId({ bootResumeSource, storage: options.bootResumeStorage })
5275
+ }));
5276
+ const resumeSessionId = resumeSessionState.dataSourceKey === dataSourceKey ? resumeSessionState.id : void 0;
5161
5277
  const [activePiSession, setActivePiSession] = useState10(void 0);
5162
5278
  const [loading, setLoading] = useState10(enabled);
5163
5279
  const [loadingMore, setLoadingMore] = useState10(false);
@@ -5174,17 +5290,30 @@ function usePiSessions(options = {}) {
5174
5290
  const loadMoreRequestSeqRef = useRef8(0);
5175
5291
  const loadMoreInFlightRef = useRef8(false);
5176
5292
  const pendingCreatedRef = useRef8(/* @__PURE__ */ new Map());
5293
+ const pendingDeletedRef = useRef8(/* @__PURE__ */ new Set());
5177
5294
  const pendingCreatedScopeRef = useRef8(requestScopeKey);
5178
5295
  const dataStorageScopeRef = useRef8(storageScope);
5179
5296
  const loadedDataSourceRef = useRef8(dataSourceKey);
5180
- const requestScopeRef = useRef8(requestScopeKey);
5181
- requestScopeRef.current = requestScopeKey;
5297
+ const committedRequestScopeRef = useRef8(requestScopeKey);
5298
+ useIsomorphicLayoutEffect(() => {
5299
+ committedRequestScopeRef.current = requestScopeKey;
5300
+ }, [requestScopeKey]);
5301
+ const sourceIsCurrent = useCallback7(
5302
+ (scope) => mountedRef.current && committedRequestScopeRef.current === scope,
5303
+ []
5304
+ );
5182
5305
  const remoteSessionOptionsRef = useRef8(options.remoteSessionOptions);
5183
5306
  remoteSessionOptionsRef.current = options.remoteSessionOptions;
5184
5307
  const remoteSessionOptionsKey = useMemo4(
5185
5308
  () => remoteSessionOptionsIdentity(options.remoteSessionOptions),
5186
5309
  [options.remoteSessionOptions]
5187
5310
  );
5311
+ useIsomorphicLayoutEffect(() => {
5312
+ setResumeSessionState((current) => current.dataSourceKey === dataSourceKey ? current : {
5313
+ dataSourceKey,
5314
+ id: readBootResumeSessionId({ bootResumeSource, storage: options.bootResumeStorage })
5315
+ });
5316
+ }, [bootResumeSource, dataSourceKey, options.bootResumeStorage]);
5188
5317
  useEffect8(() => {
5189
5318
  sessionsRef.current = sessions;
5190
5319
  }, [sessions]);
@@ -5197,48 +5326,66 @@ function usePiSessions(options = {}) {
5197
5326
  const activeSessionKnown = Boolean(activeSessionId && sessions.some((session) => session.id === activeSessionId));
5198
5327
  const requestHeaders = useCallback7(() => normalizedHeaders, [normalizedHeaders]);
5199
5328
  const sessionsUrl = useCallback7((suffix = "") => `${apiBaseUrl}${sessionsApiPath}${suffix}`, [apiBaseUrl, sessionsApiPath]);
5200
- const sessionsListUrl = useCallback7((offset = 0, includeId, cursor) => {
5201
- const query = new URLSearchParams();
5202
- if (addressed) {
5203
- query.set("limit", String(SESSION_PAGE_SIZE));
5204
- if (cursor) query.set("cursor", cursor);
5205
- } else {
5206
- if (offset > 0) {
5207
- query.set("limit", String(SESSION_PAGE_SIZE));
5208
- query.set("offset", String(offset));
5209
- }
5210
- if (offset <= 0 && includeId) query.set("activeSessionId", includeId);
5211
- }
5212
- if (query.size === 0) return sessionsUrl();
5329
+ const sessionsListUrl = useCallback7((cursor) => {
5330
+ const query = new URLSearchParams({ limit: String(SESSION_PAGE_SIZE) });
5331
+ if (cursor) query.set("cursor", cursor);
5213
5332
  return sessionsUrl(`?${query.toString()}`);
5214
- }, [addressed, sessionsUrl]);
5333
+ }, [sessionsUrl]);
5215
5334
  const persistActive = useCallback7((id) => {
5216
- writeActiveSessionId(id, { storageScope, storage: options.storage });
5217
- }, [options.storage, storageScope]);
5335
+ writeActiveSessionId(id, { storageScope: activeSessionStorageScope, storage: options.storage });
5336
+ }, [activeSessionStorageScope, options.storage]);
5337
+ const persistBootResume = useCallback7((id) => {
5338
+ writeBootResumeSessionId(id, { bootResumeSource, storage: options.bootResumeStorage });
5339
+ setResumeSessionState({ dataSourceKey, id });
5340
+ }, [bootResumeSource, dataSourceKey, options.bootResumeStorage]);
5341
+ const rememberConfirmedEmptySession = useCallback7((id) => {
5342
+ writeBootResumeSessionId(id, { bootResumeSource, storage: options.bootResumeStorage });
5343
+ setResumeSessionState({ dataSourceKey, id: void 0 });
5344
+ }, [bootResumeSource, dataSourceKey, options.bootResumeStorage]);
5218
5345
  const ensurePendingScope = useCallback7(() => {
5219
5346
  if (pendingCreatedScopeRef.current === requestScopeKey) return;
5220
5347
  pendingCreatedScopeRef.current = requestScopeKey;
5221
5348
  pendingCreatedRef.current.clear();
5349
+ pendingDeletedRef.current.clear();
5222
5350
  }, [requestScopeKey]);
5223
5351
  const preferredSessionId = useCallback7(() => {
5224
- const persisted = options.initialActiveSessionId ?? readActiveSessionId({ storageScope, storage: options.storage });
5352
+ const persisted = options.initialActiveSessionId ?? readActiveSessionId({
5353
+ storageScope: activeSessionStorageScope,
5354
+ storage: options.storage
5355
+ });
5225
5356
  if (loadedDataSourceRef.current === dataSourceKey) return activeSessionIdRef.current ?? persisted;
5226
5357
  if (dataStorageScopeRef.current !== storageScope) return persisted;
5227
5358
  return void 0;
5228
- }, [dataSourceKey, options.initialActiveSessionId, options.storage, storageScope]);
5359
+ }, [activeSessionStorageScope, dataSourceKey, options.initialActiveSessionId, options.storage, storageScope]);
5229
5360
  const applySessions = useCallback7((data, applyOptions = {}) => {
5230
5361
  ensurePendingScope();
5231
5362
  const replacingScope = loadedDataSourceRef.current !== dataSourceKey;
5232
5363
  const requestedActiveId = preferredSessionId();
5233
5364
  const replacingScopePreferred = replacingScope ? requestedActiveId : void 0;
5234
5365
  const pendingCreated = pendingCreatedRef.current;
5366
+ const pendingDeleted = pendingDeletedRef.current;
5367
+ const deletedIds = new Set(pendingDeleted);
5235
5368
  for (const session of data) pendingCreated.delete(session.id);
5236
- const canonicalCount = canonicalPageCount(data);
5237
- const pageMayHaveMore = addressed ? applyOptions.nextCursor !== void 0 : data.length >= SESSION_PAGE_SIZE;
5369
+ const filteredData = data.filter((session) => !deletedIds.has(session.id));
5370
+ if (applyOptions.nextCursor === void 0) {
5371
+ const returnedIds = new Set(data.map((session) => session.id));
5372
+ for (const deletedId of pendingDeleted) {
5373
+ if (!returnedIds.has(deletedId)) pendingDeleted.delete(deletedId);
5374
+ }
5375
+ }
5376
+ const canonicalCount = canonicalPageCount(filteredData);
5377
+ const pageMayHaveMore = applyOptions.nextCursor !== void 0;
5238
5378
  const wasExhaustedBeyondFirstPage = applyOptions.background && !hasMoreRef.current && canonicalLoadedCountRef.current >= canonicalCount;
5239
- const requestedActiveReturned = Boolean(requestedActiveId && data.some((session) => session.id === requestedActiveId));
5379
+ const requestedActiveReturned = Boolean(requestedActiveId && filteredData.some((session) => session.id === requestedActiveId));
5240
5380
  const current = applyOptions.background && pageMayHaveMore ? sessionsRef.current.filter((session) => !requestedActiveId || requestedActiveReturned || session.id !== requestedActiveId) : [];
5241
- const merged = mergeSessions(Array.from(pendingCreated.values()), data, current);
5381
+ const merged = mergeSessions(Array.from(pendingCreated.values()), filteredData, current.filter((session) => !deletedIds.has(session.id)));
5382
+ const rememberedEmptyId = readBootResumeSessionId({
5383
+ bootResumeSource,
5384
+ storage: options.bootResumeStorage
5385
+ });
5386
+ if (rememberedEmptyId && filteredData.some((session) => session.id === rememberedEmptyId)) {
5387
+ persistBootResume(void 0);
5388
+ }
5242
5389
  const nextHasMore = pageMayHaveMore && !wasExhaustedBeyondFirstPage;
5243
5390
  canonicalLoadedCountRef.current = applyOptions.background ? Math.max(canonicalLoadedCountRef.current, canonicalCount) : canonicalCount;
5244
5391
  loadedDataSourceRef.current = dataSourceKey;
@@ -5253,10 +5400,12 @@ function usePiSessions(options = {}) {
5253
5400
  persistActive(next);
5254
5401
  return next;
5255
5402
  });
5256
- }, [addressed, dataSourceKey, ensurePendingScope, persistActive, preferredSessionId, storageScope]);
5403
+ }, [bootResumeSource, dataSourceKey, ensurePendingScope, options.bootResumeStorage, persistActive, persistBootResume, preferredSessionId, storageScope]);
5257
5404
  const refresh = useCallback7(async (refreshOptions = {}) => {
5405
+ const scope = requestScopeKey;
5406
+ if (mountedRef.current && !sourceIsCurrent(scope)) throw new StaleSessionsSourceError();
5258
5407
  const version = ++refreshVersionRef.current;
5259
- const isCurrent = () => mountedRef.current && version === refreshVersionRef.current;
5408
+ const isCurrent = () => sourceIsCurrent(scope) && version === refreshVersionRef.current;
5260
5409
  clearRetryTimer(retryTimerRef);
5261
5410
  const background = refreshOptions.background === true;
5262
5411
  if (!enabled) {
@@ -5283,7 +5432,7 @@ function usePiSessions(options = {}) {
5283
5432
  let page;
5284
5433
  for (let attempt = 0; ; attempt += 1) {
5285
5434
  try {
5286
- page = await fetchSessionList(fetchImpl, sessionsListUrl(0, preferredSessionId()), requestHeaders(), addressed);
5435
+ page = await fetchSessionList(fetchImpl, sessionsListUrl(), requestHeaders());
5287
5436
  break;
5288
5437
  } catch (err) {
5289
5438
  const transient = err instanceof SessionsPreparingError || isNetworkFetchError(err);
@@ -5305,7 +5454,7 @@ function usePiSessions(options = {}) {
5305
5454
  setLoading(false);
5306
5455
  if (refreshOptions.throwOnError) throw error2;
5307
5456
  }
5308
- }, [addressed, applySessions, enabled, fetchImpl, persistActive, preferredSessionId, requestHeaders, retryBaseMs, retryMaxMs, retryMaxRetries, sessionsListUrl]);
5457
+ }, [applySessions, enabled, fetchImpl, persistActive, preferredSessionId, requestHeaders, requestScopeKey, retryBaseMs, retryMaxMs, retryMaxRetries, sessionsListUrl, sourceIsCurrent]);
5309
5458
  useEffect8(() => {
5310
5459
  mountedRef.current = true;
5311
5460
  void refresh();
@@ -5321,14 +5470,13 @@ function usePiSessions(options = {}) {
5321
5470
  loadMoreInFlightRef.current = true;
5322
5471
  const version = refreshVersionRef.current;
5323
5472
  const scope = requestScopeKey;
5324
- const offset = canonicalLoadedCountRef.current;
5325
5473
  setLoadingMore(true);
5326
5474
  try {
5327
- const page = await fetchSessionList(fetchImpl, sessionsListUrl(offset, void 0, nextCursorRef.current), requestHeaders(), addressed);
5328
- const data = page.sessions;
5329
- if (!mountedRef.current || requestSeq !== loadMoreRequestSeqRef.current || version !== refreshVersionRef.current || scope !== requestScopeRef.current) return;
5330
- const merged = mergeSessions(sessionsRef.current, data);
5331
- const nextHasMore = addressed ? page.nextCursor !== void 0 : data.length >= SESSION_PAGE_SIZE;
5475
+ const page = await fetchSessionList(fetchImpl, sessionsListUrl(nextCursorRef.current), requestHeaders());
5476
+ const data = page.sessions.filter((session) => !pendingDeletedRef.current.has(session.id));
5477
+ if (requestSeq !== loadMoreRequestSeqRef.current || version !== refreshVersionRef.current || !sourceIsCurrent(scope)) return;
5478
+ const merged = mergeSessions(sessionsRef.current.filter((session) => !pendingDeletedRef.current.has(session.id)), data);
5479
+ const nextHasMore = page.nextCursor !== void 0;
5332
5480
  canonicalLoadedCountRef.current += data.length;
5333
5481
  nextCursorRef.current = page.nextCursor;
5334
5482
  setSessions(merged);
@@ -5341,16 +5489,16 @@ function usePiSessions(options = {}) {
5341
5489
  return next;
5342
5490
  });
5343
5491
  } catch (err) {
5344
- if (mountedRef.current && requestSeq === loadMoreRequestSeqRef.current && version === refreshVersionRef.current && scope === requestScopeRef.current) {
5492
+ if (requestSeq === loadMoreRequestSeqRef.current && version === refreshVersionRef.current && sourceIsCurrent(scope)) {
5345
5493
  setError(err instanceof Error ? err : new Error(String(err)));
5346
5494
  }
5347
5495
  } finally {
5348
5496
  if (requestSeq === loadMoreRequestSeqRef.current) loadMoreInFlightRef.current = false;
5349
- if (mountedRef.current && requestSeq === loadMoreRequestSeqRef.current && version === refreshVersionRef.current && scope === requestScopeRef.current) {
5497
+ if (requestSeq === loadMoreRequestSeqRef.current && version === refreshVersionRef.current && sourceIsCurrent(scope)) {
5350
5498
  setLoadingMore(false);
5351
5499
  }
5352
5500
  }
5353
- }, [addressed, enabled, fetchImpl, hasMore, loading, loadingMore, persistActive, requestHeaders, requestScopeKey, sessionsListUrl]);
5501
+ }, [enabled, fetchImpl, hasMore, loading, loadingMore, persistActive, requestHeaders, requestScopeKey, sessionsListUrl, sourceIsCurrent]);
5354
5502
  useEffect8(() => {
5355
5503
  if (!enabled || !connectActiveSession || !activeSessionId || !activeSessionKnown) {
5356
5504
  setActivePiSession(void 0);
@@ -5372,6 +5520,8 @@ function usePiSessions(options = {}) {
5372
5520
  };
5373
5521
  }, [activeSessionId, activeSessionKnown, apiBaseUrl, connectActiveSession, createRemoteSession, enabled, fetchImpl, remoteSessionOptionsKey, options.agentTypeId, options.workspaceId, requestHeaders, storageScope]);
5374
5522
  const create = useCallback7(async (init) => {
5523
+ const scope = requestScopeKey;
5524
+ if (!sourceIsCurrent(scope)) throw new StaleSessionsSourceError();
5375
5525
  if (!enabled) throw new Error("Pi sessions are disabled");
5376
5526
  const response = await fetchImpl(sessionsUrl(), {
5377
5527
  method: "POST",
@@ -5384,8 +5534,10 @@ function usePiSessions(options = {}) {
5384
5534
  throw err;
5385
5535
  }
5386
5536
  const body = await response.json();
5387
- const session = addressed ? addressedCreatedSession(body, init?.title) : toSessionSummary(body);
5537
+ const session = addressedCreatedSession(body, options.agentTypeId, init?.title);
5538
+ if (!sourceIsCurrent(scope)) return session;
5388
5539
  ensurePendingScope();
5540
+ rememberConfirmedEmptySession(session.id);
5389
5541
  pendingCreatedRef.current.set(session.id, session);
5390
5542
  setDataStorageScope(storageScope);
5391
5543
  setSessions((previous) => mergeSessions([session], previous));
@@ -5393,17 +5545,43 @@ function usePiSessions(options = {}) {
5393
5545
  persistActive(session.id);
5394
5546
  void refresh();
5395
5547
  return session;
5396
- }, [addressed, enabled, ensurePendingScope, fetchImpl, persistActive, refresh, requestHeaders, sessionsUrl, storageScope]);
5548
+ }, [enabled, ensurePendingScope, fetchImpl, persistActive, refresh, rememberConfirmedEmptySession, requestHeaders, requestScopeKey, sessionsUrl, sourceIsCurrent, storageScope]);
5397
5549
  const switchSession = useCallback7((id) => {
5550
+ if (!sourceIsCurrent(requestScopeKey)) return;
5398
5551
  const known = sessionsRef.current.some((session) => session.id === id);
5399
5552
  const next = known ? id : sessionsRef.current[0]?.id;
5400
5553
  setActiveSessionId(next);
5401
5554
  persistActive(next);
5402
- }, [persistActive]);
5555
+ }, [persistActive, requestScopeKey, sourceIsCurrent]);
5556
+ const rename = useCallback7(async (id, title) => {
5557
+ const scope = requestScopeKey;
5558
+ if (!sourceIsCurrent(scope)) throw new StaleSessionsSourceError();
5559
+ if (!enabled) throw new Error("Pi sessions are disabled");
5560
+ const response = await fetchImpl(sessionsUrl(`/${encodeURIComponent(id)}/rename`), {
5561
+ method: "POST",
5562
+ headers: { ...requestHeaders(), "Content-Type": "application/json" },
5563
+ body: JSON.stringify({ requestId: createRequestId("rename"), title })
5564
+ });
5565
+ if (!response.ok) throw new Error(`Failed to rename session: ${response.status}`);
5566
+ const renamed = toAddressedSessionSummary(await response.json());
5567
+ if (!sourceIsCurrent(scope)) throw new StaleSessionsSourceError();
5568
+ ensurePendingScope();
5569
+ if (pendingCreatedRef.current.has(id)) pendingCreatedRef.current.set(id, renamed);
5570
+ setSessions((current) => current.map((session) => session.id === id ? renamed : session));
5571
+ return renamed;
5572
+ }, [enabled, ensurePendingScope, fetchImpl, requestHeaders, requestScopeKey, sessionsUrl, sourceIsCurrent]);
5403
5573
  const deleteSession = useCallback7(async (id) => {
5574
+ const scope = requestScopeKey;
5575
+ if (!sourceIsCurrent(scope)) throw new StaleSessionsSourceError();
5404
5576
  if (!enabled) throw new Error("Pi sessions are disabled");
5405
5577
  ensurePendingScope();
5578
+ if (readBootResumeSessionId({ bootResumeSource, storage: options.bootResumeStorage }) === id) {
5579
+ persistBootResume(void 0);
5580
+ }
5581
+ const deletedSession = sessionsRef.current.find((session) => session.id === id);
5582
+ const deletedSessionWasActive = activeSessionIdRef.current === id;
5406
5583
  pendingCreatedRef.current.delete(id);
5584
+ pendingDeletedRef.current.add(id);
5407
5585
  setDataStorageScope(storageScope);
5408
5586
  setSessions((previous) => previous.filter((session) => session.id !== id));
5409
5587
  setActiveSessionId((previous) => {
@@ -5420,18 +5598,29 @@ function usePiSessions(options = {}) {
5420
5598
  if (!response.ok && response.status !== 404) throw new Error(`Failed to delete session: ${response.status}`);
5421
5599
  } catch (err) {
5422
5600
  const error2 = err instanceof Error ? err : new Error(String(err));
5423
- setError(error2);
5424
- void refresh();
5601
+ if (sourceIsCurrent(scope)) {
5602
+ pendingDeletedRef.current.delete(id);
5603
+ if (deletedSession) {
5604
+ setSessions((previous) => mergeSessions([deletedSession], previous));
5605
+ if (deletedSessionWasActive) {
5606
+ setActiveSessionId(id);
5607
+ persistActive(id);
5608
+ }
5609
+ }
5610
+ setError(error2);
5611
+ void refresh();
5612
+ }
5425
5613
  throw error2;
5426
5614
  }
5427
- void refresh();
5428
- }, [enabled, ensurePendingScope, fetchImpl, persistActive, refresh, requestHeaders, sessionsUrl, storageScope]);
5615
+ if (sourceIsCurrent(scope)) void refresh();
5616
+ }, [bootResumeSource, enabled, ensurePendingScope, fetchImpl, options.bootResumeStorage, persistActive, persistBootResume, refresh, requestHeaders, requestScopeKey, sessionsUrl, sourceIsCurrent, storageScope]);
5429
5617
  const reset = useCallback7(() => {
5618
+ if (!sourceIsCurrent(requestScopeKey)) return;
5430
5619
  pendingCreatedRef.current.clear();
5620
+ pendingDeletedRef.current.clear();
5431
5621
  loadMoreRequestSeqRef.current += 1;
5432
5622
  loadMoreInFlightRef.current = false;
5433
5623
  canonicalLoadedCountRef.current = canonicalPageCount(sessionsRef.current);
5434
- nextCursorRef.current = void 0;
5435
5624
  loadedDataSourceRef.current = dataSourceKey;
5436
5625
  dataStorageScopeRef.current = storageScope;
5437
5626
  setDataStorageScope(storageScope);
@@ -5439,36 +5628,35 @@ function usePiSessions(options = {}) {
5439
5628
  setActivePiSession(void 0);
5440
5629
  setLoadingMore(false);
5441
5630
  persistActive(void 0);
5442
- }, [dataSourceKey, persistActive, storageScope]);
5631
+ }, [dataSourceKey, persistActive, requestScopeKey, sourceIsCurrent, storageScope]);
5443
5632
  const visibleActiveSessionId = enabled ? activeSessionId : void 0;
5444
5633
  const activeSession = enabled ? sessions.find((session) => session.id === visibleActiveSessionId) : void 0;
5445
5634
  return {
5446
5635
  sessions,
5447
5636
  activeSession,
5448
5637
  activeSessionId: visibleActiveSessionId,
5638
+ resumeSessionId,
5449
5639
  activePiSession: visibleActiveSessionId ? activePiSession : void 0,
5450
5640
  dataStorageScope,
5641
+ sourceIdentity: enabled && loadedDataSourceRef.current === dataSourceKey ? options.sourceIdentity : void 0,
5451
5642
  loading: enabled ? loading : false,
5452
5643
  loadingMore,
5453
5644
  hasMore: enabled ? hasMore : false,
5454
5645
  error,
5455
5646
  refresh,
5456
5647
  create,
5648
+ rename,
5457
5649
  switch: switchSession,
5458
5650
  delete: deleteSession,
5459
5651
  loadMore,
5460
5652
  reset
5461
5653
  };
5462
5654
  }
5463
- async function fetchSessionList(fetchImpl, url, headers, addressed) {
5655
+ async function fetchSessionList(fetchImpl, url, headers) {
5464
5656
  const response = await fetchImpl(url, Object.keys(headers).length > 0 ? { headers } : void 0);
5465
5657
  if (response.status === 503) throw new SessionsPreparingError();
5466
5658
  if (!response.ok) throw new Error(`Failed to load sessions: ${response.status}`);
5467
5659
  const body = await response.json();
5468
- if (!addressed) {
5469
- if (!Array.isArray(body)) throw new Error("Failed to load sessions: invalid response");
5470
- return { sessions: body.map(toSessionSummary) };
5471
- }
5472
5660
  if (typeof body !== "object" || body === null || !Array.isArray(body.sessions)) {
5473
5661
  throw new Error("Failed to load sessions: invalid response");
5474
5662
  }
@@ -5478,19 +5666,6 @@ async function fetchSessionList(fetchImpl, url, headers, addressed) {
5478
5666
  ...typeof page.nextCursor === "string" ? { nextCursor: page.nextCursor } : {}
5479
5667
  };
5480
5668
  }
5481
- function toSessionSummary(value) {
5482
- if (typeof value !== "object" || value === null) throw new Error("invalid session summary");
5483
- const record = value;
5484
- if (typeof record.id !== "string" || !record.id) throw new Error("invalid session id");
5485
- const now = (/* @__PURE__ */ new Date(0)).toISOString();
5486
- return {
5487
- id: record.id,
5488
- title: typeof record.title === "string" ? record.title : "Untitled",
5489
- createdAt: typeof record.createdAt === "string" ? record.createdAt : now,
5490
- updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : now,
5491
- turnCount: typeof record.turnCount === "number" ? record.turnCount : 0
5492
- };
5493
- }
5494
5669
  function toAddressedSessionSummary(value) {
5495
5670
  if (typeof value !== "object" || value === null) throw new Error("invalid addressed session summary");
5496
5671
  const record = value;
@@ -5500,21 +5675,29 @@ function toAddressedSessionSummary(value) {
5500
5675
  }
5501
5676
  const createdAt = typeof record.createdAt === "number" ? new Date(record.createdAt).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString();
5502
5677
  const updatedAt = typeof record.updatedAt === "number" ? new Date(record.updatedAt).toISOString() : createdAt;
5678
+ const agentTypeId = ref.agentTypeId;
5679
+ const status = record.status;
5680
+ const addressedStatus = status === "idle" || status === "running" || status === "aborting" || status === "error" ? status : void 0;
5503
5681
  return {
5504
5682
  id: ref.sessionId,
5505
5683
  title: typeof record.title === "string" ? record.title : "Untitled",
5506
5684
  createdAt,
5507
5685
  updatedAt,
5508
- turnCount: 0
5686
+ turnCount: typeof record.turnCount === "number" ? record.turnCount : 0,
5687
+ ...typeof agentTypeId === "string" ? { agentTypeId } : {},
5688
+ ...typeof record.nativeSessionId === "string" ? { nativeSessionId: record.nativeSessionId } : {},
5689
+ ...typeof record.hasAssistantReply === "boolean" ? { hasAssistantReply: record.hasAssistantReply } : {},
5690
+ ...addressedStatus ? { status: addressedStatus } : {}
5509
5691
  };
5510
5692
  }
5511
- function addressedCreatedSession(value, title) {
5512
- if (typeof value !== "object" || value === null || typeof value.sessionId !== "string") {
5693
+ function addressedCreatedSession(value, expectedAgentTypeId, title) {
5694
+ if (typeof value !== "object" || value === null || typeof value.sessionId !== "string" || value.agentTypeId !== expectedAgentTypeId) {
5513
5695
  throw new Error("invalid addressed session ref");
5514
5696
  }
5515
5697
  const now = (/* @__PURE__ */ new Date()).toISOString();
5516
5698
  return {
5517
5699
  id: value.sessionId,
5700
+ agentTypeId: expectedAgentTypeId,
5518
5701
  title: title ?? "Untitled",
5519
5702
  createdAt: now,
5520
5703
  updatedAt: now,
@@ -5570,31 +5753,25 @@ function mergeSessions(...lists) {
5570
5753
  }
5571
5754
  return merged;
5572
5755
  }
5573
- function buildRequestHeaders(headers, storageScope) {
5574
- const result = {};
5575
- for (const [key, value] of Object.entries(headers ?? {})) {
5576
- if (typeof value === "string") result[key] = value;
5577
- }
5578
- if (storageScope && !hasHeader2(result, "x-boring-storage-scope")) result["x-boring-storage-scope"] = storageScope;
5579
- return result;
5580
- }
5581
5756
  function headersScopeKey(headers, storageScope) {
5582
5757
  return JSON.stringify({ storageScope, headers: Object.entries(headers ?? {}).sort(([a], [b]) => a.localeCompare(b)) });
5583
5758
  }
5584
- function requestScopeIdentity(apiBaseUrl, sessionsApiPath, storageScope, headersKey) {
5759
+ function requestScopeIdentity(apiBaseUrl, sessionsApiPath, agentTypeId, storageScope, headersKey, workspaceId, sourceIdentity) {
5585
5760
  return `${apiBaseUrl}
5586
5761
  ${sessionsApiPath}
5762
+ ${agentTypeId}
5587
5763
  ${storageScope}
5588
- ${headersKey}`;
5764
+ ${headersKey}
5765
+ ${workspaceId ?? ""}
5766
+ ${sourceIdentity ?? ""}`;
5589
5767
  }
5590
- function dataSourceIdentity(apiBaseUrl, sessionsApiPath, storageScope) {
5768
+ function dataSourceIdentity(apiBaseUrl, sessionsApiPath, agentTypeId, storageScope, workspaceId, sourceIdentity) {
5591
5769
  return `${apiBaseUrl}
5592
5770
  ${sessionsApiPath}
5593
- ${storageScope}`;
5594
- }
5595
- function hasHeader2(headers, name) {
5596
- const lower = name.toLowerCase();
5597
- return Object.keys(headers).some((key) => key.toLowerCase() === lower);
5771
+ ${agentTypeId}
5772
+ ${storageScope}
5773
+ ${workspaceId ?? ""}
5774
+ ${sourceIdentity ?? ""}`;
5598
5775
  }
5599
5776
  function retryDelayMs(attempt, retry) {
5600
5777
  const base = retry.baseMs ?? DEFAULT_RETRY_BASE_MS;
@@ -5975,8 +6152,8 @@ function searchPiSessions(sessions, query, options = {}) {
5975
6152
  }
5976
6153
 
5977
6154
  // src/front/chat/components/PiConversationSurface.tsx
5978
- import { Loader2 as Loader22 } from "lucide-react";
5979
- import { useCallback as useCallback14, useEffect as useEffect14, useLayoutEffect, useRef as useRef11, useState as useState15 } from "react";
6155
+ import { Skeleton } from "@hachej/boring-ui-kit";
6156
+ import { useCallback as useCallback14, useEffect as useEffect14, useLayoutEffect as useLayoutEffect2, useRef as useRef11, useState as useState15 } from "react";
5980
6157
  import { useStickToBottomContext as useStickToBottomContext2 } from "use-stick-to-bottom";
5981
6158
 
5982
6159
  // src/front/primitives/conversation.tsx
@@ -6037,7 +6214,7 @@ var ConversationScrollButton = ({
6037
6214
  Button7,
6038
6215
  {
6039
6216
  className: cn(
6040
- "absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full dark:bg-background dark:hover:bg-muted",
6217
+ "boring-agent-scroll-latest absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full dark:bg-background dark:hover:bg-muted",
6041
6218
  className
6042
6219
  ),
6043
6220
  onClick: handleScrollToBottom,
@@ -6082,8 +6259,27 @@ function noticeTextClass(className) {
6082
6259
  import { memo as memo2 } from "react";
6083
6260
  import { AlertTriangleIcon, InfoIcon, Loader2Icon, PlugZapIcon, RefreshCwIcon, XIcon as XIcon2 } from "lucide-react";
6084
6261
  import { Button as Button8 } from "@hachej/boring-ui-kit";
6085
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
6086
- var RuntimeNotices = memo2(({ notices, onDismiss, onAction, renderAction, className, ...props }) => {
6262
+
6263
+ // src/front/chat/components/terminalChatErrors.ts
6264
+ var TERMINAL_CHAT_ERROR_IDS = /* @__PURE__ */ new Set(["chat-error", "session-navigation-error"]);
6265
+ function isTerminalChatErrorId(id) {
6266
+ return TERMINAL_CHAT_ERROR_IDS.has(id);
6267
+ }
6268
+ function isTerminalChatErrorNotice(id, historyEmpty) {
6269
+ return historyEmpty && isTerminalChatErrorId(id);
6270
+ }
6271
+ function hasTerminalChatError(notices, historyEmpty) {
6272
+ return historyEmpty && notices.some((notice) => isTerminalChatErrorId(notice.id));
6273
+ }
6274
+ var COMPETING_NOISE_NOTICE_IDS = /* @__PURE__ */ new Set(["connection-reconnecting", "auto-retry"]);
6275
+ function filterCompetingNoiseNotices(notices, historyEmpty) {
6276
+ if (!hasTerminalChatError(notices, historyEmpty)) return notices;
6277
+ return notices.filter((notice) => !COMPETING_NOISE_NOTICE_IDS.has(notice.id));
6278
+ }
6279
+
6280
+ // src/front/chat/components/RuntimeNotices.tsx
6281
+ import { Fragment as Fragment7, jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
6282
+ var RuntimeNotices = memo2(({ notices, onDismiss, onAction, renderAction, historyEmpty, className, ...props }) => {
6087
6283
  if (notices.length === 0) return null;
6088
6284
  return /* @__PURE__ */ jsx16(
6089
6285
  "div",
@@ -6091,16 +6287,29 @@ var RuntimeNotices = memo2(({ notices, onDismiss, onAction, renderAction, classN
6091
6287
  "data-boring-agent-part": "runtime-notices",
6092
6288
  className: cn("flex flex-col gap-2 px-4 py-2", className),
6093
6289
  ...props,
6094
- children: notices.map((notice) => /* @__PURE__ */ jsx16(RuntimeNoticeRow, { notice, onDismiss, onAction, renderAction }, notice.id))
6290
+ children: notices.map((notice) => /* @__PURE__ */ jsx16(
6291
+ RuntimeNoticeRow,
6292
+ {
6293
+ notice,
6294
+ onDismiss,
6295
+ onAction,
6296
+ renderAction,
6297
+ historyEmpty: historyEmpty ?? false
6298
+ },
6299
+ notice.id
6300
+ ))
6095
6301
  }
6096
6302
  );
6097
6303
  });
6098
6304
  RuntimeNotices.displayName = "RuntimeNotices";
6099
- function RuntimeNoticeRow({ notice, onDismiss, onAction, renderAction }) {
6305
+ function RuntimeNoticeRow({ notice, onDismiss, onAction, renderAction, historyEmpty }) {
6100
6306
  const kind = inferNoticeKind(notice);
6101
6307
  const Icon = iconForNotice(kind, notice.level);
6102
6308
  const actionLabel = notice.actionLabel ?? defaultActionLabel(kind);
6103
6309
  const hostAction = renderAction?.(notice);
6310
+ const isTerminalError = isTerminalChatErrorNotice(notice.id, historyEmpty);
6311
+ const explicitAction = actionLabel && onAction ? /* @__PURE__ */ jsx16(Button8, { type: "button", variant: "ghost", size: "sm", onClick: () => onAction(notice.id), children: actionLabel }) : null;
6312
+ const primaryAction = hostAction ?? explicitAction ?? (isTerminalError ? /* @__PURE__ */ jsx16(Button8, { type: "button", variant: "outline", size: "sm", className: "shrink-0", onClick: () => window.location.reload(), children: "Reload workspace" }) : null);
6104
6313
  return /* @__PURE__ */ jsxs14(
6105
6314
  "div",
6106
6315
  {
@@ -6116,9 +6325,15 @@ function RuntimeNoticeRow({ notice, onDismiss, onAction, renderAction }) {
6116
6325
  ),
6117
6326
  children: [
6118
6327
  /* @__PURE__ */ jsx16(Icon, { className: cn(noticeIconClass(notice.level), kind === "retry" && "animate-spin motion-reduce:animate-none"), "aria-hidden": "true" }),
6119
- /* @__PURE__ */ jsx16("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx16("p", { className: noticeTextClass(), children: notice.text }) }),
6120
- hostAction ?? null,
6121
- actionLabel && onAction ? /* @__PURE__ */ jsx16(Button8, { type: "button", variant: "ghost", size: "sm", onClick: () => onAction(notice.id), children: actionLabel }) : null,
6328
+ /* @__PURE__ */ jsx16("div", { className: "min-w-0 flex-1", children: isTerminalError ? /* @__PURE__ */ jsxs14(Fragment7, { children: [
6329
+ /* @__PURE__ */ jsx16("p", { className: "text-sm font-semibold text-foreground", children: "Chat history unavailable" }),
6330
+ /* @__PURE__ */ jsx16("p", { className: noticeTextClass("mt-0.5"), children: "The saved conversation could not be loaded. Reload the workspace to try again." }),
6331
+ /* @__PURE__ */ jsxs14("details", { className: "mt-2 text-xs text-muted-foreground", children: [
6332
+ /* @__PURE__ */ jsx16("summary", { className: "cursor-pointer select-none", children: "Error details" }),
6333
+ /* @__PURE__ */ jsx16("p", { className: "mt-1 break-words", children: notice.text })
6334
+ ] })
6335
+ ] }) : /* @__PURE__ */ jsx16("p", { className: noticeTextClass(), children: notice.text }) }),
6336
+ primaryAction,
6122
6337
  notice.dismissible && onDismiss ? /* @__PURE__ */ jsx16(
6123
6338
  Button8,
6124
6339
  {
@@ -6161,7 +6376,8 @@ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
6161
6376
  function RuntimeNoticeMessages({
6162
6377
  notices,
6163
6378
  onDismiss,
6164
- renderAction
6379
+ renderAction,
6380
+ historyEmpty
6165
6381
  }) {
6166
6382
  const visible = notices.filter(
6167
6383
  (notice) => notice.level === "error" || notice.level === "warning" || notice.id === "connection-reconnecting" || notice.id === "auto-retry" || notice.id === "large-state-warning" || notice.id.startsWith("command:") || notice.id.startsWith("composer-warning:")
@@ -6173,6 +6389,7 @@ function RuntimeNoticeMessages({
6173
6389
  notices: visible,
6174
6390
  onDismiss,
6175
6391
  renderAction,
6392
+ historyEmpty,
6176
6393
  className: "mx-auto w-full max-w-3xl px-0 py-0"
6177
6394
  }
6178
6395
  );
@@ -6634,11 +6851,11 @@ function extractCodeBlockText(children) {
6634
6851
  };
6635
6852
  return walk(children);
6636
6853
  }
6637
- var MarkdownPre = ({ children }) => {
6854
+ var MarkdownPre = ({ children, filename }) => {
6638
6855
  const code3 = extractCodeBlockText(children).replace(/\n$/, "");
6639
6856
  const language = extractCodeBlockLanguage(children);
6640
6857
  return /* @__PURE__ */ jsx19(CodeBlock2, { code: code3, language: language ?? "text", className: "my-3", children: /* @__PURE__ */ jsxs17(CodeBlockHeader, { children: [
6641
- /* @__PURE__ */ jsx19(CodeBlockTitle, { children: /* @__PURE__ */ jsx19(CodeBlockFilename, { children: language ?? "text" }) }),
6858
+ /* @__PURE__ */ jsx19(CodeBlockTitle, { children: /* @__PURE__ */ jsx19(CodeBlockFilename, { children: filename ?? language ?? "text" }) }),
6642
6859
  /* @__PURE__ */ jsx19(CodeBlockCopyButton, {})
6643
6860
  ] }) });
6644
6861
  };
@@ -6668,7 +6885,7 @@ var markdownComponents = {
6668
6885
  code: MarkdownCode
6669
6886
  };
6670
6887
  var MessageResponse = memo3(
6671
- ({ className, shikiTheme, components, ...props }) => /* @__PURE__ */ jsx19(
6888
+ ({ className, shikiTheme, components, codeFilename, ...props }) => /* @__PURE__ */ jsx19(
6672
6889
  Streamdown,
6673
6890
  {
6674
6891
  className: cn(
@@ -6677,11 +6894,15 @@ var MessageResponse = memo3(
6677
6894
  ),
6678
6895
  plugins: streamdownPlugins,
6679
6896
  shikiTheme: shikiTheme ?? DEFAULT_SHIKI_THEME,
6680
- components: { ...markdownComponents, ...components ?? {} },
6897
+ components: {
6898
+ ...markdownComponents,
6899
+ ...components ?? {},
6900
+ ...codeFilename ? { pre: (props2) => /* @__PURE__ */ jsx19(MarkdownPre, { ...props2, filename: codeFilename }) } : {}
6901
+ },
6681
6902
  ...props
6682
6903
  }
6683
6904
  ),
6684
- (prevProps, nextProps) => prevProps.children === nextProps.children && nextProps.isAnimating === prevProps.isAnimating && nextProps.components === prevProps.components
6905
+ (prevProps, nextProps) => prevProps.children === nextProps.children && nextProps.isAnimating === prevProps.isAnimating && nextProps.components === prevProps.components && prevProps.codeFilename === nextProps.codeFilename
6685
6906
  );
6686
6907
  MessageResponse.displayName = "MessageResponse";
6687
6908
 
@@ -6762,7 +6983,7 @@ var ShimmerComponent = ({
6762
6983
  var Shimmer = memo4(ShimmerComponent);
6763
6984
 
6764
6985
  // src/front/primitives/reasoning.tsx
6765
- import { Fragment as Fragment7, jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
6986
+ import { Fragment as Fragment8, jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
6766
6987
  var ReasoningContext = createContext6(null);
6767
6988
  var useReasoning = () => {
6768
6989
  const context = useContext6(ReasoningContext);
@@ -6875,7 +7096,7 @@ var ReasoningTrigger = memo5(
6875
7096
  className
6876
7097
  ),
6877
7098
  ...props,
6878
- children: children ?? /* @__PURE__ */ jsxs18(Fragment7, { children: [
7099
+ children: children ?? /* @__PURE__ */ jsxs18(Fragment8, { children: [
6879
7100
  /* @__PURE__ */ jsx21(BrainIcon, { className: "size-3" }),
6880
7101
  getThinkingMessage(isStreaming, duration),
6881
7102
  /* @__PURE__ */ jsx21(
@@ -7037,6 +7258,10 @@ var ToolCallGroup = memo6(({ tools, mergedToolRenderers }) => {
7037
7258
  return () => window.clearInterval(interval);
7038
7259
  }, [isRunning]);
7039
7260
  if (toolParts.length === 0) return null;
7261
+ const inlineToolKeys = new Set(toolParts.flatMap(({ part, key }) => {
7262
+ const { renderer } = resolveToolRendererForPart(part, mergedToolRenderers);
7263
+ return renderer.presentation === "inline" ? [key] : [];
7264
+ }));
7040
7265
  return /* @__PURE__ */ jsxs19(
7041
7266
  Collapsible3,
7042
7267
  {
@@ -7045,6 +7270,11 @@ var ToolCallGroup = memo6(({ tools, mergedToolRenderers }) => {
7045
7270
  className: "not-prose my-1.5",
7046
7271
  "data-boring-agent-tool-state": visualState,
7047
7272
  children: [
7273
+ toolParts.map(({ part, key }) => {
7274
+ if (!inlineToolKeys.has(key)) return null;
7275
+ const { renderer, part: resolvedPart, resolution } = resolveToolRendererForPart(part, mergedToolRenderers);
7276
+ return /* @__PURE__ */ jsx22("div", { "data-boring-agent-part": "inline-tool-renderer", "data-tool-call-id": resolvedPart.toolCallId, "data-tool-renderer-key": resolution.key, children: renderer(resolvedPart) }, key);
7277
+ }),
7048
7278
  /* @__PURE__ */ jsxs19(
7049
7279
  CollapsibleTrigger4,
7050
7280
  {
@@ -7090,7 +7320,10 @@ var ToolCallGroup = memo6(({ tools, mergedToolRenderers }) => {
7090
7320
  children: [
7091
7321
  /* @__PURE__ */ jsx22("div", { className: "absolute left-[5px] top-0 bottom-2 w-px bg-border/35" }),
7092
7322
  /* @__PURE__ */ jsx22("div", { className: "flex min-w-0 flex-col gap-1.5 [&_[data-boring-agent-part=tool-card]]:!my-0", children: toolParts.map(({ part, key }) => {
7093
- const { renderer, part: resolvedPart, resolution } = resolveToolRendererForPart(part, mergedToolRenderers);
7323
+ const { renderer, part: resolvedPart, resolution } = resolveToolRendererForPart(
7324
+ part,
7325
+ inlineToolKeys.has(key) ? {} : mergedToolRenderers
7326
+ );
7094
7327
  return /* @__PURE__ */ jsx22(
7095
7328
  "div",
7096
7329
  {
@@ -7142,7 +7375,7 @@ function ChatMessageContributionBoundary({
7142
7375
 
7143
7376
  // src/front/chat/components/MessageMentions.tsx
7144
7377
  import { Children, cloneElement, isValidElement as isValidElement2 } from "react";
7145
- import { Fragment as Fragment8, jsx as jsx24 } from "react/jsx-runtime";
7378
+ import { Fragment as Fragment9, jsx as jsx24 } from "react/jsx-runtime";
7146
7379
  var TERMINAL_BOUNDARY = String.raw`(?=$|\s|[.,;!?)}\]"'”’]+(?=$|\s))`;
7147
7380
  var PREFIX_BOUNDARY = String.raw`(^|[\s(\[{"'“‘])`;
7148
7381
  function escapeRegExp2(value) {
@@ -7300,8 +7533,8 @@ function createMessageMentionMarkdownComponents(catalog, onActivate) {
7300
7533
  const content = Children.toArray(children).filter((child) => child !== null && child !== "");
7301
7534
  if (content.length === 1 && isValidElement2(content[0])) {
7302
7535
  const child = content[0];
7303
- if (child.props.node?.tagName === "img") return /* @__PURE__ */ jsx24(Fragment8, { children });
7304
- if (child.props.node?.tagName === "code" && "data-block" in child.props) return /* @__PURE__ */ jsx24(Fragment8, { children });
7536
+ if (child.props.node?.tagName === "img") return /* @__PURE__ */ jsx24(Fragment9, { children });
7537
+ if (child.props.node?.tagName === "code" && "data-block" in child.props) return /* @__PURE__ */ jsx24(Fragment9, { children });
7305
7538
  }
7306
7539
  return /* @__PURE__ */ jsx24("p", { ...props, children: decorate(children) });
7307
7540
  };
@@ -7387,12 +7620,13 @@ function DefaultPiTimelineMessage({ message, isLast, isStreaming, showThoughts,
7387
7620
  mentionActivateRef.current?.(mention);
7388
7621
  }, [openArtifact]);
7389
7622
  const hasMentions = effectiveMentionCatalog.files || effectiveMentionCatalog.commands.length > 0 || effectiveMentionCatalog.skills.length > 0;
7390
- const mentionsEnabled = Boolean(isAssistant && !(isStreaming && isLast) && hasMentions);
7623
+ const messageIsStreaming = isAssistant && isLast && (isStreaming || message.status === "streaming");
7624
+ const mentionsEnabled = Boolean(isAssistant && !messageIsStreaming && hasMentions);
7391
7625
  const mentionMarkdownComponents = useMemo12(
7392
7626
  () => mentionsEnabled ? createMessageMentionMarkdownComponents(effectiveMentionCatalog, activateCurrentMention) : void 0,
7393
7627
  [activateCurrentMention, effectiveMentionCatalog, mentionSignature, mentionsEnabled]
7394
7628
  );
7395
- const shouldReserveStreamingActions = isStreaming && isAssistant && isLast;
7629
+ const shouldReserveStreamingActions = messageIsStreaming;
7396
7630
  return /* @__PURE__ */ jsxs20(
7397
7631
  Message,
7398
7632
  {
@@ -7451,7 +7685,7 @@ function DefaultPiTimelineMessage({ message, isLast, isStreaming, showThoughts,
7451
7685
  })
7452
7686
  }
7453
7687
  ) : null,
7454
- finalParts.map((item) => {
7688
+ finalParts.map((item, index) => {
7455
7689
  if (item.kind === "reasoning") {
7456
7690
  return /* @__PURE__ */ jsx25(
7457
7691
  TimelineReasoningPart,
@@ -7475,6 +7709,7 @@ function DefaultPiTimelineMessage({ message, isLast, isStreaming, showThoughts,
7475
7709
  MessageResponse,
7476
7710
  {
7477
7711
  components: mentionMarkdownComponents,
7712
+ codeFilename: codeFilenameForPart(finalParts, index),
7478
7713
  className: "boring-agent-markdown max-w-none",
7479
7714
  children: text
7480
7715
  },
@@ -7491,7 +7726,7 @@ function DefaultPiTimelineMessage({ message, isLast, isStreaming, showThoughts,
7491
7726
  MessageActionsBar,
7492
7727
  {
7493
7728
  text: textParts.map((part) => part.text).join("\n\n"),
7494
- visible: !isStreaming
7729
+ visible: !messageIsStreaming
7495
7730
  }
7496
7731
  ) : null
7497
7732
  ]
@@ -7526,7 +7761,7 @@ function TimelineReasoningPart({ item, showThoughts }) {
7526
7761
  ReasoningTrigger,
7527
7762
  {
7528
7763
  className: "mb-1 w-fit rounded-[var(--radius-sm)] px-0 py-0 !text-xs !font-normal !text-muted-foreground/75 hover:bg-transparent hover:!text-muted-foreground/75 [&_svg]:!text-muted-foreground/75",
7529
- getThinkingMessage: (streaming) => /* @__PURE__ */ jsx25("span", { children: streaming ? "thinking" : "thoughts" }),
7764
+ getThinkingMessage: () => /* @__PURE__ */ jsx25("span", { children: "Reasoning" }),
7530
7765
  onClick: toggleManualOpen
7531
7766
  }
7532
7767
  ),
@@ -7587,6 +7822,15 @@ ${part.text}`;
7587
7822
  flushTools();
7588
7823
  return grouped;
7589
7824
  }
7825
+ function codeFilenameForPart(parts, index) {
7826
+ const precedingPart = parts[index - 1];
7827
+ if (!precedingPart || precedingPart.kind !== "tool-plain") return void 0;
7828
+ if (precedingPart.part.toolName !== "write" && precedingPart.part.toolName !== "edit") return void 0;
7829
+ const input = precedingPart.part.input;
7830
+ if (!input || typeof input !== "object" || Array.isArray(input)) return void 0;
7831
+ const path = input.path;
7832
+ return typeof path === "string" && path.trim() ? path : void 0;
7833
+ }
7590
7834
  function NoticeBubble({ level, text }) {
7591
7835
  return /* @__PURE__ */ jsx25(
7592
7836
  "div",
@@ -7667,10 +7911,10 @@ function MessageActionsBar({
7667
7911
  }
7668
7912
  };
7669
7913
  const iconActionBtnClass = cn(
7670
- "inline-flex h-6 w-6 items-center justify-center rounded-[var(--radius-sm)]",
7671
- "text-muted-foreground/35 transition-colors",
7672
- "hover:bg-foreground/[0.04] hover:text-muted-foreground/80",
7673
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[color:var(--accent)]/40"
7914
+ "boring-agent-message-action inline-flex size-8 items-center justify-center rounded-[var(--radius-sm)]",
7915
+ "text-muted-foreground/70 transition-colors duration-150",
7916
+ "hover:bg-foreground/[0.04] hover:text-muted-foreground",
7917
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]/40"
7674
7918
  );
7675
7919
  const hiddenActionProps = visible ? {} : { tabIndex: -1 };
7676
7920
  return /* @__PURE__ */ jsx25(
@@ -7678,7 +7922,7 @@ function MessageActionsBar({
7678
7922
  {
7679
7923
  "aria-hidden": !visible,
7680
7924
  className: cn(
7681
- "flex min-h-6 items-center gap-0.5 -mt-1 transition-opacity duration-200",
7925
+ "flex min-h-8 items-center gap-1 -mt-1 text-xs text-muted-foreground transition-opacity duration-200",
7682
7926
  visible ? "opacity-100" : "pointer-events-none opacity-0"
7683
7927
  ),
7684
7928
  children: /* @__PURE__ */ jsx25(
@@ -7739,6 +7983,8 @@ function PiConversationSurface({
7739
7983
  }) {
7740
7984
  const messageItems = buildMessageRenderItems(messages);
7741
7985
  const total = messageItems.length;
7986
+ const historyEmpty = messages.length === 0;
7987
+ const terminalError = hasTerminalChatError(runtimeNotices, historyEmpty);
7742
7988
  const [visibleCount, setVisibleCount] = useState15(TRANSCRIPT_WINDOW);
7743
7989
  useEffect14(() => {
7744
7990
  setVisibleCount(TRANSCRIPT_WINDOW);
@@ -7752,7 +7998,7 @@ function PiConversationSurface({
7752
7998
  return /* @__PURE__ */ jsxs21(
7753
7999
  Conversation,
7754
8000
  {
7755
- className: emptyHero ? "max-h-[45vh] flex-none" : "flex-1",
8001
+ className: emptyHero ? cn("flex-none", runtimeNotices.length > 0 ? "max-h-[45vh]" : "max-h-[20vh]") : "flex-1",
7756
8002
  "aria-label": "Agent conversation",
7757
8003
  "aria-live": "polite",
7758
8004
  onScrollToBottomReady,
@@ -7762,19 +8008,16 @@ function PiConversationSurface({
7762
8008
  chrome ? "max-w-3xl px-6 py-8" : "max-w-[680px] px-4 py-4",
7763
8009
  emptyHero && "py-4 text-center"
7764
8010
  ), children: [
7765
- messages.length === 0 && emptyStateHydrating ? /* @__PURE__ */ jsxs21("div", { className: "flex items-center gap-2 rounded-xl border border-border bg-card/70 px-4 py-3 text-sm text-muted-foreground", children: [
7766
- /* @__PURE__ */ jsx26(Loader22, { className: "h-4 w-4 animate-spin" }),
7767
- "Loading chat history\u2026"
7768
- ] }) : null,
7769
- messages.length === 0 && !emptyStateHydrating ? /* @__PURE__ */ jsx26(
8011
+ messages.length === 0 && emptyStateHydrating && !terminalError ? /* @__PURE__ */ jsx26(ConversationHistoryLoadingState, {}) : null,
8012
+ messages.length === 0 && !emptyStateHydrating && !terminalError ? /* @__PURE__ */ jsx26(
7770
8013
  ChatEmptyState,
7771
8014
  {
7772
8015
  eyebrow: emptyState?.eyebrow,
7773
8016
  title: emptyState?.title,
7774
8017
  description: emptyState?.description,
7775
- footer: emptyState?.footer,
7776
- suggestions,
7777
- className: emptyHero ? "items-center text-center [&>p]:mx-auto" : void 0,
8018
+ footer: emptyHero ? void 0 : emptyState?.footer,
8019
+ suggestions: emptyHero ? [] : suggestions,
8020
+ hero: emptyHero,
7778
8021
  onSelect: (suggestion) => {
7779
8022
  const text = suggestion.prompt ?? suggestion.label;
7780
8023
  if (!text.trim()) return;
@@ -7798,13 +8041,45 @@ function PiConversationSurface({
7798
8041
  },
7799
8042
  key
7800
8043
  )),
7801
- /* @__PURE__ */ jsx26(RuntimeNoticeMessages, { notices: runtimeNotices, onDismiss: onDismissNotice, renderAction: renderNoticeAction })
8044
+ /* @__PURE__ */ jsx26(RuntimeNoticeMessages, { notices: runtimeNotices, onDismiss: onDismissNotice, renderAction: renderNoticeAction, historyEmpty })
7802
8045
  ] }),
7803
8046
  /* @__PURE__ */ jsx26(ConversationScrollButton, {})
7804
8047
  ]
7805
8048
  }
7806
8049
  );
7807
8050
  }
8051
+ function ConversationHistoryLoadingState() {
8052
+ const skeletonClassName = "motion-reduce:animate-none";
8053
+ return /* @__PURE__ */ jsxs21(
8054
+ "div",
8055
+ {
8056
+ role: "status",
8057
+ "aria-label": "Loading chat history",
8058
+ "aria-busy": "true",
8059
+ "data-boring-agent-part": "chat-history-loading",
8060
+ className: "flex flex-col gap-7 py-4",
8061
+ children: [
8062
+ /* @__PURE__ */ jsxs21("div", { "aria-hidden": "true", className: "flex items-start gap-3", children: [
8063
+ /* @__PURE__ */ jsx26(Skeleton, { className: cn("mt-0.5 size-7 shrink-0 rounded-lg", skeletonClassName) }),
8064
+ /* @__PURE__ */ jsxs21("div", { className: "flex min-w-0 flex-1 flex-col gap-2.5", children: [
8065
+ /* @__PURE__ */ jsx26(Skeleton, { className: cn("h-3 w-24 rounded-sm", skeletonClassName) }),
8066
+ /* @__PURE__ */ jsx26(Skeleton, { className: cn("h-3 w-full rounded-sm", skeletonClassName) }),
8067
+ /* @__PURE__ */ jsx26(Skeleton, { className: cn("h-3 w-4/5 rounded-sm", skeletonClassName) })
8068
+ ] })
8069
+ ] }),
8070
+ /* @__PURE__ */ jsx26("div", { "aria-hidden": "true", className: "flex justify-end", children: /* @__PURE__ */ jsx26(Skeleton, { className: cn("h-11 w-[min(72%,30rem)] rounded-xl", skeletonClassName) }) }),
8071
+ /* @__PURE__ */ jsxs21("div", { "aria-hidden": "true", className: "flex items-start gap-3", children: [
8072
+ /* @__PURE__ */ jsx26(Skeleton, { className: cn("mt-0.5 size-7 shrink-0 rounded-lg", skeletonClassName) }),
8073
+ /* @__PURE__ */ jsxs21("div", { className: "flex min-w-0 flex-1 flex-col gap-3", children: [
8074
+ /* @__PURE__ */ jsx26(Skeleton, { className: cn("h-3 w-20 rounded-sm", skeletonClassName) }),
8075
+ /* @__PURE__ */ jsx26(Skeleton, { className: cn("h-20 w-full rounded-lg", skeletonClassName) }),
8076
+ /* @__PURE__ */ jsx26(Skeleton, { className: cn("h-3 w-3/5 rounded-sm", skeletonClassName) })
8077
+ ] })
8078
+ ] })
8079
+ ]
8080
+ }
8081
+ );
8082
+ }
7808
8083
  function TranscriptHistoryLoader({ olderCount, onLoadOlder }) {
7809
8084
  const { scrollRef } = useStickToBottomContext2();
7810
8085
  const pendingAnchor = useRef11(null);
@@ -7823,7 +8098,7 @@ function TranscriptHistoryLoader({ olderCount, onLoadOlder }) {
7823
8098
  el.addEventListener("scroll", onScroll, { passive: true });
7824
8099
  return () => el.removeEventListener("scroll", onScroll);
7825
8100
  }, [scrollRef, onLoadOlder]);
7826
- useLayoutEffect(() => {
8101
+ useLayoutEffect2(() => {
7827
8102
  const el = scrollRef.current;
7828
8103
  if (!el) return;
7829
8104
  if (pendingAnchor.current != null) {
@@ -7837,7 +8112,7 @@ function TranscriptHistoryLoader({ olderCount, onLoadOlder }) {
7837
8112
  {
7838
8113
  type: "button",
7839
8114
  onClick: onLoadOlder,
7840
- className: "rounded-full border border-border/60 px-3 py-1 text-[11px] text-muted-foreground transition-colors hover:text-foreground",
8115
+ className: "boring-agent-history-action min-h-8 rounded-full border border-border/60 px-3 py-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground",
7841
8116
  children: [
7842
8117
  "Load ",
7843
8118
  olderCount,
@@ -7869,8 +8144,8 @@ function buildMessageRenderItems(messages) {
7869
8144
  }
7870
8145
 
7871
8146
  // src/front/chat/components/PiChatComposerSurface.tsx
7872
- import { useCallback as useCallback16, useLayoutEffect as useLayoutEffect2 } from "react";
7873
- import { motion as motion2 } from "motion/react";
8147
+ import { useCallback as useCallback16, useLayoutEffect as useLayoutEffect3 } from "react";
8148
+ import { motion as motion2, useReducedMotion } from "motion/react";
7874
8149
 
7875
8150
  // src/front/composer/PluginUpdateStatus.tsx
7876
8151
  import { useEffect as useEffect15, useRef as useRef12 } from "react";
@@ -8113,7 +8388,7 @@ function PluginUpdateStatus({
8113
8388
 
8114
8389
  // src/front/composer/CommandRunStatus.tsx
8115
8390
  import { useEffect as useEffect16, useRef as useRef13 } from "react";
8116
- import { Fragment as Fragment9, jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
8391
+ import { Fragment as Fragment10, jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
8117
8392
  function CommandRunStatus({
8118
8393
  state,
8119
8394
  onDismiss,
@@ -8138,7 +8413,7 @@ function CommandRunStatus({
8138
8413
  tone: "running",
8139
8414
  dataAttribute: "data-boring-command-run",
8140
8415
  maxWidthClassName,
8141
- runningContent: state.command ? /* @__PURE__ */ jsxs24(Fragment9, { children: [
8416
+ runningContent: state.command ? /* @__PURE__ */ jsxs24(Fragment10, { children: [
8142
8417
  " Running ",
8143
8418
  /* @__PURE__ */ jsxs24("code", { className: "font-mono", children: [
8144
8419
  "/",
@@ -8156,7 +8431,7 @@ function CommandRunStatus({
8156
8431
  tone: "success",
8157
8432
  dataAttribute: "data-boring-command-run",
8158
8433
  maxWidthClassName,
8159
- title: state.command ? /* @__PURE__ */ jsxs24(Fragment9, { children: [
8434
+ title: state.command ? /* @__PURE__ */ jsxs24(Fragment10, { children: [
8160
8435
  " Ran ",
8161
8436
  /* @__PURE__ */ jsxs24("code", { className: "font-mono", children: [
8162
8437
  "/",
@@ -8176,7 +8451,7 @@ function CommandRunStatus({
8176
8451
  tone: "error",
8177
8452
  dataAttribute: "data-boring-command-run",
8178
8453
  maxWidthClassName,
8179
- title: state.command ? /* @__PURE__ */ jsxs24(Fragment9, { children: [
8454
+ title: state.command ? /* @__PURE__ */ jsxs24(Fragment10, { children: [
8180
8455
  " ",
8181
8456
  /* @__PURE__ */ jsxs24("code", { className: "font-mono", children: [
8182
8457
  "/",
@@ -8226,12 +8501,12 @@ function displayProviderLabel(provider) {
8226
8501
  }
8227
8502
 
8228
8503
  // src/front/chatPanelComposerControls.tsx
8229
- import { Fragment as Fragment10, jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
8504
+ import { Fragment as Fragment11, jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
8230
8505
  var composerActionClass = cn(
8231
8506
  "inline-flex h-8 items-center justify-center gap-1.5 rounded-[var(--radius-md)] border-0 bg-transparent",
8232
- "text-muted-foreground shadow-none transition",
8507
+ "text-muted-foreground shadow-none transition-colors motion-reduce:transition-none",
8233
8508
  "hover:bg-muted/60 hover:text-foreground",
8234
- "focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40",
8509
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-popover",
8235
8510
  "disabled:pointer-events-none disabled:opacity-50"
8236
8511
  );
8237
8512
  var THINKING_LEVEL_LABELS = {
@@ -8241,10 +8516,10 @@ var THINKING_LEVEL_LABELS = {
8241
8516
  high: "High"
8242
8517
  };
8243
8518
  var THINKING_LEVEL_STATUS_LABELS = {
8244
- off: "off",
8245
- low: "low",
8246
- medium: "medium",
8247
- high: "high"
8519
+ off: "Off",
8520
+ low: "Low",
8521
+ medium: "Medium",
8522
+ high: "High"
8248
8523
  };
8249
8524
  var THINKING_LEVEL_DETAILS = {
8250
8525
  off: "No extra reasoning",
@@ -8254,7 +8529,7 @@ var THINKING_LEVEL_DETAILS = {
8254
8529
  };
8255
8530
  var selectorTriggerClass = cn(
8256
8531
  composerActionClass,
8257
- "h-7 rounded-lg border border-border/60 bg-transparent px-2 text-[12px] font-medium text-muted-foreground",
8532
+ "h-8 rounded-lg border border-border/60 bg-transparent px-2 text-xs font-medium text-muted-foreground",
8258
8533
  "hover:border-border/80 hover:bg-muted/55 hover:text-foreground"
8259
8534
  );
8260
8535
  var selectorContentClass = cn(
@@ -8262,12 +8537,12 @@ var selectorContentClass = cn(
8262
8537
  "bg-[color:var(--popover)] p-1 text-[color:var(--popover-foreground)] shadow-xl"
8263
8538
  );
8264
8539
  var composerPickerMenuClass = cn(
8265
- "mb-1 w-full overflow-hidden rounded-lg border border-border/60",
8266
- "bg-[color:var(--popover)] text-[color:var(--popover-foreground)] shadow-lg"
8540
+ "w-full overflow-hidden rounded-xl border border-border",
8541
+ "bg-[color:var(--popover)] text-[color:var(--popover-foreground)] shadow-xl"
8267
8542
  );
8268
8543
  function selectorItemClass(selected) {
8269
8544
  return cn(
8270
- "flex items-center gap-2 rounded-md px-2 py-1.5 text-[13px]",
8545
+ "flex min-h-10 items-center gap-2 rounded-md px-2.5 py-2 text-sm text-muted-foreground",
8271
8546
  "data-[selected=true]:bg-[color:oklch(from_var(--accent)_l_c_h/0.15)] data-[selected=true]:text-foreground",
8272
8547
  selected && "bg-foreground/[0.06] text-foreground"
8273
8548
  );
@@ -8322,7 +8597,8 @@ function ThinkingLevelGlyph({ level }) {
8322
8597
  function modelTriggerLabel(value, options, emptyLabel = "Default model") {
8323
8598
  const current = value ? options.find((m) => m.provider === value.provider && m.id === value.id) : void 0;
8324
8599
  const rawTriggerLabel = current?.label ?? value?.id;
8325
- return value ? rawTriggerLabel && current?.label && current.label !== value.id && /[A-Z]/.test(current.label) ? current.label : displayModelLabel(rawTriggerLabel ?? value.id) : emptyLabel;
8600
+ const label = value ? rawTriggerLabel && current?.label && current.label !== value.id && /[A-Z]/.test(current.label) ? current.label : displayModelLabel(rawTriggerLabel ?? value.id) : emptyLabel;
8601
+ return value ? label.replace(/\s+\([^)]*\)\s*$/, "") : label;
8326
8602
  }
8327
8603
  function modelMenuOptions(_value, options) {
8328
8604
  return options.filter((m) => m.available);
@@ -8361,16 +8637,20 @@ var ModelSelectTrigger = forwardRef(function ModelSelectTrigger2({
8361
8637
  title: trigger === "slash" ? `Open model picker. Current model: ${triggerDisplay}` : void 0,
8362
8638
  onClick,
8363
8639
  className: cn(
8364
- trigger === "slash" ? "max-w-[min(52vw,260px)] cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap rounded-[var(--radius-md)] px-1.5 py-0.5 transition-colors hover:bg-foreground/[0.045] hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/20 disabled:pointer-events-none disabled:opacity-50" : selectorTriggerClass,
8640
+ trigger === "slash" ? "h-8 max-w-[min(68vw,420px)] cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap rounded-[var(--radius-md)] px-2 text-xs transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-popover disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none" : selectorTriggerClass,
8365
8641
  trigger === "slash" ? "w-auto" : "w-auto max-w-[min(52vw,260px)] gap-1",
8366
8642
  open && (trigger === "slash" ? "bg-muted/45" : "border-border/80 bg-muted/55 text-foreground"),
8367
8643
  className
8368
8644
  ),
8369
8645
  ...props,
8370
- children: trigger === "slash" ? /* @__PURE__ */ jsxs25(Fragment10, { children: [
8371
- /* @__PURE__ */ jsx30("span", { className: "text-muted-foreground", children: "/model: " }),
8372
- /* @__PURE__ */ jsx30("span", { className: "text-foreground", children: triggerDisplay })
8373
- ] }) : /* @__PURE__ */ jsxs25(Fragment10, { children: [
8646
+ children: trigger === "slash" ? /* @__PURE__ */ jsxs25(Fragment11, { children: [
8647
+ /* @__PURE__ */ jsx30("span", { className: "text-muted-foreground", children: "Model \xB7 " }),
8648
+ /* @__PURE__ */ jsx30("span", { className: "min-w-0 truncate text-foreground", children: triggerLabel }),
8649
+ value ? /* @__PURE__ */ jsxs25("span", { className: "shrink-0 text-muted-foreground", children: [
8650
+ " \xB7 ",
8651
+ displayProviderLabel(value.provider)
8652
+ ] }) : null
8653
+ ] }) : /* @__PURE__ */ jsxs25(Fragment11, { children: [
8374
8654
  /* @__PURE__ */ jsx30("span", { className: "min-w-0 truncate", children: triggerDisplay }),
8375
8655
  /* @__PURE__ */ jsx30(ChevronDownIcon5, { className: "h-3 w-3 shrink-0 text-muted-foreground/50", "aria-hidden": "true" })
8376
8656
  ] })
@@ -8435,11 +8715,11 @@ function ModelPickerMenu({
8435
8715
  {
8436
8716
  placeholder: "Search models\u2026",
8437
8717
  autoFocus: true,
8438
- className: "h-8 w-full border-0 bg-transparent text-[13px] outline-none focus:ring-0"
8718
+ className: "h-8 w-full border-0 bg-transparent text-sm outline-none focus:ring-0"
8439
8719
  }
8440
8720
  ) }),
8441
8721
  /* @__PURE__ */ jsxs25(CommandList, { className: "max-h-[300px] p-0.5", children: [
8442
- /* @__PURE__ */ jsx30(CommandEmpty, { className: "py-4 text-center text-[13px] text-muted-foreground", children: "No models found" }),
8722
+ /* @__PURE__ */ jsx30(CommandEmpty, { className: "py-4 text-center text-sm text-muted-foreground", children: "No models found" }),
8443
8723
  !hideDefaultOption ? /* @__PURE__ */ jsx30(CommandGroup, { children: /* @__PURE__ */ jsxs25(
8444
8724
  CommandItem,
8445
8725
  {
@@ -8461,7 +8741,7 @@ function ModelPickerMenu({
8461
8741
  }
8462
8742
  ),
8463
8743
  /* @__PURE__ */ jsx30("span", { className: "truncate", children: "Default model" }),
8464
- /* @__PURE__ */ jsx30("span", { className: "ml-auto shrink-0 text-[10px] text-muted-foreground/60", children: "auto" })
8744
+ /* @__PURE__ */ jsx30("span", { className: "ml-auto shrink-0 text-xs text-muted-foreground", children: "Auto" })
8465
8745
  ]
8466
8746
  }
8467
8747
  ) }) : null,
@@ -8469,7 +8749,7 @@ function ModelPickerMenu({
8469
8749
  CommandGroup,
8470
8750
  {
8471
8751
  heading: displayProviderLabel(provider),
8472
- className: "[&_[cmdk-group-heading]]:px-2.5 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-[10px] [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-[0.08em] [&_[cmdk-group-heading]]:text-muted-foreground/60",
8752
+ className: "[&_[cmdk-group-heading]]:px-2.5 [&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-[0.08em] [&_[cmdk-group-heading]]:text-muted-foreground",
8473
8753
  children: list.map((m) => {
8474
8754
  const key = encodeModelKey(m);
8475
8755
  const label = m.label || displayModelLabel(m.id);
@@ -8495,7 +8775,7 @@ function ModelPickerMenu({
8495
8775
  }
8496
8776
  ),
8497
8777
  /* @__PURE__ */ jsx30("span", { className: "min-w-0 flex-1 truncate whitespace-nowrap", children: label }),
8498
- /* @__PURE__ */ jsx30("span", { className: "ml-auto max-w-[45%] shrink-0 truncate whitespace-nowrap text-right text-[10px] text-muted-foreground/60", children: m.id })
8778
+ /* @__PURE__ */ jsx30("span", { className: "ml-auto max-w-[45%] shrink-0 truncate whitespace-nowrap text-right text-xs text-muted-foreground", children: m.id })
8499
8779
  ]
8500
8780
  },
8501
8781
  key
@@ -8646,7 +8926,7 @@ var ThinkingSelectTrigger = forwardRef(function ThinkingSelectTrigger2({
8646
8926
  "data-boring-state": disabled ? "disabled" : void 0,
8647
8927
  disabled,
8648
8928
  className: cn(
8649
- trigger === "slash" ? "cursor-pointer rounded-[var(--radius-md)] px-1.5 py-0.5 transition-colors hover:bg-foreground/[0.045] hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/20 disabled:pointer-events-none disabled:opacity-50" : selectorTriggerClass,
8929
+ trigger === "slash" ? "h-8 cursor-pointer rounded-[var(--radius-md)] px-2 text-xs transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-popover disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none" : selectorTriggerClass,
8650
8930
  trigger === "button" && "gap-1.5",
8651
8931
  trigger === "button" && value !== "off" && !open && "border-[color:oklch(from_var(--accent)_l_c_h/0.35)] bg-[color:oklch(from_var(--accent)_l_c_h/0.08)] text-foreground",
8652
8932
  open && (trigger === "slash" ? "bg-muted/45" : "border-border/80 bg-muted/55 text-foreground"),
@@ -8659,10 +8939,10 @@ var ThinkingSelectTrigger = forwardRef(function ThinkingSelectTrigger2({
8659
8939
  ...props,
8660
8940
  children: [
8661
8941
  trigger === "button" ? /* @__PURE__ */ jsx30(ThinkingLevelGlyph, { level: value }) : null,
8662
- trigger === "slash" ? /* @__PURE__ */ jsxs25(Fragment10, { children: [
8663
- /* @__PURE__ */ jsx30("span", { className: "text-muted-foreground", children: "/thinking: " }),
8942
+ trigger === "slash" ? /* @__PURE__ */ jsxs25(Fragment11, { children: [
8943
+ /* @__PURE__ */ jsx30("span", { className: "text-muted-foreground", children: "Thinking \xB7 " }),
8664
8944
  /* @__PURE__ */ jsx30("span", { className: "text-foreground", children: THINKING_LEVEL_STATUS_LABELS[value] })
8665
- ] }) : /* @__PURE__ */ jsxs25(Fragment10, { children: [
8945
+ ] }) : /* @__PURE__ */ jsxs25(Fragment11, { children: [
8666
8946
  /* @__PURE__ */ jsx30("span", { children: THINKING_LEVEL_LABELS[value] }),
8667
8947
  /* @__PURE__ */ jsx30(ChevronDownIcon5, { className: "h-3 w-3 shrink-0 text-muted-foreground/50", "aria-hidden": "true" })
8668
8948
  ] })
@@ -8735,7 +9015,7 @@ function ThinkingPickerMenu({
8735
9015
  ),
8736
9016
  /* @__PURE__ */ jsx30(ThinkingLevelGlyph, { level }),
8737
9017
  /* @__PURE__ */ jsx30("span", { className: "font-medium", children: THINKING_LEVEL_LABELS[level] }),
8738
- /* @__PURE__ */ jsx30("span", { className: "ml-auto text-[11px] text-muted-foreground/65", children: THINKING_LEVEL_DETAILS[level] })
9018
+ /* @__PURE__ */ jsx30("span", { className: "ml-auto text-xs text-muted-foreground", children: THINKING_LEVEL_DETAILS[level] })
8739
9019
  ]
8740
9020
  },
8741
9021
  level
@@ -8826,7 +9106,7 @@ var PromptInputFooter = ({
8826
9106
  );
8827
9107
 
8828
9108
  // src/front/primitives/prompt-input.tsx
8829
- import { Fragment as Fragment11, jsx as jsx32, jsxs as jsxs27 } from "react/jsx-runtime";
9109
+ import { Fragment as Fragment12, jsx as jsx32, jsxs as jsxs27 } from "react/jsx-runtime";
8830
9110
  var PromptInput = ({
8831
9111
  className,
8832
9112
  accept,
@@ -9169,7 +9449,7 @@ var PromptInput = ({
9169
9449
  },
9170
9450
  [usingProvider, controller, files, onSubmit, clear]
9171
9451
  );
9172
- const inner = /* @__PURE__ */ jsxs27(Fragment11, { children: [
9452
+ const inner = /* @__PURE__ */ jsxs27(Fragment12, { children: [
9173
9453
  /* @__PURE__ */ jsx32(
9174
9454
  Input,
9175
9455
  {
@@ -9338,7 +9618,7 @@ var PromptInputSubmit = ({
9338
9618
 
9339
9619
  // src/front/primitives/slash-command-picker.tsx
9340
9620
  import { useEffect as useEffect19, useMemo as useMemo14, useRef as useRef16, useState as useState18 } from "react";
9341
- import { Fragment as Fragment12, jsx as jsx33, jsxs as jsxs28 } from "react/jsx-runtime";
9621
+ import { Fragment as Fragment13, jsx as jsx33, jsxs as jsxs28 } from "react/jsx-runtime";
9342
9622
  var ALL_PLUGINS = "__all__";
9343
9623
  function commandGroup(cmd) {
9344
9624
  if (cmd.source === "skill") return "skills";
@@ -9450,7 +9730,7 @@ function SlashCommandPicker({ query, commands, onSelect, onDismiss }) {
9450
9730
  "/",
9451
9731
  cmd.name
9452
9732
  ] }),
9453
- cmd.source === "skill" ? /* @__PURE__ */ jsxs28(Fragment12, { children: [
9733
+ cmd.source === "skill" ? /* @__PURE__ */ jsxs28(Fragment13, { children: [
9454
9734
  /* @__PURE__ */ jsx33("span", { className: "rounded-sm bg-accent/15 px-1 py-px text-[9px] font-medium uppercase tracking-wide text-accent-foreground", children: "skill" }),
9455
9735
  cmd.sourcePlugin ? /* @__PURE__ */ jsx33("span", { className: "rounded-sm bg-muted px-1 py-px text-[9px] font-medium text-muted-foreground", children: cmd.sourcePlugin }) : null
9456
9736
  ] }) : cmd.sourcePlugin ? (
@@ -9467,7 +9747,7 @@ function SlashCommandPicker({ query, commands, onSelect, onDismiss }) {
9467
9747
  }
9468
9748
 
9469
9749
  // src/front/chat/components/ComposerAttachments.tsx
9470
- import { AlertCircleIcon as AlertCircleIcon2, Loader2 as Loader23, PaperclipIcon as PaperclipIcon2 } from "lucide-react";
9750
+ import { AlertCircleIcon as AlertCircleIcon2, Loader2 as Loader22, PaperclipIcon as PaperclipIcon2 } from "lucide-react";
9471
9751
  import { IconButton as IconButton3 } from "@hachej/boring-ui-kit";
9472
9752
  import { jsx as jsx34, jsxs as jsxs29 } from "react/jsx-runtime";
9473
9753
  function AttachmentButton({ disabled, className }) {
@@ -9482,10 +9762,10 @@ function AttachmentButton({ disabled, className }) {
9482
9762
  onClick: () => {
9483
9763
  if (!disabled) attachments.openFileDialog();
9484
9764
  },
9485
- className: cn(composerActionClass, "w-8", className),
9765
+ className: cn(composerActionClass, "composer-attachment-button !h-10 !w-10", className),
9486
9766
  "aria-label": "Attach files",
9487
9767
  title: disabled ? "Attachments are available when the composer is ready." : "Attach files",
9488
- children: /* @__PURE__ */ jsx34(PaperclipIcon2, { className: "h-3.5 w-3.5", strokeWidth: 1.75 })
9768
+ children: /* @__PURE__ */ jsx34(PaperclipIcon2, { className: "h-4 w-4", strokeWidth: 1.75 })
9489
9769
  }
9490
9770
  );
9491
9771
  }
@@ -9505,18 +9785,18 @@ function AttachmentsList() {
9505
9785
  data: file,
9506
9786
  onRemove: () => attachments.remove(file.id),
9507
9787
  className: cn(
9508
- "!h-9 !gap-2 !rounded-full !border-input/80 !bg-muted/40 !pl-1 !pr-2",
9788
+ "composer-attachment-chip !h-9 !gap-2 !rounded-full !border-input/80 !bg-muted/40 !pl-1 !pr-2",
9509
9789
  "transition-colors hover:!bg-muted/70 hover:!text-foreground",
9510
9790
  file.status === "error" && "!border-destructive/50 !bg-destructive/10"
9511
9791
  ),
9512
9792
  children: [
9513
9793
  /* @__PURE__ */ jsxs29("div", { className: "relative shrink-0", children: [
9514
9794
  /* @__PURE__ */ jsx34(AttachmentPreview, { className: "!size-7 overflow-hidden !rounded-full bg-background/60" }),
9515
- file.status === "uploading" ? /* @__PURE__ */ jsx34("div", { className: "absolute inset-0 flex items-center justify-center rounded-full bg-background/70", children: /* @__PURE__ */ jsx34(Loader23, { className: "size-3.5 animate-spin text-muted-foreground" }) }) : null,
9795
+ file.status === "uploading" ? /* @__PURE__ */ jsx34("div", { className: "absolute inset-0 flex items-center justify-center rounded-full bg-background/70", children: /* @__PURE__ */ jsx34(Loader22, { className: "size-3.5 animate-spin text-muted-foreground" }) }) : null,
9516
9796
  file.status === "error" ? /* @__PURE__ */ jsx34("div", { className: "absolute inset-0 flex items-center justify-center rounded-full bg-destructive/20", children: /* @__PURE__ */ jsx34(AlertCircleIcon2, { className: "size-3.5 text-destructive" }) }) : null
9517
9797
  ] }),
9518
9798
  /* @__PURE__ */ jsx34(AttachmentInfo, { className: "min-w-0 !max-w-[180px] truncate text-[13px] font-medium" }),
9519
- /* @__PURE__ */ jsx34(AttachmentRemove, { className: "!size-5 !rounded-full !opacity-100 text-muted-foreground/80 hover:text-foreground" })
9799
+ /* @__PURE__ */ jsx34(AttachmentRemove, { className: "composer-attachment-remove !size-5 !rounded-full !opacity-100 text-muted-foreground/80 hover:text-foreground" })
9520
9800
  ]
9521
9801
  },
9522
9802
  file.id
@@ -9550,6 +9830,7 @@ function hasSoftWrappedLine(node, style) {
9550
9830
  }
9551
9831
  function PiChatComposerSurface({
9552
9832
  chrome,
9833
+ pickerPlacement = "above",
9553
9834
  isStreaming,
9554
9835
  status,
9555
9836
  disabled,
@@ -9607,6 +9888,7 @@ function PiChatComposerSurface({
9607
9888
  onSubmitMessage,
9608
9889
  onStop
9609
9890
  }) {
9891
+ const prefersReducedMotion = useReducedMotion();
9610
9892
  const workspaceRequestId = getHeaderValue(requestHeaders, "x-boring-workspace-id");
9611
9893
  const composerContributions = useComposerContributions();
9612
9894
  const uploadAttachment = useCallback16((file) => uploadFile(file, {
@@ -9636,10 +9918,13 @@ function PiChatComposerSurface({
9636
9918
  rail?.style.setProperty("--composer-input-group-height", `${inputGroupHeight}px`);
9637
9919
  rail?.setAttribute("data-composer-multiline", inputGroupHeight > COMPOSER_INPUT_GROUP_MIN_HEIGHT ? "true" : "false");
9638
9920
  }, []);
9639
- useLayoutEffect2(() => {
9921
+ useLayoutEffect3(() => {
9640
9922
  resizeTextarea(textareaRef.current);
9641
9923
  }, [draft, resizeTextarea, textareaRef]);
9642
- return /* @__PURE__ */ jsxs30("div", { className: cn("relative z-20", chrome ? "px-4 pb-4 pt-2 sm:px-6 sm:pb-5" : "px-3 pb-2 pt-1"), children: [
9924
+ return /* @__PURE__ */ jsxs30("div", { className: cn(
9925
+ "relative z-20",
9926
+ chrome ? "pb-[calc(1rem+env(safe-area-inset-bottom))] pl-[calc(1rem+env(safe-area-inset-left))] pr-[calc(1rem+env(safe-area-inset-right))] pt-2 sm:pb-[calc(1.25rem+env(safe-area-inset-bottom))] sm:pl-[calc(1.5rem+env(safe-area-inset-left))] sm:pr-[calc(1.5rem+env(safe-area-inset-right))]" : "pb-[calc(0.5rem+env(safe-area-inset-bottom))] pl-[calc(0.75rem+env(safe-area-inset-left))] pr-[calc(0.75rem+env(safe-area-inset-right))] pt-1"
9927
+ ), children: [
9643
9928
  /* @__PURE__ */ jsx35(
9644
9929
  "div",
9645
9930
  {
@@ -9656,15 +9941,15 @@ function PiChatComposerSurface({
9656
9941
  "data-testid": isStreaming ? "chat-working" : void 0,
9657
9942
  role: isStreaming ? "status" : void 0,
9658
9943
  "aria-live": isStreaming ? "polite" : void 0,
9659
- className: "inline-flex items-center gap-2 rounded-full border border-border/50 bg-background/85 px-2.5 py-1 text-[12px] text-muted-foreground/75 shadow-sm backdrop-blur",
9944
+ className: "inline-flex items-center gap-2 rounded-full border border-border/50 bg-background/85 px-2.5 py-1 text-[12px] text-foreground shadow-sm backdrop-blur",
9660
9945
  children: [
9661
9946
  /* @__PURE__ */ jsx35(
9662
9947
  motion2.span,
9663
9948
  {
9664
9949
  "aria-hidden": "true",
9665
9950
  className: "inline-block size-1.5 rounded-full bg-[color:var(--accent)]",
9666
- animate: { opacity: [0.35, 1, 0.35] },
9667
- transition: { duration: 1.2, repeat: Infinity, ease: "easeInOut" }
9951
+ animate: prefersReducedMotion ? { opacity: 1 } : { opacity: [0.35, 1, 0.35] },
9952
+ transition: prefersReducedMotion ? { duration: 0 } : { duration: 1.2, repeat: Infinity, ease: "easeInOut" }
9668
9953
  }
9669
9954
  ),
9670
9955
  /* @__PURE__ */ jsx35("span", { children: "Working\u2026" })
@@ -9707,219 +9992,225 @@ function PiChatComposerSurface({
9707
9992
  children: attachmentNotice
9708
9993
  }
9709
9994
  ) : null,
9710
- /* @__PURE__ */ jsxs30("div", { className: cn("mx-auto w-full", chrome ? "max-w-3xl" : "max-w-[680px]"), children: [
9711
- mentionState ? /* @__PURE__ */ jsx35(
9712
- MentionPicker,
9713
- {
9714
- mention: mentionState,
9715
- apiBaseUrl,
9716
- fetch: fetch2,
9717
- requestHeaders,
9718
- storageScope,
9719
- onSelect: onSelectMention,
9720
- onDismiss: onDismissMention
9721
- }
9722
- ) : null,
9723
- slashQuery !== null ? /* @__PURE__ */ jsx35(
9724
- SlashCommandPicker,
9995
+ /* @__PURE__ */ jsxs30("div", { className: cn("@container group/composer relative mx-auto w-full", chrome ? "max-w-3xl" : "max-w-[680px]"), children: [
9996
+ /* @__PURE__ */ jsxs30("div", { className: cn(
9997
+ "absolute inset-x-0 z-30",
9998
+ pickerPlacement === "above-compact" ? "bottom-[calc(100%+4px)] [&_[cmdk-list]]:!max-h-[200px]" : "bottom-[calc(100%+4px)]"
9999
+ ), children: [
10000
+ mentionState ? /* @__PURE__ */ jsx35("div", { className: "mr-px ml-[43px]", children: /* @__PURE__ */ jsx35(
10001
+ MentionPicker,
10002
+ {
10003
+ mention: mentionState,
10004
+ apiBaseUrl,
10005
+ fetch: fetch2,
10006
+ requestHeaders,
10007
+ storageScope,
10008
+ onSelect: onSelectMention,
10009
+ onDismiss: onDismissMention
10010
+ }
10011
+ ) }) : null,
10012
+ slashQuery !== null ? /* @__PURE__ */ jsx35("div", { className: "mr-px ml-[43px]", children: /* @__PURE__ */ jsx35(
10013
+ SlashCommandPicker,
10014
+ {
10015
+ query: slashQuery,
10016
+ commands,
10017
+ onSelect: onSelectSlashCommand,
10018
+ onDismiss: onDismissSlash
10019
+ }
10020
+ ) }) : null,
10021
+ mentionState === null && slashQuery === null && modelPickerOpen ? /* @__PURE__ */ jsx35("div", { className: "mr-px mb-1 ml-[43px]", children: /* @__PURE__ */ jsx35(
10022
+ ModelPickerMenu,
10023
+ {
10024
+ value: selectedModel,
10025
+ onChange: onModelChange,
10026
+ options: modelOptions,
10027
+ disabled: isStreaming || modelControlled,
10028
+ hideDefaultOption: hideDefaultModelOption,
10029
+ onClose: () => onSetModelPickerOpen(false)
10030
+ }
10031
+ ) }) : null,
10032
+ mentionState === null && slashQuery === null && thinkingPickerOpen ? /* @__PURE__ */ jsx35("div", { className: "mr-px mb-1 ml-[43px]", children: /* @__PURE__ */ jsx35(
10033
+ ThinkingPickerMenu,
10034
+ {
10035
+ value: selectedThinking,
10036
+ onChange: onThinkingChange,
10037
+ disabled: isStreaming || thinkingControlled,
10038
+ onClose: () => onSetThinkingPickerOpen(false)
10039
+ }
10040
+ ) }) : null
10041
+ ] }),
10042
+ composerContributions.map(({ id, Top }) => Top ? /* @__PURE__ */ jsx35(
10043
+ "div",
9725
10044
  {
9726
- query: slashQuery,
9727
- commands,
9728
- onSelect: onSelectSlashCommand,
9729
- onDismiss: onDismissSlash
9730
- }
9731
- ) : null,
9732
- mentionState === null && slashQuery === null && modelPickerOpen ? /* @__PURE__ */ jsx35(
9733
- ModelPickerMenu,
10045
+ "data-boring-agent-part": "composer-contribution-top",
10046
+ "data-boring-agent-contribution-id": id,
10047
+ className: cn("mx-auto mb-2 w-full empty:hidden", chrome ? "max-w-3xl" : "max-w-[680px]"),
10048
+ children: /* @__PURE__ */ jsx35(Top, {})
10049
+ },
10050
+ id
10051
+ ) : null),
10052
+ /* @__PURE__ */ jsx35(
10053
+ "div",
9734
10054
  {
9735
- value: selectedModel,
9736
- onChange: onModelChange,
9737
- options: modelOptions,
9738
- disabled: isStreaming || modelControlled,
9739
- hideDefaultOption: hideDefaultModelOption,
9740
- onClose: () => onSetModelPickerOpen(false)
10055
+ "data-boring-agent-part": "composer-rail",
10056
+ "data-composer-multiline": "false",
10057
+ style: { "--composer-input-group-height": `${COMPOSER_INPUT_GROUP_MIN_HEIGHT}px` },
10058
+ className: cn(
10059
+ "relative mx-auto w-full overflow-visible rounded-xl",
10060
+ chrome ? "max-w-3xl bg-transparent shadow-[0_1px_2px_-1px_oklch(0_0_0/0.06),0_6px_18px_-12px_oklch(0_0_0/0.12),inset_0_0_0_1px_oklch(from_var(--border)_l_c_h/0.7)] focus-within:shadow-[0_1px_3px_-1px_oklch(0_0_0/0.08),0_10px_28px_-14px_oklch(0_0_0/0.16),inset_0_0_0_1px_oklch(from_var(--accent)_l_c_h/0.45)]" : "max-w-[680px] bg-transparent shadow-[inset_0_0_0_1px_oklch(from_var(--border)_l_c_h/0.7)] focus-within:shadow-[inset_0_0_0_1px_oklch(from_var(--accent)_l_c_h/0.45)]",
10061
+ "transition-shadow duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]",
10062
+ "[&_[data-slot=input-group]]:!h-auto [&_[data-slot=input-group]]:!min-h-[var(--composer-input-group-height)]",
10063
+ "[&_[data-slot=input-group]]:!flex-col [&_[data-slot=input-group]]:!items-stretch [&_[data-slot=input-group]]:!overflow-hidden",
10064
+ "[&_[data-slot=input-group]]:!border-0 [&_[data-slot=input-group]]:!rounded-xl",
10065
+ "[&_[data-slot=input-group]]:!bg-transparent [&_[data-slot=input-group]]:!shadow-none",
10066
+ "[&_[data-slot=input-group]]:dark:!bg-transparent [&_[data-slot=input-group]]:!ring-0",
10067
+ "[&_[data-slot=input-group]]:has-[:focus]:!ring-0",
10068
+ "[&[data-composer-multiline=true]_[data-boring-agent-part=composer-submit-addon]]:self-end",
10069
+ "[&[data-composer-multiline=true]_[data-boring-agent-part=composer-submit-addon]]:mb-2"
10070
+ ),
10071
+ children: /* @__PURE__ */ jsxs30(
10072
+ PromptInput,
10073
+ {
10074
+ "data-boring-state": status,
10075
+ onSubmit: (message) => onSubmitMessage({ text: message.text, files: message.files }),
10076
+ onUploadFile: uploadAttachment,
10077
+ multiple: true,
10078
+ maxFiles: disabled || isStreaming ? 0 : MAX_PROMPT_ATTACHMENTS,
10079
+ maxFileSize: MAX_PROMPT_ATTACHMENT_BYTES,
10080
+ onError: (err) => {
10081
+ if (err.code === "max_files") onAttachmentNotice(`Up to ${MAX_PROMPT_ATTACHMENTS} attachments per message.`);
10082
+ else if (err.code === "max_file_size") onAttachmentNotice("Files must be under 4 MB each.");
10083
+ else if (err.code === "accept") onAttachmentNotice("That file type isn't supported here.");
10084
+ else onAttachmentNotice(err.message || "Attachment rejected.");
10085
+ },
10086
+ children: [
10087
+ /* @__PURE__ */ jsx35(AttachmentsList, {}),
10088
+ /* @__PURE__ */ jsxs30(
10089
+ "div",
10090
+ {
10091
+ "data-boring-agent-part": "composer-input-row",
10092
+ className: "flex min-h-[var(--composer-input-group-height)] w-full items-center",
10093
+ children: [
10094
+ /* @__PURE__ */ jsx35("div", { className: "flex h-14 shrink-0 items-center pl-2", children: /* @__PURE__ */ jsx35(AttachmentButton, { disabled: disabled || isStreaming }) }),
10095
+ /* @__PURE__ */ jsx35(
10096
+ PromptInputTextarea,
10097
+ {
10098
+ value: draft,
10099
+ placeholder: composerBlocked ? workspaceWarmupBlocked ? composerBlockerLabel : "" : composerPlaceholder ?? "Ask anything\u2026",
10100
+ disabled,
10101
+ readOnly: composerBlocked,
10102
+ "aria-label": "Agent prompt",
10103
+ ref: (node) => {
10104
+ textareaRef.current = node;
10105
+ resizeTextarea(node);
10106
+ },
10107
+ onChange: onTextareaChange,
10108
+ onKeyDown: onTextareaKeyDown,
10109
+ style: { fieldSizing: "fixed" },
10110
+ className: cn(
10111
+ "min-w-0 flex-1 !min-h-10 !max-h-40 resize-none overflow-hidden border-0 bg-transparent shadow-none",
10112
+ "[field-sizing:fixed]",
10113
+ "px-2 py-2 text-sm leading-6 text-foreground",
10114
+ "placeholder:text-muted-foreground/60",
10115
+ "focus-visible:ring-0 focus-visible:ring-offset-0"
10116
+ )
10117
+ }
10118
+ ),
10119
+ /* @__PURE__ */ jsx35(
10120
+ PromptInputFooter,
10121
+ {
10122
+ align: "inline-end",
10123
+ "data-boring-agent-part": "composer-submit-addon",
10124
+ className: "!order-none !w-auto shrink-0 self-center justify-between border-0 bg-transparent !px-2 !py-0",
10125
+ children: /* @__PURE__ */ jsxs30("div", { className: "ml-auto flex items-center gap-1.5", children: [
10126
+ composerContributions.map(({ id, Action }) => Action ? /* @__PURE__ */ jsx35(
10127
+ "span",
10128
+ {
10129
+ "data-boring-agent-part": "composer-contribution-action",
10130
+ "data-boring-agent-contribution-id": id,
10131
+ className: "contents",
10132
+ children: /* @__PURE__ */ jsx35(Action, { updateDraft })
10133
+ },
10134
+ id
10135
+ ) : null),
10136
+ /* @__PURE__ */ jsx35(
10137
+ PromptInputSubmit,
10138
+ {
10139
+ "data-boring-agent-part": "composer-submit",
10140
+ status: submitStatus,
10141
+ onStop,
10142
+ disabled: submitDisabled,
10143
+ className: cn(
10144
+ "composer-submit-control h-8 w-8 shrink-0 rounded-full",
10145
+ "bg-foreground text-background",
10146
+ "transition-all duration-150 ease-[cubic-bezier(0.22,1,0.36,1)]",
10147
+ "hover:bg-foreground/90 hover:shadow-[0_0_0_3px_oklch(from_var(--foreground)_l_c_h/0.12)] hover:scale-[1.04]",
10148
+ "active:scale-[0.93] active:brightness-95",
10149
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/20",
10150
+ "disabled:pointer-events-none disabled:opacity-40",
10151
+ "[&>svg]:size-3.5"
10152
+ )
10153
+ }
10154
+ )
10155
+ ] })
10156
+ }
10157
+ )
10158
+ ]
10159
+ }
10160
+ )
10161
+ ]
10162
+ }
10163
+ )
9741
10164
  }
9742
- ) : null,
9743
- mentionState === null && slashQuery === null && thinkingPickerOpen ? /* @__PURE__ */ jsx35(
9744
- ThinkingPickerMenu,
10165
+ ),
10166
+ hideComposerSettings ? null : /* @__PURE__ */ jsx35(
10167
+ "div",
9745
10168
  {
9746
- value: selectedThinking,
9747
- onChange: onThinkingChange,
9748
- disabled: isStreaming || thinkingControlled,
9749
- onClose: () => onSetThinkingPickerOpen(false)
9750
- }
9751
- ) : null
9752
- ] }),
9753
- composerContributions.map(({ id, Top }) => Top ? /* @__PURE__ */ jsx35(
9754
- "div",
9755
- {
9756
- "data-boring-agent-part": "composer-contribution-top",
9757
- "data-boring-agent-contribution-id": id,
9758
- className: cn("mx-auto mb-2 w-full empty:hidden", chrome ? "max-w-3xl" : "max-w-[680px]"),
9759
- children: /* @__PURE__ */ jsx35(Top, {})
9760
- },
9761
- id
9762
- ) : null),
9763
- /* @__PURE__ */ jsx35(
9764
- "div",
9765
- {
9766
- "data-boring-agent-part": "composer-rail",
9767
- "data-composer-multiline": "false",
9768
- style: { "--composer-input-group-height": `${COMPOSER_INPUT_GROUP_MIN_HEIGHT}px` },
9769
- className: cn(
9770
- "relative mx-auto w-full overflow-visible rounded-[28px]",
9771
- chrome ? "max-w-3xl bg-transparent shadow-[0_1px_2px_-1px_oklch(0_0_0/0.06),0_6px_18px_-12px_oklch(0_0_0/0.12),inset_0_0_0_1px_oklch(from_var(--border)_l_c_h/0.7)] focus-within:shadow-[0_1px_3px_-1px_oklch(0_0_0/0.08),0_10px_28px_-14px_oklch(0_0_0/0.16),inset_0_0_0_1px_oklch(from_var(--accent)_l_c_h/0.45)]" : "max-w-[680px] bg-transparent shadow-[inset_0_0_0_1px_oklch(from_var(--border)_l_c_h/0.7)] focus-within:shadow-[inset_0_0_0_1px_oklch(from_var(--accent)_l_c_h/0.45)]",
9772
- "transition-shadow duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]",
9773
- "[&_[data-slot=input-group]]:!h-auto [&_[data-slot=input-group]]:!min-h-[var(--composer-input-group-height)]",
9774
- "[&_[data-slot=input-group]]:!flex-col [&_[data-slot=input-group]]:!items-stretch [&_[data-slot=input-group]]:!overflow-hidden",
9775
- "[&_[data-slot=input-group]]:!border-0 [&_[data-slot=input-group]]:!rounded-[28px]",
9776
- "[&_[data-slot=input-group]]:!bg-transparent [&_[data-slot=input-group]]:!shadow-none",
9777
- "[&_[data-slot=input-group]]:dark:!bg-transparent [&_[data-slot=input-group]]:!ring-0",
9778
- "[&_[data-slot=input-group]]:has-[:focus]:!ring-0",
9779
- "[&[data-composer-multiline=true]_[data-boring-agent-part=composer-submit-addon]]:self-end",
9780
- "[&[data-composer-multiline=true]_[data-boring-agent-part=composer-submit-addon]]:mb-2"
9781
- ),
9782
- children: /* @__PURE__ */ jsxs30(
9783
- PromptInput,
9784
- {
9785
- "data-boring-state": status,
9786
- onSubmit: (message) => onSubmitMessage({ text: message.text, files: message.files }),
9787
- onUploadFile: uploadAttachment,
9788
- multiple: true,
9789
- maxFiles: disabled || isStreaming ? 0 : MAX_PROMPT_ATTACHMENTS,
9790
- maxFileSize: MAX_PROMPT_ATTACHMENT_BYTES,
9791
- onError: (err) => {
9792
- if (err.code === "max_files") onAttachmentNotice(`Up to ${MAX_PROMPT_ATTACHMENTS} attachments per message.`);
9793
- else if (err.code === "max_file_size") onAttachmentNotice("Files must be under 4 MB each.");
9794
- else if (err.code === "accept") onAttachmentNotice("That file type isn't supported here.");
9795
- else onAttachmentNotice(err.message || "Attachment rejected.");
9796
- },
9797
- children: [
9798
- /* @__PURE__ */ jsx35(AttachmentsList, {}),
9799
- /* @__PURE__ */ jsxs30(
9800
- "div",
9801
- {
9802
- "data-boring-agent-part": "composer-input-row",
9803
- className: "flex min-h-[var(--composer-input-group-height)] w-full items-center",
9804
- children: [
9805
- /* @__PURE__ */ jsx35("div", { className: "flex h-14 shrink-0 items-center pl-2", children: /* @__PURE__ */ jsx35(AttachmentButton, { disabled: disabled || isStreaming }) }),
9806
- /* @__PURE__ */ jsx35(
9807
- PromptInputTextarea,
9808
- {
9809
- value: draft,
9810
- placeholder: composerBlocked ? workspaceWarmupBlocked ? composerBlockerLabel : "" : composerPlaceholder ?? "Ask anything\u2026",
9811
- disabled,
9812
- readOnly: composerBlocked,
9813
- "aria-label": "Agent prompt",
9814
- ref: (node) => {
9815
- textareaRef.current = node;
9816
- resizeTextarea(node);
9817
- },
9818
- onChange: onTextareaChange,
9819
- onKeyDown: onTextareaKeyDown,
9820
- style: { fieldSizing: "fixed" },
9821
- className: cn(
9822
- "min-w-0 flex-1 !min-h-10 !max-h-40 resize-none overflow-hidden border-0 bg-transparent shadow-none",
9823
- "[field-sizing:fixed]",
9824
- "px-2 py-2 text-[13px] leading-6",
9825
- "placeholder:text-muted-foreground/45",
9826
- "focus-visible:ring-0 focus-visible:ring-offset-0"
9827
- )
9828
- }
9829
- ),
9830
- /* @__PURE__ */ jsx35(
9831
- PromptInputFooter,
9832
- {
9833
- align: "inline-end",
9834
- "data-boring-agent-part": "composer-submit-addon",
9835
- className: "!order-none !w-auto shrink-0 self-center justify-between border-0 bg-transparent !px-2 !py-0",
9836
- children: /* @__PURE__ */ jsxs30("div", { className: "ml-auto flex items-center gap-1.5", children: [
9837
- composerContributions.map(({ id, Action }) => Action ? /* @__PURE__ */ jsx35(
9838
- "span",
9839
- {
9840
- "data-boring-agent-part": "composer-contribution-action",
9841
- "data-boring-agent-contribution-id": id,
9842
- className: "contents",
9843
- children: /* @__PURE__ */ jsx35(Action, { updateDraft })
9844
- },
9845
- id
9846
- ) : null),
9847
- /* @__PURE__ */ jsx35(
9848
- PromptInputSubmit,
9849
- {
9850
- "data-boring-agent-part": "composer-submit",
9851
- status: submitStatus,
9852
- onStop,
9853
- disabled: submitDisabled,
9854
- className: cn(
9855
- "h-8 w-8 shrink-0 rounded-full",
9856
- "bg-foreground",
9857
- "text-background",
9858
- "transition-all duration-150 ease-[cubic-bezier(0.22,1,0.36,1)]",
9859
- "hover:bg-foreground/90 hover:shadow-[0_0_0_3px_oklch(from_var(--foreground)_l_c_h/0.12)] hover:scale-[1.04]",
9860
- "active:scale-[0.93] active:brightness-95",
9861
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/20",
9862
- "disabled:pointer-events-none disabled:opacity-40",
9863
- "[&>svg]:size-3.5"
9864
- )
9865
- }
9866
- )
9867
- ] })
9868
- }
9869
- )
9870
- ]
10169
+ "data-boring-agent-part": "composer-settings-row",
10170
+ className: cn(
10171
+ "mx-auto mt-2 grid min-h-8 w-full grid-cols-[minmax(0,1fr)_auto] items-center text-xs text-muted-foreground",
10172
+ chrome ? "max-w-3xl" : "max-w-[680px]"
10173
+ ),
10174
+ children: /* @__PURE__ */ jsxs30("div", { className: "flex min-w-0 items-center justify-center gap-1", children: [
10175
+ /* @__PURE__ */ jsx35(
10176
+ ModelSelectTrigger,
10177
+ {
10178
+ value: selectedModel,
10179
+ options: modelOptions,
10180
+ disabled: isStreaming || modelControlled,
10181
+ trigger: "slash",
10182
+ open: modelPickerOpen,
10183
+ className: "composer-settings-trigger",
10184
+ onClick: () => {
10185
+ if (modelPickerOpen) {
10186
+ onSetModelPickerOpen(false);
10187
+ return;
10188
+ }
10189
+ void onOpenModelPicker();
9871
10190
  }
9872
- )
9873
- ]
9874
- }
9875
- )
9876
- }
9877
- ),
9878
- hideComposerSettings ? null : /* @__PURE__ */ jsxs30(
9879
- "div",
9880
- {
9881
- "data-boring-agent-part": "composer-settings-row",
9882
- className: cn(
9883
- "mx-auto mt-1.5 flex w-full items-center justify-center gap-1.5 text-[10.5px] text-muted-foreground/45",
9884
- chrome ? "max-w-3xl" : "max-w-[680px]"
9885
- ),
9886
- children: [
9887
- /* @__PURE__ */ jsx35(
9888
- ModelSelectTrigger,
9889
- {
9890
- value: selectedModel,
9891
- options: modelOptions,
9892
- disabled: isStreaming || modelControlled,
9893
- trigger: "slash",
9894
- open: modelPickerOpen,
9895
- onClick: () => {
9896
- if (modelPickerOpen) {
9897
- onSetModelPickerOpen(false);
9898
- return;
9899
- }
9900
- void onOpenModelPicker();
9901
10191
  }
9902
- }
9903
- ),
9904
- thinkingControl ? /* @__PURE__ */ jsx35(
9905
- ThinkingSelectTrigger,
9906
- {
9907
- value: selectedThinking,
9908
- disabled: isStreaming || thinkingControlled,
9909
- trigger: "slash",
9910
- open: thinkingPickerOpen,
9911
- onClick: () => {
9912
- if (thinkingPickerOpen) {
9913
- onSetThinkingPickerOpen(false);
9914
- return;
10192
+ ),
10193
+ thinkingControl ? /* @__PURE__ */ jsx35(
10194
+ ThinkingSelectTrigger,
10195
+ {
10196
+ value: selectedThinking,
10197
+ disabled: isStreaming || thinkingControlled,
10198
+ trigger: "slash",
10199
+ open: thinkingPickerOpen,
10200
+ className: "composer-settings-trigger",
10201
+ onClick: () => {
10202
+ if (thinkingPickerOpen) {
10203
+ onSetThinkingPickerOpen(false);
10204
+ return;
10205
+ }
10206
+ void onOpenThinkingPicker();
9915
10207
  }
9916
- void onOpenThinkingPicker();
9917
10208
  }
9918
- }
9919
- ) : null
9920
- ]
9921
- }
9922
- )
10209
+ ) : null
10210
+ ] })
10211
+ }
10212
+ )
10213
+ ] })
9923
10214
  ] });
9924
10215
  }
9925
10216
  function getHeaderValue(headers, name) {
@@ -9929,86 +10220,6 @@ function getHeaderValue(headers, name) {
9929
10220
 
9930
10221
  // src/front/chat/piChatPanelHooks.ts
9931
10222
  import { useCallback as useCallback17, useEffect as useEffect20, useMemo as useMemo15, useRef as useRef17, useState as useState19, useSyncExternalStore } from "react";
9932
- function useExternalRemotePiSession({
9933
- sessionId,
9934
- agentTypeId,
9935
- workspaceId,
9936
- storageScope,
9937
- apiBaseUrl,
9938
- requestHeaders,
9939
- fetch: fetch2,
9940
- createRemoteSession,
9941
- remoteSessionOptions
9942
- }) {
9943
- const [session, setSession] = useState19();
9944
- const remoteSessionOptionsRef = useRef17(remoteSessionOptions);
9945
- remoteSessionOptionsRef.current = remoteSessionOptions;
9946
- const remoteSessionOptionsKey = useMemo15(
9947
- () => remoteSessionOptionsIdentity2(remoteSessionOptions),
9948
- [remoteSessionOptions]
9949
- );
9950
- useEffect20(() => {
9951
- if (!sessionId) {
9952
- setSession(void 0);
9953
- return;
9954
- }
9955
- const next = (createRemoteSession ?? createRemotePiSession)({
9956
- ...remoteSessionOptionsRef.current,
9957
- sessionId,
9958
- agentTypeId,
9959
- workspaceId,
9960
- storageScope,
9961
- apiBaseUrl,
9962
- headers: requestHeaders,
9963
- fetch: fetch2
9964
- });
9965
- setSession(next);
9966
- return () => next.dispose();
9967
- }, [agentTypeId, apiBaseUrl, createRemoteSession, fetch2, remoteSessionOptionsKey, requestHeaders, sessionId, storageScope, workspaceId]);
9968
- return session;
9969
- }
9970
- var remoteSessionOptionObjectIds2 = /* @__PURE__ */ new WeakMap();
9971
- var remoteSessionOptionObjectSeq2 = 0;
9972
- function remoteSessionOptionObjectIdentity2(value) {
9973
- if (typeof value !== "object" && typeof value !== "function" || value === null) return void 0;
9974
- const object = value;
9975
- let id = remoteSessionOptionObjectIds2.get(object);
9976
- if (!id) {
9977
- id = ++remoteSessionOptionObjectSeq2;
9978
- remoteSessionOptionObjectIds2.set(object, id);
9979
- }
9980
- return String(id);
9981
- }
9982
- function remoteSessionOptionsIdentity2(options) {
9983
- if (!options) return "{}";
9984
- return JSON.stringify({
9985
- autoStart: options.autoStart,
9986
- requestTimeoutMs: options.requestTimeoutMs,
9987
- onEvent: remoteSessionOptionObjectIdentity2(options.onEvent),
9988
- storeOptions: remoteSessionOptionObjectIdentity2(options.storeOptions),
9989
- setTimeoutFn: remoteSessionOptionObjectIdentity2(options.setTimeoutFn),
9990
- clearTimeoutFn: remoteSessionOptionObjectIdentity2(options.clearTimeoutFn),
9991
- reconnect: options.reconnect ? {
9992
- baseMs: options.reconnect.baseMs,
9993
- maxMs: options.reconnect.maxMs,
9994
- jitterRatio: options.reconnect.jitterRatio,
9995
- random: remoteSessionOptionObjectIdentity2(options.reconnect.random)
9996
- } : void 0,
9997
- debug: options.debug ? {
9998
- largeStateWarningBytes: options.debug.largeStateWarningBytes,
9999
- largeStateWarningMessages: options.debug.largeStateWarningMessages,
10000
- onWarning: remoteSessionOptionObjectIdentity2(options.debug.onWarning)
10001
- } : void 0
10002
- });
10003
- }
10004
- function useRemotePiSessionState(session) {
10005
- return useSyncExternalStore(
10006
- useCallback17((listener) => session?.subscribe(listener) ?? (() => {
10007
- }), [session]),
10008
- useCallback17(() => session?.getState(), [session]),
10009
- useCallback17(() => session?.getState(), [session])
10010
- );
10011
- }
10012
10223
 
10013
10224
  // src/front/chat/piChatPanelUtils.ts
10014
10225
  function headersContentKey(headers) {
@@ -10115,9 +10326,103 @@ function errorMessage2(error, fallback) {
10115
10326
  return error instanceof Error && error.message ? error.message : fallback;
10116
10327
  }
10117
10328
 
10329
+ // src/front/chat/piChatPanelHooks.ts
10330
+ function useExternalRemotePiSession({
10331
+ sessionId,
10332
+ agentTypeId,
10333
+ workspaceId,
10334
+ storageScope,
10335
+ apiBaseUrl,
10336
+ requestHeaders,
10337
+ fetch: fetch2,
10338
+ createRemoteSession,
10339
+ remoteSessionOptions,
10340
+ enabled = true
10341
+ }) {
10342
+ const [session, setSession] = useState19();
10343
+ const remoteSessionOptionsRef = useRef17(remoteSessionOptions);
10344
+ remoteSessionOptionsRef.current = remoteSessionOptions;
10345
+ const requestHeadersKey = headersContentKey(requestHeaders);
10346
+ const stableRequestHeaders = useMemo15(
10347
+ () => normalizedHeadersFromContentKey(requestHeadersKey),
10348
+ [requestHeadersKey]
10349
+ );
10350
+ const remoteSessionOptionsKey = useMemo15(
10351
+ () => remoteSessionOptionsIdentity2(remoteSessionOptions),
10352
+ [remoteSessionOptions]
10353
+ );
10354
+ useEffect20(() => {
10355
+ if (!sessionId) {
10356
+ setSession(void 0);
10357
+ return;
10358
+ }
10359
+ const next = (createRemoteSession ?? createRemotePiSession)({
10360
+ ...remoteSessionOptionsRef.current,
10361
+ autoStart: false,
10362
+ sessionId,
10363
+ agentTypeId,
10364
+ workspaceId,
10365
+ storageScope,
10366
+ apiBaseUrl,
10367
+ headers: stableRequestHeaders,
10368
+ fetch: fetch2
10369
+ });
10370
+ setSession(next);
10371
+ return () => next.dispose();
10372
+ }, [agentTypeId, apiBaseUrl, createRemoteSession, fetch2, remoteSessionOptionsKey, sessionId, stableRequestHeaders, storageScope, workspaceId]);
10373
+ useEffect20(() => {
10374
+ if (!session) return;
10375
+ if (enabled) session.resumeStream?.();
10376
+ else session.suspendStream?.();
10377
+ }, [enabled, session]);
10378
+ return session;
10379
+ }
10380
+ var remoteSessionOptionObjectIds2 = /* @__PURE__ */ new WeakMap();
10381
+ var remoteSessionOptionObjectSeq2 = 0;
10382
+ function remoteSessionOptionObjectIdentity2(value) {
10383
+ if (typeof value !== "object" && typeof value !== "function" || value === null) return void 0;
10384
+ const object = value;
10385
+ let id = remoteSessionOptionObjectIds2.get(object);
10386
+ if (!id) {
10387
+ id = ++remoteSessionOptionObjectSeq2;
10388
+ remoteSessionOptionObjectIds2.set(object, id);
10389
+ }
10390
+ return String(id);
10391
+ }
10392
+ function remoteSessionOptionsIdentity2(options) {
10393
+ if (!options) return "{}";
10394
+ return JSON.stringify({
10395
+ autoStart: options.autoStart,
10396
+ requestTimeoutMs: options.requestTimeoutMs,
10397
+ onEvent: remoteSessionOptionObjectIdentity2(options.onEvent),
10398
+ storeOptions: remoteSessionOptionObjectIdentity2(options.storeOptions),
10399
+ setTimeoutFn: remoteSessionOptionObjectIdentity2(options.setTimeoutFn),
10400
+ clearTimeoutFn: remoteSessionOptionObjectIdentity2(options.clearTimeoutFn),
10401
+ reconnect: options.reconnect ? {
10402
+ baseMs: options.reconnect.baseMs,
10403
+ maxMs: options.reconnect.maxMs,
10404
+ jitterRatio: options.reconnect.jitterRatio,
10405
+ random: remoteSessionOptionObjectIdentity2(options.reconnect.random)
10406
+ } : void 0,
10407
+ debug: options.debug ? {
10408
+ largeStateWarningBytes: options.debug.largeStateWarningBytes,
10409
+ largeStateWarningMessages: options.debug.largeStateWarningMessages,
10410
+ onWarning: remoteSessionOptionObjectIdentity2(options.debug.onWarning)
10411
+ } : void 0
10412
+ });
10413
+ }
10414
+ function useRemotePiSessionState(session) {
10415
+ return useSyncExternalStore(
10416
+ useCallback17((listener) => session?.subscribe(listener) ?? (() => {
10417
+ }), [session]),
10418
+ useCallback17(() => session?.getState(), [session]),
10419
+ useCallback17(() => session?.getState(), [session])
10420
+ );
10421
+ }
10422
+
10118
10423
  // src/front/chat/PiChatPanel.tsx
10119
10424
  import { jsx as jsx36, jsxs as jsxs31 } from "react/jsx-runtime";
10120
- var DebugDrawer2 = lazy(() => import("../DebugDrawer-RYEBQ6CO.js").then((m) => ({ default: m.DebugDrawer })));
10425
+ var DebugDrawer2 = lazy(() => import("../DebugDrawer-2TO5SE2D.js").then((m) => ({ default: m.DebugDrawer })));
10121
10426
  var EMPTY_COMMANDS = [];
10122
10427
  var EMPTY_COMMAND_NAMES = [];
10123
10428
  var EMPTY_BLOCKERS = [];
@@ -10146,7 +10451,7 @@ function PiChatPanel({
10146
10451
  hotReloadEnabled = true,
10147
10452
  suggestions = defaultChatSuggestions,
10148
10453
  emptyState,
10149
- emptyPlacement = "default",
10454
+ emptyPlacement = "hero",
10150
10455
  composerPlaceholder,
10151
10456
  initialDraft,
10152
10457
  autoSubmitInitialDraft = false,
@@ -10253,6 +10558,7 @@ function PiChatPanel({
10253
10558
  const selectedSessionPending = Boolean(activeSessionId && !selectedChatState);
10254
10559
  const modelDiscoveryEnabled = serverResourcesEnabled && availableModels === void 0;
10255
10560
  const modelDiscovery = useChatModelSelection({
10561
+ agentTypeId,
10256
10562
  apiBaseUrl,
10257
10563
  defaultModel,
10258
10564
  fetch: fetch2,
@@ -10356,6 +10662,7 @@ function PiChatPanel({
10356
10662
  return next;
10357
10663
  }, [apiBaseUrl, commands, contributedCommands, excludeBuiltinCommands, extraCommands, hotReloadEnabled, normalizedRequestHeaders, serverResourcesEnabled, serverSkillsRefreshKey, storageScope]);
10358
10664
  const commandsStamp = useServerCommands({
10665
+ agentTypeId,
10359
10666
  registry,
10360
10667
  requestHeaders: normalizedRequestHeaders,
10361
10668
  sessionId: activeSessionId ?? "default",
@@ -10385,7 +10692,8 @@ function PiChatPanel({
10385
10692
  const queuePreview = selectedChatState ? selectQueuePreview(selectedChatState) : [];
10386
10693
  const messages = canonicalMessages;
10387
10694
  const userHistory = useMemo16(() => selectComposerHistoryFromCanonicalUsers(canonicalMessages), [canonicalMessages]);
10388
- const emptyStateHydrating = statusForState(selectedChatState, sessionsLoading || chatStatePending || selectedSessionPending) === "hydrating";
10695
+ const awaitingHydration = Boolean(hydrateMessages && selectedChatState && !selectedChatState.hydrated);
10696
+ const emptyStateHydrating = statusForState(selectedChatState, sessionsLoading || chatStatePending || selectedSessionPending) === "hydrating" || awaitingHydration;
10389
10697
  const emptyHero = emptyPlacement === "hero" && messages.length === 0 && queuePreview.length === 0 && !emptyStateHydrating;
10390
10698
  const debugState = selectedPiSession?.getDebugState();
10391
10699
  const composerBlocked = workspaceWarmupBlocked || activeBlockers.length > 0;
@@ -10401,8 +10709,9 @@ function PiChatPanel({
10401
10709
  text: `Large chat state: ${debugState.largeStateWarning.messageCount} messages, approximately ${debugState.largeStateWarning.approxBytes} bytes.`,
10402
10710
  dismissible: true
10403
10711
  }] : [];
10404
- return [...fromState, ...sessionNotice, ...largeStateNotice, ...localNotices].filter((notice) => !dismissedNoticeIds.has(notice.id));
10405
- }, [debug, debugState?.largeStateWarning, dismissedNoticeIds, localNotices, selectedChatState, sessionsError]);
10712
+ const combined = [...fromState, ...sessionNotice, ...largeStateNotice, ...localNotices].filter((notice) => !dismissedNoticeIds.has(notice.id));
10713
+ return filterCompetingNoiseNotices(combined, messages.length === 0);
10714
+ }, [debug, debugState?.largeStateWarning, dismissedNoticeIds, localNotices, messages.length, selectedChatState, sessionsError]);
10406
10715
  const addLocalNotice = useCallback18((notice) => {
10407
10716
  setLocalNotices((previous) => {
10408
10717
  const next = previous.filter((candidate) => candidate.id !== notice.id);
@@ -10433,11 +10742,11 @@ function PiChatPanel({
10433
10742
  if (resetInProgressRef.current) return;
10434
10743
  if (autoCreateInFlightRef.current) return;
10435
10744
  autoCreateInFlightRef.current = true;
10436
- void sessions.create().catch((error) => {
10745
+ void sessions.create({ resumeSessionId: sessions.resumeSessionId }).catch((error) => {
10437
10746
  autoCreateInFlightRef.current = false;
10438
10747
  addLocalNotice({ id: "session-auto-create-error", level: "error", text: errorMessage2(error, "Could not create a chat session."), dismissible: true });
10439
10748
  });
10440
- }, [activeSessionId, addLocalNotice, externalSessionId, sessionList.length, sessions.create, sessionsError, sessionsLoading]);
10749
+ }, [activeSessionId, addLocalNotice, externalSessionId, sessionList.length, sessions.create, sessions.resumeSessionId, sessionsError, sessionsLoading]);
10441
10750
  useEffect21(() => {
10442
10751
  if (externalSessionId || sessionsError || activeSessionId || sessionList.length > 0) {
10443
10752
  autoCreateInFlightRef.current = false;
@@ -10885,14 +11194,18 @@ function PiChatPanel({
10885
11194
  }, [isStreaming]);
10886
11195
  useEffect21(() => {
10887
11196
  if (typeof window === "undefined" || !activeChatSessionId) return;
10888
- window.dispatchEvent(new CustomEvent("boring:chat-session-status", {
11197
+ const emitStatus = () => window.dispatchEvent(new CustomEvent("boring:chat-session-status", {
10889
11198
  detail: {
11199
+ ...workspaceId ? { workspaceId } : {},
10890
11200
  sessionId: activeChatSessionId,
10891
11201
  ...agentTypeId ? { agentTypeId } : {},
10892
11202
  working: isStreaming
10893
11203
  }
10894
11204
  }));
10895
- }, [activeChatSessionId, agentTypeId, isStreaming]);
11205
+ emitStatus();
11206
+ window.addEventListener("boring:chat-session-status-request", emitStatus);
11207
+ return () => window.removeEventListener("boring:chat-session-status-request", emitStatus);
11208
+ }, [activeChatSessionId, agentTypeId, isStreaming, workspaceId]);
10896
11209
  const onTextareaKeyDown = useCallback18((event) => {
10897
11210
  if (event.key === "Escape" && isStreaming) {
10898
11211
  if (event.defaultPrevented || mentionState !== null || slashQuery !== null) {
@@ -10905,11 +11218,84 @@ function PiChatPanel({
10905
11218
  }
10906
11219
  handleComposerKeyDown(event);
10907
11220
  }, [handleComposerKeyDown, interrupt, isStreaming, mentionState, slashQuery]);
11221
+ const selectEmptySuggestion = useCallback18((suggestion) => {
11222
+ const text = suggestion.prompt ?? suggestion.label;
11223
+ if (!text.trim()) return;
11224
+ void sendComposerMessage({ text, files: [], source: "suggestion" }).then((result) => {
11225
+ if (result === false) setComposerDraft(text);
11226
+ });
11227
+ }, [sendComposerMessage, setComposerDraft]);
11228
+ const composerSurface = /* @__PURE__ */ jsx36(
11229
+ PiChatComposerSurface,
11230
+ {
11231
+ chrome,
11232
+ pickerPlacement: emptyHero ? "above-compact" : "above",
11233
+ isStreaming,
11234
+ status,
11235
+ disabled,
11236
+ submitStatus,
11237
+ submitDisabled,
11238
+ composerBlocked,
11239
+ composerBlockerLabel,
11240
+ composerPlaceholder,
11241
+ composerStatusNotice,
11242
+ workspaceWarmupBlocked,
11243
+ primaryComposerBlocker,
11244
+ onComposerBlockerAction,
11245
+ queuePreview,
11246
+ onEditQueued: editQueued,
11247
+ hotReloadEnabled,
11248
+ pluginUpdateState,
11249
+ onDismissPluginUpdate: dismissPluginUpdate,
11250
+ onRunPluginUpdate: runPluginUpdate,
11251
+ commandNotifyState,
11252
+ onDismissCommandNotify: dismissCommandNotify,
11253
+ attachmentNotice,
11254
+ onAttachmentNotice: setAttachmentNotice,
11255
+ mentionState,
11256
+ slashQuery,
11257
+ apiBaseUrl,
11258
+ fetch: fetch2,
11259
+ requestHeaders: normalizedRequestHeaders,
11260
+ storageScope,
11261
+ onSelectMention: selectMention,
11262
+ onDismissMention: dismissMention,
11263
+ commands: allCommands,
11264
+ onSelectSlashCommand: selectSlashCommand,
11265
+ onDismissSlash: dismissSlash,
11266
+ modelPickerOpen,
11267
+ selectedModel,
11268
+ modelOptions,
11269
+ modelControlled: model !== void 0,
11270
+ hideDefaultModelOption,
11271
+ hideComposerSettings,
11272
+ onModelChange: modelDiscovery.setModel,
11273
+ onSetModelPickerOpen: setModelPickerOpen,
11274
+ onOpenModelPicker: openModelPicker,
11275
+ thinkingPickerOpen,
11276
+ selectedThinking,
11277
+ thinkingControl,
11278
+ thinkingControlled: thinkingLevel !== void 0,
11279
+ onThinkingChange: setStoredThinkingLevel,
11280
+ onSetThinkingPickerOpen: setThinkingPickerOpen,
11281
+ onOpenThinkingPicker: openThinkingPicker,
11282
+ draft,
11283
+ textareaRef,
11284
+ onTextareaChange,
11285
+ onTextareaKeyDown,
11286
+ updateDraft: (update, options) => {
11287
+ setComposerDraft(update(draftRef.current), options?.focus ?? true);
11288
+ },
11289
+ onSubmitMessage: ({ text, files }) => sendComposerMessage({ text, files }),
11290
+ onStop: stop
11291
+ }
11292
+ );
10908
11293
  return /* @__PURE__ */ jsx36(ArtifactOpenProvider, { onOpenArtifact, children: /* @__PURE__ */ jsxs31(
10909
11294
  "div",
10910
11295
  {
10911
11296
  "data-boring-agent": "",
10912
11297
  "data-boring-agent-part": "chat",
11298
+ "data-agent-type-id": agentTypeId,
10913
11299
  "data-pi-chat-session-id": activeSessionId,
10914
11300
  "data-pi-chat-connection": debugState?.connection ?? "disconnected",
10915
11301
  "data-pi-chat-last-seq": debugState?.lastSeq ?? 0,
@@ -10940,8 +11326,8 @@ function PiChatPanel({
10940
11326
  "div",
10941
11327
  {
10942
11328
  className: cn(
10943
- "flex h-full min-h-0 flex-col overflow-hidden",
10944
- emptyHero && "justify-center",
11329
+ "flex h-full min-h-0 flex-col",
11330
+ emptyHero ? "justify-center overflow-y-auto py-6" : "overflow-hidden",
10945
11331
  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)]"
10946
11332
  ),
10947
11333
  children: [
@@ -10970,76 +11356,25 @@ function PiChatPanel({
10970
11356
  windowResetKey: activeSessionId
10971
11357
  }
10972
11358
  ),
10973
- /* @__PURE__ */ jsx36(
10974
- PiChatComposerSurface,
11359
+ composerSurface,
11360
+ emptyHero ? /* @__PURE__ */ jsxs31(
11361
+ "div",
10975
11362
  {
10976
- chrome,
10977
- isStreaming,
10978
- status,
10979
- disabled,
10980
- submitStatus,
10981
- submitDisabled,
10982
- composerBlocked,
10983
- composerBlockerLabel,
10984
- composerPlaceholder,
10985
- composerStatusNotice,
10986
- workspaceWarmupBlocked,
10987
- primaryComposerBlocker,
10988
- onComposerBlockerAction,
10989
- queuePreview,
10990
- onEditQueued: editQueued,
10991
- hotReloadEnabled,
10992
- pluginUpdateState,
10993
- onDismissPluginUpdate: dismissPluginUpdate,
10994
- onRunPluginUpdate: runPluginUpdate,
10995
- commandNotifyState,
10996
- onDismissCommandNotify: dismissCommandNotify,
10997
- attachmentNotice,
10998
- onAttachmentNotice: setAttachmentNotice,
10999
- mentionState,
11000
- slashQuery,
11001
- apiBaseUrl,
11002
- fetch: fetch2,
11003
- requestHeaders: normalizedRequestHeaders,
11004
- storageScope,
11005
- onSelectMention: selectMention,
11006
- onDismissMention: dismissMention,
11007
- commands: allCommands,
11008
- onSelectSlashCommand: selectSlashCommand,
11009
- onDismissSlash: dismissSlash,
11010
- modelPickerOpen,
11011
- selectedModel,
11012
- modelOptions,
11013
- modelControlled: model !== void 0,
11014
- hideDefaultModelOption,
11015
- hideComposerSettings,
11016
- onModelChange: modelDiscovery.setModel,
11017
- onSetModelPickerOpen: setModelPickerOpen,
11018
- onOpenModelPicker: openModelPicker,
11019
- thinkingPickerOpen,
11020
- selectedThinking,
11021
- thinkingControl,
11022
- thinkingControlled: thinkingLevel !== void 0,
11023
- onThinkingChange: setStoredThinkingLevel,
11024
- onSetThinkingPickerOpen: setThinkingPickerOpen,
11025
- onOpenThinkingPicker: openThinkingPicker,
11026
- draft,
11027
- textareaRef,
11028
- onTextareaChange,
11029
- onTextareaKeyDown,
11030
- updateDraft: (update, options) => {
11031
- setComposerDraft(update(draftRef.current), options?.focus ?? true);
11032
- },
11033
- onSubmitMessage: ({ text, files }) => sendComposerMessage({ text, files }),
11034
- onStop: stop
11363
+ "data-boring-agent-part": "empty-hero-suggestions",
11364
+ className: "@container mx-auto w-full max-w-[640px] px-2 pb-4",
11365
+ children: [
11366
+ /* @__PURE__ */ jsx36(ChatSuggestionGrid, { suggestions, onSelect: selectEmptySuggestion, className: "mt-4" }),
11367
+ emptyState?.footer
11368
+ ]
11035
11369
  }
11036
- )
11370
+ ) : null
11037
11371
  ]
11038
11372
  }
11039
11373
  ) }),
11040
11374
  debug ? /* @__PURE__ */ jsx36(Suspense, { fallback: null, children: /* @__PURE__ */ jsx36("div", { "aria-label": "Chat debug metadata", className: "contents", role: "region", children: /* @__PURE__ */ jsx36(
11041
11375
  DebugDrawer2,
11042
11376
  {
11377
+ agentTypeId,
11043
11378
  apiBaseUrl,
11044
11379
  fetch: fetch2,
11045
11380
  sessionId: activeSessionId ?? activeChatSessionId ?? "unknown",
@@ -11056,6 +11391,207 @@ function PiChatPanel({
11056
11391
  ) });
11057
11392
  }
11058
11393
 
11394
+ // src/front/chat/useAddressedAgentSelection.ts
11395
+ import { useCallback as useCallback19, useEffect as useEffect22, useMemo as useMemo17, useState as useState21 } from "react";
11396
+
11397
+ // src/front/chat/gatewayResponseError.ts
11398
+ var RUNTIME_SCOPE_MISMATCH_MESSAGE = "This chat belongs to a previous runtime configuration and can no longer be changed.";
11399
+ var GatewayResponseError = class extends Error {
11400
+ constructor(status, message, code3, details, body, operation) {
11401
+ super(message);
11402
+ this.status = status;
11403
+ this.code = code3;
11404
+ this.details = details;
11405
+ this.body = body;
11406
+ this.operation = operation;
11407
+ this.name = "GatewayResponseError";
11408
+ this.errorCode = code3;
11409
+ }
11410
+ status;
11411
+ code;
11412
+ details;
11413
+ body;
11414
+ operation;
11415
+ errorCode;
11416
+ };
11417
+ async function gatewayResponseError(response, fallbackMessage, operation) {
11418
+ const body = await safeReadJson2(response);
11419
+ return gatewayResponseErrorFromBody(response.status, body, fallbackMessage, operation);
11420
+ }
11421
+ function gatewayResponseErrorFromBody(status, body, fallbackMessage, operation) {
11422
+ const payload = errorPayload(body);
11423
+ const code3 = gatewayResponseErrorCode(payload?.code);
11424
+ const serverMessage = typeof payload?.message === "string" && payload.message.trim().length > 0 ? payload.message : fallbackMessage;
11425
+ const message = code3 === AgentGatewayErrorCode.AGENT_SESSION_RUNTIME_SCOPE_MISMATCH ? RUNTIME_SCOPE_MISMATCH_MESSAGE : serverMessage;
11426
+ const details = isJsonValue(payload?.details) ? payload.details : void 0;
11427
+ return new GatewayResponseError(status, message, code3, details, body, operation);
11428
+ }
11429
+ function gatewayResponseErrorCode(value) {
11430
+ const sharedCode = ErrorCode.safeParse(value);
11431
+ if (sharedCode.success) return sharedCode.data;
11432
+ return typeof value === "string" && AGENT_GATEWAY_ERROR_CODES.includes(value) ? value : void 0;
11433
+ }
11434
+ function errorResponseCode(error) {
11435
+ if (error instanceof GatewayResponseError) return error.code;
11436
+ const record = typeof error === "object" && error !== null ? error : void 0;
11437
+ return gatewayResponseErrorCode(record?.code ?? record?.errorCode);
11438
+ }
11439
+ function isRuntimeScopeMismatchError(error) {
11440
+ return errorResponseCode(error) === AgentGatewayErrorCode.AGENT_SESSION_RUNTIME_SCOPE_MISMATCH;
11441
+ }
11442
+ async function safeReadJson2(response) {
11443
+ const text = await response.text();
11444
+ if (!text) return void 0;
11445
+ try {
11446
+ return JSON.parse(text);
11447
+ } catch {
11448
+ return void 0;
11449
+ }
11450
+ }
11451
+ function errorPayload(body) {
11452
+ if (typeof body !== "object" || body === null) return void 0;
11453
+ const record = body;
11454
+ return typeof record.error === "object" && record.error !== null ? record.error : record;
11455
+ }
11456
+ function isJsonValue(value) {
11457
+ if (value === null || ["string", "number", "boolean"].includes(typeof value)) return true;
11458
+ if (Array.isArray(value)) return value.every(isJsonValue);
11459
+ return typeof value === "object" && value !== null && Object.values(value).every(isJsonValue);
11460
+ }
11461
+
11462
+ // src/front/chat/useAddressedAgentSelection.ts
11463
+ var inFlightDiscoveries = /* @__PURE__ */ new WeakMap();
11464
+ function useAddressedAgentSelection({
11465
+ apiBaseUrl,
11466
+ requestHeaders,
11467
+ storageScope,
11468
+ fetch: fetch2,
11469
+ enabled = false
11470
+ } = {}) {
11471
+ const normalizedBaseUrl = apiBaseUrl?.replace(/\/$/, "") ?? "";
11472
+ const headersKey = useMemo17(
11473
+ () => JSON.stringify({
11474
+ storageScope: storageScope ?? "",
11475
+ headers: Object.entries(requestHeaders ?? {}).sort(([a], [b]) => a.localeCompare(b))
11476
+ }),
11477
+ [requestHeaders, storageScope]
11478
+ );
11479
+ const discoveryKey = `${normalizedBaseUrl}
11480
+ ${headersKey}`;
11481
+ const fetchImpl = fetch2 ?? globalThis.fetch;
11482
+ const [state, setState] = useState21(() => ({
11483
+ discoveryKey: enabled ? "" : discoveryKey,
11484
+ agents: [],
11485
+ selectedAgentTypeId: void 0,
11486
+ loading: enabled,
11487
+ error: void 0
11488
+ }));
11489
+ useEffect22(() => {
11490
+ if (!enabled) {
11491
+ setState({
11492
+ discoveryKey,
11493
+ agents: [],
11494
+ selectedAgentTypeId: void 0,
11495
+ loading: false,
11496
+ error: void 0
11497
+ });
11498
+ return;
11499
+ }
11500
+ let cancelled = false;
11501
+ setState((previous) => ({
11502
+ discoveryKey,
11503
+ agents: previous.discoveryKey === discoveryKey ? previous.agents : [],
11504
+ selectedAgentTypeId: previous.discoveryKey === discoveryKey ? previous.selectedAgentTypeId : void 0,
11505
+ loading: true,
11506
+ error: void 0
11507
+ }));
11508
+ void discoverAgents(fetchImpl, discoveryKey, `${normalizedBaseUrl}/api/v1/agents`, {
11509
+ headers: scopedHeaders(requestHeaders, storageScope)
11510
+ }).then((agents2) => {
11511
+ if (cancelled) return;
11512
+ setState((previous) => ({
11513
+ discoveryKey,
11514
+ agents: agents2,
11515
+ selectedAgentTypeId: agents2.some((agent) => agent.agentTypeId === previous.selectedAgentTypeId) ? previous.selectedAgentTypeId : agents2[0]?.agentTypeId,
11516
+ loading: false,
11517
+ error: void 0
11518
+ }));
11519
+ }).catch((error) => {
11520
+ if (cancelled) return;
11521
+ setState({
11522
+ discoveryKey,
11523
+ agents: [],
11524
+ selectedAgentTypeId: void 0,
11525
+ loading: false,
11526
+ error: error instanceof Error ? error : new Error(String(error))
11527
+ });
11528
+ });
11529
+ return () => {
11530
+ cancelled = true;
11531
+ };
11532
+ }, [discoveryKey, enabled, fetchImpl, normalizedBaseUrl]);
11533
+ const current = state.discoveryKey === discoveryKey;
11534
+ const agents = enabled && current ? state.agents : [];
11535
+ const selectedAgentTypeId = enabled && current ? state.selectedAgentTypeId : void 0;
11536
+ const selectAgentTypeId = useCallback19((agentTypeId) => {
11537
+ setState((previous) => {
11538
+ if (previous.discoveryKey !== discoveryKey) return previous;
11539
+ if (!previous.agents.some((agent) => agent.agentTypeId === agentTypeId)) return previous;
11540
+ return { ...previous, selectedAgentTypeId: agentTypeId };
11541
+ });
11542
+ }, [discoveryKey]);
11543
+ return {
11544
+ agents,
11545
+ selectedAgentTypeId,
11546
+ loading: enabled && (!current || state.loading),
11547
+ error: enabled && current ? state.error : void 0,
11548
+ selectAgentTypeId
11549
+ };
11550
+ }
11551
+ function discoverAgents(fetchImpl, discoveryKey, url, init) {
11552
+ let requests = inFlightDiscoveries.get(fetchImpl);
11553
+ if (!requests) {
11554
+ requests = /* @__PURE__ */ new Map();
11555
+ inFlightDiscoveries.set(fetchImpl, requests);
11556
+ }
11557
+ const existing = requests.get(discoveryKey);
11558
+ if (existing) return existing;
11559
+ const request = fetchImpl(url, init).then(async (response) => {
11560
+ if (!response.ok) throw await gatewayResponseError(response, "Failed to load agents.", "load agents");
11561
+ return parseAgentOptions(await response.json());
11562
+ });
11563
+ requests.set(discoveryKey, request);
11564
+ void request.finally(() => {
11565
+ if (requests?.get(discoveryKey) === request) requests.delete(discoveryKey);
11566
+ }).catch(() => void 0);
11567
+ return request;
11568
+ }
11569
+ function parseAgentOptions(value) {
11570
+ if (!Array.isArray(value)) throw new Error("Failed to load agents: invalid response");
11571
+ return value.map((item) => {
11572
+ if (typeof item !== "object" || item === null) throw new Error("Failed to load agents: invalid response");
11573
+ const record = item;
11574
+ if (typeof record.agentTypeId !== "string" || record.agentTypeId.length === 0) {
11575
+ throw new Error("Failed to load agents: invalid response");
11576
+ }
11577
+ return {
11578
+ agentTypeId: record.agentTypeId,
11579
+ label: typeof record.label === "string" && record.label.length > 0 ? record.label : record.agentTypeId,
11580
+ ...typeof record.description === "string" ? { description: record.description } : {},
11581
+ ...Array.isArray(record.pluginIds) ? { pluginIds: record.pluginIds.filter((pluginId) => typeof pluginId === "string" && pluginId.length > 0) } : {}
11582
+ };
11583
+ });
11584
+ }
11585
+ function scopedHeaders(headers, storageScope) {
11586
+ const result = {};
11587
+ for (const [key, value] of Object.entries(headers ?? {})) {
11588
+ if (typeof value === "string") result[key] = value;
11589
+ }
11590
+ const hasStorageScope = Object.keys(result).some((key) => key.toLowerCase() === "x-boring-storage-scope");
11591
+ if (storageScope && !hasStorageScope) result["x-boring-storage-scope"] = storageScope;
11592
+ return result;
11593
+ }
11594
+
11059
11595
  // src/front/commands.ts
11060
11596
  var AGENT_COMMAND_SOURCE = "agent";
11061
11597
  function getAgentCommands(options = {}) {
@@ -11101,6 +11637,7 @@ export {
11101
11637
  ConversationContent,
11102
11638
  ConversationScrollButton,
11103
11639
  DebugDrawer,
11640
+ GatewayResponseError,
11104
11641
  Message,
11105
11642
  MessageAction,
11106
11643
  MessageActions,
@@ -11116,6 +11653,7 @@ export {
11116
11653
  PromptInputFooter,
11117
11654
  PromptInputSubmit,
11118
11655
  PromptInputTextarea,
11656
+ RUNTIME_SCOPE_MISMATCH_MESSAGE,
11119
11657
  Reasoning,
11120
11658
  ReasoningContent,
11121
11659
  ReasoningTrigger,
@@ -11126,12 +11664,15 @@ export {
11126
11664
  ToolHeader,
11127
11665
  activeSessionStorageKey,
11128
11666
  builtinCommands,
11129
- clearActiveSessionId,
11130
11667
  cn,
11131
11668
  createCommandRegistry,
11132
11669
  defaultChatSuggestions,
11133
11670
  defaultToolRenderers,
11671
+ errorResponseCode,
11672
+ gatewayResponseError,
11673
+ gatewayResponseErrorFromBody,
11134
11674
  getAgentCommands,
11675
+ isRuntimeScopeMismatchError,
11135
11676
  matchPiSessionSearch,
11136
11677
  mergeShadcnToolRenderers,
11137
11678
  mergeToolRenderers,
@@ -11141,6 +11682,7 @@ export {
11141
11682
  resolveToolRenderer,
11142
11683
  searchPiSessions,
11143
11684
  uploadFile,
11685
+ useAddressedAgentSelection,
11144
11686
  useOpenArtifact,
11145
11687
  usePiSessions,
11146
11688
  writeActiveSessionId