@hachej/boring-agent 0.1.42 → 0.1.44

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.
@@ -13,12 +13,12 @@ import {
13
13
  StopReceiptSchema,
14
14
  extractToolUiMetadata,
15
15
  sanitizeToolUiMetadata
16
- } from "../chunk-VQ7VSSSX.js";
16
+ } from "../chunk-HEEJSOFF.js";
17
17
  import {
18
18
  DebugDrawer,
19
19
  cn,
20
20
  copyTextToClipboard
21
- } from "../chunk-IUJ22EFB.js";
21
+ } from "../chunk-BOIV2I3N.js";
22
22
 
23
23
  // src/front/upload/uploadFile.ts
24
24
  function readAsDataUrl(file) {
@@ -30,14 +30,14 @@ function readAsDataUrl(file) {
30
30
  });
31
31
  }
32
32
  async function uploadFile(file, opts = {}) {
33
- const { apiBaseUrl = "", workspaceRequestId, directory, sourcePath } = opts;
33
+ const { apiBaseUrl = "", workspaceRequestId, directory, sourcePath, fetch: fetchImpl = globalThis.fetch } = opts;
34
34
  const dataUrl = await readAsDataUrl(file);
35
35
  const comma = dataUrl.indexOf(",");
36
36
  const contentBase64 = comma >= 0 ? dataUrl.slice(comma + 1) : "";
37
37
  const headers = { "Content-Type": "application/json" };
38
38
  if (workspaceRequestId) headers["x-boring-workspace-id"] = workspaceRequestId;
39
39
  const base = apiBaseUrl.replace(/\/$/, "");
40
- const res = await fetch(`${base}/api/v1/files/upload`, {
40
+ const res = await fetchImpl(`${base}/api/v1/files/upload`, {
41
41
  method: "POST",
42
42
  headers,
43
43
  credentials: "include",
@@ -2230,7 +2230,8 @@ async function resolveAttachmentUrls(files) {
2230
2230
  return Promise.all(files.map(async (file) => ({
2231
2231
  filename: file.filename,
2232
2232
  mediaType: file.mediaType,
2233
- url: file.url.startsWith("blob:") ? await convertBlobUrlToDataUrl(file.url) ?? file.url : file.url
2233
+ url: file.url.startsWith("blob:") ? await convertBlobUrlToDataUrl(file.url) ?? file.url : file.url,
2234
+ ...typeof file.path === "string" ? { path: file.path } : {}
2234
2235
  })));
2235
2236
  }
2236
2237
  async function readFileAsText(file) {
@@ -2255,14 +2256,18 @@ async function createEnrichedSubmitPayload({
2255
2256
  for (const file of files ?? []) {
2256
2257
  const label = file.filename ?? "attachment";
2257
2258
  const mime = file.mediaType ?? "application/octet-stream";
2259
+ const workspacePath = typeof file.path === "string" ? file.path : void 0;
2260
+ const pathNote = workspacePath ? `
2261
+ Saved in workspace at: ${workspacePath}
2262
+ Use the workspace file/read tools with this path if you need to inspect it.` : "";
2258
2263
  const content = await readFileAsText(file);
2259
2264
  if (content !== null) {
2260
- attachmentSummaries.push(`[attached: ${label} (${mime})]
2265
+ attachmentSummaries.push(`[attached: ${label} (${mime})${pathNote}]
2261
2266
  \`\`\`
2262
2267
  ${content}
2263
2268
  \`\`\``);
2264
2269
  } else {
2265
- attachmentSummaries.push(`[attached: ${label} (${mime}, not inlined \u2014 binary)]`);
2270
+ attachmentSummaries.push(`[attached: ${label} (${mime}, not inlined \u2014 binary)${pathNote}]`);
2266
2271
  }
2267
2272
  }
2268
2273
  const mentionNote = mentionedFiles.length > 0 ? `@files: ${mentionedFiles.join(", ")}` : null;
@@ -2558,10 +2563,8 @@ function writePiComposerThinking(value, options = {}) {
2558
2563
  function writePiComposerShowThoughts(value, options = {}) {
2559
2564
  writeStorage(options, "show-thoughts", value ? "1" : "0");
2560
2565
  }
2561
- function modelOptionsForSelection(options, selected) {
2562
- if (!selected) return options.filter((model) => model.available);
2563
- const available = options.filter((model) => model.available);
2564
- return available.some((model) => model.provider === selected.provider && model.id === selected.id) ? available : [{ ...selected, label: selected.id, available: true }, ...available];
2566
+ function modelOptionsForSelection(options, _selected) {
2567
+ return options.filter((model) => model.available);
2565
2568
  }
2566
2569
  function readStoredScopedModel(options) {
2567
2570
  const userSelected = readStorage(options, "model:user-selected") === "1";
@@ -3248,7 +3251,7 @@ function selectQueuePreview(state) {
3248
3251
  }
3249
3252
  function selectRuntimeNotices(state) {
3250
3253
  const notices = [...state.notices];
3251
- if (state.connection.state === "reconnecting") {
3254
+ if (state.connection.state === "reconnecting" && hasVisibleReconnectWork(state)) {
3252
3255
  notices.push({ id: "connection-reconnecting", level: "warning", text: "Reconnecting to the agent session\u2026" });
3253
3256
  }
3254
3257
  if (state.retryNotice) {
@@ -3263,6 +3266,9 @@ function selectRuntimeNotices(state) {
3263
3266
  }
3264
3267
  return notices;
3265
3268
  }
3269
+ function hasVisibleReconnectWork(state) {
3270
+ return state.status !== "idle" || state.queue.followUps.length > 0 || Object.keys(state.optimisticOutbox).length > 0 || state.pendingToolCallIds.size > 0;
3271
+ }
3266
3272
  function optimisticText(message) {
3267
3273
  const text = message.parts.filter((part) => part.type === "text").map((part) => part.text).join("\n\n");
3268
3274
  return text;
@@ -3348,48 +3354,6 @@ function messageCreatedAtMs(message) {
3348
3354
  return Number.isFinite(timestamp) ? timestamp : void 0;
3349
3355
  }
3350
3356
 
3351
- // src/front/chat/session/activeSessionStorage.ts
3352
- var ACTIVE_SESSION_KEY_PREFIX = "boring-agent:v2";
3353
- var ACTIVE_SESSION_KEY_SUFFIX = "activeSessionId";
3354
- var DEFAULT_STORAGE_SCOPE2 = "default";
3355
- function activeSessionStorageKey(storageScope) {
3356
- const scope = storageScope && storageScope.length > 0 ? storageScope : DEFAULT_STORAGE_SCOPE2;
3357
- return `${ACTIVE_SESSION_KEY_PREFIX}:${scope}:${ACTIVE_SESSION_KEY_SUFFIX}`;
3358
- }
3359
- function readActiveSessionId(options = {}) {
3360
- const storage = resolveStorage2(options.storage);
3361
- if (!storage) return void 0;
3362
- try {
3363
- return storage.getItem(activeSessionStorageKey(options.storageScope)) ?? void 0;
3364
- } catch {
3365
- return void 0;
3366
- }
3367
- }
3368
- function writeActiveSessionId(sessionId, options = {}) {
3369
- const storage = resolveStorage2(options.storage);
3370
- if (!storage) return;
3371
- try {
3372
- const key = activeSessionStorageKey(options.storageScope);
3373
- if (sessionId === void 0 || sessionId.length === 0) storage.removeItem(key);
3374
- else storage.setItem(key, sessionId);
3375
- } catch {
3376
- }
3377
- }
3378
- function clearActiveSessionId(options = {}) {
3379
- writeActiveSessionId(void 0, options);
3380
- }
3381
- function resolveStorage2(storage) {
3382
- if (storage) return storage;
3383
- try {
3384
- return globalThis.localStorage;
3385
- } catch {
3386
- return void 0;
3387
- }
3388
- }
3389
-
3390
- // src/front/chat/session/usePiSessions.ts
3391
- import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo3, useRef as useRef8, useState as useState10 } from "react";
3392
-
3393
3357
  // src/front/chat/pi/piChatAssistantCommit.ts
3394
3358
  function commitFinalMessage(state, messageId, final) {
3395
3359
  const plan = buildAssistantCommitPlan(state, messageId, final);
@@ -3747,10 +3711,18 @@ function piChatReducer(state, action) {
3747
3711
  queue: { followUps: clearQueuedFollowUps(state.queue.followUps, action) },
3748
3712
  optimisticOutbox: clearOptimisticFollowUps(state.optimisticOutbox, action)
3749
3713
  };
3750
- case "connection-state":
3751
- return { ...state, connection: { ...state.connection, state: action.state } };
3714
+ case "connection-state": {
3715
+ const nextState = {
3716
+ ...state,
3717
+ connection: { ...state.connection, state: action.state }
3718
+ };
3719
+ return action.state === "connected" ? clearProtocolError(nextState) : nextState;
3720
+ }
3752
3721
  case "heartbeat":
3753
- return { ...state, connection: { ...state.connection, lastHeartbeatAt: action.now ?? Date.now() } };
3722
+ return clearProtocolError({
3723
+ ...state,
3724
+ connection: { ...state.connection, lastHeartbeatAt: action.now ?? Date.now() }
3725
+ });
3754
3726
  case "protocol-error":
3755
3727
  return {
3756
3728
  ...state,
@@ -3767,6 +3739,14 @@ function piChatReducer(state, action) {
3767
3739
  return { ...state, notices: state.notices.filter((notice) => notice.id !== action.id) };
3768
3740
  }
3769
3741
  }
3742
+ function clearProtocolError(state) {
3743
+ if (!state.notices.some((notice) => notice.id === "protocol-error")) return state;
3744
+ return {
3745
+ ...state,
3746
+ error: void 0,
3747
+ notices: state.notices.filter((notice) => notice.id !== "protocol-error")
3748
+ };
3749
+ }
3770
3750
  function syncCursor(state, cursor) {
3771
3751
  if (cursor <= state.lastSeq) return { ...state, hydrated: true, needsResync: void 0 };
3772
3752
  return { ...state, lastSeq: cursor, hydrated: true, needsResync: void 0 };
@@ -3774,7 +3754,9 @@ function syncCursor(state, cursor) {
3774
3754
  function hydrateFromSnapshot(state, snapshot, options = {}) {
3775
3755
  if (snapshot.seq < state.lastSeq && !options.allowSeqRewind) return state;
3776
3756
  const queue = { followUps: enrichQueueWithKnownMetadata(snapshot.queue.followUps, state.optimisticOutbox, state.queue.followUps) };
3777
- const committedMessages = normalizeSnapshotMessages(snapshot.messages);
3757
+ const snapshotMessages = normalizeSnapshotMessages(snapshot.messages);
3758
+ const preserveLocalHistory = shouldPreserveLocalCommittedHistory(state, snapshot, snapshotMessages, options);
3759
+ const committedMessages = preserveLocalHistory ? mergeSnapshotMessagesIntoLocal(state.committedMessages, snapshotMessages) : snapshotMessages;
3778
3760
  const serverNonces = /* @__PURE__ */ new Set();
3779
3761
  for (const message of committedMessages) {
3780
3762
  if (message.clientNonce) serverNonces.add(message.clientNonce);
@@ -3783,7 +3765,12 @@ function hydrateFromSnapshot(state, snapshot, options = {}) {
3783
3765
  if (followUp.clientNonce) serverNonces.add(followUp.clientNonce);
3784
3766
  }
3785
3767
  const nextOutbox = {};
3786
- const staleOutbox = Object.values(state.optimisticOutbox).filter((message) => !serverNonces.has(message.clientNonce));
3768
+ if (preserveLocalHistory) {
3769
+ for (const message of Object.values(state.optimisticOutbox)) {
3770
+ if (!serverNonces.has(message.clientNonce)) nextOutbox[message.clientNonce] = message;
3771
+ }
3772
+ }
3773
+ const staleOutbox = preserveLocalHistory ? [] : Object.values(state.optimisticOutbox).filter((message) => !serverNonces.has(message.clientNonce));
3787
3774
  const notices = staleOutbox.length > 0 ? upsertNotice(state.notices, {
3788
3775
  id: "stale-outbox-cleared",
3789
3776
  level: "warning",
@@ -3810,6 +3797,22 @@ function hydrateFromSnapshot(state, snapshot, options = {}) {
3810
3797
  needsResync: void 0
3811
3798
  };
3812
3799
  }
3800
+ function shouldPreserveLocalCommittedHistory(state, snapshot, snapshotMessages, options) {
3801
+ if (options.allowSeqRewind) return false;
3802
+ if (!state.hydrated) return false;
3803
+ if (state.sessionId !== snapshot.sessionId) return false;
3804
+ if (state.committedMessages.length === 0) return false;
3805
+ if (snapshotMessages.length < state.committedMessages.length) return true;
3806
+ for (let index = 0; index < state.committedMessages.length; index += 1) {
3807
+ if (snapshotMessages[index]?.id !== state.committedMessages[index]?.id) return true;
3808
+ }
3809
+ return false;
3810
+ }
3811
+ function mergeSnapshotMessagesIntoLocal(currentMessages, snapshotMessages) {
3812
+ let merged = currentMessages;
3813
+ for (const message of snapshotMessages) merged = replaceOrAppendMessage(merged, message);
3814
+ return merged;
3815
+ }
3813
3816
  function applySequencedEvent(state, event) {
3814
3817
  if (event.seq <= state.lastSeq) return state;
3815
3818
  const expectedSeq = state.lastSeq + 1;
@@ -4538,6 +4541,7 @@ var RemotePiSession = class {
4538
4541
  this.store.dispatch({ type: "optimistic-user-message", message: toOptimisticUserMessage(payload) }, { flush: true });
4539
4542
  }
4540
4543
  if (!this.started) await this.start(this.store.getState().lastSeq);
4544
+ else this.ensureReconnectScheduled();
4541
4545
  try {
4542
4546
  const receipt = await this.postCommand("/prompt", payload, PromptReceiptSchema);
4543
4547
  return receipt;
@@ -4551,6 +4555,7 @@ var RemotePiSession = class {
4551
4555
  this.store.dispatch({ type: "optimistic-user-message", message: toOptimisticUserMessage(payload) }, { flush: true });
4552
4556
  }
4553
4557
  if (!this.started) await this.start(this.store.getState().lastSeq);
4558
+ else this.ensureReconnectScheduled();
4554
4559
  try {
4555
4560
  const receipt = await this.postCommand("/followup", payload, FollowUpReceiptSchema);
4556
4561
  return receipt;
@@ -4719,7 +4724,9 @@ var RemotePiSession = class {
4719
4724
  markOpen();
4720
4725
  if (!this.isStreamActive(generation, runId)) return;
4721
4726
  if (!connectTimedOut && shouldIgnoreStreamClose(error, controller)) return;
4722
- this.dispatchProtocolError(connectTimedOut ? `Pi chat event stream timed out after ${this.requestTimeoutMs}ms.` : errorMessage(error, "Pi chat event stream disconnected."));
4727
+ if (!connectTimedOut) {
4728
+ this.dispatchProtocolError(errorMessage(error, "Pi chat event stream disconnected."));
4729
+ }
4723
4730
  this.scheduleReconnect(generation);
4724
4731
  } finally {
4725
4732
  clearConnectTimer();
@@ -4731,6 +4738,13 @@ var RemotePiSession = class {
4731
4738
  this.abortEventStream();
4732
4739
  void this.hydrateAndConnect(generation, options);
4733
4740
  }
4741
+ ensureReconnectScheduled() {
4742
+ if (!this.isGenerationActive(this.generation)) return;
4743
+ const state = this.store.getState();
4744
+ if (state.connection.state === "connected" || state.connection.state === "connecting") return;
4745
+ if (this.reconnectTimer !== void 0) return;
4746
+ void this.hydrateAndConnect(this.generation);
4747
+ }
4734
4748
  scheduleReconnect(generation) {
4735
4749
  if (!this.isGenerationActive(generation)) return;
4736
4750
  this.clearReconnectTimer();
@@ -4783,7 +4797,7 @@ var RemotePiSession = class {
4783
4797
  try {
4784
4798
  const response = await this.fetchImpl(url, { ...init, signal: controller.signal });
4785
4799
  const body = await safeReadJson(response);
4786
- if (!response.ok) throw new RemotePiSessionHttpError(response.status, routeErrorMessage(body, `HTTP ${response.status}`), body);
4800
+ if (!response.ok) throw new RemotePiSessionHttpError(response.status, routeErrorMessage(body, `HTTP ${response.status}`), body, routeErrorCode(body));
4787
4801
  return body;
4788
4802
  } catch (error) {
4789
4803
  if (timedOut) throw new Error(`Request to ${url} timed out after ${this.requestTimeoutMs}ms.`);
@@ -4858,15 +4872,22 @@ function createRemotePiSession(options) {
4858
4872
  return new RemotePiSession(options);
4859
4873
  }
4860
4874
  var RemotePiSessionHttpError = class extends Error {
4861
- constructor(status, message, body) {
4875
+ constructor(status, message, body, errorCode) {
4862
4876
  super(message);
4863
4877
  this.status = status;
4864
4878
  this.body = body;
4879
+ this.errorCode = errorCode;
4865
4880
  this.name = "RemotePiSessionHttpError";
4866
4881
  }
4867
4882
  status;
4868
4883
  body;
4884
+ errorCode;
4869
4885
  };
4886
+ function piChatErrorCode(error) {
4887
+ if (error instanceof RemotePiSessionHttpError) return error.errorCode;
4888
+ const parsed = ErrorCode.safeParse(error?.errorCode);
4889
+ return parsed.success ? parsed.data : void 0;
4890
+ }
4870
4891
  function toOptimisticUserMessage(payload) {
4871
4892
  const displayText = payload.displayMessage ?? payload.message;
4872
4893
  return {
@@ -4903,6 +4924,13 @@ function routeErrorMessage(body, fallback) {
4903
4924
  const payload = typeof error === "object" && error !== null ? error : body;
4904
4925
  return typeof payload.message === "string" && payload.message ? payload.message : fallback;
4905
4926
  }
4927
+ function routeErrorCode(body) {
4928
+ if (typeof body !== "object" || body === null) return void 0;
4929
+ const error = body.error;
4930
+ const payload = typeof error === "object" && error !== null ? error : body;
4931
+ const parsed = ErrorCode.safeParse(payload.code);
4932
+ return parsed.success ? parsed.data : void 0;
4933
+ }
4906
4934
  function errorMessage(error, fallback) {
4907
4935
  return error instanceof Error && error.message ? error.message : fallback;
4908
4936
  }
@@ -4957,7 +4985,47 @@ function clearPageUnloading() {
4957
4985
  pageUnloading = false;
4958
4986
  }
4959
4987
 
4988
+ // src/front/chat/session/activeSessionStorage.ts
4989
+ var ACTIVE_SESSION_KEY_PREFIX = "boring-agent:v2";
4990
+ var ACTIVE_SESSION_KEY_SUFFIX = "activeSessionId";
4991
+ var DEFAULT_STORAGE_SCOPE2 = "default";
4992
+ function activeSessionStorageKey(storageScope) {
4993
+ const scope = storageScope && storageScope.length > 0 ? storageScope : DEFAULT_STORAGE_SCOPE2;
4994
+ return `${ACTIVE_SESSION_KEY_PREFIX}:${scope}:${ACTIVE_SESSION_KEY_SUFFIX}`;
4995
+ }
4996
+ function readActiveSessionId(options = {}) {
4997
+ const storage = resolveStorage2(options.storage);
4998
+ if (!storage) return void 0;
4999
+ try {
5000
+ return storage.getItem(activeSessionStorageKey(options.storageScope)) ?? void 0;
5001
+ } catch {
5002
+ return void 0;
5003
+ }
5004
+ }
5005
+ function writeActiveSessionId(sessionId, options = {}) {
5006
+ const storage = resolveStorage2(options.storage);
5007
+ if (!storage) return;
5008
+ try {
5009
+ const key = activeSessionStorageKey(options.storageScope);
5010
+ if (sessionId === void 0 || sessionId.length === 0) storage.removeItem(key);
5011
+ else storage.setItem(key, sessionId);
5012
+ } catch {
5013
+ }
5014
+ }
5015
+ function clearActiveSessionId(options = {}) {
5016
+ writeActiveSessionId(void 0, options);
5017
+ }
5018
+ function resolveStorage2(storage) {
5019
+ if (storage) return storage;
5020
+ try {
5021
+ return globalThis.localStorage;
5022
+ } catch {
5023
+ return void 0;
5024
+ }
5025
+ }
5026
+
4960
5027
  // src/front/chat/session/usePiSessions.ts
5028
+ import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo3, useRef as useRef8, useState as useState10 } from "react";
4961
5029
  var DEFAULT_SESSIONS_API_PATH = "/api/v1/agent/pi-chat/sessions";
4962
5030
  var SESSION_PAGE_SIZE = 50;
4963
5031
  var DEFAULT_MAX_RETRIES = 60;
@@ -5637,7 +5705,7 @@ import { memo as memo2 } from "react";
5637
5705
  import { AlertTriangleIcon, InfoIcon, Loader2Icon, PlugZapIcon, RefreshCwIcon, XIcon as XIcon2 } from "lucide-react";
5638
5706
  import { Button as Button8 } from "@hachej/boring-ui-kit";
5639
5707
  import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
5640
- var RuntimeNotices = memo2(({ notices, onDismiss, onAction, className, ...props }) => {
5708
+ var RuntimeNotices = memo2(({ notices, onDismiss, onAction, renderAction, className, ...props }) => {
5641
5709
  if (notices.length === 0) return null;
5642
5710
  return /* @__PURE__ */ jsx15(
5643
5711
  "div",
@@ -5645,15 +5713,16 @@ var RuntimeNotices = memo2(({ notices, onDismiss, onAction, className, ...props
5645
5713
  "data-boring-agent-part": "runtime-notices",
5646
5714
  className: cn("flex flex-col gap-2 px-4 py-2", className),
5647
5715
  ...props,
5648
- children: notices.map((notice) => /* @__PURE__ */ jsx15(RuntimeNoticeRow, { notice, onDismiss, onAction }, notice.id))
5716
+ children: notices.map((notice) => /* @__PURE__ */ jsx15(RuntimeNoticeRow, { notice, onDismiss, onAction, renderAction }, notice.id))
5649
5717
  }
5650
5718
  );
5651
5719
  });
5652
5720
  RuntimeNotices.displayName = "RuntimeNotices";
5653
- function RuntimeNoticeRow({ notice, onDismiss, onAction }) {
5721
+ function RuntimeNoticeRow({ notice, onDismiss, onAction, renderAction }) {
5654
5722
  const kind = inferNoticeKind(notice);
5655
5723
  const Icon = iconForNotice(kind, notice.level);
5656
5724
  const actionLabel = notice.actionLabel ?? defaultActionLabel(kind);
5725
+ const hostAction = renderAction?.(notice);
5657
5726
  return /* @__PURE__ */ jsxs14(
5658
5727
  "div",
5659
5728
  {
@@ -5670,6 +5739,7 @@ function RuntimeNoticeRow({ notice, onDismiss, onAction }) {
5670
5739
  children: [
5671
5740
  /* @__PURE__ */ jsx15(Icon, { className: cn(noticeIconClass(notice.level), kind === "retry" && "animate-spin motion-reduce:animate-none"), "aria-hidden": "true" }),
5672
5741
  /* @__PURE__ */ jsx15("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx15("p", { className: noticeTextClass(), children: notice.text }) }),
5742
+ hostAction ?? null,
5673
5743
  actionLabel && onAction ? /* @__PURE__ */ jsx15(Button8, { type: "button", variant: "ghost", size: "sm", onClick: () => onAction(notice.id), children: actionLabel }) : null,
5674
5744
  notice.dismissible && onDismiss ? /* @__PURE__ */ jsx15(
5675
5745
  Button8,
@@ -5710,7 +5780,11 @@ function defaultActionLabel(kind) {
5710
5780
 
5711
5781
  // src/front/chat/components/ChatNotices.tsx
5712
5782
  import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
5713
- function RuntimeNoticeMessages({ notices, onDismiss }) {
5783
+ function RuntimeNoticeMessages({
5784
+ notices,
5785
+ onDismiss,
5786
+ renderAction
5787
+ }) {
5714
5788
  const visible = notices.filter(
5715
5789
  (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:")
5716
5790
  );
@@ -5720,6 +5794,7 @@ function RuntimeNoticeMessages({ notices, onDismiss }) {
5720
5794
  {
5721
5795
  notices: visible,
5722
5796
  onDismiss,
5797
+ renderAction,
5723
5798
  className: "mx-auto w-full max-w-3xl px-0 py-0"
5724
5799
  }
5725
5800
  );
@@ -6976,6 +7051,7 @@ function PiConversationSurface({
6976
7051
  toolRenderers,
6977
7052
  runtimeNotices,
6978
7053
  onDismissNotice,
7054
+ renderNoticeAction,
6979
7055
  onScrollToBottomReady,
6980
7056
  onSuggestionSubmit,
6981
7057
  onRestoreDraft,
@@ -7016,6 +7092,7 @@ function PiConversationSurface({
7016
7092
  eyebrow: emptyState?.eyebrow,
7017
7093
  title: emptyState?.title,
7018
7094
  description: emptyState?.description,
7095
+ footer: emptyState?.footer,
7019
7096
  suggestions,
7020
7097
  className: emptyHero ? "items-center text-center [&>p]:mx-auto" : void 0,
7021
7098
  onSelect: (suggestion) => {
@@ -7039,7 +7116,7 @@ function PiConversationSurface({
7039
7116
  },
7040
7117
  key
7041
7118
  )),
7042
- /* @__PURE__ */ jsx23(RuntimeNoticeMessages, { notices: runtimeNotices, onDismiss: onDismissNotice })
7119
+ /* @__PURE__ */ jsx23(RuntimeNoticeMessages, { notices: runtimeNotices, onDismiss: onDismissNotice, renderAction: renderNoticeAction })
7043
7120
  ] }),
7044
7121
  /* @__PURE__ */ jsx23(ConversationScrollButton, {})
7045
7122
  ]
@@ -7257,7 +7334,7 @@ function PluginUpdateStatus({
7257
7334
  tone: "running",
7258
7335
  dataAttribute: "data-boring-plugin-update",
7259
7336
  maxWidthClassName,
7260
- runningContent: "Updating plugins\u2026"
7337
+ runningContent: "Updating extensions\u2026"
7261
7338
  }
7262
7339
  );
7263
7340
  }
@@ -7267,7 +7344,7 @@ function PluginUpdateStatus({
7267
7344
  const frontEvents = state.frontEvents ?? [];
7268
7345
  const hasWarningsOrDiagnostics = warnings.length > 0 || diagnostics.length > 0;
7269
7346
  const title = state.reloaded ? hasWarningsOrDiagnostics ? "Reload finished with warnings" : "Reload complete" : "Reload queued";
7270
- const detail = state.reloaded ? hasWarningsOrDiagnostics ? "Some plugin changes need attention." : frontEvents.length > 0 ? `${frontEvents.length} plugin module${frontEvents.length === 1 ? "" : "s"} refreshed. Changes are live.` : "Changes are live." : void 0;
7347
+ const detail = state.reloaded ? hasWarningsOrDiagnostics ? "Some extension changes need attention." : frontEvents.length > 0 ? `${frontEvents.length} extension module${frontEvents.length === 1 ? "" : "s"} refreshed. Changes are live.` : "Changes are live." : void 0;
7271
7348
  return /* @__PURE__ */ jsxs23(
7272
7349
  ComposerStatusBanner,
7273
7350
  {
@@ -7293,7 +7370,7 @@ function PluginUpdateStatus({
7293
7370
  /* @__PURE__ */ jsxs23("span", { children: [
7294
7371
  "Reload diagnostics for ",
7295
7372
  diagnostics.length,
7296
- " plugin",
7373
+ " extension",
7297
7374
  diagnostics.length === 1 ? "" : "s"
7298
7375
  ] })
7299
7376
  ] }),
@@ -7319,7 +7396,7 @@ function PluginUpdateStatus({
7319
7396
  /* @__PURE__ */ jsxs23("span", { children: [
7320
7397
  "Restart needed for ",
7321
7398
  warnings.length,
7322
- " plugin",
7399
+ " extension",
7323
7400
  warnings.length === 1 ? "" : "s"
7324
7401
  ] })
7325
7402
  ] }),
@@ -7328,7 +7405,7 @@ function PluginUpdateStatus({
7328
7405
  " \u2014 ",
7329
7406
  w.surfaces.join(" + ")
7330
7407
  ] }, w.id)) }),
7331
- /* @__PURE__ */ jsx25("p", { className: "mt-1 text-foreground/70", children: "The front bundle reloaded successfully, but routes and agent tools were wired at boot. Stop and restart the workspace process (Ctrl-C, then re-run your dev command) to pick up the new code." })
7408
+ /* @__PURE__ */ jsx25("p", { className: "mt-1 text-foreground/70", children: "The interface reloaded successfully, but routes and tools were wired at boot. Restart the workspace process to pick up the new code." })
7332
7409
  ]
7333
7410
  }
7334
7411
  ) : null
@@ -7342,7 +7419,7 @@ function PluginUpdateStatus({
7342
7419
  tone: "error",
7343
7420
  dataAttribute: "data-boring-plugin-update",
7344
7421
  maxWidthClassName,
7345
- title: "Plugin update failed.",
7422
+ title: "Extension update failed.",
7346
7423
  message: state.message,
7347
7424
  onRetry,
7348
7425
  onDismiss,
@@ -7563,22 +7640,10 @@ function ThinkingLevelGlyph({ level }) {
7563
7640
  function modelTriggerLabel(value, options) {
7564
7641
  const current = value ? options.find((m) => m.provider === value.provider && m.id === value.id) : void 0;
7565
7642
  const rawTriggerLabel = current?.label ?? value?.id;
7566
- return value ? rawTriggerLabel && current?.label && current.label !== value.id && /[A-Z]/.test(current.label) ? current.label : displayModelLabel(rawTriggerLabel ?? value.id) : "Pi default";
7643
+ return value ? rawTriggerLabel && current?.label && current.label !== value.id && /[A-Z]/.test(current.label) ? current.label : displayModelLabel(rawTriggerLabel ?? value.id) : "Default model";
7567
7644
  }
7568
- function modelMenuOptions(value, options) {
7569
- const currentKey = value ? encodeModelKey(value) : null;
7570
- const triggerLabel = modelTriggerLabel(value, options);
7571
- const availableOptions = options.filter((m) => m.available);
7572
- const hasCurrentOption = currentKey ? availableOptions.some((m) => encodeModelKey(m) === currentKey) : true;
7573
- return hasCurrentOption || !value ? availableOptions : [
7574
- {
7575
- provider: value.provider,
7576
- id: value.id,
7577
- label: triggerLabel,
7578
- available: true
7579
- },
7580
- ...availableOptions
7581
- ];
7645
+ function modelMenuOptions(_value, options) {
7646
+ return options.filter((m) => m.available);
7582
7647
  }
7583
7648
  function groupModelOptions(options) {
7584
7649
  const groups = /* @__PURE__ */ new Map();
@@ -7634,6 +7699,7 @@ function ModelPickerMenu({
7634
7699
  onChange,
7635
7700
  options,
7636
7701
  disabled,
7702
+ hideDefaultOption = false,
7637
7703
  onClose,
7638
7704
  className
7639
7705
  }) {
@@ -7641,7 +7707,7 @@ function ModelPickerMenu({
7641
7707
  const menuOptions = modelMenuOptions(value, options);
7642
7708
  const groups = groupModelOptions(menuOptions);
7643
7709
  const groupedOptions = [...groups.values()].flat();
7644
- const keyboardOptions = [null, ...groupedOptions];
7710
+ const keyboardOptions = hideDefaultOption ? groupedOptions : [null, ...groupedOptions];
7645
7711
  const selectedIndex = currentKey ? Math.max(0, keyboardOptions.findIndex((option) => option && encodeModelKey(option) === currentKey)) : 0;
7646
7712
  const [activeIndex, setActiveIndex] = useState16(selectedIndex);
7647
7713
  const activeIndexRef = useRef13(selectedIndex);
@@ -7678,7 +7744,8 @@ function ModelPickerMenu({
7678
7744
  window.addEventListener("keydown", handler, { capture: true });
7679
7745
  return () => window.removeEventListener("keydown", handler, { capture: true });
7680
7746
  }, [activeIndex, disabled, keyboardOptions, onChange, onClose]);
7681
- const optionIndexes = new Map(groupedOptions.map((option, index) => [encodeModelKey(option), index + 1]));
7747
+ const optionIndexOffset = hideDefaultOption ? 0 : 1;
7748
+ const optionIndexes = new Map(groupedOptions.map((option, index) => [encodeModelKey(option), index + optionIndexOffset]));
7682
7749
  return /* @__PURE__ */ jsx27("div", { ref: menuRef, "data-boring-agent": "", "data-boring-agent-part": "model-picker-menu", className: cn(composerPickerMenuClass, className), children: /* @__PURE__ */ jsxs25(Command, { className: "bg-transparent text-[color:var(--popover-foreground)]", children: [
7683
7750
  menuOptions.length > 8 && /* @__PURE__ */ jsx27("div", { className: "border-b border-border/60 px-2", children: /* @__PURE__ */ jsx27(
7684
7751
  CommandInput,
@@ -7690,10 +7757,10 @@ function ModelPickerMenu({
7690
7757
  ) }),
7691
7758
  /* @__PURE__ */ jsxs25(CommandList, { className: "max-h-[300px] p-0.5", children: [
7692
7759
  /* @__PURE__ */ jsx27(CommandEmpty, { className: "py-4 text-center text-[13px] text-muted-foreground", children: "No models found" }),
7693
- /* @__PURE__ */ jsx27(CommandGroup, { children: /* @__PURE__ */ jsxs25(
7760
+ !hideDefaultOption ? /* @__PURE__ */ jsx27(CommandGroup, { children: /* @__PURE__ */ jsxs25(
7694
7761
  CommandItem,
7695
7762
  {
7696
- value: "Pi default automatic auto",
7763
+ value: "Default model automatic auto",
7697
7764
  onSelect: () => {
7698
7765
  if (disabled) return;
7699
7766
  onChange(null);
@@ -7710,11 +7777,11 @@ function ModelPickerMenu({
7710
7777
  )
7711
7778
  }
7712
7779
  ),
7713
- /* @__PURE__ */ jsx27("span", { className: "truncate", children: "Pi default" }),
7780
+ /* @__PURE__ */ jsx27("span", { className: "truncate", children: "Default model" }),
7714
7781
  /* @__PURE__ */ jsx27("span", { className: "ml-auto shrink-0 text-[10px] text-muted-foreground/60", children: "auto" })
7715
7782
  ]
7716
7783
  }
7717
- ) }),
7784
+ ) }) : null,
7718
7785
  [...groups.entries()].map(([provider, list]) => /* @__PURE__ */ jsx27(
7719
7786
  CommandGroup,
7720
7787
  {
@@ -7979,11 +8046,11 @@ var PromptInput = ({
7979
8046
  const inputRef = useRef14(null);
7980
8047
  const formRef = useRef14(null);
7981
8048
  const [items, setItems] = useState17([]);
7982
- const setFileUrlLocal = useCallback15((id, url, status) => {
8049
+ const setFileUrlLocal = useCallback15((id, url, status, path) => {
7983
8050
  setItems((prev) => prev.map((f) => {
7984
8051
  if (f.id !== id) return f;
7985
8052
  if (f.url !== url && f.url.startsWith("blob:")) URL.revokeObjectURL(f.url);
7986
- return { ...f, url, status };
8053
+ return { ...f, url, status, ...path ? { path } : {} };
7987
8054
  }));
7988
8055
  }, []);
7989
8056
  const files = usingProvider ? controller.attachments.files : items;
@@ -8052,7 +8119,7 @@ var PromptInput = ({
8052
8119
  for (let i = 0; i < entries.length; i++) {
8053
8120
  const entry = entries[i];
8054
8121
  const file = capped[i];
8055
- onUploadFile(file).then(({ url }) => setFileUrlLocal(entry.id, url, "ready")).catch(() => setFileUrlLocal(entry.id, entry.url, "error"));
8122
+ onUploadFile(file).then(({ url, path }) => setFileUrlLocal(entry.id, url, "ready", path)).catch(() => setFileUrlLocal(entry.id, entry.url, "error"));
8056
8123
  }
8057
8124
  }
8058
8125
  },
@@ -8716,6 +8783,8 @@ function PiChatComposerSurface({
8716
8783
  selectedModel,
8717
8784
  modelOptions,
8718
8785
  modelControlled,
8786
+ hideDefaultModelOption = false,
8787
+ hideComposerSettings = false,
8719
8788
  onModelChange,
8720
8789
  onSetModelPickerOpen,
8721
8790
  onOpenModelPicker,
@@ -8733,6 +8802,11 @@ function PiChatComposerSurface({
8733
8802
  onSubmitMessage,
8734
8803
  onStop
8735
8804
  }) {
8805
+ const uploadAttachment = useCallback16((file) => uploadFile(file, {
8806
+ apiBaseUrl,
8807
+ workspaceRequestId: requestHeaders?.["x-boring-workspace-id"],
8808
+ fetch: fetch2
8809
+ }), [apiBaseUrl, fetch2, requestHeaders]);
8736
8810
  const resizeTextarea = useCallback16((node) => {
8737
8811
  if (!node) return;
8738
8812
  node.style.height = "auto";
@@ -8854,6 +8928,7 @@ function PiChatComposerSurface({
8854
8928
  onChange: onModelChange,
8855
8929
  options: modelOptions,
8856
8930
  disabled: isStreaming || modelControlled,
8931
+ hideDefaultOption: hideDefaultModelOption,
8857
8932
  onClose: () => onSetModelPickerOpen(false)
8858
8933
  }
8859
8934
  ) : null,
@@ -8891,6 +8966,7 @@ function PiChatComposerSurface({
8891
8966
  {
8892
8967
  "data-boring-state": status,
8893
8968
  onSubmit: (message) => onSubmitMessage({ text: message.text, files: message.files }),
8969
+ onUploadFile: uploadAttachment,
8894
8970
  multiple: true,
8895
8971
  maxFiles: disabled || isStreaming ? 0 : MAX_PROMPT_ATTACHMENTS,
8896
8972
  maxFileSize: MAX_PROMPT_ATTACHMENT_BYTES,
@@ -8969,7 +9045,7 @@ function PiChatComposerSurface({
8969
9045
  )
8970
9046
  }
8971
9047
  ),
8972
- /* @__PURE__ */ jsxs30(
9048
+ hideComposerSettings ? null : /* @__PURE__ */ jsxs30(
8973
9049
  "div",
8974
9050
  {
8975
9051
  "data-boring-agent-part": "composer-settings-row",
@@ -9171,10 +9247,11 @@ function errorMessage2(error, fallback) {
9171
9247
 
9172
9248
  // src/front/chat/PiChatPanel.tsx
9173
9249
  import { jsx as jsx33, jsxs as jsxs31 } from "react/jsx-runtime";
9174
- var DebugDrawer2 = lazy(() => import("../DebugDrawer-2PUDZ2RL.js").then((m) => ({ default: m.DebugDrawer })));
9250
+ var DebugDrawer2 = lazy(() => import("../DebugDrawer-RYEBQ6CO.js").then((m) => ({ default: m.DebugDrawer })));
9175
9251
  var EMPTY_COMMANDS = [];
9176
9252
  var EMPTY_COMMAND_NAMES = [];
9177
9253
  var EMPTY_BLOCKERS = [];
9254
+ var RUN_REJECTED_NOTICE_ID = "run-rejected";
9178
9255
  function PiChatPanel({
9179
9256
  sessionId,
9180
9257
  extraCommands,
@@ -9201,6 +9278,9 @@ function PiChatPanel({
9201
9278
  model,
9202
9279
  defaultModel,
9203
9280
  availableModels,
9281
+ hideDefaultModelOption = false,
9282
+ hideComposerSettings = false,
9283
+ suppressPreSubmitCancelledWarning = false,
9204
9284
  thinkingLevel,
9205
9285
  thinkingControl = true,
9206
9286
  serverResourcesEnabled = true,
@@ -9222,12 +9302,16 @@ function PiChatPanel({
9222
9302
  onOpenArtifact,
9223
9303
  composerBlockers = EMPTY_BLOCKERS,
9224
9304
  onComposerStop,
9225
- onComposerBlockerAction
9305
+ onComposerBlockerAction,
9306
+ onTurnComplete,
9307
+ renderNoticeAction
9226
9308
  }) {
9227
9309
  const externalSessionId = sessionId?.trim() || void 0;
9228
9310
  const showSessionSidebar = showSessions ?? externalSessionId === void 0;
9229
9311
  const onDataRef = useRef16(onData);
9230
9312
  onDataRef.current = onData;
9313
+ const onTurnCompleteRef = useRef16(onTurnComplete);
9314
+ onTurnCompleteRef.current = onTurnComplete;
9231
9315
  const sessionListRefreshRef = useRef16(void 0);
9232
9316
  const requestHeadersKey = useMemo12(() => headersContentKey(requestHeaders), [requestHeaders]);
9233
9317
  const normalizedRequestHeaders = useMemo12(() => normalizedHeadersFromContentKey(requestHeadersKey), [requestHeadersKey]);
@@ -9237,6 +9321,7 @@ function PiChatPanel({
9237
9321
  onEvent: (event) => {
9238
9322
  remoteSessionOptions?.onEvent?.(event);
9239
9323
  onDataRef.current?.(event);
9324
+ if (event.type === "agent-end" && !event.willRetry) onTurnCompleteRef.current?.();
9240
9325
  if (shouldRefreshSessionListAfterEvent(event)) sessionListRefreshRef.current?.();
9241
9326
  }
9242
9327
  }), [hydrateMessages, remoteSessionOptions]);
@@ -9411,7 +9496,7 @@ function PiChatPanel({
9411
9496
  const largeStateNotice = debug && debugState?.largeStateWarning ? [{
9412
9497
  id: "large-state-warning",
9413
9498
  level: "warning",
9414
- text: `Large Pi chat state: ${debugState.largeStateWarning.messageCount} messages, approximately ${debugState.largeStateWarning.approxBytes} bytes.`,
9499
+ text: `Large chat state: ${debugState.largeStateWarning.messageCount} messages, approximately ${debugState.largeStateWarning.approxBytes} bytes.`,
9415
9500
  dismissible: true
9416
9501
  }] : [];
9417
9502
  return [...fromState, ...sessionNotice, ...largeStateNotice, ...localNotices].filter((notice) => !dismissedNoticeIds.has(notice.id));
@@ -9426,10 +9511,19 @@ function PiChatPanel({
9426
9511
  setDismissedNoticeIds((previous) => new Set(previous).add(id));
9427
9512
  setLocalNotices((previous) => previous.filter((notice) => notice.id !== id));
9428
9513
  }, []);
9514
+ const dropLocalNotice = useCallback18((id) => {
9515
+ setDismissedNoticeIds((previous) => {
9516
+ if (!previous.has(id)) return previous;
9517
+ const next = new Set(previous);
9518
+ next.delete(id);
9519
+ return next;
9520
+ });
9521
+ setLocalNotices((previous) => previous.filter((notice) => notice.id !== id));
9522
+ }, []);
9429
9523
  const createSession = useCallback18(() => {
9430
9524
  if (externalSessionId) return;
9431
9525
  void sessions.create().catch((error) => {
9432
- addLocalNotice({ id: "session-create-error", level: "error", text: errorMessage2(error, "Could not create a Pi session."), dismissible: true });
9526
+ addLocalNotice({ id: "session-create-error", level: "error", text: errorMessage2(error, "Could not create a chat session."), dismissible: true });
9433
9527
  });
9434
9528
  }, [addLocalNotice, externalSessionId, sessions.create]);
9435
9529
  useEffect21(() => {
@@ -9439,7 +9533,7 @@ function PiChatPanel({
9439
9533
  autoCreateInFlightRef.current = true;
9440
9534
  void sessions.create().catch((error) => {
9441
9535
  autoCreateInFlightRef.current = false;
9442
- addLocalNotice({ id: "session-auto-create-error", level: "error", text: errorMessage2(error, "Could not create a Pi session."), dismissible: true });
9536
+ addLocalNotice({ id: "session-auto-create-error", level: "error", text: errorMessage2(error, "Could not create a chat session."), dismissible: true });
9443
9537
  });
9444
9538
  }, [activeSessionId, addLocalNotice, externalSessionId, sessionList.length, sessions.create, sessionsError, sessionsLoading]);
9445
9539
  useEffect21(() => {
@@ -9450,7 +9544,7 @@ function PiChatPanel({
9450
9544
  const deleteSession = useCallback18((sessionId2) => {
9451
9545
  if (externalSessionId) return;
9452
9546
  void sessions.delete(sessionId2).catch((error) => {
9453
- addLocalNotice({ id: `session-delete-error:${sessionId2}`, level: "error", text: errorMessage2(error, "Could not delete the Pi session."), dismissible: true });
9547
+ addLocalNotice({ id: `session-delete-error:${sessionId2}`, level: "error", text: errorMessage2(error, "Could not delete the chat session."), dismissible: true });
9454
9548
  });
9455
9549
  }, [addLocalNotice, externalSessionId, sessions.delete]);
9456
9550
  const resetSession = useCallback18(() => {
@@ -9466,7 +9560,7 @@ function PiChatPanel({
9466
9560
  await sessions.create();
9467
9561
  await onSessionReset?.();
9468
9562
  })().catch((error) => {
9469
- addLocalNotice({ id: "session-reset-error", level: "error", text: errorMessage2(error, "Could not reset the Pi session."), dismissible: true });
9563
+ addLocalNotice({ id: "session-reset-error", level: "error", text: errorMessage2(error, "Could not reset the chat session."), dismissible: true });
9470
9564
  }).finally(() => {
9471
9565
  resetInProgressRef.current = false;
9472
9566
  });
@@ -9482,15 +9576,15 @@ function PiChatPanel({
9482
9576
  const failureMessage = pluginReloadFailureMessage(message);
9483
9577
  if (failureMessage) {
9484
9578
  setPluginUpdateState({ kind: "error", message: failureMessage });
9485
- return `Plugin update failed: ${failureMessage}`;
9579
+ return `Extension update failed: ${failureMessage}`;
9486
9580
  }
9487
9581
  setPluginUpdateState({ kind: "success", reloaded: !/will reload on the next message/i.test(message) });
9488
9582
  setServerSkillsRefreshKey((key) => key + 1);
9489
9583
  return message;
9490
9584
  } catch (error) {
9491
- const message = errorMessage2(error, "Agent plugin reload failed.");
9585
+ const message = errorMessage2(error, "Extension reload failed.");
9492
9586
  setPluginUpdateState({ kind: "error", message });
9493
- return `Plugin update failed: ${message}`;
9587
+ return `Extension update failed: ${message}`;
9494
9588
  }
9495
9589
  }, [reloadAgentPlugins]);
9496
9590
  const dismissPluginUpdate = useCallback18(() => setPluginUpdateState(null), []);
@@ -9639,7 +9733,7 @@ function PiChatPanel({
9639
9733
  clearMessages: () => addLocalNotice({
9640
9734
  id: "clear-not-supported",
9641
9735
  level: "info",
9642
- text: "/clear is not available in the Pi-native chat panel.",
9736
+ text: "/clear is not available in this chat panel.",
9643
9737
  dismissible: true
9644
9738
  }),
9645
9739
  resetSession,
@@ -9671,6 +9765,7 @@ function PiChatPanel({
9671
9765
  addLocalNotice({ id: `command:${Date.now()}`, level: "info", text: message, dismissible: true });
9672
9766
  },
9673
9767
  onWarning: (message) => {
9768
+ if (suppressPreSubmitCancelledWarning && message === "Submit was cancelled before sending.") return;
9674
9769
  onComposerWarning?.(message);
9675
9770
  addLocalNotice({ id: `composer-warning:${Date.now()}`, level: "warning", text: message, dismissible: true });
9676
9771
  },
@@ -9679,10 +9774,21 @@ function PiChatPanel({
9679
9774
  onMentionedFilesConsumed?.();
9680
9775
  }
9681
9776
  });
9682
- }, [activeChatSessionId, addLocalNotice, clearMentionedFiles, composerBlocked, composerBlockerLabel, effectiveMentionedFiles, markLocalSubmitted, onBeforeSubmit, onCommandResult, onComposerWarning, onMentionedFilesConsumed, openModelPicker, openThinkingPicker, registry, reloadAgentPlugins, resetSession, runPluginUpdate, selectComposerModel, selectComposerThinking, selectedModel, selectedPiSession, selectedThinking, setComposerDraft, submitThinkingControl]);
9777
+ }, [activeChatSessionId, addLocalNotice, clearMentionedFiles, composerBlocked, composerBlockerLabel, effectiveMentionedFiles, markLocalSubmitted, onBeforeSubmit, onCommandResult, onComposerWarning, onMentionedFilesConsumed, openModelPicker, openThinkingPicker, registry, reloadAgentPlugins, resetSession, runPluginUpdate, selectComposerModel, selectComposerThinking, selectedModel, selectedPiSession, selectedThinking, setComposerDraft, submitThinkingControl, suppressPreSubmitCancelledWarning]);
9778
+ const surfaceRunRejected = useCallback18((error) => {
9779
+ const errorCode = piChatErrorCode(error);
9780
+ dropLocalNotice(RUN_REJECTED_NOTICE_ID);
9781
+ addLocalNotice({
9782
+ id: RUN_REJECTED_NOTICE_ID,
9783
+ level: "error",
9784
+ text: errorMessage2(error, "Could not send your message."),
9785
+ dismissible: true,
9786
+ ...errorCode ? { errorCode } : {}
9787
+ });
9788
+ }, [addLocalNotice, dropLocalNotice]);
9683
9789
  const sendComposerMessage = useCallback18(async ({ text, files, source = "composer" }) => {
9684
9790
  if (!policy) {
9685
- addLocalNotice({ id: "composer-no-session", level: "warning", text: "Create or select a Pi session before sending.", dismissible: true });
9791
+ addLocalNotice({ id: "composer-no-session", level: "warning", text: "Create or select a chat session before sending.", dismissible: true });
9686
9792
  return false;
9687
9793
  }
9688
9794
  const submittedDraft = text;
@@ -9697,6 +9803,9 @@ function PiChatPanel({
9697
9803
  restoreSubmittedDraft();
9698
9804
  return false;
9699
9805
  }
9806
+ if (result.type === "prompt" || result.type === "followup") {
9807
+ dropLocalNotice(RUN_REJECTED_NOTICE_ID);
9808
+ }
9700
9809
  if (result.type === "prompt" && activeChatSessionId) {
9701
9810
  if (shouldHoldLocalSubmitted(selectedPiSession, result.cursor)) markLocalSubmitted(activeChatSessionId);
9702
9811
  else clearLocalSubmitted(activeChatSessionId);
@@ -9705,9 +9814,10 @@ function PiChatPanel({
9705
9814
  } catch (error) {
9706
9815
  clearLocalSubmitted(activeChatSessionId);
9707
9816
  restoreSubmittedDraft();
9708
- throw error;
9817
+ surfaceRunRejected(error);
9818
+ return false;
9709
9819
  }
9710
- }, [activeChatSessionId, addLocalNotice, clearLocalSubmitted, markLocalSubmitted, policy, selectedPiSession, setComposerDraft]);
9820
+ }, [activeChatSessionId, clearLocalSubmitted, dropLocalNotice, markLocalSubmitted, policy, selectedPiSession, setComposerDraft, surfaceRunRejected]);
9711
9821
  const editQueued = useCallback18(() => {
9712
9822
  if (!policy) return;
9713
9823
  void policy.editQueued().then((result) => {
@@ -9719,12 +9829,12 @@ function PiChatPanel({
9719
9829
  const stop = useCallback18(() => {
9720
9830
  onComposerStop?.();
9721
9831
  void policy?.stop().catch((error) => {
9722
- addLocalNotice({ id: "stop-error", level: "error", text: errorMessage2(error, "Could not stop the Pi session."), dismissible: true });
9832
+ addLocalNotice({ id: "stop-error", level: "error", text: errorMessage2(error, "Could not stop the chat session."), dismissible: true });
9723
9833
  });
9724
9834
  }, [addLocalNotice, onComposerStop, policy]);
9725
9835
  const interrupt = useCallback18(() => {
9726
9836
  void policy?.interrupt().catch((error) => {
9727
- addLocalNotice({ id: "interrupt-error", level: "error", text: errorMessage2(error, "Could not interrupt the Pi session."), dismissible: true });
9837
+ addLocalNotice({ id: "interrupt-error", level: "error", text: errorMessage2(error, "Could not interrupt the chat session."), dismissible: true });
9728
9838
  });
9729
9839
  }, [addLocalNotice, policy]);
9730
9840
  useEffect21(() => {
@@ -9779,6 +9889,9 @@ function PiChatPanel({
9779
9889
  }
9780
9890
  return;
9781
9891
  }
9892
+ if (result.type === "prompt" || result.type === "followup") {
9893
+ dropLocalNotice(RUN_REJECTED_NOTICE_ID);
9894
+ }
9782
9895
  if (result.type === "prompt") {
9783
9896
  if (shouldHoldLocalSubmitted(selectedPiSession, result.cursor)) markLocalSubmitted(activeSessionId);
9784
9897
  else clearLocalSubmitted(activeSessionId);
@@ -9792,9 +9905,9 @@ function PiChatPanel({
9792
9905
  clearLocalSubmitted(activeSessionId);
9793
9906
  restoreSubmittedDraft();
9794
9907
  settlePendingAutoSubmit(activeSessionId);
9795
- addLocalNotice({ id: "auto-submit-error", level: "error", text: errorMessage2(error, "Could not auto-submit the initial draft."), dismissible: true });
9908
+ surfaceRunRejected(error);
9796
9909
  });
9797
- }, [activeSessionId, addLocalNotice, autoSubmitInitialDraft, clearLocalSubmitted, composerBlocked, initialDraft, markLocalSubmitted, onAutoSubmitInitialDraftAccepted, policy, selectedPiSession, setComposerDraft, settlePendingAutoSubmit]);
9910
+ }, [activeSessionId, autoSubmitInitialDraft, clearLocalSubmitted, composerBlocked, dropLocalNotice, initialDraft, markLocalSubmitted, onAutoSubmitInitialDraftAccepted, policy, selectedPiSession, setComposerDraft, settlePendingAutoSubmit, surfaceRunRejected]);
9798
9911
  useEffect21(() => {
9799
9912
  if (workspaceWarmupStatus?.status === "ready") {
9800
9913
  clearLocalNotice("workspace-warmup");
@@ -9896,6 +10009,7 @@ function PiChatPanel({
9896
10009
  toolRenderers: mergedToolRenderers,
9897
10010
  runtimeNotices,
9898
10011
  onDismissNotice: clearLocalNotice,
10012
+ renderNoticeAction,
9899
10013
  onScrollToBottomReady: (scrollToBottom) => {
9900
10014
  scrollToBottomRef.current = scrollToBottom;
9901
10015
  },
@@ -9945,6 +10059,8 @@ function PiChatPanel({
9945
10059
  selectedModel,
9946
10060
  modelOptions,
9947
10061
  modelControlled: model !== void 0,
10062
+ hideDefaultModelOption,
10063
+ hideComposerSettings,
9948
10064
  onModelChange: modelDiscovery.setModel,
9949
10065
  onSetModelPickerOpen: setModelPickerOpen,
9950
10066
  onOpenModelPicker: openModelPicker,
@@ -9966,7 +10082,7 @@ function PiChatPanel({
9966
10082
  ]
9967
10083
  }
9968
10084
  ) }),
9969
- debug ? /* @__PURE__ */ jsx33(Suspense, { fallback: null, children: /* @__PURE__ */ jsx33("div", { "aria-label": "Pi chat debug metadata", className: "contents", role: "region", children: /* @__PURE__ */ jsx33(
10085
+ debug ? /* @__PURE__ */ jsx33(Suspense, { fallback: null, children: /* @__PURE__ */ jsx33("div", { "aria-label": "Chat debug metadata", className: "contents", role: "region", children: /* @__PURE__ */ jsx33(
9970
10086
  DebugDrawer2,
9971
10087
  {
9972
10088
  apiBaseUrl,