@hachej/boring-agent 0.1.95 → 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-2EMAKBVR.js → chunk-NSE2RHU3.js} +8967 -7740
  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 +84 -16
  18. package/dist/front/index.js +832 -339
  19. package/dist/front/styles.css +1171 -2129
  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 +569 -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) {
@@ -2012,14 +2018,16 @@ function MentionPicker({
2012
2018
  setActiveIdx(0);
2013
2019
  const ctrl = new AbortController();
2014
2020
  const runFetch = fetchImpl ?? globalThis.fetch.bind(globalThis);
2015
- const headers = scopedHeaders(requestHeaders, storageScope);
2021
+ const headers = withStorageScope(requestHeaders, storageScope);
2016
2022
  const base = apiBaseUrl.replace(/\/$/, "");
2017
2023
  const t = setTimeout(() => {
2018
2024
  runFetch(`${base}/api/v1/files/search?q=${encodeURIComponent(mentionSearchGlob(mention.query))}&limit=8`, {
2019
2025
  signal: ctrl.signal,
2020
2026
  ...headers ? { headers } : {}
2021
2027
  }).then((r) => r.ok ? r.json() : null).then((data) => {
2022
- 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
+ }
2023
2031
  }).catch(() => {
2024
2032
  });
2025
2033
  }, 120);
@@ -2070,15 +2078,6 @@ function detectMention(value, cursorPos) {
2070
2078
  const atIdx = before.lastIndexOf("@");
2071
2079
  return { query: match[2], anchorStart: atIdx, anchorEnd: cursorPos };
2072
2080
  }
2073
- function scopedHeaders(requestHeaders, storageScope) {
2074
- const headers = {};
2075
- for (const [key, value] of Object.entries(requestHeaders ?? {})) {
2076
- if (value !== void 0) headers[key] = value;
2077
- }
2078
- const hasStorageScope = Object.keys(headers).some((key) => key.toLowerCase() === "x-boring-storage-scope");
2079
- if (storageScope && !hasStorageScope) headers["x-boring-storage-scope"] = storageScope;
2080
- return Object.keys(headers).length > 0 ? headers : void 0;
2081
- }
2082
2081
 
2083
2082
  // src/front/useComposerPickers.ts
2084
2083
  function setTextareaValue2(textarea, value) {
@@ -2599,6 +2598,7 @@ function resolveStorage(storage) {
2599
2598
 
2600
2599
  // src/front/hooks/useChatModelSelection.ts
2601
2600
  function useChatModelSelection({
2601
+ agentTypeId,
2602
2602
  apiBaseUrl,
2603
2603
  defaultModel,
2604
2604
  fetch: fetchImpl,
@@ -2620,14 +2620,16 @@ function useChatModelSelection({
2620
2620
  userSelectedModelRef.current = userSelectedModel;
2621
2621
  }, [userSelectedModel]);
2622
2622
  const setModel = useCallback6((next) => {
2623
- userSelectedModelRef.current = next !== null;
2624
- setUserSelectedModel(next !== null);
2625
- setModelState(next);
2626
- 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 });
2627
2628
  }, [storage, storageScope]);
2628
2629
  const discoveryKey = useMemo2(
2629
2630
  () => JSON.stringify({
2630
2631
  apiBaseUrl: apiBaseUrl ?? "",
2632
+ agentTypeId,
2631
2633
  headers: Object.entries(requestHeaders ?? {}).sort(([a], [b]) => a.localeCompare(b)),
2632
2634
  storageScope: storageScope ?? ""
2633
2635
  }),
@@ -2661,8 +2663,8 @@ function useChatModelSelection({
2661
2663
  let aborted = false;
2662
2664
  setLoaded(false);
2663
2665
  const nextFetch = fetchImpl ?? globalThis.fetch.bind(globalThis);
2664
- nextFetch(agentResourceUrl(apiBaseUrl, "/api/v1/agent/models"), {
2665
- headers: scopedHeaders2(requestHeaders, storageScope)
2666
+ nextFetch(agentResourceUrl(apiBaseUrl, `/api/v1/agents/${encodeURIComponent(agentTypeId)}/models`), {
2667
+ headers: withStorageScope(requestHeaders, storageScope)
2666
2668
  }).then((res) => res.ok ? res.json() : null).then((payload) => {
2667
2669
  if (aborted) return;
2668
2670
  if (!payload?.models) {
@@ -2702,7 +2704,7 @@ function useChatModelSelection({
2702
2704
  return () => {
2703
2705
  aborted = true;
2704
2706
  };
2705
- }, [apiBaseUrl, discoveryKey, enabled, fetchImpl, requestHeaders, storage, storageScope]);
2707
+ }, [agentTypeId, apiBaseUrl, discoveryKey, enabled, fetchImpl, requestHeaders, storage, storageScope]);
2706
2708
  useEffect5(() => {
2707
2709
  const onChange = (event) => {
2708
2710
  const next = parseModelSelection(event.detail);
@@ -2716,17 +2718,6 @@ function useChatModelSelection({
2716
2718
  const currentModel = currentDiscoveryLoaded ? model : null;
2717
2719
  return { availableModels: currentAvailableModels, loaded: currentDiscoveryLoaded, model: currentModel, setModel };
2718
2720
  }
2719
- function agentResourceUrl(apiBaseUrl, path) {
2720
- const base = apiBaseUrl?.replace(/\/$/, "") ?? "";
2721
- return `${base}${path}`;
2722
- }
2723
- function scopedHeaders2(headers, storageScope) {
2724
- if (!headers && !storageScope) return void 0;
2725
- const result = { ...headers ?? {} };
2726
- const hasStorageScope = Object.keys(result).some((key) => key.toLowerCase() === "x-boring-storage-scope");
2727
- if (storageScope && !hasStorageScope) result["x-boring-storage-scope"] = storageScope;
2728
- return result;
2729
- }
2730
2721
 
2731
2722
  // src/front/hooks/useServerCommands.ts
2732
2723
  import { useEffect as useEffect6, useRef as useRef7, useState as useState8 } from "react";
@@ -2735,21 +2726,21 @@ function serverCommandErrorMessage(body, fallback) {
2735
2726
  if (body.error && typeof body.error === "object" && typeof body.error.message === "string") return body.error.message;
2736
2727
  return fallback;
2737
2728
  }
2738
- function toSlashCommand(command, getSessionId, apiBaseUrl, requestHeaders, fetchImpl) {
2729
+ function toSlashCommand(command, identity, isCurrentIdentity, apiBaseUrl, requestHeaders, fetchImpl) {
2739
2730
  return {
2740
2731
  name: command.name,
2741
2732
  description: command.description ?? "",
2742
2733
  source: command.source,
2743
2734
  ...command.sourcePlugin ? { sourcePlugin: command.sourcePlugin } : {},
2744
2735
  handler: async (args) => {
2736
+ if (!isCurrentIdentity(identity.key)) return;
2745
2737
  const base = apiBaseUrl?.replace(/\/$/, "") ?? "";
2746
- const params = new URLSearchParams({ sessionId: getSessionId() });
2747
- const url = `${base}/api/v1/agent/commands/execute?${params.toString()}`;
2738
+ const url = `${base}/api/v1/agents/${encodeURIComponent(identity.agentTypeId)}/commands/execute`;
2748
2739
  try {
2749
2740
  const res = await fetchImpl(url, {
2750
2741
  method: "POST",
2751
2742
  headers: { ...requestHeaders ?? {}, "content-type": "application/json" },
2752
- body: JSON.stringify({ name: command.name, args })
2743
+ body: JSON.stringify({ requestId: createRequestId("command"), sessionId: identity.sessionId, name: command.name, args })
2753
2744
  });
2754
2745
  if (!res.ok) {
2755
2746
  const body = await res.json().catch(() => ({}));
@@ -2770,6 +2761,7 @@ function toSlashCommand(command, getSessionId, apiBaseUrl, requestHeaders, fetch
2770
2761
  };
2771
2762
  }
2772
2763
  function useServerCommands({
2764
+ agentTypeId,
2773
2765
  registry,
2774
2766
  requestHeaders,
2775
2767
  sessionId,
@@ -2780,41 +2772,62 @@ function useServerCommands({
2780
2772
  refreshKey = 0
2781
2773
  }) {
2782
2774
  const [stamp, setStamp] = useState8(0);
2783
- const registeredNamesRef = useRef7(/* @__PURE__ */ new Set());
2784
- const sessionIdRef = useRef7(sessionId);
2785
- useEffect6(() => {
2786
- sessionIdRef.current = sessionId;
2787
- }, [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
+ }, []);
2788
2785
  useEffect6(() => {
2789
2786
  const clearRegistered = () => {
2790
- if (registeredNamesRef.current.size === 0) return false;
2791
- for (const name of registeredNamesRef.current) registry.unregister(name);
2792
- registeredNamesRef.current = /* @__PURE__ */ new Set();
2793
- 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;
2794
2792
  };
2795
- if (!enabled) {
2793
+ if (registrationsRef.current.registry !== registry) {
2794
+ if (clearRegistered()) setStamp((n) => n + 1);
2795
+ }
2796
+ if (!enabled || !canonicalSessionId || !identityKey) {
2796
2797
  if (clearRegistered()) setStamp((n) => n + 1);
2797
2798
  return;
2798
2799
  }
2800
+ if (registrationsRef.current.identity !== identityKey) {
2801
+ if (clearRegistered()) setStamp((n) => n + 1);
2802
+ }
2799
2803
  let aborted = false;
2800
2804
  const nextFetch = fetchImpl ?? globalThis.fetch.bind(globalThis);
2801
2805
  const base = apiBaseUrl?.replace(/\/$/, "") ?? "";
2802
- const url = `${base}/api/v1/agent/commands?sessionId=${encodeURIComponent(sessionIdRef.current)}`;
2803
- const headers = scopedHeaders3(requestHeaders, storageScope);
2806
+ const url = `${base}/api/v1/agents/${encodeURIComponent(agentTypeId)}/commands?sessionId=${encodeURIComponent(canonicalSessionId)}`;
2807
+ const headers = withStorageScope(requestHeaders, storageScope);
2804
2808
  nextFetch(url, { headers }).then(async (res) => {
2805
2809
  if (!res.ok) throw new Error(`commands request failed (${res.status})`);
2806
2810
  return await res.json();
2807
2811
  }).then((payload) => {
2808
2812
  if (aborted) return;
2809
2813
  const removed = clearRegistered();
2814
+ const registeredNames = /* @__PURE__ */ new Set();
2810
2815
  let added = false;
2811
2816
  for (const serverCommand of payload.commands ?? []) {
2812
- 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
+ );
2813
2825
  if (registry.get(command.name)) continue;
2814
2826
  registry.register(command);
2815
- registeredNamesRef.current.add(command.name);
2827
+ registeredNames.add(command.name);
2816
2828
  added = true;
2817
2829
  }
2830
+ registrationsRef.current = { registry, identity: identityKey, names: registeredNames };
2818
2831
  if (removed || added) setStamp((n) => n + 1);
2819
2832
  }).catch(() => {
2820
2833
  if (aborted) return;
@@ -2823,16 +2836,9 @@ function useServerCommands({
2823
2836
  return () => {
2824
2837
  aborted = true;
2825
2838
  };
2826
- }, [apiBaseUrl, enabled, fetchImpl, refreshKey, requestHeaders, registry, storageScope]);
2839
+ }, [agentTypeId, apiBaseUrl, canonicalSessionId, enabled, fetchImpl, identityKey, refreshKey, requestHeaders, registry, storageScope]);
2827
2840
  return stamp;
2828
2841
  }
2829
- function scopedHeaders3(headers, storageScope) {
2830
- if (!headers && !storageScope) return void 0;
2831
- const result = { ...headers ?? {} };
2832
- const hasScope = Object.keys(result).some((k) => k.toLowerCase() === "x-boring-storage-scope");
2833
- if (storageScope && !hasScope) result["x-boring-storage-scope"] = storageScope;
2834
- return result;
2835
- }
2836
2842
 
2837
2843
  // src/front/hooks/useAttachmentNotice.ts
2838
2844
  import { useEffect as useEffect7, useState as useState9 } from "react";
@@ -3804,7 +3810,8 @@ function syncCursor(state, cursor) {
3804
3810
  return { ...state, lastSeq: cursor, hydrated: true, needsResync: void 0 };
3805
3811
  }
3806
3812
  function hydrateFromSnapshot(state, snapshot, options = {}) {
3807
- 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;
3808
3815
  const queue = { followUps: enrichQueueWithKnownMetadata(snapshot.queue.followUps, state.optimisticOutbox, state.queue.followUps) };
3809
3816
  const snapshotMessages = normalizeSnapshotMessages(snapshot.messages);
3810
3817
  const preserveLocalHistory = shouldPreserveLocalCommittedHistory(state, snapshot, snapshotMessages, options);
@@ -4467,11 +4474,12 @@ function parsePiChatReplayRangeError(status, body) {
4467
4474
  }
4468
4475
  function buildPiChatEventsUrl({
4469
4476
  apiBaseUrl = "",
4477
+ agentTypeId,
4470
4478
  sessionId,
4471
4479
  cursor
4472
4480
  }) {
4473
4481
  const base = apiBaseUrl.replace(/\/$/, "");
4474
- 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))}`;
4475
4483
  }
4476
4484
  function calculateJitteredBackoffDelayMs({
4477
4485
  attempt,
@@ -4548,6 +4556,7 @@ var RemotePiSession = class {
4548
4556
  streamRunId = 0;
4549
4557
  reconnectAttempt = 0;
4550
4558
  started = false;
4559
+ suspended = false;
4551
4560
  disposed = false;
4552
4561
  streamAbortController;
4553
4562
  reconnectTimer;
@@ -4581,6 +4590,7 @@ var RemotePiSession = class {
4581
4590
  streamingMessageCount: state.streamingMessage ? 1 : 0
4582
4591
  },
4583
4592
  disposed: this.disposed,
4593
+ suspended: this.suspended,
4584
4594
  generation: this.generation,
4585
4595
  streamRunId: this.streamRunId,
4586
4596
  reconnectAttempt: this.reconnectAttempt,
@@ -4592,7 +4602,7 @@ var RemotePiSession = class {
4592
4602
  return this.store.subscribe(listener);
4593
4603
  }
4594
4604
  start(cursor) {
4595
- if (this.disposed || this.started) return Promise.resolve();
4605
+ if (this.disposed || this.suspended || this.started) return Promise.resolve();
4596
4606
  this.started = true;
4597
4607
  const generation = this.generation;
4598
4608
  if (cursor === void 0) {
@@ -4607,8 +4617,8 @@ var RemotePiSession = class {
4607
4617
  if (!this.disposed) {
4608
4618
  this.store.dispatch({ type: "optimistic-user-message", message: toOptimisticUserMessage(payload) }, { flush: true });
4609
4619
  }
4610
- if (!this.started) await this.start(this.store.getState().lastSeq);
4611
- else this.ensureReconnectScheduled();
4620
+ if (!this.started && !this.suspended) await this.start(this.store.getState().lastSeq);
4621
+ else if (!this.suspended) this.ensureReconnectScheduled();
4612
4622
  try {
4613
4623
  const receipt = await this.postCommand("/prompt", payload, PromptReceiptSchema);
4614
4624
  return receipt;
@@ -4621,8 +4631,8 @@ var RemotePiSession = class {
4621
4631
  if (!this.disposed) {
4622
4632
  this.store.dispatch({ type: "optimistic-user-message", message: toOptimisticUserMessage(payload) }, { flush: true });
4623
4633
  }
4624
- if (!this.started) await this.start(this.store.getState().lastSeq);
4625
- else this.ensureReconnectScheduled();
4634
+ if (!this.started && !this.suspended) await this.start(this.store.getState().lastSeq);
4635
+ else if (!this.suspended) this.ensureReconnectScheduled();
4626
4636
  try {
4627
4637
  const receipt = await this.postCommand("/followup", payload, FollowUpReceiptSchema);
4628
4638
  return receipt;
@@ -4648,6 +4658,28 @@ var RemotePiSession = class {
4648
4658
  }
4649
4659
  return receipt;
4650
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
+ }
4651
4683
  dispose() {
4652
4684
  if (this.disposed) return;
4653
4685
  this.disposed = true;
@@ -4661,33 +4693,34 @@ var RemotePiSession = class {
4661
4693
  this.store.dispose();
4662
4694
  }
4663
4695
  async hydrateAndConnect(generation, options = {}) {
4664
- if (!this.isGenerationActive(generation)) return;
4696
+ const hydrationRunId = this.streamRunId;
4697
+ if (!this.isHydrationActive(generation, hydrationRunId)) return;
4665
4698
  this.clearReconnectTimer();
4666
4699
  this.abortEventStream();
4667
4700
  this.store.dispatch({ type: "connection-state", state: this.store.getState().hydrated ? "reconnecting" : "connecting" }, { flush: true });
4668
4701
  try {
4669
4702
  const headers = await this.requestHeaders();
4670
- if (!this.isGenerationActive(generation)) return;
4703
+ if (!this.isHydrationActive(generation, hydrationRunId)) return;
4671
4704
  const responseBody = await this.fetchJson(this.stateUrl(), { method: "GET", headers });
4672
- if (!this.isGenerationActive(generation)) return;
4673
- const raw = this.options.agentTypeId ? addressedSnapshot(responseBody) : responseBody;
4705
+ if (!this.isHydrationActive(generation, hydrationRunId)) return;
4706
+ const raw = addressedSnapshot(responseBody);
4674
4707
  this.recordLargeStateWarning(raw);
4675
4708
  if (readProtocolVersion(raw) !== SUPPORTED_PROTOCOL_VERSION) {
4676
4709
  this.dispatchProtocolError(`Unsupported Pi chat protocol version: ${String(readProtocolVersion(raw) ?? "missing")}`);
4677
4710
  return;
4678
4711
  }
4679
4712
  const snapshot = PiChatSnapshotSchema.parse(raw);
4680
- if (!this.isGenerationActive(generation)) return;
4713
+ if (!this.isHydrationActive(generation, hydrationRunId)) return;
4681
4714
  this.store.dispatch({ type: "hydrate", snapshot, allowSeqRewind: options?.allowSeqRewind }, { flush: true });
4682
4715
  this.connectEvents(snapshot.seq, generation);
4683
4716
  } catch (error) {
4684
- if (!this.isGenerationActive(generation) || isAbortError(error)) return;
4717
+ if (!this.isHydrationActive(generation, hydrationRunId) || isAbortError(error)) return;
4685
4718
  this.dispatchProtocolError(errorMessage(error, "Failed to hydrate Pi chat session state."));
4686
4719
  this.scheduleReconnect(generation);
4687
4720
  }
4688
4721
  }
4689
4722
  connectEvents(cursor, generation) {
4690
- if (!this.isGenerationActive(generation)) return Promise.resolve();
4723
+ if (!this.isStreamLifecycleActive(generation)) return Promise.resolve();
4691
4724
  this.clearReconnectTimer();
4692
4725
  this.abortEventStream();
4693
4726
  const runId = ++this.streamRunId;
@@ -4801,20 +4834,20 @@ var RemotePiSession = class {
4801
4834
  }
4802
4835
  }
4803
4836
  rehydrateAfterStreamReset(generation, options = {}) {
4804
- if (!this.isGenerationActive(generation)) return;
4837
+ if (!this.isStreamLifecycleActive(generation)) return;
4805
4838
  this.streamRunId += 1;
4806
4839
  this.abortEventStream();
4807
4840
  void this.hydrateAndConnect(generation, options);
4808
4841
  }
4809
4842
  ensureReconnectScheduled() {
4810
- if (!this.isGenerationActive(this.generation)) return;
4843
+ if (!this.isStreamLifecycleActive(this.generation)) return;
4811
4844
  const state = this.store.getState();
4812
4845
  if (state.connection.state === "connected" || state.connection.state === "connecting") return;
4813
4846
  if (this.reconnectTimer !== void 0) return;
4814
4847
  void this.hydrateAndConnect(this.generation);
4815
4848
  }
4816
4849
  scheduleReconnect(generation) {
4817
- if (!this.isGenerationActive(generation)) return;
4850
+ if (!this.isStreamLifecycleActive(generation)) return;
4818
4851
  this.clearReconnectTimer();
4819
4852
  this.store.dispatch({ type: "connection-state", state: "reconnecting" }, { flush: true });
4820
4853
  const attempt = this.reconnectAttempt++;
@@ -4828,7 +4861,7 @@ var RemotePiSession = class {
4828
4861
  clearTimeoutFn: this.clearTimeoutFn,
4829
4862
  reconnect: () => {
4830
4863
  this.reconnectTimer = void 0;
4831
- if (!this.isGenerationActive(generation)) return;
4864
+ if (!this.isStreamLifecycleActive(generation)) return;
4832
4865
  const state = this.store.getState();
4833
4866
  if (state.hydrated) this.connectEvents(state.lastSeq, generation);
4834
4867
  else void this.hydrateAndConnect(generation);
@@ -4851,7 +4884,7 @@ var RemotePiSession = class {
4851
4884
  return schema.parse(this.addressedCommandReceipt(path, raw));
4852
4885
  }
4853
4886
  addressedCommandReceipt(path, receipt) {
4854
- if (!this.options.agentTypeId || path !== "/followup" || typeof receipt !== "object" || receipt === null) {
4887
+ if (path !== "/followup" || typeof receipt !== "object" || receipt === null) {
4855
4888
  return receipt;
4856
4889
  }
4857
4890
  const record = receipt;
@@ -4897,19 +4930,18 @@ var RemotePiSession = class {
4897
4930
  return this.sessionUrl("/state");
4898
4931
  }
4899
4932
  eventsUrl(cursor) {
4900
- if (!this.options.agentTypeId) {
4901
- return buildPiChatEventsUrl({ apiBaseUrl: this.apiBaseUrl, sessionId: this.options.sessionId, cursor });
4902
- }
4903
- 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
+ });
4904
4939
  }
4905
4940
  sessionUrl(path) {
4906
- if (this.options.agentTypeId) {
4907
- return `${this.apiBaseUrl}/api/v1/agents/${encodeURIComponent(this.options.agentTypeId)}/sessions/${encodeURIComponent(this.options.sessionId)}${path}`;
4908
- }
4909
- 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}`;
4910
4942
  }
4911
4943
  addressedCommandPayload(path, payload) {
4912
- if (!this.options.agentTypeId || typeof payload !== "object" || payload === null) return payload;
4944
+ if (typeof payload !== "object" || payload === null) return payload;
4913
4945
  const record = payload;
4914
4946
  if (typeof record.requestId === "string" && record.requestId.length > 0) return payload;
4915
4947
  if (path === "/prompt" && typeof record.clientNonce === "string" && typeof record.message === "string") {
@@ -4972,8 +5004,14 @@ var RemotePiSession = class {
4972
5004
  isGenerationActive(generation) {
4973
5005
  return !this.disposed && generation === this.generation;
4974
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
+ }
4975
5013
  isStreamActive(generation, runId) {
4976
- return this.isGenerationActive(generation) && runId === this.streamRunId;
5014
+ return this.isStreamLifecycleActive(generation) && runId === this.streamRunId;
4977
5015
  }
4978
5016
  };
4979
5017
  function addressedSnapshot(value) {
@@ -5100,13 +5138,23 @@ function clearPageUnloading() {
5100
5138
  pageUnloading = false;
5101
5139
  }
5102
5140
 
5103
- // src/front/chat/session/activeSessionStorage.ts
5141
+ // src/front/chat/session/sessionSelectionStorage.ts
5104
5142
  var ACTIVE_SESSION_KEY_PREFIX = "boring-agent:v2";
5105
5143
  var ACTIVE_SESSION_KEY_SUFFIX = "activeSessionId";
5144
+ var BOOT_RESUME_SESSION_KEY_SUFFIX = "bootResumeSessionId";
5106
5145
  var DEFAULT_STORAGE_SCOPE2 = "default";
5107
5146
  function activeSessionStorageKey(storageScope) {
5108
- const scope = storageScope && storageScope.length > 0 ? storageScope : DEFAULT_STORAGE_SCOPE2;
5109
- 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);
5110
5158
  }
5111
5159
  function readActiveSessionId(options = {}) {
5112
5160
  const storage = resolveStorage2(options.storage);
@@ -5127,8 +5175,28 @@ function writeActiveSessionId(sessionId, options = {}) {
5127
5175
  } catch {
5128
5176
  }
5129
5177
  }
5130
- function clearActiveSessionId(options = {}) {
5131
- 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}`;
5132
5200
  }
5133
5201
  function resolveStorage2(storage) {
5134
5202
  if (storage) return storage;
@@ -5138,29 +5206,43 @@ function resolveStorage2(storage) {
5138
5206
  return void 0;
5139
5207
  }
5140
5208
  }
5209
+ function resolveBootStorage(storage) {
5210
+ if (storage) return storage;
5211
+ try {
5212
+ return globalThis.sessionStorage;
5213
+ } catch {
5214
+ return void 0;
5215
+ }
5216
+ }
5141
5217
 
5142
5218
  // src/front/chat/session/usePiSessions.ts
5143
- import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo4, useRef as useRef8, useState as useState10 } from "react";
5144
- 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";
5145
5220
  var SESSION_PAGE_SIZE = 50;
5146
5221
  var DEFAULT_MAX_RETRIES = 60;
5147
5222
  var DEFAULT_RETRY_BASE_MS = 250;
5148
5223
  var DEFAULT_RETRY_MAX_MS = 2e3;
5224
+ var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect8 : useLayoutEffect;
5149
5225
  var SessionsPreparingError = class extends Error {
5150
5226
  constructor() {
5151
5227
  super("Agent runtime is still preparing");
5152
5228
  this.name = "SessionsPreparingError";
5153
5229
  }
5154
5230
  };
5231
+ var StaleSessionsSourceError = class extends Error {
5232
+ constructor() {
5233
+ super("Pi sessions source is stale");
5234
+ this.name = "StaleSessionsSourceError";
5235
+ }
5236
+ };
5155
5237
  function isNetworkFetchError(error) {
5156
5238
  return error instanceof TypeError;
5157
5239
  }
5158
- function usePiSessions(options = {}) {
5240
+ function usePiSessions(options) {
5159
5241
  const enabled = options.enabled ?? true;
5160
5242
  const apiBaseUrl = options.apiBaseUrl?.replace(/\/$/, "") ?? "";
5161
- const addressed = typeof options.agentTypeId === "string" && options.agentTypeId.length > 0;
5162
- 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`;
5163
5244
  const storageScope = options.storageScope ?? "default";
5245
+ const activeSessionStorageScope = options.activeSessionStorageScope ?? storageScope;
5164
5246
  const fetchImpl = useMemo4(() => options.fetch ?? globalThis.fetch.bind(globalThis), [options.fetch]);
5165
5247
  const createRemoteSession = options.createRemoteSession ?? createRemotePiSession;
5166
5248
  const connectActiveSession = options.connectActiveSession ?? true;
@@ -5168,12 +5250,30 @@ function usePiSessions(options = {}) {
5168
5250
  const retryBaseMs = options.retry?.baseMs ?? DEFAULT_RETRY_BASE_MS;
5169
5251
  const retryMaxMs = options.retry?.maxMs ?? DEFAULT_RETRY_MAX_MS;
5170
5252
  const headersKey = useMemo4(() => headersScopeKey(options.requestHeaders, storageScope), [options.requestHeaders, storageScope]);
5171
- const normalizedHeaders = useMemo4(() => buildRequestHeaders(options.requestHeaders, storageScope), [headersKey, storageScope]);
5172
- const requestScopeKey = useMemo4(() => requestScopeIdentity(apiBaseUrl, sessionsApiPath, storageScope, headersKey), [apiBaseUrl, headersKey, sessionsApiPath, storageScope]);
5173
- 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]);
5174
5269
  const [sessions, setSessions] = useState10([]);
5175
5270
  const [dataStorageScope, setDataStorageScope] = useState10(storageScope);
5176
- 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;
5177
5277
  const [activePiSession, setActivePiSession] = useState10(void 0);
5178
5278
  const [loading, setLoading] = useState10(enabled);
5179
5279
  const [loadingMore, setLoadingMore] = useState10(false);
@@ -5190,17 +5290,30 @@ function usePiSessions(options = {}) {
5190
5290
  const loadMoreRequestSeqRef = useRef8(0);
5191
5291
  const loadMoreInFlightRef = useRef8(false);
5192
5292
  const pendingCreatedRef = useRef8(/* @__PURE__ */ new Map());
5293
+ const pendingDeletedRef = useRef8(/* @__PURE__ */ new Set());
5193
5294
  const pendingCreatedScopeRef = useRef8(requestScopeKey);
5194
5295
  const dataStorageScopeRef = useRef8(storageScope);
5195
5296
  const loadedDataSourceRef = useRef8(dataSourceKey);
5196
- const requestScopeRef = useRef8(requestScopeKey);
5197
- 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
+ );
5198
5305
  const remoteSessionOptionsRef = useRef8(options.remoteSessionOptions);
5199
5306
  remoteSessionOptionsRef.current = options.remoteSessionOptions;
5200
5307
  const remoteSessionOptionsKey = useMemo4(
5201
5308
  () => remoteSessionOptionsIdentity(options.remoteSessionOptions),
5202
5309
  [options.remoteSessionOptions]
5203
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]);
5204
5317
  useEffect8(() => {
5205
5318
  sessionsRef.current = sessions;
5206
5319
  }, [sessions]);
@@ -5213,48 +5326,66 @@ function usePiSessions(options = {}) {
5213
5326
  const activeSessionKnown = Boolean(activeSessionId && sessions.some((session) => session.id === activeSessionId));
5214
5327
  const requestHeaders = useCallback7(() => normalizedHeaders, [normalizedHeaders]);
5215
5328
  const sessionsUrl = useCallback7((suffix = "") => `${apiBaseUrl}${sessionsApiPath}${suffix}`, [apiBaseUrl, sessionsApiPath]);
5216
- const sessionsListUrl = useCallback7((offset = 0, includeId, cursor) => {
5217
- const query = new URLSearchParams();
5218
- if (addressed) {
5219
- query.set("limit", String(SESSION_PAGE_SIZE));
5220
- if (cursor) query.set("cursor", cursor);
5221
- } else {
5222
- if (offset > 0) {
5223
- query.set("limit", String(SESSION_PAGE_SIZE));
5224
- query.set("offset", String(offset));
5225
- }
5226
- if (offset <= 0 && includeId) query.set("activeSessionId", includeId);
5227
- }
5228
- 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);
5229
5332
  return sessionsUrl(`?${query.toString()}`);
5230
- }, [addressed, sessionsUrl]);
5333
+ }, [sessionsUrl]);
5231
5334
  const persistActive = useCallback7((id) => {
5232
- writeActiveSessionId(id, { storageScope, storage: options.storage });
5233
- }, [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]);
5234
5345
  const ensurePendingScope = useCallback7(() => {
5235
5346
  if (pendingCreatedScopeRef.current === requestScopeKey) return;
5236
5347
  pendingCreatedScopeRef.current = requestScopeKey;
5237
5348
  pendingCreatedRef.current.clear();
5349
+ pendingDeletedRef.current.clear();
5238
5350
  }, [requestScopeKey]);
5239
5351
  const preferredSessionId = useCallback7(() => {
5240
- const persisted = options.initialActiveSessionId ?? readActiveSessionId({ storageScope, storage: options.storage });
5352
+ const persisted = options.initialActiveSessionId ?? readActiveSessionId({
5353
+ storageScope: activeSessionStorageScope,
5354
+ storage: options.storage
5355
+ });
5241
5356
  if (loadedDataSourceRef.current === dataSourceKey) return activeSessionIdRef.current ?? persisted;
5242
5357
  if (dataStorageScopeRef.current !== storageScope) return persisted;
5243
5358
  return void 0;
5244
- }, [dataSourceKey, options.initialActiveSessionId, options.storage, storageScope]);
5359
+ }, [activeSessionStorageScope, dataSourceKey, options.initialActiveSessionId, options.storage, storageScope]);
5245
5360
  const applySessions = useCallback7((data, applyOptions = {}) => {
5246
5361
  ensurePendingScope();
5247
5362
  const replacingScope = loadedDataSourceRef.current !== dataSourceKey;
5248
5363
  const requestedActiveId = preferredSessionId();
5249
5364
  const replacingScopePreferred = replacingScope ? requestedActiveId : void 0;
5250
5365
  const pendingCreated = pendingCreatedRef.current;
5366
+ const pendingDeleted = pendingDeletedRef.current;
5367
+ const deletedIds = new Set(pendingDeleted);
5251
5368
  for (const session of data) pendingCreated.delete(session.id);
5252
- const canonicalCount = canonicalPageCount(data);
5253
- 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;
5254
5378
  const wasExhaustedBeyondFirstPage = applyOptions.background && !hasMoreRef.current && canonicalLoadedCountRef.current >= canonicalCount;
5255
- const requestedActiveReturned = Boolean(requestedActiveId && data.some((session) => session.id === requestedActiveId));
5379
+ const requestedActiveReturned = Boolean(requestedActiveId && filteredData.some((session) => session.id === requestedActiveId));
5256
5380
  const current = applyOptions.background && pageMayHaveMore ? sessionsRef.current.filter((session) => !requestedActiveId || requestedActiveReturned || session.id !== requestedActiveId) : [];
5257
- 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
+ }
5258
5389
  const nextHasMore = pageMayHaveMore && !wasExhaustedBeyondFirstPage;
5259
5390
  canonicalLoadedCountRef.current = applyOptions.background ? Math.max(canonicalLoadedCountRef.current, canonicalCount) : canonicalCount;
5260
5391
  loadedDataSourceRef.current = dataSourceKey;
@@ -5269,10 +5400,12 @@ function usePiSessions(options = {}) {
5269
5400
  persistActive(next);
5270
5401
  return next;
5271
5402
  });
5272
- }, [addressed, dataSourceKey, ensurePendingScope, persistActive, preferredSessionId, storageScope]);
5403
+ }, [bootResumeSource, dataSourceKey, ensurePendingScope, options.bootResumeStorage, persistActive, persistBootResume, preferredSessionId, storageScope]);
5273
5404
  const refresh = useCallback7(async (refreshOptions = {}) => {
5405
+ const scope = requestScopeKey;
5406
+ if (mountedRef.current && !sourceIsCurrent(scope)) throw new StaleSessionsSourceError();
5274
5407
  const version = ++refreshVersionRef.current;
5275
- const isCurrent = () => mountedRef.current && version === refreshVersionRef.current;
5408
+ const isCurrent = () => sourceIsCurrent(scope) && version === refreshVersionRef.current;
5276
5409
  clearRetryTimer(retryTimerRef);
5277
5410
  const background = refreshOptions.background === true;
5278
5411
  if (!enabled) {
@@ -5299,7 +5432,7 @@ function usePiSessions(options = {}) {
5299
5432
  let page;
5300
5433
  for (let attempt = 0; ; attempt += 1) {
5301
5434
  try {
5302
- page = await fetchSessionList(fetchImpl, sessionsListUrl(0, preferredSessionId()), requestHeaders(), addressed);
5435
+ page = await fetchSessionList(fetchImpl, sessionsListUrl(), requestHeaders());
5303
5436
  break;
5304
5437
  } catch (err) {
5305
5438
  const transient = err instanceof SessionsPreparingError || isNetworkFetchError(err);
@@ -5321,7 +5454,7 @@ function usePiSessions(options = {}) {
5321
5454
  setLoading(false);
5322
5455
  if (refreshOptions.throwOnError) throw error2;
5323
5456
  }
5324
- }, [addressed, applySessions, enabled, fetchImpl, persistActive, preferredSessionId, requestHeaders, retryBaseMs, retryMaxMs, retryMaxRetries, sessionsListUrl]);
5457
+ }, [applySessions, enabled, fetchImpl, persistActive, preferredSessionId, requestHeaders, requestScopeKey, retryBaseMs, retryMaxMs, retryMaxRetries, sessionsListUrl, sourceIsCurrent]);
5325
5458
  useEffect8(() => {
5326
5459
  mountedRef.current = true;
5327
5460
  void refresh();
@@ -5337,14 +5470,13 @@ function usePiSessions(options = {}) {
5337
5470
  loadMoreInFlightRef.current = true;
5338
5471
  const version = refreshVersionRef.current;
5339
5472
  const scope = requestScopeKey;
5340
- const offset = canonicalLoadedCountRef.current;
5341
5473
  setLoadingMore(true);
5342
5474
  try {
5343
- const page = await fetchSessionList(fetchImpl, sessionsListUrl(offset, void 0, nextCursorRef.current), requestHeaders(), addressed);
5344
- const data = page.sessions;
5345
- if (!mountedRef.current || requestSeq !== loadMoreRequestSeqRef.current || version !== refreshVersionRef.current || scope !== requestScopeRef.current) return;
5346
- const merged = mergeSessions(sessionsRef.current, data);
5347
- 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;
5348
5480
  canonicalLoadedCountRef.current += data.length;
5349
5481
  nextCursorRef.current = page.nextCursor;
5350
5482
  setSessions(merged);
@@ -5357,16 +5489,16 @@ function usePiSessions(options = {}) {
5357
5489
  return next;
5358
5490
  });
5359
5491
  } catch (err) {
5360
- if (mountedRef.current && requestSeq === loadMoreRequestSeqRef.current && version === refreshVersionRef.current && scope === requestScopeRef.current) {
5492
+ if (requestSeq === loadMoreRequestSeqRef.current && version === refreshVersionRef.current && sourceIsCurrent(scope)) {
5361
5493
  setError(err instanceof Error ? err : new Error(String(err)));
5362
5494
  }
5363
5495
  } finally {
5364
5496
  if (requestSeq === loadMoreRequestSeqRef.current) loadMoreInFlightRef.current = false;
5365
- if (mountedRef.current && requestSeq === loadMoreRequestSeqRef.current && version === refreshVersionRef.current && scope === requestScopeRef.current) {
5497
+ if (requestSeq === loadMoreRequestSeqRef.current && version === refreshVersionRef.current && sourceIsCurrent(scope)) {
5366
5498
  setLoadingMore(false);
5367
5499
  }
5368
5500
  }
5369
- }, [addressed, enabled, fetchImpl, hasMore, loading, loadingMore, persistActive, requestHeaders, requestScopeKey, sessionsListUrl]);
5501
+ }, [enabled, fetchImpl, hasMore, loading, loadingMore, persistActive, requestHeaders, requestScopeKey, sessionsListUrl, sourceIsCurrent]);
5370
5502
  useEffect8(() => {
5371
5503
  if (!enabled || !connectActiveSession || !activeSessionId || !activeSessionKnown) {
5372
5504
  setActivePiSession(void 0);
@@ -5388,6 +5520,8 @@ function usePiSessions(options = {}) {
5388
5520
  };
5389
5521
  }, [activeSessionId, activeSessionKnown, apiBaseUrl, connectActiveSession, createRemoteSession, enabled, fetchImpl, remoteSessionOptionsKey, options.agentTypeId, options.workspaceId, requestHeaders, storageScope]);
5390
5522
  const create = useCallback7(async (init) => {
5523
+ const scope = requestScopeKey;
5524
+ if (!sourceIsCurrent(scope)) throw new StaleSessionsSourceError();
5391
5525
  if (!enabled) throw new Error("Pi sessions are disabled");
5392
5526
  const response = await fetchImpl(sessionsUrl(), {
5393
5527
  method: "POST",
@@ -5400,8 +5534,10 @@ function usePiSessions(options = {}) {
5400
5534
  throw err;
5401
5535
  }
5402
5536
  const body = await response.json();
5403
- const session = addressed ? addressedCreatedSession(body, init?.title) : toSessionSummary(body);
5537
+ const session = addressedCreatedSession(body, options.agentTypeId, init?.title);
5538
+ if (!sourceIsCurrent(scope)) return session;
5404
5539
  ensurePendingScope();
5540
+ rememberConfirmedEmptySession(session.id);
5405
5541
  pendingCreatedRef.current.set(session.id, session);
5406
5542
  setDataStorageScope(storageScope);
5407
5543
  setSessions((previous) => mergeSessions([session], previous));
@@ -5409,17 +5545,43 @@ function usePiSessions(options = {}) {
5409
5545
  persistActive(session.id);
5410
5546
  void refresh();
5411
5547
  return session;
5412
- }, [addressed, enabled, ensurePendingScope, fetchImpl, persistActive, refresh, requestHeaders, sessionsUrl, storageScope]);
5548
+ }, [enabled, ensurePendingScope, fetchImpl, persistActive, refresh, rememberConfirmedEmptySession, requestHeaders, requestScopeKey, sessionsUrl, sourceIsCurrent, storageScope]);
5413
5549
  const switchSession = useCallback7((id) => {
5550
+ if (!sourceIsCurrent(requestScopeKey)) return;
5414
5551
  const known = sessionsRef.current.some((session) => session.id === id);
5415
5552
  const next = known ? id : sessionsRef.current[0]?.id;
5416
5553
  setActiveSessionId(next);
5417
5554
  persistActive(next);
5418
- }, [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]);
5419
5573
  const deleteSession = useCallback7(async (id) => {
5574
+ const scope = requestScopeKey;
5575
+ if (!sourceIsCurrent(scope)) throw new StaleSessionsSourceError();
5420
5576
  if (!enabled) throw new Error("Pi sessions are disabled");
5421
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;
5422
5583
  pendingCreatedRef.current.delete(id);
5584
+ pendingDeletedRef.current.add(id);
5423
5585
  setDataStorageScope(storageScope);
5424
5586
  setSessions((previous) => previous.filter((session) => session.id !== id));
5425
5587
  setActiveSessionId((previous) => {
@@ -5436,18 +5598,29 @@ function usePiSessions(options = {}) {
5436
5598
  if (!response.ok && response.status !== 404) throw new Error(`Failed to delete session: ${response.status}`);
5437
5599
  } catch (err) {
5438
5600
  const error2 = err instanceof Error ? err : new Error(String(err));
5439
- setError(error2);
5440
- 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
+ }
5441
5613
  throw error2;
5442
5614
  }
5443
- void refresh();
5444
- }, [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]);
5445
5617
  const reset = useCallback7(() => {
5618
+ if (!sourceIsCurrent(requestScopeKey)) return;
5446
5619
  pendingCreatedRef.current.clear();
5620
+ pendingDeletedRef.current.clear();
5447
5621
  loadMoreRequestSeqRef.current += 1;
5448
5622
  loadMoreInFlightRef.current = false;
5449
5623
  canonicalLoadedCountRef.current = canonicalPageCount(sessionsRef.current);
5450
- nextCursorRef.current = void 0;
5451
5624
  loadedDataSourceRef.current = dataSourceKey;
5452
5625
  dataStorageScopeRef.current = storageScope;
5453
5626
  setDataStorageScope(storageScope);
@@ -5455,36 +5628,35 @@ function usePiSessions(options = {}) {
5455
5628
  setActivePiSession(void 0);
5456
5629
  setLoadingMore(false);
5457
5630
  persistActive(void 0);
5458
- }, [dataSourceKey, persistActive, storageScope]);
5631
+ }, [dataSourceKey, persistActive, requestScopeKey, sourceIsCurrent, storageScope]);
5459
5632
  const visibleActiveSessionId = enabled ? activeSessionId : void 0;
5460
5633
  const activeSession = enabled ? sessions.find((session) => session.id === visibleActiveSessionId) : void 0;
5461
5634
  return {
5462
5635
  sessions,
5463
5636
  activeSession,
5464
5637
  activeSessionId: visibleActiveSessionId,
5638
+ resumeSessionId,
5465
5639
  activePiSession: visibleActiveSessionId ? activePiSession : void 0,
5466
5640
  dataStorageScope,
5641
+ sourceIdentity: enabled && loadedDataSourceRef.current === dataSourceKey ? options.sourceIdentity : void 0,
5467
5642
  loading: enabled ? loading : false,
5468
5643
  loadingMore,
5469
5644
  hasMore: enabled ? hasMore : false,
5470
5645
  error,
5471
5646
  refresh,
5472
5647
  create,
5648
+ rename,
5473
5649
  switch: switchSession,
5474
5650
  delete: deleteSession,
5475
5651
  loadMore,
5476
5652
  reset
5477
5653
  };
5478
5654
  }
5479
- async function fetchSessionList(fetchImpl, url, headers, addressed) {
5655
+ async function fetchSessionList(fetchImpl, url, headers) {
5480
5656
  const response = await fetchImpl(url, Object.keys(headers).length > 0 ? { headers } : void 0);
5481
5657
  if (response.status === 503) throw new SessionsPreparingError();
5482
5658
  if (!response.ok) throw new Error(`Failed to load sessions: ${response.status}`);
5483
5659
  const body = await response.json();
5484
- if (!addressed) {
5485
- if (!Array.isArray(body)) throw new Error("Failed to load sessions: invalid response");
5486
- return { sessions: body.map(toSessionSummary) };
5487
- }
5488
5660
  if (typeof body !== "object" || body === null || !Array.isArray(body.sessions)) {
5489
5661
  throw new Error("Failed to load sessions: invalid response");
5490
5662
  }
@@ -5494,19 +5666,6 @@ async function fetchSessionList(fetchImpl, url, headers, addressed) {
5494
5666
  ...typeof page.nextCursor === "string" ? { nextCursor: page.nextCursor } : {}
5495
5667
  };
5496
5668
  }
5497
- function toSessionSummary(value) {
5498
- if (typeof value !== "object" || value === null) throw new Error("invalid session summary");
5499
- const record = value;
5500
- if (typeof record.id !== "string" || !record.id) throw new Error("invalid session id");
5501
- const now = (/* @__PURE__ */ new Date(0)).toISOString();
5502
- return {
5503
- id: record.id,
5504
- title: typeof record.title === "string" ? record.title : "Untitled",
5505
- createdAt: typeof record.createdAt === "string" ? record.createdAt : now,
5506
- updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : now,
5507
- turnCount: typeof record.turnCount === "number" ? record.turnCount : 0
5508
- };
5509
- }
5510
5669
  function toAddressedSessionSummary(value) {
5511
5670
  if (typeof value !== "object" || value === null) throw new Error("invalid addressed session summary");
5512
5671
  const record = value;
@@ -5516,21 +5675,29 @@ function toAddressedSessionSummary(value) {
5516
5675
  }
5517
5676
  const createdAt = typeof record.createdAt === "number" ? new Date(record.createdAt).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString();
5518
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;
5519
5681
  return {
5520
5682
  id: ref.sessionId,
5521
5683
  title: typeof record.title === "string" ? record.title : "Untitled",
5522
5684
  createdAt,
5523
5685
  updatedAt,
5524
- 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 } : {}
5525
5691
  };
5526
5692
  }
5527
- function addressedCreatedSession(value, title) {
5528
- 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) {
5529
5695
  throw new Error("invalid addressed session ref");
5530
5696
  }
5531
5697
  const now = (/* @__PURE__ */ new Date()).toISOString();
5532
5698
  return {
5533
5699
  id: value.sessionId,
5700
+ agentTypeId: expectedAgentTypeId,
5534
5701
  title: title ?? "Untitled",
5535
5702
  createdAt: now,
5536
5703
  updatedAt: now,
@@ -5586,31 +5753,25 @@ function mergeSessions(...lists) {
5586
5753
  }
5587
5754
  return merged;
5588
5755
  }
5589
- function buildRequestHeaders(headers, storageScope) {
5590
- const result = {};
5591
- for (const [key, value] of Object.entries(headers ?? {})) {
5592
- if (typeof value === "string") result[key] = value;
5593
- }
5594
- if (storageScope && !hasHeader2(result, "x-boring-storage-scope")) result["x-boring-storage-scope"] = storageScope;
5595
- return result;
5596
- }
5597
5756
  function headersScopeKey(headers, storageScope) {
5598
5757
  return JSON.stringify({ storageScope, headers: Object.entries(headers ?? {}).sort(([a], [b]) => a.localeCompare(b)) });
5599
5758
  }
5600
- function requestScopeIdentity(apiBaseUrl, sessionsApiPath, storageScope, headersKey) {
5759
+ function requestScopeIdentity(apiBaseUrl, sessionsApiPath, agentTypeId, storageScope, headersKey, workspaceId, sourceIdentity) {
5601
5760
  return `${apiBaseUrl}
5602
5761
  ${sessionsApiPath}
5762
+ ${agentTypeId}
5603
5763
  ${storageScope}
5604
- ${headersKey}`;
5764
+ ${headersKey}
5765
+ ${workspaceId ?? ""}
5766
+ ${sourceIdentity ?? ""}`;
5605
5767
  }
5606
- function dataSourceIdentity(apiBaseUrl, sessionsApiPath, storageScope) {
5768
+ function dataSourceIdentity(apiBaseUrl, sessionsApiPath, agentTypeId, storageScope, workspaceId, sourceIdentity) {
5607
5769
  return `${apiBaseUrl}
5608
5770
  ${sessionsApiPath}
5609
- ${storageScope}`;
5610
- }
5611
- function hasHeader2(headers, name) {
5612
- const lower = name.toLowerCase();
5613
- return Object.keys(headers).some((key) => key.toLowerCase() === lower);
5771
+ ${agentTypeId}
5772
+ ${storageScope}
5773
+ ${workspaceId ?? ""}
5774
+ ${sourceIdentity ?? ""}`;
5614
5775
  }
5615
5776
  function retryDelayMs(attempt, retry) {
5616
5777
  const base = retry.baseMs ?? DEFAULT_RETRY_BASE_MS;
@@ -5991,8 +6152,8 @@ function searchPiSessions(sessions, query, options = {}) {
5991
6152
  }
5992
6153
 
5993
6154
  // src/front/chat/components/PiConversationSurface.tsx
5994
- import { Loader2 as Loader22 } from "lucide-react";
5995
- 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";
5996
6157
  import { useStickToBottomContext as useStickToBottomContext2 } from "use-stick-to-bottom";
5997
6158
 
5998
6159
  // src/front/primitives/conversation.tsx
@@ -6053,7 +6214,7 @@ var ConversationScrollButton = ({
6053
6214
  Button7,
6054
6215
  {
6055
6216
  className: cn(
6056
- "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",
6057
6218
  className
6058
6219
  ),
6059
6220
  onClick: handleScrollToBottom,
@@ -6098,8 +6259,27 @@ function noticeTextClass(className) {
6098
6259
  import { memo as memo2 } from "react";
6099
6260
  import { AlertTriangleIcon, InfoIcon, Loader2Icon, PlugZapIcon, RefreshCwIcon, XIcon as XIcon2 } from "lucide-react";
6100
6261
  import { Button as Button8 } from "@hachej/boring-ui-kit";
6101
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
6102
- 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 }) => {
6103
6283
  if (notices.length === 0) return null;
6104
6284
  return /* @__PURE__ */ jsx16(
6105
6285
  "div",
@@ -6107,16 +6287,29 @@ var RuntimeNotices = memo2(({ notices, onDismiss, onAction, renderAction, classN
6107
6287
  "data-boring-agent-part": "runtime-notices",
6108
6288
  className: cn("flex flex-col gap-2 px-4 py-2", className),
6109
6289
  ...props,
6110
- 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
+ ))
6111
6301
  }
6112
6302
  );
6113
6303
  });
6114
6304
  RuntimeNotices.displayName = "RuntimeNotices";
6115
- function RuntimeNoticeRow({ notice, onDismiss, onAction, renderAction }) {
6305
+ function RuntimeNoticeRow({ notice, onDismiss, onAction, renderAction, historyEmpty }) {
6116
6306
  const kind = inferNoticeKind(notice);
6117
6307
  const Icon = iconForNotice(kind, notice.level);
6118
6308
  const actionLabel = notice.actionLabel ?? defaultActionLabel(kind);
6119
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);
6120
6313
  return /* @__PURE__ */ jsxs14(
6121
6314
  "div",
6122
6315
  {
@@ -6132,9 +6325,15 @@ function RuntimeNoticeRow({ notice, onDismiss, onAction, renderAction }) {
6132
6325
  ),
6133
6326
  children: [
6134
6327
  /* @__PURE__ */ jsx16(Icon, { className: cn(noticeIconClass(notice.level), kind === "retry" && "animate-spin motion-reduce:animate-none"), "aria-hidden": "true" }),
6135
- /* @__PURE__ */ jsx16("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx16("p", { className: noticeTextClass(), children: notice.text }) }),
6136
- hostAction ?? null,
6137
- 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,
6138
6337
  notice.dismissible && onDismiss ? /* @__PURE__ */ jsx16(
6139
6338
  Button8,
6140
6339
  {
@@ -6177,7 +6376,8 @@ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
6177
6376
  function RuntimeNoticeMessages({
6178
6377
  notices,
6179
6378
  onDismiss,
6180
- renderAction
6379
+ renderAction,
6380
+ historyEmpty
6181
6381
  }) {
6182
6382
  const visible = notices.filter(
6183
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:")
@@ -6189,6 +6389,7 @@ function RuntimeNoticeMessages({
6189
6389
  notices: visible,
6190
6390
  onDismiss,
6191
6391
  renderAction,
6392
+ historyEmpty,
6192
6393
  className: "mx-auto w-full max-w-3xl px-0 py-0"
6193
6394
  }
6194
6395
  );
@@ -6650,11 +6851,11 @@ function extractCodeBlockText(children) {
6650
6851
  };
6651
6852
  return walk(children);
6652
6853
  }
6653
- var MarkdownPre = ({ children }) => {
6854
+ var MarkdownPre = ({ children, filename }) => {
6654
6855
  const code3 = extractCodeBlockText(children).replace(/\n$/, "");
6655
6856
  const language = extractCodeBlockLanguage(children);
6656
6857
  return /* @__PURE__ */ jsx19(CodeBlock2, { code: code3, language: language ?? "text", className: "my-3", children: /* @__PURE__ */ jsxs17(CodeBlockHeader, { children: [
6657
- /* @__PURE__ */ jsx19(CodeBlockTitle, { children: /* @__PURE__ */ jsx19(CodeBlockFilename, { children: language ?? "text" }) }),
6858
+ /* @__PURE__ */ jsx19(CodeBlockTitle, { children: /* @__PURE__ */ jsx19(CodeBlockFilename, { children: filename ?? language ?? "text" }) }),
6658
6859
  /* @__PURE__ */ jsx19(CodeBlockCopyButton, {})
6659
6860
  ] }) });
6660
6861
  };
@@ -6684,7 +6885,7 @@ var markdownComponents = {
6684
6885
  code: MarkdownCode
6685
6886
  };
6686
6887
  var MessageResponse = memo3(
6687
- ({ className, shikiTheme, components, ...props }) => /* @__PURE__ */ jsx19(
6888
+ ({ className, shikiTheme, components, codeFilename, ...props }) => /* @__PURE__ */ jsx19(
6688
6889
  Streamdown,
6689
6890
  {
6690
6891
  className: cn(
@@ -6693,11 +6894,15 @@ var MessageResponse = memo3(
6693
6894
  ),
6694
6895
  plugins: streamdownPlugins,
6695
6896
  shikiTheme: shikiTheme ?? DEFAULT_SHIKI_THEME,
6696
- components: { ...markdownComponents, ...components ?? {} },
6897
+ components: {
6898
+ ...markdownComponents,
6899
+ ...components ?? {},
6900
+ ...codeFilename ? { pre: (props2) => /* @__PURE__ */ jsx19(MarkdownPre, { ...props2, filename: codeFilename }) } : {}
6901
+ },
6697
6902
  ...props
6698
6903
  }
6699
6904
  ),
6700
- (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
6701
6906
  );
6702
6907
  MessageResponse.displayName = "MessageResponse";
6703
6908
 
@@ -6778,7 +6983,7 @@ var ShimmerComponent = ({
6778
6983
  var Shimmer = memo4(ShimmerComponent);
6779
6984
 
6780
6985
  // src/front/primitives/reasoning.tsx
6781
- 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";
6782
6987
  var ReasoningContext = createContext6(null);
6783
6988
  var useReasoning = () => {
6784
6989
  const context = useContext6(ReasoningContext);
@@ -6891,7 +7096,7 @@ var ReasoningTrigger = memo5(
6891
7096
  className
6892
7097
  ),
6893
7098
  ...props,
6894
- children: children ?? /* @__PURE__ */ jsxs18(Fragment7, { children: [
7099
+ children: children ?? /* @__PURE__ */ jsxs18(Fragment8, { children: [
6895
7100
  /* @__PURE__ */ jsx21(BrainIcon, { className: "size-3" }),
6896
7101
  getThinkingMessage(isStreaming, duration),
6897
7102
  /* @__PURE__ */ jsx21(
@@ -7053,6 +7258,10 @@ var ToolCallGroup = memo6(({ tools, mergedToolRenderers }) => {
7053
7258
  return () => window.clearInterval(interval);
7054
7259
  }, [isRunning]);
7055
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
+ }));
7056
7265
  return /* @__PURE__ */ jsxs19(
7057
7266
  Collapsible3,
7058
7267
  {
@@ -7061,6 +7270,11 @@ var ToolCallGroup = memo6(({ tools, mergedToolRenderers }) => {
7061
7270
  className: "not-prose my-1.5",
7062
7271
  "data-boring-agent-tool-state": visualState,
7063
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
+ }),
7064
7278
  /* @__PURE__ */ jsxs19(
7065
7279
  CollapsibleTrigger4,
7066
7280
  {
@@ -7106,7 +7320,10 @@ var ToolCallGroup = memo6(({ tools, mergedToolRenderers }) => {
7106
7320
  children: [
7107
7321
  /* @__PURE__ */ jsx22("div", { className: "absolute left-[5px] top-0 bottom-2 w-px bg-border/35" }),
7108
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 }) => {
7109
- const { renderer, part: resolvedPart, resolution } = resolveToolRendererForPart(part, mergedToolRenderers);
7323
+ const { renderer, part: resolvedPart, resolution } = resolveToolRendererForPart(
7324
+ part,
7325
+ inlineToolKeys.has(key) ? {} : mergedToolRenderers
7326
+ );
7110
7327
  return /* @__PURE__ */ jsx22(
7111
7328
  "div",
7112
7329
  {
@@ -7158,7 +7375,7 @@ function ChatMessageContributionBoundary({
7158
7375
 
7159
7376
  // src/front/chat/components/MessageMentions.tsx
7160
7377
  import { Children, cloneElement, isValidElement as isValidElement2 } from "react";
7161
- import { Fragment as Fragment8, jsx as jsx24 } from "react/jsx-runtime";
7378
+ import { Fragment as Fragment9, jsx as jsx24 } from "react/jsx-runtime";
7162
7379
  var TERMINAL_BOUNDARY = String.raw`(?=$|\s|[.,;!?)}\]"'”’]+(?=$|\s))`;
7163
7380
  var PREFIX_BOUNDARY = String.raw`(^|[\s(\[{"'“‘])`;
7164
7381
  function escapeRegExp2(value) {
@@ -7316,8 +7533,8 @@ function createMessageMentionMarkdownComponents(catalog, onActivate) {
7316
7533
  const content = Children.toArray(children).filter((child) => child !== null && child !== "");
7317
7534
  if (content.length === 1 && isValidElement2(content[0])) {
7318
7535
  const child = content[0];
7319
- if (child.props.node?.tagName === "img") return /* @__PURE__ */ jsx24(Fragment8, { children });
7320
- 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 });
7321
7538
  }
7322
7539
  return /* @__PURE__ */ jsx24("p", { ...props, children: decorate(children) });
7323
7540
  };
@@ -7403,12 +7620,13 @@ function DefaultPiTimelineMessage({ message, isLast, isStreaming, showThoughts,
7403
7620
  mentionActivateRef.current?.(mention);
7404
7621
  }, [openArtifact]);
7405
7622
  const hasMentions = effectiveMentionCatalog.files || effectiveMentionCatalog.commands.length > 0 || effectiveMentionCatalog.skills.length > 0;
7406
- const mentionsEnabled = Boolean(isAssistant && !(isStreaming && isLast) && hasMentions);
7623
+ const messageIsStreaming = isAssistant && isLast && (isStreaming || message.status === "streaming");
7624
+ const mentionsEnabled = Boolean(isAssistant && !messageIsStreaming && hasMentions);
7407
7625
  const mentionMarkdownComponents = useMemo12(
7408
7626
  () => mentionsEnabled ? createMessageMentionMarkdownComponents(effectiveMentionCatalog, activateCurrentMention) : void 0,
7409
7627
  [activateCurrentMention, effectiveMentionCatalog, mentionSignature, mentionsEnabled]
7410
7628
  );
7411
- const shouldReserveStreamingActions = isStreaming && isAssistant && isLast;
7629
+ const shouldReserveStreamingActions = messageIsStreaming;
7412
7630
  return /* @__PURE__ */ jsxs20(
7413
7631
  Message,
7414
7632
  {
@@ -7467,7 +7685,7 @@ function DefaultPiTimelineMessage({ message, isLast, isStreaming, showThoughts,
7467
7685
  })
7468
7686
  }
7469
7687
  ) : null,
7470
- finalParts.map((item) => {
7688
+ finalParts.map((item, index) => {
7471
7689
  if (item.kind === "reasoning") {
7472
7690
  return /* @__PURE__ */ jsx25(
7473
7691
  TimelineReasoningPart,
@@ -7491,6 +7709,7 @@ function DefaultPiTimelineMessage({ message, isLast, isStreaming, showThoughts,
7491
7709
  MessageResponse,
7492
7710
  {
7493
7711
  components: mentionMarkdownComponents,
7712
+ codeFilename: codeFilenameForPart(finalParts, index),
7494
7713
  className: "boring-agent-markdown max-w-none",
7495
7714
  children: text
7496
7715
  },
@@ -7507,7 +7726,7 @@ function DefaultPiTimelineMessage({ message, isLast, isStreaming, showThoughts,
7507
7726
  MessageActionsBar,
7508
7727
  {
7509
7728
  text: textParts.map((part) => part.text).join("\n\n"),
7510
- visible: !isStreaming
7729
+ visible: !messageIsStreaming
7511
7730
  }
7512
7731
  ) : null
7513
7732
  ]
@@ -7542,7 +7761,7 @@ function TimelineReasoningPart({ item, showThoughts }) {
7542
7761
  ReasoningTrigger,
7543
7762
  {
7544
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",
7545
- getThinkingMessage: (streaming) => /* @__PURE__ */ jsx25("span", { children: streaming ? "thinking" : "thoughts" }),
7764
+ getThinkingMessage: () => /* @__PURE__ */ jsx25("span", { children: "Reasoning" }),
7546
7765
  onClick: toggleManualOpen
7547
7766
  }
7548
7767
  ),
@@ -7603,6 +7822,15 @@ ${part.text}`;
7603
7822
  flushTools();
7604
7823
  return grouped;
7605
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
+ }
7606
7834
  function NoticeBubble({ level, text }) {
7607
7835
  return /* @__PURE__ */ jsx25(
7608
7836
  "div",
@@ -7683,10 +7911,10 @@ function MessageActionsBar({
7683
7911
  }
7684
7912
  };
7685
7913
  const iconActionBtnClass = cn(
7686
- "inline-flex h-6 w-6 items-center justify-center rounded-[var(--radius-sm)]",
7687
- "text-muted-foreground/35 transition-colors",
7688
- "hover:bg-foreground/[0.04] hover:text-muted-foreground/80",
7689
- "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"
7690
7918
  );
7691
7919
  const hiddenActionProps = visible ? {} : { tabIndex: -1 };
7692
7920
  return /* @__PURE__ */ jsx25(
@@ -7694,7 +7922,7 @@ function MessageActionsBar({
7694
7922
  {
7695
7923
  "aria-hidden": !visible,
7696
7924
  className: cn(
7697
- "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",
7698
7926
  visible ? "opacity-100" : "pointer-events-none opacity-0"
7699
7927
  ),
7700
7928
  children: /* @__PURE__ */ jsx25(
@@ -7755,6 +7983,8 @@ function PiConversationSurface({
7755
7983
  }) {
7756
7984
  const messageItems = buildMessageRenderItems(messages);
7757
7985
  const total = messageItems.length;
7986
+ const historyEmpty = messages.length === 0;
7987
+ const terminalError = hasTerminalChatError(runtimeNotices, historyEmpty);
7758
7988
  const [visibleCount, setVisibleCount] = useState15(TRANSCRIPT_WINDOW);
7759
7989
  useEffect14(() => {
7760
7990
  setVisibleCount(TRANSCRIPT_WINDOW);
@@ -7778,11 +8008,8 @@ function PiConversationSurface({
7778
8008
  chrome ? "max-w-3xl px-6 py-8" : "max-w-[680px] px-4 py-4",
7779
8009
  emptyHero && "py-4 text-center"
7780
8010
  ), children: [
7781
- 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: [
7782
- /* @__PURE__ */ jsx26(Loader22, { className: "h-4 w-4 animate-spin" }),
7783
- "Loading chat history\u2026"
7784
- ] }) : null,
7785
- messages.length === 0 && !emptyStateHydrating ? /* @__PURE__ */ jsx26(
8011
+ messages.length === 0 && emptyStateHydrating && !terminalError ? /* @__PURE__ */ jsx26(ConversationHistoryLoadingState, {}) : null,
8012
+ messages.length === 0 && !emptyStateHydrating && !terminalError ? /* @__PURE__ */ jsx26(
7786
8013
  ChatEmptyState,
7787
8014
  {
7788
8015
  eyebrow: emptyState?.eyebrow,
@@ -7814,13 +8041,45 @@ function PiConversationSurface({
7814
8041
  },
7815
8042
  key
7816
8043
  )),
7817
- /* @__PURE__ */ jsx26(RuntimeNoticeMessages, { notices: runtimeNotices, onDismiss: onDismissNotice, renderAction: renderNoticeAction })
8044
+ /* @__PURE__ */ jsx26(RuntimeNoticeMessages, { notices: runtimeNotices, onDismiss: onDismissNotice, renderAction: renderNoticeAction, historyEmpty })
7818
8045
  ] }),
7819
8046
  /* @__PURE__ */ jsx26(ConversationScrollButton, {})
7820
8047
  ]
7821
8048
  }
7822
8049
  );
7823
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
+ }
7824
8083
  function TranscriptHistoryLoader({ olderCount, onLoadOlder }) {
7825
8084
  const { scrollRef } = useStickToBottomContext2();
7826
8085
  const pendingAnchor = useRef11(null);
@@ -7839,7 +8098,7 @@ function TranscriptHistoryLoader({ olderCount, onLoadOlder }) {
7839
8098
  el.addEventListener("scroll", onScroll, { passive: true });
7840
8099
  return () => el.removeEventListener("scroll", onScroll);
7841
8100
  }, [scrollRef, onLoadOlder]);
7842
- useLayoutEffect(() => {
8101
+ useLayoutEffect2(() => {
7843
8102
  const el = scrollRef.current;
7844
8103
  if (!el) return;
7845
8104
  if (pendingAnchor.current != null) {
@@ -7853,7 +8112,7 @@ function TranscriptHistoryLoader({ olderCount, onLoadOlder }) {
7853
8112
  {
7854
8113
  type: "button",
7855
8114
  onClick: onLoadOlder,
7856
- 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",
7857
8116
  children: [
7858
8117
  "Load ",
7859
8118
  olderCount,
@@ -7885,8 +8144,8 @@ function buildMessageRenderItems(messages) {
7885
8144
  }
7886
8145
 
7887
8146
  // src/front/chat/components/PiChatComposerSurface.tsx
7888
- import { useCallback as useCallback16, useLayoutEffect as useLayoutEffect2 } from "react";
7889
- 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";
7890
8149
 
7891
8150
  // src/front/composer/PluginUpdateStatus.tsx
7892
8151
  import { useEffect as useEffect15, useRef as useRef12 } from "react";
@@ -8129,7 +8388,7 @@ function PluginUpdateStatus({
8129
8388
 
8130
8389
  // src/front/composer/CommandRunStatus.tsx
8131
8390
  import { useEffect as useEffect16, useRef as useRef13 } from "react";
8132
- 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";
8133
8392
  function CommandRunStatus({
8134
8393
  state,
8135
8394
  onDismiss,
@@ -8154,7 +8413,7 @@ function CommandRunStatus({
8154
8413
  tone: "running",
8155
8414
  dataAttribute: "data-boring-command-run",
8156
8415
  maxWidthClassName,
8157
- runningContent: state.command ? /* @__PURE__ */ jsxs24(Fragment9, { children: [
8416
+ runningContent: state.command ? /* @__PURE__ */ jsxs24(Fragment10, { children: [
8158
8417
  " Running ",
8159
8418
  /* @__PURE__ */ jsxs24("code", { className: "font-mono", children: [
8160
8419
  "/",
@@ -8172,7 +8431,7 @@ function CommandRunStatus({
8172
8431
  tone: "success",
8173
8432
  dataAttribute: "data-boring-command-run",
8174
8433
  maxWidthClassName,
8175
- title: state.command ? /* @__PURE__ */ jsxs24(Fragment9, { children: [
8434
+ title: state.command ? /* @__PURE__ */ jsxs24(Fragment10, { children: [
8176
8435
  " Ran ",
8177
8436
  /* @__PURE__ */ jsxs24("code", { className: "font-mono", children: [
8178
8437
  "/",
@@ -8192,7 +8451,7 @@ function CommandRunStatus({
8192
8451
  tone: "error",
8193
8452
  dataAttribute: "data-boring-command-run",
8194
8453
  maxWidthClassName,
8195
- title: state.command ? /* @__PURE__ */ jsxs24(Fragment9, { children: [
8454
+ title: state.command ? /* @__PURE__ */ jsxs24(Fragment10, { children: [
8196
8455
  " ",
8197
8456
  /* @__PURE__ */ jsxs24("code", { className: "font-mono", children: [
8198
8457
  "/",
@@ -8242,7 +8501,7 @@ function displayProviderLabel(provider) {
8242
8501
  }
8243
8502
 
8244
8503
  // src/front/chatPanelComposerControls.tsx
8245
- 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";
8246
8505
  var composerActionClass = cn(
8247
8506
  "inline-flex h-8 items-center justify-center gap-1.5 rounded-[var(--radius-md)] border-0 bg-transparent",
8248
8507
  "text-muted-foreground shadow-none transition-colors motion-reduce:transition-none",
@@ -8384,14 +8643,14 @@ var ModelSelectTrigger = forwardRef(function ModelSelectTrigger2({
8384
8643
  className
8385
8644
  ),
8386
8645
  ...props,
8387
- children: trigger === "slash" ? /* @__PURE__ */ jsxs25(Fragment10, { children: [
8646
+ children: trigger === "slash" ? /* @__PURE__ */ jsxs25(Fragment11, { children: [
8388
8647
  /* @__PURE__ */ jsx30("span", { className: "text-muted-foreground", children: "Model \xB7 " }),
8389
8648
  /* @__PURE__ */ jsx30("span", { className: "min-w-0 truncate text-foreground", children: triggerLabel }),
8390
8649
  value ? /* @__PURE__ */ jsxs25("span", { className: "shrink-0 text-muted-foreground", children: [
8391
8650
  " \xB7 ",
8392
8651
  displayProviderLabel(value.provider)
8393
8652
  ] }) : null
8394
- ] }) : /* @__PURE__ */ jsxs25(Fragment10, { children: [
8653
+ ] }) : /* @__PURE__ */ jsxs25(Fragment11, { children: [
8395
8654
  /* @__PURE__ */ jsx30("span", { className: "min-w-0 truncate", children: triggerDisplay }),
8396
8655
  /* @__PURE__ */ jsx30(ChevronDownIcon5, { className: "h-3 w-3 shrink-0 text-muted-foreground/50", "aria-hidden": "true" })
8397
8656
  ] })
@@ -8680,10 +8939,10 @@ var ThinkingSelectTrigger = forwardRef(function ThinkingSelectTrigger2({
8680
8939
  ...props,
8681
8940
  children: [
8682
8941
  trigger === "button" ? /* @__PURE__ */ jsx30(ThinkingLevelGlyph, { level: value }) : null,
8683
- trigger === "slash" ? /* @__PURE__ */ jsxs25(Fragment10, { children: [
8942
+ trigger === "slash" ? /* @__PURE__ */ jsxs25(Fragment11, { children: [
8684
8943
  /* @__PURE__ */ jsx30("span", { className: "text-muted-foreground", children: "Thinking \xB7 " }),
8685
8944
  /* @__PURE__ */ jsx30("span", { className: "text-foreground", children: THINKING_LEVEL_STATUS_LABELS[value] })
8686
- ] }) : /* @__PURE__ */ jsxs25(Fragment10, { children: [
8945
+ ] }) : /* @__PURE__ */ jsxs25(Fragment11, { children: [
8687
8946
  /* @__PURE__ */ jsx30("span", { children: THINKING_LEVEL_LABELS[value] }),
8688
8947
  /* @__PURE__ */ jsx30(ChevronDownIcon5, { className: "h-3 w-3 shrink-0 text-muted-foreground/50", "aria-hidden": "true" })
8689
8948
  ] })
@@ -8847,7 +9106,7 @@ var PromptInputFooter = ({
8847
9106
  );
8848
9107
 
8849
9108
  // src/front/primitives/prompt-input.tsx
8850
- 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";
8851
9110
  var PromptInput = ({
8852
9111
  className,
8853
9112
  accept,
@@ -9190,7 +9449,7 @@ var PromptInput = ({
9190
9449
  },
9191
9450
  [usingProvider, controller, files, onSubmit, clear]
9192
9451
  );
9193
- const inner = /* @__PURE__ */ jsxs27(Fragment11, { children: [
9452
+ const inner = /* @__PURE__ */ jsxs27(Fragment12, { children: [
9194
9453
  /* @__PURE__ */ jsx32(
9195
9454
  Input,
9196
9455
  {
@@ -9359,7 +9618,7 @@ var PromptInputSubmit = ({
9359
9618
 
9360
9619
  // src/front/primitives/slash-command-picker.tsx
9361
9620
  import { useEffect as useEffect19, useMemo as useMemo14, useRef as useRef16, useState as useState18 } from "react";
9362
- 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";
9363
9622
  var ALL_PLUGINS = "__all__";
9364
9623
  function commandGroup(cmd) {
9365
9624
  if (cmd.source === "skill") return "skills";
@@ -9471,7 +9730,7 @@ function SlashCommandPicker({ query, commands, onSelect, onDismiss }) {
9471
9730
  "/",
9472
9731
  cmd.name
9473
9732
  ] }),
9474
- cmd.source === "skill" ? /* @__PURE__ */ jsxs28(Fragment12, { children: [
9733
+ cmd.source === "skill" ? /* @__PURE__ */ jsxs28(Fragment13, { children: [
9475
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" }),
9476
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
9477
9736
  ] }) : cmd.sourcePlugin ? (
@@ -9488,7 +9747,7 @@ function SlashCommandPicker({ query, commands, onSelect, onDismiss }) {
9488
9747
  }
9489
9748
 
9490
9749
  // src/front/chat/components/ComposerAttachments.tsx
9491
- 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";
9492
9751
  import { IconButton as IconButton3 } from "@hachej/boring-ui-kit";
9493
9752
  import { jsx as jsx34, jsxs as jsxs29 } from "react/jsx-runtime";
9494
9753
  function AttachmentButton({ disabled, className }) {
@@ -9503,7 +9762,7 @@ function AttachmentButton({ disabled, className }) {
9503
9762
  onClick: () => {
9504
9763
  if (!disabled) attachments.openFileDialog();
9505
9764
  },
9506
- className: cn(composerActionClass, "!h-10 !w-10", className),
9765
+ className: cn(composerActionClass, "composer-attachment-button !h-10 !w-10", className),
9507
9766
  "aria-label": "Attach files",
9508
9767
  title: disabled ? "Attachments are available when the composer is ready." : "Attach files",
9509
9768
  children: /* @__PURE__ */ jsx34(PaperclipIcon2, { className: "h-4 w-4", strokeWidth: 1.75 })
@@ -9526,18 +9785,18 @@ function AttachmentsList() {
9526
9785
  data: file,
9527
9786
  onRemove: () => attachments.remove(file.id),
9528
9787
  className: cn(
9529
- "!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",
9530
9789
  "transition-colors hover:!bg-muted/70 hover:!text-foreground",
9531
9790
  file.status === "error" && "!border-destructive/50 !bg-destructive/10"
9532
9791
  ),
9533
9792
  children: [
9534
9793
  /* @__PURE__ */ jsxs29("div", { className: "relative shrink-0", children: [
9535
9794
  /* @__PURE__ */ jsx34(AttachmentPreview, { className: "!size-7 overflow-hidden !rounded-full bg-background/60" }),
9536
- 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,
9537
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
9538
9797
  ] }),
9539
9798
  /* @__PURE__ */ jsx34(AttachmentInfo, { className: "min-w-0 !max-w-[180px] truncate text-[13px] font-medium" }),
9540
- /* @__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" })
9541
9800
  ]
9542
9801
  },
9543
9802
  file.id
@@ -9629,6 +9888,7 @@ function PiChatComposerSurface({
9629
9888
  onSubmitMessage,
9630
9889
  onStop
9631
9890
  }) {
9891
+ const prefersReducedMotion = useReducedMotion();
9632
9892
  const workspaceRequestId = getHeaderValue(requestHeaders, "x-boring-workspace-id");
9633
9893
  const composerContributions = useComposerContributions();
9634
9894
  const uploadAttachment = useCallback16((file) => uploadFile(file, {
@@ -9658,7 +9918,7 @@ function PiChatComposerSurface({
9658
9918
  rail?.style.setProperty("--composer-input-group-height", `${inputGroupHeight}px`);
9659
9919
  rail?.setAttribute("data-composer-multiline", inputGroupHeight > COMPOSER_INPUT_GROUP_MIN_HEIGHT ? "true" : "false");
9660
9920
  }, []);
9661
- useLayoutEffect2(() => {
9921
+ useLayoutEffect3(() => {
9662
9922
  resizeTextarea(textareaRef.current);
9663
9923
  }, [draft, resizeTextarea, textareaRef]);
9664
9924
  return /* @__PURE__ */ jsxs30("div", { className: cn(
@@ -9681,15 +9941,15 @@ function PiChatComposerSurface({
9681
9941
  "data-testid": isStreaming ? "chat-working" : void 0,
9682
9942
  role: isStreaming ? "status" : void 0,
9683
9943
  "aria-live": isStreaming ? "polite" : void 0,
9684
- 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",
9685
9945
  children: [
9686
9946
  /* @__PURE__ */ jsx35(
9687
9947
  motion2.span,
9688
9948
  {
9689
9949
  "aria-hidden": "true",
9690
9950
  className: "inline-block size-1.5 rounded-full bg-[color:var(--accent)]",
9691
- animate: { opacity: [0.35, 1, 0.35] },
9692
- 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" }
9693
9953
  }
9694
9954
  ),
9695
9955
  /* @__PURE__ */ jsx35("span", { children: "Working\u2026" })
@@ -9881,7 +10141,7 @@ function PiChatComposerSurface({
9881
10141
  onStop,
9882
10142
  disabled: submitDisabled,
9883
10143
  className: cn(
9884
- "h-8 w-8 shrink-0 rounded-full",
10144
+ "composer-submit-control h-8 w-8 shrink-0 rounded-full",
9885
10145
  "bg-foreground text-background",
9886
10146
  "transition-all duration-150 ease-[cubic-bezier(0.22,1,0.36,1)]",
9887
10147
  "hover:bg-foreground/90 hover:shadow-[0_0_0_3px_oklch(from_var(--foreground)_l_c_h/0.12)] hover:scale-[1.04]",
@@ -9920,6 +10180,7 @@ function PiChatComposerSurface({
9920
10180
  disabled: isStreaming || modelControlled,
9921
10181
  trigger: "slash",
9922
10182
  open: modelPickerOpen,
10183
+ className: "composer-settings-trigger",
9923
10184
  onClick: () => {
9924
10185
  if (modelPickerOpen) {
9925
10186
  onSetModelPickerOpen(false);
@@ -9936,6 +10197,7 @@ function PiChatComposerSurface({
9936
10197
  disabled: isStreaming || thinkingControlled,
9937
10198
  trigger: "slash",
9938
10199
  open: thinkingPickerOpen,
10200
+ className: "composer-settings-trigger",
9939
10201
  onClick: () => {
9940
10202
  if (thinkingPickerOpen) {
9941
10203
  onSetThinkingPickerOpen(false);
@@ -9958,86 +10220,6 @@ function getHeaderValue(headers, name) {
9958
10220
 
9959
10221
  // src/front/chat/piChatPanelHooks.ts
9960
10222
  import { useCallback as useCallback17, useEffect as useEffect20, useMemo as useMemo15, useRef as useRef17, useState as useState19, useSyncExternalStore } from "react";
9961
- function useExternalRemotePiSession({
9962
- sessionId,
9963
- agentTypeId,
9964
- workspaceId,
9965
- storageScope,
9966
- apiBaseUrl,
9967
- requestHeaders,
9968
- fetch: fetch2,
9969
- createRemoteSession,
9970
- remoteSessionOptions
9971
- }) {
9972
- const [session, setSession] = useState19();
9973
- const remoteSessionOptionsRef = useRef17(remoteSessionOptions);
9974
- remoteSessionOptionsRef.current = remoteSessionOptions;
9975
- const remoteSessionOptionsKey = useMemo15(
9976
- () => remoteSessionOptionsIdentity2(remoteSessionOptions),
9977
- [remoteSessionOptions]
9978
- );
9979
- useEffect20(() => {
9980
- if (!sessionId) {
9981
- setSession(void 0);
9982
- return;
9983
- }
9984
- const next = (createRemoteSession ?? createRemotePiSession)({
9985
- ...remoteSessionOptionsRef.current,
9986
- sessionId,
9987
- agentTypeId,
9988
- workspaceId,
9989
- storageScope,
9990
- apiBaseUrl,
9991
- headers: requestHeaders,
9992
- fetch: fetch2
9993
- });
9994
- setSession(next);
9995
- return () => next.dispose();
9996
- }, [agentTypeId, apiBaseUrl, createRemoteSession, fetch2, remoteSessionOptionsKey, requestHeaders, sessionId, storageScope, workspaceId]);
9997
- return session;
9998
- }
9999
- var remoteSessionOptionObjectIds2 = /* @__PURE__ */ new WeakMap();
10000
- var remoteSessionOptionObjectSeq2 = 0;
10001
- function remoteSessionOptionObjectIdentity2(value) {
10002
- if (typeof value !== "object" && typeof value !== "function" || value === null) return void 0;
10003
- const object = value;
10004
- let id = remoteSessionOptionObjectIds2.get(object);
10005
- if (!id) {
10006
- id = ++remoteSessionOptionObjectSeq2;
10007
- remoteSessionOptionObjectIds2.set(object, id);
10008
- }
10009
- return String(id);
10010
- }
10011
- function remoteSessionOptionsIdentity2(options) {
10012
- if (!options) return "{}";
10013
- return JSON.stringify({
10014
- autoStart: options.autoStart,
10015
- requestTimeoutMs: options.requestTimeoutMs,
10016
- onEvent: remoteSessionOptionObjectIdentity2(options.onEvent),
10017
- storeOptions: remoteSessionOptionObjectIdentity2(options.storeOptions),
10018
- setTimeoutFn: remoteSessionOptionObjectIdentity2(options.setTimeoutFn),
10019
- clearTimeoutFn: remoteSessionOptionObjectIdentity2(options.clearTimeoutFn),
10020
- reconnect: options.reconnect ? {
10021
- baseMs: options.reconnect.baseMs,
10022
- maxMs: options.reconnect.maxMs,
10023
- jitterRatio: options.reconnect.jitterRatio,
10024
- random: remoteSessionOptionObjectIdentity2(options.reconnect.random)
10025
- } : void 0,
10026
- debug: options.debug ? {
10027
- largeStateWarningBytes: options.debug.largeStateWarningBytes,
10028
- largeStateWarningMessages: options.debug.largeStateWarningMessages,
10029
- onWarning: remoteSessionOptionObjectIdentity2(options.debug.onWarning)
10030
- } : void 0
10031
- });
10032
- }
10033
- function useRemotePiSessionState(session) {
10034
- return useSyncExternalStore(
10035
- useCallback17((listener) => session?.subscribe(listener) ?? (() => {
10036
- }), [session]),
10037
- useCallback17(() => session?.getState(), [session]),
10038
- useCallback17(() => session?.getState(), [session])
10039
- );
10040
- }
10041
10223
 
10042
10224
  // src/front/chat/piChatPanelUtils.ts
10043
10225
  function headersContentKey(headers) {
@@ -10144,9 +10326,103 @@ function errorMessage2(error, fallback) {
10144
10326
  return error instanceof Error && error.message ? error.message : fallback;
10145
10327
  }
10146
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
+
10147
10423
  // src/front/chat/PiChatPanel.tsx
10148
10424
  import { jsx as jsx36, jsxs as jsxs31 } from "react/jsx-runtime";
10149
- 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 })));
10150
10426
  var EMPTY_COMMANDS = [];
10151
10427
  var EMPTY_COMMAND_NAMES = [];
10152
10428
  var EMPTY_BLOCKERS = [];
@@ -10282,6 +10558,7 @@ function PiChatPanel({
10282
10558
  const selectedSessionPending = Boolean(activeSessionId && !selectedChatState);
10283
10559
  const modelDiscoveryEnabled = serverResourcesEnabled && availableModels === void 0;
10284
10560
  const modelDiscovery = useChatModelSelection({
10561
+ agentTypeId,
10285
10562
  apiBaseUrl,
10286
10563
  defaultModel,
10287
10564
  fetch: fetch2,
@@ -10385,6 +10662,7 @@ function PiChatPanel({
10385
10662
  return next;
10386
10663
  }, [apiBaseUrl, commands, contributedCommands, excludeBuiltinCommands, extraCommands, hotReloadEnabled, normalizedRequestHeaders, serverResourcesEnabled, serverSkillsRefreshKey, storageScope]);
10387
10664
  const commandsStamp = useServerCommands({
10665
+ agentTypeId,
10388
10666
  registry,
10389
10667
  requestHeaders: normalizedRequestHeaders,
10390
10668
  sessionId: activeSessionId ?? "default",
@@ -10414,7 +10692,8 @@ function PiChatPanel({
10414
10692
  const queuePreview = selectedChatState ? selectQueuePreview(selectedChatState) : [];
10415
10693
  const messages = canonicalMessages;
10416
10694
  const userHistory = useMemo16(() => selectComposerHistoryFromCanonicalUsers(canonicalMessages), [canonicalMessages]);
10417
- 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;
10418
10697
  const emptyHero = emptyPlacement === "hero" && messages.length === 0 && queuePreview.length === 0 && !emptyStateHydrating;
10419
10698
  const debugState = selectedPiSession?.getDebugState();
10420
10699
  const composerBlocked = workspaceWarmupBlocked || activeBlockers.length > 0;
@@ -10430,8 +10709,9 @@ function PiChatPanel({
10430
10709
  text: `Large chat state: ${debugState.largeStateWarning.messageCount} messages, approximately ${debugState.largeStateWarning.approxBytes} bytes.`,
10431
10710
  dismissible: true
10432
10711
  }] : [];
10433
- return [...fromState, ...sessionNotice, ...largeStateNotice, ...localNotices].filter((notice) => !dismissedNoticeIds.has(notice.id));
10434
- }, [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]);
10435
10715
  const addLocalNotice = useCallback18((notice) => {
10436
10716
  setLocalNotices((previous) => {
10437
10717
  const next = previous.filter((candidate) => candidate.id !== notice.id);
@@ -10462,11 +10742,11 @@ function PiChatPanel({
10462
10742
  if (resetInProgressRef.current) return;
10463
10743
  if (autoCreateInFlightRef.current) return;
10464
10744
  autoCreateInFlightRef.current = true;
10465
- void sessions.create().catch((error) => {
10745
+ void sessions.create({ resumeSessionId: sessions.resumeSessionId }).catch((error) => {
10466
10746
  autoCreateInFlightRef.current = false;
10467
10747
  addLocalNotice({ id: "session-auto-create-error", level: "error", text: errorMessage2(error, "Could not create a chat session."), dismissible: true });
10468
10748
  });
10469
- }, [activeSessionId, addLocalNotice, externalSessionId, sessionList.length, sessions.create, sessionsError, sessionsLoading]);
10749
+ }, [activeSessionId, addLocalNotice, externalSessionId, sessionList.length, sessions.create, sessions.resumeSessionId, sessionsError, sessionsLoading]);
10470
10750
  useEffect21(() => {
10471
10751
  if (externalSessionId || sessionsError || activeSessionId || sessionList.length > 0) {
10472
10752
  autoCreateInFlightRef.current = false;
@@ -10914,14 +11194,18 @@ function PiChatPanel({
10914
11194
  }, [isStreaming]);
10915
11195
  useEffect21(() => {
10916
11196
  if (typeof window === "undefined" || !activeChatSessionId) return;
10917
- window.dispatchEvent(new CustomEvent("boring:chat-session-status", {
11197
+ const emitStatus = () => window.dispatchEvent(new CustomEvent("boring:chat-session-status", {
10918
11198
  detail: {
11199
+ ...workspaceId ? { workspaceId } : {},
10919
11200
  sessionId: activeChatSessionId,
10920
11201
  ...agentTypeId ? { agentTypeId } : {},
10921
11202
  working: isStreaming
10922
11203
  }
10923
11204
  }));
10924
- }, [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]);
10925
11209
  const onTextareaKeyDown = useCallback18((event) => {
10926
11210
  if (event.key === "Escape" && isStreaming) {
10927
11211
  if (event.defaultPrevented || mentionState !== null || slashQuery !== null) {
@@ -11011,6 +11295,7 @@ function PiChatPanel({
11011
11295
  {
11012
11296
  "data-boring-agent": "",
11013
11297
  "data-boring-agent-part": "chat",
11298
+ "data-agent-type-id": agentTypeId,
11014
11299
  "data-pi-chat-session-id": activeSessionId,
11015
11300
  "data-pi-chat-connection": debugState?.connection ?? "disconnected",
11016
11301
  "data-pi-chat-last-seq": debugState?.lastSeq ?? 0,
@@ -11089,6 +11374,7 @@ function PiChatPanel({
11089
11374
  debug ? /* @__PURE__ */ jsx36(Suspense, { fallback: null, children: /* @__PURE__ */ jsx36("div", { "aria-label": "Chat debug metadata", className: "contents", role: "region", children: /* @__PURE__ */ jsx36(
11090
11375
  DebugDrawer2,
11091
11376
  {
11377
+ agentTypeId,
11092
11378
  apiBaseUrl,
11093
11379
  fetch: fetch2,
11094
11380
  sessionId: activeSessionId ?? activeChatSessionId ?? "unknown",
@@ -11105,6 +11391,207 @@ function PiChatPanel({
11105
11391
  ) });
11106
11392
  }
11107
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
+
11108
11595
  // src/front/commands.ts
11109
11596
  var AGENT_COMMAND_SOURCE = "agent";
11110
11597
  function getAgentCommands(options = {}) {
@@ -11150,6 +11637,7 @@ export {
11150
11637
  ConversationContent,
11151
11638
  ConversationScrollButton,
11152
11639
  DebugDrawer,
11640
+ GatewayResponseError,
11153
11641
  Message,
11154
11642
  MessageAction,
11155
11643
  MessageActions,
@@ -11165,6 +11653,7 @@ export {
11165
11653
  PromptInputFooter,
11166
11654
  PromptInputSubmit,
11167
11655
  PromptInputTextarea,
11656
+ RUNTIME_SCOPE_MISMATCH_MESSAGE,
11168
11657
  Reasoning,
11169
11658
  ReasoningContent,
11170
11659
  ReasoningTrigger,
@@ -11175,12 +11664,15 @@ export {
11175
11664
  ToolHeader,
11176
11665
  activeSessionStorageKey,
11177
11666
  builtinCommands,
11178
- clearActiveSessionId,
11179
11667
  cn,
11180
11668
  createCommandRegistry,
11181
11669
  defaultChatSuggestions,
11182
11670
  defaultToolRenderers,
11671
+ errorResponseCode,
11672
+ gatewayResponseError,
11673
+ gatewayResponseErrorFromBody,
11183
11674
  getAgentCommands,
11675
+ isRuntimeScopeMismatchError,
11184
11676
  matchPiSessionSearch,
11185
11677
  mergeShadcnToolRenderers,
11186
11678
  mergeToolRenderers,
@@ -11190,6 +11682,7 @@ export {
11190
11682
  resolveToolRenderer,
11191
11683
  searchPiSessions,
11192
11684
  uploadFile,
11685
+ useAddressedAgentSelection,
11193
11686
  useOpenArtifact,
11194
11687
  usePiSessions,
11195
11688
  writeActiveSessionId