@hachej/boring-agent 0.1.103 → 0.1.105

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.
@@ -31,7 +31,7 @@ import {
31
31
  WORKSPACE_COMMAND_NOTIFY_EVENT,
32
32
  extractToolUiMetadata,
33
33
  sanitizeToolUiMetadata
34
- } from "../chunk-7QM22DU7.js";
34
+ } from "../chunk-6DRHERVK.js";
35
35
  import "../chunk-YGRQ4Q5P.js";
36
36
  import {
37
37
  ErrorCode
@@ -85,7 +85,7 @@ function rawWorkspaceFileUrl(path, opts) {
85
85
  }
86
86
 
87
87
  // src/front/chat/PiChatPanel.tsx
88
- import { lazy, Suspense, useCallback as useCallback18, useEffect as useEffect21, useMemo as useMemo16, useRef as useRef18, useState as useState20 } from "react";
88
+ import { lazy, Suspense, useCallback as useCallback18, useEffect as useEffect21, useMemo as useMemo16, useRef as useRef17, useState as useState20 } from "react";
89
89
 
90
90
  // src/front/chatPanelSettings.ts
91
91
  var DEFAULT_THINKING = "medium";
@@ -280,6 +280,14 @@ function createCommandRegistry(initial) {
280
280
  }
281
281
 
282
282
  // src/front/slashCommands/builtins.ts
283
+ var MAX_RELOAD_MODEL_MESSAGE = 2e3;
284
+ function reloadResultForModel(message) {
285
+ const body = message.trim() || "No result reported.";
286
+ const bounded = body.length > MAX_RELOAD_MODEL_MESSAGE ? `${body.slice(0, MAX_RELOAD_MODEL_MESSAGE)}
287
+ \u2026 (truncated)` : body;
288
+ return `/reload result:
289
+ ${bounded}`;
290
+ }
283
291
  var builtinCommands = [
284
292
  {
285
293
  name: "reset",
@@ -301,9 +309,9 @@ var builtinCommands = [
301
309
  name: "reload",
302
310
  description: "Reload agent plugins",
303
311
  clickBehavior: "execute",
304
- handler(_, ctx) {
305
- if (ctx.pluginUpdate) return ctx.pluginUpdate.run();
306
- return ctx.reloadAgentPlugins();
312
+ async handler(_, ctx) {
313
+ const message = ctx.pluginUpdate ? await ctx.pluginUpdate.run() : await ctx.reloadAgentPlugins();
314
+ return { message, modelMessage: reloadResultForModel(message) };
307
315
  }
308
316
  },
309
317
  {
@@ -2126,7 +2134,7 @@ function useComposerPickers({
2126
2134
  }
2127
2135
 
2128
2136
  // src/front/hooks/useChatModelSelection.ts
2129
- import { useCallback as useCallback6, useEffect as useEffect5, useMemo as useMemo2, useRef as useRef6, useState as useState7 } from "react";
2137
+ import { useCallback as useCallback6, useEffect as useEffect5, useMemo as useMemo2, useState as useState7 } from "react";
2130
2138
 
2131
2139
  // src/front/browserFiles.ts
2132
2140
  var convertBlobUrlToDataUrl = async (url) => {
@@ -2365,7 +2373,7 @@ var PiComposerPolicyController = class {
2365
2373
  if (command?.kind === "skill") {
2366
2374
  return this.submitExpandedText(skillCommandText(parsed.name, parsed.args), source, false);
2367
2375
  }
2368
- if (command) return this.runLocalCommand(parsed.name, parsed.args);
2376
+ if (command) return this.runLocalCommand(parsed.name, parsed.args, source);
2369
2377
  }
2370
2378
  if (isPiChatBusy(this.options.session.getState().status) && files.length > 0) {
2371
2379
  return this.fromQueueResult(await this.queueController.submit({ text, attachments: this.toAttachmentPayloads(files) }));
@@ -2408,7 +2416,7 @@ var PiComposerPolicyController = class {
2408
2416
  ...this.options.thinkingControl ? { thinkingLevel: this.options.thinkingLevel ?? DEFAULT_THINKING } : {}
2409
2417
  }));
2410
2418
  }
2411
- async runLocalCommand(commandName, args) {
2419
+ async runLocalCommand(commandName, args, source) {
2412
2420
  const command = this.options.registry.get(commandName);
2413
2421
  if (isPiChatBusy(this.options.session.getState().status) && command?.allowWhileBusy?.(args) !== true) {
2414
2422
  return this.block("busy-slash-command", "Slash commands are not queued while the agent is responding.");
@@ -2417,6 +2425,10 @@ var PiComposerPolicyController = class {
2417
2425
  const message = typeof result === "string" ? result : result && typeof result === "object" ? result.message : void 0;
2418
2426
  const preserveDraft = Boolean(result && typeof result === "object" && result.preserveDraft === true);
2419
2427
  if (message) this.options.onCommandResult?.(message);
2428
+ const modelMessage = result && typeof result === "object" ? result.modelMessage?.trim() : void 0;
2429
+ if (modelMessage) {
2430
+ return await this.submitExpandedText(modelMessage, source, false);
2431
+ }
2420
2432
  return { type: "command", command: commandName, ...message ? { result: message } : {}, preserveDraft };
2421
2433
  }
2422
2434
  async runBeforeSubmit(draft, files, source) {
@@ -2550,35 +2562,59 @@ function resolveStorage(storage) {
2550
2562
  }
2551
2563
 
2552
2564
  // src/front/hooks/useChatModelSelection.ts
2565
+ function sameModel(left, right) {
2566
+ return left?.provider === right?.provider && left?.id === right?.id;
2567
+ }
2568
+ function availableModel(models, model) {
2569
+ return Boolean(model && models.some((candidate) => candidate.available && candidate.provider === model.provider && candidate.id === model.id));
2570
+ }
2553
2571
  function useChatModelSelection({
2554
2572
  agentTypeId,
2555
2573
  apiBaseUrl,
2556
2574
  defaultModel,
2575
+ sessionId,
2576
+ sessionHydrated = sessionId === void 0,
2577
+ sessionIsNew = sessionId === void 0,
2578
+ sessionModel,
2557
2579
  fetch: fetchImpl,
2558
2580
  requestHeaders,
2559
2581
  storage,
2560
2582
  storageScope,
2561
2583
  enabled = true
2562
2584
  }) {
2563
- const initialModelState = useMemo2(() => readPiComposerSettings({ storageScope, storage }), []);
2564
- const [model, setModelState] = useState7(
2565
- () => initialModelState.model ?? defaultModel ?? null
2566
- );
2567
- const [userSelectedModel, setUserSelectedModel] = useState7(
2568
- () => initialModelState.userSelectedModel
2569
- );
2570
- const userSelectedModelRef = useRef6(userSelectedModel);
2571
- const loadedSettingsSourceRef = useRef6({ storage, storageScope });
2585
+ const initialSettings = useMemo2(() => readPiComposerSettings({ storageScope, storage }), []);
2586
+ const [selection, setSelection] = useState7(() => ({
2587
+ sessionId,
2588
+ storage,
2589
+ storageScope,
2590
+ localDefault: initialSettings.model ?? defaultModel ?? null
2591
+ }));
2572
2592
  useEffect5(() => {
2573
- userSelectedModelRef.current = userSelectedModel;
2574
- }, [userSelectedModel]);
2593
+ const settings = readPiComposerSettings({ storageScope, storage });
2594
+ setSelection((current) => {
2595
+ const sameSession = current.sessionId === sessionId;
2596
+ const pendingOverride2 = sameSession && current.pendingOverride && sessionModel && !sameModel(current.pendingOverride, sessionModel) ? current.pendingOverride : sessionIsNew && sessionModel && settings.model && !sameModel(settings.model, sessionModel) ? settings.model : void 0;
2597
+ return {
2598
+ sessionId,
2599
+ storage,
2600
+ storageScope,
2601
+ localDefault: settings.model ?? defaultModel ?? null,
2602
+ ...pendingOverride2 ? { pendingOverride: pendingOverride2 } : {}
2603
+ };
2604
+ });
2605
+ }, [defaultModel, sessionId, sessionIsNew, sessionModel?.id, sessionModel?.provider, storage, storageScope]);
2575
2606
  const setModel = useCallback6((next) => {
2576
2607
  const normalized = next === null ? null : parseModelSelection(next);
2577
- userSelectedModelRef.current = normalized !== null;
2578
- setUserSelectedModel(normalized !== null);
2579
- setModelState(normalized);
2608
+ setSelection((current) => {
2609
+ if (sessionModel) {
2610
+ const pendingOverride2 = normalized && !sameModel(normalized, sessionModel) ? normalized : void 0;
2611
+ return { ...current, sessionId, ...pendingOverride2 ? { pendingOverride: pendingOverride2 } : { pendingOverride: void 0 } };
2612
+ }
2613
+ if (sessionId && !sessionIsNew) return current;
2614
+ return { ...current, sessionId, localDefault: normalized, pendingOverride: void 0 };
2615
+ });
2580
2616
  writePiComposerModelSelection(normalized, { storageScope, storage });
2581
- }, [storage, storageScope]);
2617
+ }, [sessionId, sessionIsNew, sessionModel, storage, storageScope]);
2582
2618
  const discoveryKey = useMemo2(
2583
2619
  () => JSON.stringify({
2584
2620
  apiBaseUrl: apiBaseUrl ?? "",
@@ -2591,22 +2627,6 @@ function useChatModelSelection({
2591
2627
  const [availableModels, setAvailableModels] = useState7([]);
2592
2628
  const [loaded, setLoaded] = useState7(!enabled);
2593
2629
  const [loadedDiscoveryKey, setLoadedDiscoveryKey] = useState7(enabled ? null : discoveryKey);
2594
- useEffect5(() => {
2595
- if (loadedSettingsSourceRef.current.storage !== storage || loadedSettingsSourceRef.current.storageScope !== storageScope) return;
2596
- if (!userSelectedModel || !model) return;
2597
- writePiComposerModelSelection(model, { storageScope, storage });
2598
- }, [model, storage, storageScope, userSelectedModel]);
2599
- useEffect5(() => {
2600
- const settings = readPiComposerSettings({ storageScope, storage });
2601
- loadedSettingsSourceRef.current = { storage, storageScope };
2602
- userSelectedModelRef.current = settings.userSelectedModel;
2603
- setUserSelectedModel(settings.userSelectedModel);
2604
- setModelState(settings.model ?? defaultModel ?? null);
2605
- }, [storage, storageScope]);
2606
- useEffect5(() => {
2607
- if (userSelectedModelRef.current || !defaultModel) return;
2608
- setModelState(defaultModel);
2609
- }, [defaultModel]);
2610
2630
  useEffect5(() => {
2611
2631
  if (!enabled) {
2612
2632
  setLoaded(true);
@@ -2618,46 +2638,44 @@ function useChatModelSelection({
2618
2638
  const nextFetch = fetchImpl ?? globalThis.fetch.bind(globalThis);
2619
2639
  nextFetch(agentResourceUrl(apiBaseUrl, `/api/v1/agents/${encodeURIComponent(agentTypeId)}/models`), {
2620
2640
  headers: withStorageScope(requestHeaders, storageScope)
2621
- }).then((res) => res.ok ? res.json() : null).then((payload) => {
2641
+ }).then((response) => response.ok ? response.json() : null).then((payload) => {
2622
2642
  if (aborted) return;
2623
- if (!payload?.models) {
2624
- userSelectedModelRef.current = false;
2625
- setUserSelectedModel(false);
2643
+ const models = payload?.models;
2644
+ if (!models) {
2626
2645
  setAvailableModels([]);
2627
- setModelState(null);
2628
- writePiComposerModelSelection(null, { storageScope, storage });
2646
+ setSelection((current) => ({ ...current, pendingOverride: void 0, localDefault: sessionModel ?? null }));
2647
+ if (!sessionModel) writePiComposerModelSelection(null, { storageScope, storage });
2629
2648
  setLoadedDiscoveryKey(discoveryKey);
2630
2649
  setLoaded(true);
2631
2650
  return;
2632
2651
  }
2633
- setAvailableModels(payload.models);
2652
+ setAvailableModels(models);
2653
+ setSelection((current) => {
2654
+ const selected = current.pendingOverride ?? sessionModel ?? current.localDefault;
2655
+ if (availableModel(models, selected)) return current;
2656
+ if (sessionModel) return { ...current, pendingOverride: void 0 };
2657
+ if (sessionId && !sessionIsNew) return { ...current, pendingOverride: void 0, localDefault: null };
2658
+ const firstAvailable = models.find((candidate) => candidate.available);
2659
+ return {
2660
+ ...current,
2661
+ pendingOverride: void 0,
2662
+ localDefault: payload.defaultModel ?? (firstAvailable ? { provider: firstAvailable.provider, id: firstAvailable.id } : null)
2663
+ };
2664
+ });
2634
2665
  setLoadedDiscoveryKey(discoveryKey);
2635
2666
  setLoaded(true);
2636
- const available = payload.models.filter((m) => m.available);
2637
- setModelState((current) => {
2638
- const currentAvailable = current ? available.some((m) => m.provider === current.provider && m.id === current.id) : false;
2639
- if (currentAvailable) return current;
2640
- userSelectedModelRef.current = false;
2641
- setUserSelectedModel(false);
2642
- writePiComposerModelSelection(null, { storageScope, storage });
2643
- if (payload.defaultModel) return { provider: payload.defaultModel.provider, id: payload.defaultModel.id };
2644
- const firstAvailable = available[0];
2645
- return firstAvailable ? { provider: firstAvailable.provider, id: firstAvailable.id } : null;
2646
- });
2647
2667
  }).catch(() => {
2648
2668
  if (aborted) return;
2649
- userSelectedModelRef.current = false;
2650
- setUserSelectedModel(false);
2651
2669
  setAvailableModels([]);
2652
- setModelState(null);
2653
- writePiComposerModelSelection(null, { storageScope, storage });
2670
+ setSelection((current) => ({ ...current, pendingOverride: void 0, localDefault: sessionModel ?? null }));
2671
+ if (!sessionModel) writePiComposerModelSelection(null, { storageScope, storage });
2654
2672
  setLoadedDiscoveryKey(discoveryKey);
2655
2673
  setLoaded(true);
2656
2674
  });
2657
2675
  return () => {
2658
2676
  aborted = true;
2659
2677
  };
2660
- }, [agentTypeId, apiBaseUrl, discoveryKey, enabled, fetchImpl, requestHeaders, storage, storageScope]);
2678
+ }, [agentTypeId, apiBaseUrl, discoveryKey, enabled, fetchImpl, requestHeaders, sessionId, sessionIsNew, sessionModel, storage, storageScope]);
2661
2679
  useEffect5(() => {
2662
2680
  const onChange = (event) => {
2663
2681
  const next = parseModelSelection(event.detail);
@@ -2667,13 +2685,23 @@ function useChatModelSelection({
2667
2685
  return () => globalThis.removeEventListener?.("boring:model-change", onChange);
2668
2686
  }, [setModel]);
2669
2687
  const currentDiscoveryLoaded = !enabled || loaded && loadedDiscoveryKey === discoveryKey;
2670
- const currentAvailableModels = currentDiscoveryLoaded ? availableModels : [];
2671
- const currentModel = currentDiscoveryLoaded ? model : null;
2672
- return { availableModels: currentAvailableModels, loaded: currentDiscoveryLoaded, model: currentModel, setModel };
2688
+ const selectionBelongsToSession = selection.sessionId === sessionId && selection.storage === storage && selection.storageScope === storageScope;
2689
+ const pendingOverride = selectionBelongsToSession ? selection.pendingOverride : void 0;
2690
+ const isOverride = Boolean(pendingOverride && sessionModel && !sameModel(pendingOverride, sessionModel));
2691
+ const sessionAuthorityReady = !sessionId || sessionHydrated && (sessionModel !== void 0 || sessionIsNew);
2692
+ const model = currentDiscoveryLoaded && sessionAuthorityReady ? pendingOverride ?? sessionModel ?? selection.localDefault : null;
2693
+ return {
2694
+ availableModels: currentDiscoveryLoaded ? availableModels : [],
2695
+ loaded: currentDiscoveryLoaded,
2696
+ model,
2697
+ sessionModel,
2698
+ isOverride,
2699
+ setModel
2700
+ };
2673
2701
  }
2674
2702
 
2675
2703
  // src/front/hooks/useServerCommands.ts
2676
- import { useEffect as useEffect6, useRef as useRef7, useState as useState8 } from "react";
2704
+ import { useEffect as useEffect6, useRef as useRef6, useState as useState8 } from "react";
2677
2705
  function serverCommandErrorMessage(body, fallback) {
2678
2706
  if (typeof body.error === "string") return body.error;
2679
2707
  if (body.error && typeof body.error === "object" && typeof body.error.message === "string") return body.error.message;
@@ -2725,10 +2753,10 @@ function useServerCommands({
2725
2753
  refreshKey = 0
2726
2754
  }) {
2727
2755
  const [stamp, setStamp] = useState8(0);
2728
- const registrationsRef = useRef7({ registry, identity: void 0, names: /* @__PURE__ */ new Set() });
2756
+ const registrationsRef = useRef6({ registry, identity: void 0, names: /* @__PURE__ */ new Set() });
2729
2757
  const canonicalSessionId = sessionId.trim() || void 0;
2730
2758
  const identityKey = canonicalSessionId ? `${agentTypeId}\0${canonicalSessionId}` : void 0;
2731
- const identityKeyRef = useRef7(identityKey);
2759
+ const identityKeyRef = useRef6(identityKey);
2732
2760
  identityKeyRef.current = identityKey;
2733
2761
  useEffect6(() => () => {
2734
2762
  const registrations = registrationsRef.current;
@@ -3760,6 +3788,8 @@ function piChatReducer(state, action) {
3760
3788
  return hydrateFromSnapshot(state, action.snapshot, { allowSeqRewind: action.allowSeqRewind });
3761
3789
  case "cursor-sync":
3762
3790
  return syncCursor(state, action.cursor);
3791
+ case "model-confirmed":
3792
+ return { ...state, currentModel: action.model };
3763
3793
  case "event":
3764
3794
  return applySequencedEvent(state, action.event);
3765
3795
  case "optimistic-user-message":
@@ -3856,6 +3886,7 @@ function hydrateFromSnapshot(state, snapshot, options = {}) {
3856
3886
  ...state,
3857
3887
  sessionId: snapshot.sessionId,
3858
3888
  status: snapshot.status,
3889
+ currentModel: snapshot.currentModel,
3859
3890
  turnId: snapshot.activeTurnId,
3860
3891
  lastSeq: snapshot.seq,
3861
3892
  committedMessages,
@@ -3906,6 +3937,8 @@ function applySequencedEvent(state, event) {
3906
3937
  }
3907
3938
  function reduceEvent(state, event) {
3908
3939
  switch (event.type) {
3940
+ case "model-changed":
3941
+ return { ...state, currentModel: event.currentModel };
3909
3942
  case "agent-start":
3910
3943
  return { ...state, status: "streaming", turnId: event.turnId, error: void 0, streamingPreservedTextPartKeys: void 0 };
3911
3944
  case "agent-end":
@@ -4638,6 +4671,9 @@ var RemotePiSession = class {
4638
4671
  else if (!this.suspended) this.ensureReconnectScheduled();
4639
4672
  try {
4640
4673
  const receipt = await this.postCommand("/prompt", payload, PromptReceiptSchema);
4674
+ if (!this.disposed && payload.model) {
4675
+ this.store.dispatch({ type: "model-confirmed", model: payload.model }, { flush: true });
4676
+ }
4641
4677
  return receipt;
4642
4678
  } catch (error) {
4643
4679
  this.rollbackOptimisticMessage(payload.clientNonce);
@@ -5212,7 +5248,7 @@ function resolveBootStorage(storage) {
5212
5248
  }
5213
5249
 
5214
5250
  // src/front/chat/session/usePiSessions.ts
5215
- import { useCallback as useCallback7, useEffect as useEffect8, useLayoutEffect, useMemo as useMemo4, useRef as useRef8, useState as useState10 } from "react";
5251
+ import { useCallback as useCallback7, useEffect as useEffect8, useLayoutEffect, useMemo as useMemo4, useRef as useRef7, useState as useState10 } from "react";
5216
5252
  var SESSION_PAGE_SIZE = 50;
5217
5253
  var DEFAULT_MAX_RETRIES = 60;
5218
5254
  var DEFAULT_RETRY_BASE_MS = 250;
@@ -5275,22 +5311,22 @@ function usePiSessions(options) {
5275
5311
  const [loadingMore, setLoadingMore] = useState10(false);
5276
5312
  const [hasMore, setHasMore] = useState10(false);
5277
5313
  const [error, setError] = useState10(void 0);
5278
- const mountedRef = useRef8(false);
5279
- const refreshVersionRef = useRef8(0);
5280
- const retryTimerRef = useRef8(void 0);
5281
- const sessionsRef = useRef8([]);
5282
- const activeSessionIdRef = useRef8(activeSessionId);
5283
- const hasMoreRef = useRef8(hasMore);
5284
- const canonicalLoadedCountRef = useRef8(0);
5285
- const nextCursorRef = useRef8(void 0);
5286
- const loadMoreRequestSeqRef = useRef8(0);
5287
- const loadMoreInFlightRef = useRef8(false);
5288
- const pendingCreatedRef = useRef8(/* @__PURE__ */ new Map());
5289
- const pendingDeletedRef = useRef8(/* @__PURE__ */ new Set());
5290
- const pendingCreatedScopeRef = useRef8(requestScopeKey);
5291
- const dataStorageScopeRef = useRef8(storageScope);
5292
- const loadedDataSourceRef = useRef8(dataSourceKey);
5293
- const committedRequestScopeRef = useRef8(requestScopeKey);
5314
+ const mountedRef = useRef7(false);
5315
+ const refreshVersionRef = useRef7(0);
5316
+ const retryTimerRef = useRef7(void 0);
5317
+ const sessionsRef = useRef7([]);
5318
+ const activeSessionIdRef = useRef7(activeSessionId);
5319
+ const hasMoreRef = useRef7(hasMore);
5320
+ const canonicalLoadedCountRef = useRef7(0);
5321
+ const nextCursorRef = useRef7(void 0);
5322
+ const loadMoreRequestSeqRef = useRef7(0);
5323
+ const loadMoreInFlightRef = useRef7(false);
5324
+ const pendingCreatedRef = useRef7(/* @__PURE__ */ new Map());
5325
+ const pendingDeletedRef = useRef7(/* @__PURE__ */ new Set());
5326
+ const pendingCreatedScopeRef = useRef7(requestScopeKey);
5327
+ const dataStorageScopeRef = useRef7(storageScope);
5328
+ const loadedDataSourceRef = useRef7(dataSourceKey);
5329
+ const committedRequestScopeRef = useRef7(requestScopeKey);
5294
5330
  useIsomorphicLayoutEffect(() => {
5295
5331
  committedRequestScopeRef.current = requestScopeKey;
5296
5332
  }, [requestScopeKey]);
@@ -5298,7 +5334,7 @@ function usePiSessions(options) {
5298
5334
  (scope) => mountedRef.current && committedRequestScopeRef.current === scope,
5299
5335
  []
5300
5336
  );
5301
- const remoteSessionOptionsRef = useRef8(options.remoteSessionOptions);
5337
+ const remoteSessionOptionsRef = useRef7(options.remoteSessionOptions);
5302
5338
  remoteSessionOptionsRef.current = options.remoteSessionOptions;
5303
5339
  const remoteSessionOptionsKey = useMemo4(
5304
5340
  () => remoteSessionOptionsIdentity(options.remoteSessionOptions),
@@ -6149,7 +6185,7 @@ function searchPiSessions(sessions, query, options = {}) {
6149
6185
 
6150
6186
  // src/front/chat/components/PiConversationSurface.tsx
6151
6187
  import { Skeleton } from "@hachej/boring-ui-kit";
6152
- import { useCallback as useCallback14, useEffect as useEffect14, useLayoutEffect as useLayoutEffect2, useRef as useRef11, useState as useState15 } from "react";
6188
+ import { useCallback as useCallback14, useEffect as useEffect14, useLayoutEffect as useLayoutEffect2, useRef as useRef10, useState as useState15 } from "react";
6153
6189
  import { useStickToBottomContext as useStickToBottomContext2 } from "use-stick-to-bottom";
6154
6190
 
6155
6191
  // src/front/primitives/conversation.tsx
@@ -6497,7 +6533,7 @@ function QueuedComposerNotice({ followUps, onEdit }) {
6497
6533
  }
6498
6534
 
6499
6535
  // src/front/chat/components/PiTimelineMessage.tsx
6500
- import { useCallback as useCallback13, useEffect as useEffect13, useMemo as useMemo12, useRef as useRef10, useState as useState14 } from "react";
6536
+ import { useCallback as useCallback13, useEffect as useEffect13, useMemo as useMemo12, useRef as useRef9, useState as useState14 } from "react";
6501
6537
  import { CheckIcon as CheckIcon2, CopyIcon as CopyIcon3 } from "lucide-react";
6502
6538
  import { Button as Button10 } from "@hachej/boring-ui-kit";
6503
6539
 
@@ -6921,7 +6957,7 @@ import {
6921
6957
  useContext as useContext5,
6922
6958
  useEffect as useEffect11,
6923
6959
  useMemo as useMemo9,
6924
- useRef as useRef9,
6960
+ useRef as useRef8,
6925
6961
  useState as useState12
6926
6962
  } from "react";
6927
6963
  import { Streamdown as Streamdown2 } from "streamdown";
@@ -7013,9 +7049,9 @@ var Reasoning = memo5(
7013
7049
  defaultProp: void 0,
7014
7050
  prop: durationProp
7015
7051
  });
7016
- const hasEverStreamedRef = useRef9(isStreaming);
7052
+ const hasEverStreamedRef = useRef8(isStreaming);
7017
7053
  const [hasAutoClosed, setHasAutoClosed] = useState12(false);
7018
- const startTimeRef = useRef9(null);
7054
+ const startTimeRef = useRef8(null);
7019
7055
  useEffect11(() => {
7020
7056
  if (isStreaming) {
7021
7057
  hasEverStreamedRef.current = true;
@@ -7606,7 +7642,7 @@ function DefaultPiTimelineMessage({ message, isLast, isStreaming, showThoughts,
7606
7642
  ...effectiveMentionCatalog.skills.map((skill) => `skill:${skill.name}:${skill.commandName}`),
7607
7643
  effectiveMentionCatalog.files ? "files:enabled" : "files:disabled"
7608
7644
  ].join("|");
7609
- const mentionActivateRef = useRef10(onMentionActivate);
7645
+ const mentionActivateRef = useRef9(onMentionActivate);
7610
7646
  mentionActivateRef.current = onMentionActivate;
7611
7647
  const activateCurrentMention = useCallback13((mention) => {
7612
7648
  if (mention.kind === "file") {
@@ -8078,8 +8114,8 @@ function ConversationHistoryLoadingState() {
8078
8114
  }
8079
8115
  function TranscriptHistoryLoader({ olderCount, onLoadOlder }) {
8080
8116
  const { scrollRef } = useStickToBottomContext2();
8081
- const pendingAnchor = useRef11(null);
8082
- const armed = useRef11(true);
8117
+ const pendingAnchor = useRef10(null);
8118
+ const armed = useRef10(true);
8083
8119
  useEffect14(() => {
8084
8120
  const el = scrollRef.current;
8085
8121
  if (!el) return;
@@ -8144,7 +8180,7 @@ import { useCallback as useCallback16, useLayoutEffect as useLayoutEffect3 } fro
8144
8180
  import { motion as motion2, useReducedMotion } from "motion/react";
8145
8181
 
8146
8182
  // src/front/composer/PluginUpdateStatus.tsx
8147
- import { useEffect as useEffect15, useRef as useRef12 } from "react";
8183
+ import { useEffect as useEffect15, useRef as useRef11 } from "react";
8148
8184
 
8149
8185
  // src/front/composer/ComposerStatusBanner.tsx
8150
8186
  import { jsx as jsx25, jsxs as jsxs21 } from "react/jsx-runtime";
@@ -8267,7 +8303,7 @@ function PluginUpdateStatus({
8267
8303
  successAutoDismissMs = 1400,
8268
8304
  maxWidthClassName = "max-w-3xl"
8269
8305
  }) {
8270
- const onDismissRef = useRef12(onDismiss);
8306
+ const onDismissRef = useRef11(onDismiss);
8271
8307
  useEffect15(() => {
8272
8308
  onDismissRef.current = onDismiss;
8273
8309
  }, [onDismiss]);
@@ -8383,7 +8419,7 @@ function PluginUpdateStatus({
8383
8419
  }
8384
8420
 
8385
8421
  // src/front/composer/CommandRunStatus.tsx
8386
- import { useEffect as useEffect16, useRef as useRef13 } from "react";
8422
+ import { useEffect as useEffect16, useRef as useRef12 } from "react";
8387
8423
  import { Fragment as Fragment10, jsx as jsx27, jsxs as jsxs23 } from "react/jsx-runtime";
8388
8424
  function CommandRunStatus({
8389
8425
  state,
@@ -8391,7 +8427,7 @@ function CommandRunStatus({
8391
8427
  successAutoDismissMs = 1400,
8392
8428
  maxWidthClassName = "max-w-3xl"
8393
8429
  }) {
8394
- const onDismissRef = useRef13(onDismiss);
8430
+ const onDismissRef = useRef12(onDismiss);
8395
8431
  useEffect16(() => {
8396
8432
  onDismissRef.current = onDismiss;
8397
8433
  }, [onDismiss]);
@@ -8463,7 +8499,7 @@ function CommandRunStatus({
8463
8499
  }
8464
8500
 
8465
8501
  // src/front/chatPanelComposerControls.tsx
8466
- import { forwardRef, useEffect as useEffect17, useRef as useRef14, useState as useState16 } from "react";
8502
+ import { forwardRef, useEffect as useEffect17, useRef as useRef13, useState as useState16 } from "react";
8467
8503
  import { CheckIcon as CheckIcon3, ChevronDownIcon as ChevronDownIcon5 } from "lucide-react";
8468
8504
  import {
8469
8505
  Command,
@@ -8611,6 +8647,8 @@ function groupModelOptions(options) {
8611
8647
  var ModelSelectTrigger = forwardRef(function ModelSelectTrigger2({
8612
8648
  value,
8613
8649
  options,
8650
+ sessionModel,
8651
+ isOverride = false,
8614
8652
  disabled,
8615
8653
  trigger = "button",
8616
8654
  open = false,
@@ -8620,7 +8658,10 @@ var ModelSelectTrigger = forwardRef(function ModelSelectTrigger2({
8620
8658
  ...props
8621
8659
  }, ref) {
8622
8660
  const triggerLabel = modelTriggerLabel(value, options, emptyLabel);
8661
+ const sessionTriggerLabel = sessionModel ? modelTriggerLabel(sessionModel, options) : void 0;
8623
8662
  const triggerDisplay = value ? `${triggerLabel} (${displayProviderLabel(value.provider)})` : triggerLabel;
8663
+ const sessionDisplay = sessionModel && sessionTriggerLabel ? `${sessionTriggerLabel} (${displayProviderLabel(sessionModel.provider)})` : triggerDisplay;
8664
+ const accessibleDisplay = isOverride && value ? `Current model: ${sessionDisplay}. Next-message override: ${triggerDisplay}` : `Current model: ${sessionDisplay}`;
8624
8665
  return /* @__PURE__ */ jsx28(
8625
8666
  "button",
8626
8667
  {
@@ -8628,9 +8669,10 @@ var ModelSelectTrigger = forwardRef(function ModelSelectTrigger2({
8628
8669
  type: "button",
8629
8670
  "data-boring-agent-part": "model-select",
8630
8671
  "data-boring-state": disabled ? "disabled" : void 0,
8672
+ "data-boring-model-override": isOverride ? "true" : void 0,
8631
8673
  disabled,
8632
- "aria-label": trigger === "slash" ? `Open model picker. Current model: ${triggerDisplay}` : "Model",
8633
- title: trigger === "slash" ? `Open model picker. Current model: ${triggerDisplay}` : void 0,
8674
+ "aria-label": trigger === "slash" ? `Open model picker. ${accessibleDisplay}` : "Model",
8675
+ title: trigger === "slash" ? accessibleDisplay : void 0,
8634
8676
  onClick,
8635
8677
  className: cn(
8636
8678
  trigger === "slash" ? "h-8 max-w-[min(68vw,420px)] cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap rounded-[var(--radius-md)] px-2 text-xs transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-popover disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none" : selectorTriggerClass,
@@ -8641,8 +8683,17 @@ var ModelSelectTrigger = forwardRef(function ModelSelectTrigger2({
8641
8683
  ...props,
8642
8684
  children: trigger === "slash" ? /* @__PURE__ */ jsxs24(Fragment11, { children: [
8643
8685
  /* @__PURE__ */ jsx28("span", { className: "text-muted-foreground", children: "Model \xB7 " }),
8644
- /* @__PURE__ */ jsx28("span", { className: "min-w-0 truncate text-foreground", children: triggerLabel }),
8645
- value ? /* @__PURE__ */ jsxs24("span", { className: "shrink-0 text-muted-foreground", children: [
8686
+ /* @__PURE__ */ jsx28("span", { className: "min-w-0 truncate text-foreground", children: sessionTriggerLabel ?? triggerLabel }),
8687
+ sessionModel ? /* @__PURE__ */ jsxs24("span", { className: "shrink-0 text-muted-foreground", children: [
8688
+ " \xB7 ",
8689
+ displayProviderLabel(sessionModel.provider)
8690
+ ] }) : value ? /* @__PURE__ */ jsxs24("span", { className: "shrink-0 text-muted-foreground", children: [
8691
+ " \xB7 ",
8692
+ displayProviderLabel(value.provider)
8693
+ ] }) : null,
8694
+ isOverride && value ? /* @__PURE__ */ jsxs24("span", { className: "shrink-0 text-amber-600 dark:text-amber-400", children: [
8695
+ " \xB7 Next override: ",
8696
+ triggerLabel,
8646
8697
  " \xB7 ",
8647
8698
  displayProviderLabel(value.provider)
8648
8699
  ] }) : null
@@ -8669,8 +8720,8 @@ function ModelPickerMenu({
8669
8720
  const keyboardOptions = hideDefaultOption ? groupedOptions : [null, ...groupedOptions];
8670
8721
  const selectedIndex = currentKey ? Math.max(0, keyboardOptions.findIndex((option) => option && encodeModelKey(option) === currentKey)) : 0;
8671
8722
  const [activeIndex, setActiveIndex] = useState16(selectedIndex);
8672
- const activeIndexRef = useRef14(selectedIndex);
8673
- const menuRef = useRef14(null);
8723
+ const activeIndexRef = useRef13(selectedIndex);
8724
+ const menuRef = useRef13(null);
8674
8725
  useDismissOnOutsidePointer(menuRef, onClose);
8675
8726
  const setKeyboardActiveIndex = (next) => {
8676
8727
  const resolved = typeof next === "function" ? next(activeIndexRef.current) : next;
@@ -8797,7 +8848,7 @@ function ModelSelect({
8797
8848
  hideDefaultOption = false
8798
8849
  }) {
8799
8850
  const [open, setOpen] = useState16(false);
8800
- const previousOpenSignalRef = useRef14(openSignal);
8851
+ const previousOpenSignalRef = useRef13(openSignal);
8801
8852
  useEffect17(() => {
8802
8853
  if (disabled) setOpen(false);
8803
8854
  }, [disabled]);
@@ -8858,7 +8909,7 @@ function ThinkingSelect({
8858
8909
  className
8859
8910
  }) {
8860
8911
  const [open, setOpen] = useState16(false);
8861
- const previousOpenSignalRef = useRef14(openSignal);
8912
+ const previousOpenSignalRef = useRef13(openSignal);
8862
8913
  useEffect17(() => {
8863
8914
  if (disabled) setOpen(false);
8864
8915
  }, [disabled]);
@@ -8955,8 +9006,8 @@ function ThinkingPickerMenu({
8955
9006
  }) {
8956
9007
  const selectedIndex = Math.max(0, THINKING_LEVELS.indexOf(value));
8957
9008
  const [activeIndex, setActiveIndex] = useState16(selectedIndex);
8958
- const activeIndexRef = useRef14(selectedIndex);
8959
- const menuRef = useRef14(null);
9009
+ const activeIndexRef = useRef13(selectedIndex);
9010
+ const menuRef = useRef13(null);
8960
9011
  useDismissOnOutsidePointer(menuRef, onClose);
8961
9012
  const setKeyboardActiveIndex = (next) => {
8962
9013
  const resolved = typeof next === "function" ? next(activeIndexRef.current) : next;
@@ -9052,7 +9103,7 @@ import {
9052
9103
  XIcon as XIcon4
9053
9104
  } from "lucide-react";
9054
9105
  import { nanoid } from "nanoid";
9055
- import { useCallback as useCallback15, useEffect as useEffect18, useMemo as useMemo13, useRef as useRef15, useState as useState17 } from "react";
9106
+ import { useCallback as useCallback15, useEffect as useEffect18, useMemo as useMemo13, useRef as useRef14, useState as useState17 } from "react";
9056
9107
 
9057
9108
  // src/front/primitives/prompt-input-context.ts
9058
9109
  import { createContext as createContext7, useContext as useContext7 } from "react";
@@ -9119,12 +9170,12 @@ var PromptInput = ({
9119
9170
  }) => {
9120
9171
  const controller = useOptionalPromptInputController();
9121
9172
  const usingProvider = !!controller;
9122
- const providerTextValueRef = useRef15("");
9173
+ const providerTextValueRef = useRef14("");
9123
9174
  if (usingProvider) {
9124
9175
  providerTextValueRef.current = controller.textInput.value;
9125
9176
  }
9126
- const inputRef = useRef15(null);
9127
- const formRef = useRef15(null);
9177
+ const inputRef = useRef14(null);
9178
+ const formRef = useRef14(null);
9128
9179
  const [items, setItems] = useState17([]);
9129
9180
  const setFileUrlLocal = useCallback15((id, url, status, path) => {
9130
9181
  setItems((prev) => prev.map((f) => {
@@ -9135,7 +9186,7 @@ var PromptInput = ({
9135
9186
  }, []);
9136
9187
  const files = usingProvider ? controller.attachments.files : items;
9137
9188
  const [referencedSources, setReferencedSources] = useState17([]);
9138
- const filesRef = useRef15(files);
9189
+ const filesRef = useRef14(files);
9139
9190
  useEffect18(() => {
9140
9191
  filesRef.current = files;
9141
9192
  }, [files]);
@@ -9613,7 +9664,7 @@ var PromptInputSubmit = ({
9613
9664
  };
9614
9665
 
9615
9666
  // src/front/primitives/slash-command-picker.tsx
9616
- import { useEffect as useEffect19, useMemo as useMemo14, useRef as useRef16, useState as useState18 } from "react";
9667
+ import { useEffect as useEffect19, useMemo as useMemo14, useRef as useRef15, useState as useState18 } from "react";
9617
9668
  import { Fragment as Fragment13, jsx as jsx31, jsxs as jsxs27 } from "react/jsx-runtime";
9618
9669
  var ALL_PLUGINS = "__all__";
9619
9670
  function commandGroup(cmd) {
@@ -9625,9 +9676,9 @@ function SlashCommandPicker({ query, commands, onSelect, onDismiss }) {
9625
9676
  const [plugin, setPlugin] = useState18(ALL_PLUGINS);
9626
9677
  const [searchQuery, setSearchQuery] = useState18(query);
9627
9678
  const [activeIdx, setActiveIdx] = useState18(0);
9628
- const containerRef = useRef16(null);
9629
- const listRef = useRef16(null);
9630
- const inputRef = useRef16(null);
9679
+ const containerRef = useRef15(null);
9680
+ const listRef = useRef15(null);
9681
+ const inputRef = useRef15(null);
9631
9682
  useEffect19(() => {
9632
9683
  setSearchQuery(query);
9633
9684
  }, [query]);
@@ -9862,6 +9913,8 @@ function PiChatComposerSurface({
9862
9913
  onDismissSlash,
9863
9914
  modelPickerOpen,
9864
9915
  selectedModel,
9916
+ sessionModel,
9917
+ modelOverride,
9865
9918
  modelOptions,
9866
9919
  modelControlled,
9867
9920
  hideDefaultModelOption = false,
@@ -10172,6 +10225,8 @@ function PiChatComposerSurface({
10172
10225
  ModelSelectTrigger,
10173
10226
  {
10174
10227
  value: selectedModel,
10228
+ sessionModel,
10229
+ isOverride: modelOverride,
10175
10230
  options: modelOptions,
10176
10231
  disabled: isStreaming || modelControlled,
10177
10232
  trigger: "slash",
@@ -10215,7 +10270,7 @@ function getHeaderValue(headers, name) {
10215
10270
  }
10216
10271
 
10217
10272
  // src/front/chat/piChatPanelHooks.ts
10218
- import { useCallback as useCallback17, useEffect as useEffect20, useMemo as useMemo15, useRef as useRef17, useState as useState19, useSyncExternalStore } from "react";
10273
+ import { useCallback as useCallback17, useEffect as useEffect20, useMemo as useMemo15, useRef as useRef16, useState as useState19, useSyncExternalStore } from "react";
10219
10274
 
10220
10275
  // src/front/chat/piChatPanelUtils.ts
10221
10276
  function headersContentKey(headers) {
@@ -10336,7 +10391,7 @@ function useExternalRemotePiSession({
10336
10391
  enabled = true
10337
10392
  }) {
10338
10393
  const [session, setSession] = useState19();
10339
- const remoteSessionOptionsRef = useRef17(remoteSessionOptions);
10394
+ const remoteSessionOptionsRef = useRef16(remoteSessionOptions);
10340
10395
  remoteSessionOptionsRef.current = remoteSessionOptions;
10341
10396
  const requestHeadersKey = headersContentKey(requestHeaders);
10342
10397
  const stableRequestHeaders = useMemo15(
@@ -10489,11 +10544,11 @@ function PiChatPanel({
10489
10544
  }) {
10490
10545
  const externalSessionId = sessionId?.trim() || void 0;
10491
10546
  const showSessionSidebar = showSessions ?? externalSessionId === void 0;
10492
- const onDataRef = useRef18(onData);
10547
+ const onDataRef = useRef17(onData);
10493
10548
  onDataRef.current = onData;
10494
- const onTurnCompleteRef = useRef18(onTurnComplete);
10549
+ const onTurnCompleteRef = useRef17(onTurnComplete);
10495
10550
  onTurnCompleteRef.current = onTurnComplete;
10496
- const sessionListRefreshRef = useRef18(void 0);
10551
+ const sessionListRefreshRef = useRef17(void 0);
10497
10552
  const requestHeadersKey = useMemo16(() => headersContentKey(requestHeaders), [requestHeaders]);
10498
10553
  const normalizedRequestHeaders = useMemo16(() => normalizedHeadersFromContentKey(requestHeadersKey), [requestHeadersKey]);
10499
10554
  const remoteSessionOptionsWithEvents = useMemo16(() => ({
@@ -10557,13 +10612,21 @@ function PiChatPanel({
10557
10612
  agentTypeId,
10558
10613
  apiBaseUrl,
10559
10614
  defaultModel,
10615
+ sessionId: activeSessionId,
10616
+ sessionHydrated: selectedChatState?.hydrated ?? false,
10617
+ sessionIsNew: Boolean(
10618
+ selectedChatState?.hydrated && selectedChatState.history.messageCount === 0 && selectedChatState.committedMessages.length === 0 && selectedChatState.queue.followUps.length === 0 && Object.keys(selectedChatState.optimisticOutbox).length === 0 && !selectedChatState.streamingMessage
10619
+ ),
10620
+ sessionModel: selectedChatState?.currentModel,
10560
10621
  fetch: fetch2,
10561
10622
  requestHeaders: normalizedRequestHeaders,
10562
10623
  storage,
10563
10624
  storageScope,
10564
10625
  enabled: modelDiscoveryEnabled
10565
10626
  });
10627
+ const sessionModel = selectedChatState?.currentModel;
10566
10628
  const selectedModel = model === void 0 ? modelDiscovery.model : model;
10629
+ const modelOverride = model === void 0 ? modelDiscovery.isOverride : Boolean(model && sessionModel && (model.provider !== sessionModel.provider || model.id !== sessionModel.id));
10567
10630
  const modelOptions = useMemo16(
10568
10631
  () => modelOptionsForSelection(availableModels ?? modelDiscovery.availableModels, selectedModel),
10569
10632
  [availableModels, modelDiscovery.availableModels, selectedModel]
@@ -10602,13 +10665,13 @@ function PiChatPanel({
10602
10665
  const [modelPickerOpen, setModelPickerOpen] = useState20(false);
10603
10666
  const [thinkingPickerOpen, setThinkingPickerOpen] = useState20(false);
10604
10667
  const [draft, setDraft] = useState20(() => initialDraft ?? "");
10605
- const draftRef = useRef18(draft);
10668
+ const draftRef = useRef17(draft);
10606
10669
  draftRef.current = draft;
10607
- const initialDraftGuard = useRef18(new InitialDraftAutoSubmitGuard());
10608
- const pendingAutoSubmitSettleRef = useRef18(void 0);
10609
- const acceptedAutoSubmitSettleRef = useRef18(void 0);
10610
- const resetInProgressRef = useRef18(false);
10611
- const autoCreateInFlightRef = useRef18(false);
10670
+ const initialDraftGuard = useRef17(new InitialDraftAutoSubmitGuard());
10671
+ const pendingAutoSubmitSettleRef = useRef17(void 0);
10672
+ const acceptedAutoSubmitSettleRef = useRef17(void 0);
10673
+ const resetInProgressRef = useRef17(false);
10674
+ const autoCreateInFlightRef = useRef17(false);
10612
10675
  const settlePendingAutoSubmit = useCallback18((sessionId2) => {
10613
10676
  const pendingSessionId = pendingAutoSubmitSettleRef.current;
10614
10677
  if (!pendingSessionId || sessionId2 && pendingSessionId !== sessionId2) return false;
@@ -10617,19 +10680,19 @@ function PiChatPanel({
10617
10680
  onAutoSubmitInitialDraftSettled?.();
10618
10681
  return true;
10619
10682
  }, [onAutoSubmitInitialDraftSettled]);
10620
- const prevStatusRef = useRef18("idle");
10621
- const statusRef = useRef18("idle");
10622
- const scrollToBottomRef = useRef18(() => {
10683
+ const prevStatusRef = useRef17("idle");
10684
+ const statusRef = useRef17("idle");
10685
+ const scrollToBottomRef = useRef17(() => {
10623
10686
  });
10624
- const textareaRef = useRef18(null);
10687
+ const textareaRef = useRef17(null);
10625
10688
  const [localNotices, setLocalNotices] = useState20([]);
10626
10689
  const [dismissedNoticeIds, setDismissedNoticeIds] = useState20(() => /* @__PURE__ */ new Set());
10627
10690
  const [pluginUpdateState, setPluginUpdateState] = useState20(null);
10628
10691
  const [commandNotifyState, setCommandNotifyState] = useState20(null);
10629
- const commandRunIdRef = useRef18(0);
10692
+ const commandRunIdRef = useRef17(0);
10630
10693
  const [serverSkillsRefreshKey, setServerSkillsRefreshKey] = useState20(0);
10631
10694
  const [localSubmittedSessionId, setLocalSubmittedSessionId] = useState20();
10632
- const localSubmittedSessionRef = useRef18(void 0);
10695
+ const localSubmittedSessionRef = useRef17(void 0);
10633
10696
  const { attachmentNotice, setAttachmentNotice } = useAttachmentNotice();
10634
10697
  const composerContributions = useComposerContributions();
10635
10698
  const contributedCommands = useMemo16(
@@ -11261,6 +11324,8 @@ function PiChatPanel({
11261
11324
  onDismissSlash: dismissSlash,
11262
11325
  modelPickerOpen,
11263
11326
  selectedModel,
11327
+ sessionModel,
11328
+ modelOverride,
11264
11329
  modelOptions,
11265
11330
  modelControlled: model !== void 0,
11266
11331
  hideDefaultModelOption,