@invergent/agent-chat-react 2.11.4 → 2.13.0

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.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { cn, __publicField } from './chunk-47MF6JUW.js';
2
2
  import { createContext, memo, useRef, useEffect, useCallback, useMemo, useState, useLayoutEffect, forwardRef, useId, lazy, useContext, useSyncExternalStore, Fragment as Fragment$1, Children, Suspense } from 'react';
3
- import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, CreditCard, File as File$1, FileImage, FileAudio, FileVideo, FileText, FileCode, FileArchive, FileSpreadsheet, PlugZapIcon, ChevronRightIcon, ArrowLeftIcon, Loader2Icon, InboxIcon, Loader2, AlertCircle, Pause, Play, Ban, TargetIcon, CalendarClockIcon, UsersIcon, MessageSquareIcon, ZapIcon, Maximize2Icon, FolderOpenIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, ExternalLinkIcon, ClipboardCheckIcon, ShieldAlertIcon, TimerIcon, CircleDotIcon, XIcon, Trash2Icon, SquareIcon, ArrowDownIcon, RotateCcwIcon, MousePointer2Icon, DownloadIcon, TrashIcon, ImageIcon, FileTextIcon, CheckIcon, Clock, ArrowRight, TerminalIcon, SparklesIcon, ClockIcon, PlusIcon, CloudIcon, HardDriveIcon, PaperclipIcon, GlobeIcon, IdCardIcon, FolderIcon, CopyIcon, FileIcon, ChevronLeftIcon, MinusIcon, SearchIcon, CircleDot, XCircle, CheckCircle2, ArrowUpIcon, CircleCheckIcon, ThumbsUpIcon, ThumbsDownIcon, BookOpenIcon, GitBranchIcon, ActivityIcon, EyeIcon, ListTodoIcon, BotIcon, ListIcon, PenLineIcon, FileEditIcon, WrenchIcon, FileCodeIcon, CodeIcon, FileArchiveIcon, FileVideoIcon, FileAudioIcon, FileImageIcon, FileChartLineIcon, FileSpreadsheetIcon, XCircleIcon, CheckCircle2Icon, Code, CircleXIcon, SkullIcon, CheckCircleIcon, CircleIcon } from 'lucide-react';
3
+ import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, CreditCard, File as File$1, FileImage, FileAudio, FileVideo, FileText, FileCode, FileArchive, FileSpreadsheet, PlugZapIcon, ChevronRightIcon, ArrowLeftIcon, Loader2Icon, InboxIcon, Loader2, AlertCircle, Pause, Play, Ban, TargetIcon, CalendarClockIcon, UsersIcon, MessageSquareIcon, ZapIcon, Maximize2Icon, FolderOpenIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, ExternalLinkIcon, ClipboardCheckIcon, ShieldAlertIcon, TimerIcon, CircleDotIcon, ChevronLeftIcon, XIcon, Trash2Icon, SquareIcon, ArrowDownIcon, RotateCcwIcon, MousePointer2Icon, DownloadIcon, TrashIcon, ImageIcon, FileTextIcon, CheckIcon, Clock, ArrowRight, ListTreeIcon, TerminalIcon, SparklesIcon, ClockIcon, PaperclipIcon, CloudIcon, HardDriveIcon, GlobeIcon, FolderIcon, IdCardIcon, PlusIcon, CopyIcon, FileIcon, MinusIcon, SearchIcon, CircleDot, XCircle, CheckCircle2, ArrowUpIcon, CircleCheckIcon, ThumbsUpIcon, ThumbsDownIcon, BookOpenIcon, GitBranchIcon, ActivityIcon, EyeIcon, ListTodoIcon, BotIcon, ListIcon, PenLineIcon, FileEditIcon, WrenchIcon, FileCodeIcon, CodeIcon, FileArchiveIcon, FileVideoIcon, FileAudioIcon, FileImageIcon, FileChartLineIcon, FileSpreadsheetIcon, XCircleIcon, CheckCircle2Icon, Code, CircleXIcon, SkullIcon, CheckCircleIcon, CircleIcon } from 'lucide-react';
4
4
  import { cva } from 'class-variance-authority';
5
5
  import { Collapsible as Collapsible$1, Slot, Tooltip as Tooltip$1, Progress as Progress$1, Tabs as Tabs$1, Dialog as Dialog$1, ScrollArea as ScrollArea$1, Popover as Popover$1 } from 'radix-ui';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -2275,7 +2275,7 @@ function TabsTrigger({
2275
2275
  {
2276
2276
  "data-slot": "tabs-trigger",
2277
2277
  className: cn(
2278
- "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-2 border border-transparent px-4 py-1.5 text-xs font-semibold tracking-wider whitespace-nowrap text-foreground/60 uppercase transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:px-4 group-data-vertical/tabs:py-2 hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
2278
+ "relative inline-flex min-h-11 h-[calc(100%-1px)] flex-1 items-center justify-center gap-2 border border-transparent px-4 py-1.5 pointer-fine:min-h-0 text-xs font-semibold tracking-wider whitespace-nowrap text-foreground/60 uppercase transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:px-4 group-data-vertical/tabs:py-2 hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
2279
2279
  "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
2280
2280
  "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
2281
2281
  "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
@@ -4033,21 +4033,162 @@ var OTHER_INDEX_OFFSET = 1e6;
4033
4033
  function emptySelection() {
4034
4034
  return { index: -1, other: "" };
4035
4035
  }
4036
+ function hasChoices(question) {
4037
+ return (question.choices?.length ?? 0) > 0;
4038
+ }
4036
4039
  function buildAnswer(q, sel) {
4037
4040
  if (sel.index < 0) return null;
4038
4041
  if (sel.index >= OTHER_INDEX_OFFSET) {
4039
4042
  const text = sel.other.trim();
4040
4043
  if (!text) return null;
4041
- return { question: q.prompt, answer: text, is_other: true };
4044
+ return { question: q.prompt, answer: text, is_other: hasChoices(q) };
4042
4045
  }
4043
4046
  const choice = q.choices?.[sel.index];
4044
4047
  if (!choice) return null;
4045
4048
  return { question: q.prompt, answer: choice.label, is_other: false };
4046
4049
  }
4047
- function AskUserQuestionToolBlock({ tc }) {
4050
+ function askQuestionsOf(tc) {
4051
+ return parseArgs(tc.args)?.questions ?? [];
4052
+ }
4053
+ function isConversationalAsk(questions) {
4054
+ return questions.length === 1;
4055
+ }
4056
+ function conversationalAskAcceptsFreeText(question) {
4057
+ return !hasChoices(question) || question.allow_other !== false;
4058
+ }
4059
+ function promptEchoedInContent(content, prompt) {
4060
+ const normalize = (s) => s.replace(/[*_`]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
4061
+ const body = normalize(content ?? "");
4062
+ const question = normalize(prompt);
4063
+ return !!body && !!question && body.endsWith(question);
4064
+ }
4065
+ function AskUserQuestionToolBlock({
4066
+ tc,
4067
+ assistantContent,
4068
+ viewMode = "simple"
4069
+ }) {
4070
+ const questions = useMemo(() => askQuestionsOf(tc), [tc.args]);
4071
+ if (questions.length === 0) {
4072
+ return /* @__PURE__ */ jsx("div", { className: "rounded-md border border-destructive/50 bg-destructive/5 px-3 py-2 text-xs text-destructive", children: "ask_user_question: no questions provided" });
4073
+ }
4074
+ if (isConversationalAsk(questions)) {
4075
+ return /* @__PURE__ */ jsx(
4076
+ ConversationalAsk,
4077
+ {
4078
+ tc,
4079
+ question: questions[0],
4080
+ assistantContent
4081
+ }
4082
+ );
4083
+ }
4084
+ return /* @__PURE__ */ jsx(BatchAsk, { tc, questions, viewMode });
4085
+ }
4086
+ function ConversationalAsk({
4087
+ tc,
4088
+ question,
4089
+ assistantContent
4090
+ }) {
4091
+ const { adapter, sessionId } = useAgentChatAdapterContext();
4092
+ const [submitting, setSubmitting] = useState(false);
4093
+ const [error, setError] = useState(null);
4094
+ const answered = tc.askUserQuestionAnswers !== void 0;
4095
+ const pending = !answered && tc.status === "running";
4096
+ const choices = question.choices ?? [];
4097
+ const showPrompt = useMemo(
4098
+ () => !promptEchoedInContent(assistantContent, question.prompt),
4099
+ [assistantContent, question.prompt]
4100
+ );
4101
+ useEffect(() => {
4102
+ if (!pending || !sessionId) return;
4103
+ const handler = (e) => {
4104
+ if (e.key !== "Escape") return;
4105
+ e.preventDefault();
4106
+ void adapter.pauseSession({ sessionId }).catch(() => {
4107
+ });
4108
+ };
4109
+ window.addEventListener("keydown", handler);
4110
+ return () => window.removeEventListener("keydown", handler);
4111
+ }, [adapter, sessionId, pending]);
4112
+ const pick = useCallback(
4113
+ async (label) => {
4114
+ if (!sessionId || submitting) return;
4115
+ setError(null);
4116
+ setSubmitting(true);
4117
+ try {
4118
+ await adapter.submitAskUserQuestionResponse({
4119
+ sessionId,
4120
+ toolCallId: tc.id,
4121
+ responses: [
4122
+ { question: question.prompt, answer: label, is_other: false }
4123
+ ]
4124
+ });
4125
+ } catch (e) {
4126
+ setError(e instanceof Error ? e.message : "Submit failed.");
4127
+ setSubmitting(false);
4128
+ }
4129
+ },
4130
+ [adapter, sessionId, tc.id, question.prompt, submitting]
4131
+ );
4132
+ const stacked = choices.some((c) => !!c.description);
4133
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
4134
+ showPrompt && /* @__PURE__ */ jsx(MessageResponse, { children: question.prompt }),
4135
+ pending && choices.length > 0 && /* @__PURE__ */ jsx(
4136
+ "div",
4137
+ {
4138
+ className: cn(
4139
+ stacked ? "flex flex-col gap-1.5" : "flex flex-wrap gap-2",
4140
+ "max-w-2xl"
4141
+ ),
4142
+ role: "group",
4143
+ "aria-label": "Suggested answers",
4144
+ children: choices.map((choice, i) => /* @__PURE__ */ jsx(
4145
+ QuickReply,
4146
+ {
4147
+ choice,
4148
+ stacked,
4149
+ disabled: submitting,
4150
+ onSelect: () => void pick(choice.label)
4151
+ },
4152
+ i
4153
+ ))
4154
+ }
4155
+ ),
4156
+ error && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: error }),
4157
+ !answered && !pending && /* @__PURE__ */ jsx("p", { className: "text-xs italic text-muted-foreground/70", children: "No answer recorded." })
4158
+ ] });
4159
+ }
4160
+ function QuickReply({
4161
+ choice,
4162
+ stacked,
4163
+ disabled,
4164
+ onSelect
4165
+ }) {
4166
+ return /* @__PURE__ */ jsxs(
4167
+ "button",
4168
+ {
4169
+ type: "button",
4170
+ disabled,
4171
+ onClick: onSelect,
4172
+ className: cn(
4173
+ "border border-border bg-background text-left text-foreground transition-colors",
4174
+ "hover:border-foreground/30 hover:bg-muted",
4175
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
4176
+ "disabled:cursor-not-allowed disabled:opacity-50",
4177
+ stacked ? "rounded-md px-3 py-2" : "rounded-full px-3.5 py-1.5 text-sm"
4178
+ ),
4179
+ children: [
4180
+ /* @__PURE__ */ jsx("span", { className: cn("block", stacked && "text-sm"), children: choice.label }),
4181
+ stacked && choice.description && /* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-xs text-muted-foreground", children: choice.description })
4182
+ ]
4183
+ }
4184
+ );
4185
+ }
4186
+ function BatchAsk({
4187
+ tc,
4188
+ questions,
4189
+ viewMode
4190
+ }) {
4048
4191
  const { adapter, sessionId } = useAgentChatAdapterContext();
4049
- const args = useMemo(() => parseArgs(tc.args), [tc.args]);
4050
- const questions = args?.questions ?? [];
4051
4192
  const [active, setActive] = useState(0);
4052
4193
  const [selections, setSelections] = useState(
4053
4194
  () => questions.map(emptySelection)
@@ -4141,11 +4282,8 @@ function AskUserQuestionToolBlock({ tc }) {
4141
4282
  window.addEventListener("keydown", handler);
4142
4283
  return () => window.removeEventListener("keydown", handler);
4143
4284
  }, [handleCancel, handleSubmit, locked]);
4144
- if (questions.length === 0) {
4145
- return /* @__PURE__ */ jsx("div", { className: "rounded-md border border-destructive/50 bg-destructive/5 px-3 py-2 text-xs text-destructive", children: "ask_user_question: no questions provided" });
4146
- }
4147
4285
  if (locked) {
4148
- return /* @__PURE__ */ jsx(AskUserQuestionLocked, { tc, questions });
4286
+ return /* @__PURE__ */ jsx(BatchAskLocked, { tc, questions, viewMode });
4149
4287
  }
4150
4288
  const current = questions[active];
4151
4289
  const currentSel = selections[active] ?? emptySelection();
@@ -4223,7 +4361,7 @@ function AskUserQuestionToolBlock({ tc }) {
4223
4361
  ] }),
4224
4362
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-t border-border px-3 py-2", children: [
4225
4363
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 text-xs text-muted-foreground", children: [
4226
- questions.length > 1 && /* @__PURE__ */ jsxs("span", { className: "mr-2 tabular-nums", children: [
4364
+ /* @__PURE__ */ jsxs("span", { className: "mr-2 tabular-nums", children: [
4227
4365
  answeredCount,
4228
4366
  " of ",
4229
4367
  questions.length,
@@ -4232,11 +4370,11 @@ function AskUserQuestionToolBlock({ tc }) {
4232
4370
  /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/70", children: "Esc to cancel" }),
4233
4371
  error && /* @__PURE__ */ jsx("span", { className: "ml-2 text-destructive", children: error })
4234
4372
  ] }),
4235
- allAnswered || questions.length === 1 ? /* @__PURE__ */ jsx(
4373
+ allAnswered ? /* @__PURE__ */ jsx(
4236
4374
  "button",
4237
4375
  {
4238
4376
  type: "button",
4239
- disabled: !allAnswered || submitting,
4377
+ disabled: submitting,
4240
4378
  onClick: () => void handleSubmit(),
4241
4379
  className: cn(
4242
4380
  "shrink-0 rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground transition-colors",
@@ -4346,9 +4484,10 @@ function Radio({ selected }) {
4346
4484
  }
4347
4485
  );
4348
4486
  }
4349
- function AskUserQuestionLocked({
4487
+ function BatchAskLocked({
4350
4488
  tc,
4351
- questions
4489
+ questions,
4490
+ viewMode
4352
4491
  }) {
4353
4492
  const answers = tc.askUserQuestionAnswers;
4354
4493
  const byPrompt = new Map(
@@ -4373,7 +4512,7 @@ function AskUserQuestionLocked({
4373
4512
  /* @__PURE__ */ jsx("span", { className: "text-emerald-500", children: "\u2192" }),
4374
4513
  " ",
4375
4514
  a.answer,
4376
- a.is_other && /* @__PURE__ */ jsx("span", { className: "ml-1 text-[10px] text-muted-foreground/70", children: "(other)" })
4515
+ a.is_other && viewMode === "expert" && /* @__PURE__ */ jsx("span", { className: "ml-1 text-[10px] text-muted-foreground/70", children: "(other)" })
4377
4516
  ] }) : /* @__PURE__ */ jsx("div", { className: "ml-5 text-muted-foreground/60 italic", children: "\u2014 no answer \u2014" })
4378
4517
  ] }, i);
4379
4518
  }) })
@@ -4772,6 +4911,8 @@ function CodeRunToolBlock({
4772
4911
  function ToolCallBlock({
4773
4912
  tc,
4774
4913
  resolvedArtifactName,
4914
+ assistantContent,
4915
+ viewMode,
4775
4916
  onFileSelect
4776
4917
  }) {
4777
4918
  if (tc.toolName.startsWith("mcp__")) {
@@ -4814,7 +4955,14 @@ function ToolCallBlock({
4814
4955
  case "skill_manage":
4815
4956
  return /* @__PURE__ */ jsx(SkillManageToolBlock, { tc });
4816
4957
  case "ask_user_question":
4817
- return /* @__PURE__ */ jsx(AskUserQuestionToolBlock, { tc });
4958
+ return /* @__PURE__ */ jsx(
4959
+ AskUserQuestionToolBlock,
4960
+ {
4961
+ tc,
4962
+ assistantContent,
4963
+ viewMode
4964
+ }
4965
+ );
4818
4966
  case "create_artifact":
4819
4967
  return /* @__PURE__ */ jsx(ArtifactToolBlock, { tc, resolvedName: resolvedArtifactName });
4820
4968
  case "delegate_task":
@@ -5219,6 +5367,22 @@ function CommandEmpty({
5219
5367
  }
5220
5368
  );
5221
5369
  }
5370
+ function CommandGroup({
5371
+ className,
5372
+ ...props
5373
+ }) {
5374
+ return /* @__PURE__ */ jsx(
5375
+ Command$1.Group,
5376
+ {
5377
+ "data-slot": "command-group",
5378
+ className: cn(
5379
+ "overflow-hidden p-1.5 text-foreground **:[[cmdk-group-heading]]:mb-1 **:[[cmdk-group-heading]]:rounded-sm **:[[cmdk-group-heading]]:bg-orange-100 **:[[cmdk-group-heading]]:px-3 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-sm **:[[cmdk-group-heading]]:font-semibold **:[[cmdk-group-heading]]:tracking-wider **:[[cmdk-group-heading]]:text-foreground dark:**:[[cmdk-group-heading]]:bg-orange-400/40 dark:**:[[cmdk-group-heading]]:text-foreground/90",
5380
+ className
5381
+ ),
5382
+ ...props
5383
+ }
5384
+ );
5385
+ }
5222
5386
  function CommandItem({
5223
5387
  className,
5224
5388
  children,
@@ -8114,13 +8278,20 @@ var PromptInputTextarea = ({
8114
8278
  var PromptInputFooter = ({
8115
8279
  className,
8116
8280
  ...props
8117
- }) => /* @__PURE__ */ jsx(
8118
- InputGroupAddon,
8119
- {
8120
- align: "block-end",
8121
- className: cn("justify-between gap-1", className),
8122
- ...props
8123
- }
8281
+ }) => (
8282
+ // flex-wrap: the footer holds a left tools group (add, browser profile,
8283
+ // workspace) and a right group (view mode, send). On a phone those stop
8284
+ // fitting one line as soon as a profile or workspace chip appears, and a
8285
+ // non-wrapping row crushes the icons instead. Wrapping only engages when
8286
+ // the space is genuinely gone, so wider screens are unchanged.
8287
+ /* @__PURE__ */ jsx(
8288
+ InputGroupAddon,
8289
+ {
8290
+ align: "block-end",
8291
+ className: cn("flex-wrap justify-between gap-1", className),
8292
+ ...props
8293
+ }
8294
+ )
8124
8295
  );
8125
8296
  var PromptInputTools = ({
8126
8297
  className,
@@ -8233,41 +8404,6 @@ function splitComposerFiles(files) {
8233
8404
  }
8234
8405
  return { images, pending };
8235
8406
  }
8236
- function Popover({
8237
- ...props
8238
- }) {
8239
- return /* @__PURE__ */ jsx(Popover$1.Root, { "data-slot": "popover", ...props });
8240
- }
8241
- function PopoverTrigger({
8242
- ...props
8243
- }) {
8244
- return /* @__PURE__ */ jsx(Popover$1.Trigger, { "data-slot": "popover-trigger", ...props });
8245
- }
8246
- function PopoverContent({
8247
- className,
8248
- align = "center",
8249
- sideOffset = 4,
8250
- ...props
8251
- }) {
8252
- return /* @__PURE__ */ jsx(Popover$1.Portal, { children: /* @__PURE__ */ jsx(
8253
- Popover$1.Content,
8254
- {
8255
- "data-slot": "popover-content",
8256
- align,
8257
- sideOffset,
8258
- className: cn(
8259
- "z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-4 rounded-none bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-150 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
8260
- className
8261
- ),
8262
- ...props
8263
- }
8264
- ) });
8265
- }
8266
- function PopoverAnchor({
8267
- ...props
8268
- }) {
8269
- return /* @__PURE__ */ jsx(Popover$1.Anchor, { "data-slot": "popover-anchor", ...props });
8270
- }
8271
8407
  function Progress({
8272
8408
  className,
8273
8409
  value,
@@ -8302,24 +8438,26 @@ var ContextContext = createContext(null);
8302
8438
  var useContextValue = () => {
8303
8439
  const context = useContext(ContextContext);
8304
8440
  if (!context) {
8305
- throw new Error("Context components must be used within Context");
8441
+ throw new Error(
8442
+ "Context components must be used within ContextValueProvider"
8443
+ );
8306
8444
  }
8307
8445
  return context;
8308
8446
  };
8309
- var Context = ({
8447
+ var ContextValueProvider = ({
8310
8448
  usedTokens,
8311
8449
  maxTokens,
8312
8450
  usage,
8313
8451
  modelId,
8314
- ...props
8452
+ children
8315
8453
  }) => {
8316
8454
  const contextValue = useMemo(
8317
8455
  () => ({ maxTokens, modelId, usage, usedTokens }),
8318
8456
  [maxTokens, modelId, usage, usedTokens]
8319
8457
  );
8320
- return /* @__PURE__ */ jsx(ContextContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(Popover, { ...props }) });
8458
+ return /* @__PURE__ */ jsx(ContextContext.Provider, { value: contextValue, children });
8321
8459
  };
8322
- var ContextIcon = () => {
8460
+ var ContextIcon = ({ className }) => {
8323
8461
  const { usedTokens, maxTokens } = useContextValue();
8324
8462
  const circumference = 2 * Math.PI * ICON_RADIUS;
8325
8463
  const usedPercent = usedTokens / maxTokens;
@@ -8328,6 +8466,7 @@ var ContextIcon = () => {
8328
8466
  "svg",
8329
8467
  {
8330
8468
  "aria-label": "Model context usage",
8469
+ className,
8331
8470
  height: "20",
8332
8471
  role: "img",
8333
8472
  style: { color: "currentcolor" },
@@ -8366,19 +8505,6 @@ var ContextIcon = () => {
8366
8505
  }
8367
8506
  );
8368
8507
  };
8369
- var ContextTrigger = ({ children, ...props }) => {
8370
- return /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: children ?? /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", ...props, children: /* @__PURE__ */ jsx(ContextIcon, {}) }) });
8371
- };
8372
- var ContextContent = ({
8373
- className,
8374
- ...props
8375
- }) => /* @__PURE__ */ jsx(
8376
- PopoverContent,
8377
- {
8378
- className: cn("min-w-60 divide-y overflow-hidden p-0", className),
8379
- ...props
8380
- }
8381
- );
8382
8508
  var ContextContentHeader = ({
8383
8509
  children,
8384
8510
  className,
@@ -8676,6 +8802,114 @@ function ItemActions({ className, ...props }) {
8676
8802
  }
8677
8803
  );
8678
8804
  }
8805
+ function Popover({
8806
+ ...props
8807
+ }) {
8808
+ return /* @__PURE__ */ jsx(Popover$1.Root, { "data-slot": "popover", ...props });
8809
+ }
8810
+ function PopoverTrigger({
8811
+ ...props
8812
+ }) {
8813
+ return /* @__PURE__ */ jsx(Popover$1.Trigger, { "data-slot": "popover-trigger", ...props });
8814
+ }
8815
+ function PopoverContent({
8816
+ className,
8817
+ align = "center",
8818
+ sideOffset = 4,
8819
+ ...props
8820
+ }) {
8821
+ return /* @__PURE__ */ jsx(Popover$1.Portal, { children: /* @__PURE__ */ jsx(
8822
+ Popover$1.Content,
8823
+ {
8824
+ "data-slot": "popover-content",
8825
+ align,
8826
+ sideOffset,
8827
+ className: cn(
8828
+ "z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-4 rounded-none bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-150 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
8829
+ className
8830
+ ),
8831
+ ...props
8832
+ }
8833
+ ) });
8834
+ }
8835
+ function PopoverAnchor({
8836
+ ...props
8837
+ }) {
8838
+ return /* @__PURE__ */ jsx(Popover$1.Anchor, { "data-slot": "popover-anchor", ...props });
8839
+ }
8840
+ var MOBILE_QUERY = "(max-width: 767px)";
8841
+ function subscribe(onChange) {
8842
+ const mql = window.matchMedia(MOBILE_QUERY);
8843
+ mql.addEventListener("change", onChange);
8844
+ return () => mql.removeEventListener("change", onChange);
8845
+ }
8846
+ function useIsMobile() {
8847
+ return useSyncExternalStore(
8848
+ subscribe,
8849
+ () => window.matchMedia(MOBILE_QUERY).matches,
8850
+ // No viewport when server-rendered; assume desktop, as the SDK does.
8851
+ () => false
8852
+ );
8853
+ }
8854
+ function ResponsivePanel({
8855
+ open,
8856
+ onOpenChange,
8857
+ trigger,
8858
+ title,
8859
+ children,
8860
+ align = "start",
8861
+ popoverClassName
8862
+ }) {
8863
+ const isMobile = useIsMobile();
8864
+ if (!isMobile) {
8865
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange, children: [
8866
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: trigger }),
8867
+ /* @__PURE__ */ jsx(
8868
+ PopoverContent,
8869
+ {
8870
+ side: "top",
8871
+ align,
8872
+ className: cn("w-64 overflow-hidden rounded-xl p-1", popoverClassName),
8873
+ children
8874
+ }
8875
+ )
8876
+ ] });
8877
+ }
8878
+ return /* @__PURE__ */ jsxs(Dialog$1.Root, { open, onOpenChange, children: [
8879
+ /* @__PURE__ */ jsx(Dialog$1.Trigger, { asChild: true, children: trigger }),
8880
+ /* @__PURE__ */ jsxs(Dialog$1.Portal, { children: [
8881
+ /* @__PURE__ */ jsx(
8882
+ Dialog$1.Overlay,
8883
+ {
8884
+ "data-slot": "responsive-panel-overlay",
8885
+ className: "fixed inset-0 z-50 bg-black/40 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0"
8886
+ }
8887
+ ),
8888
+ /* @__PURE__ */ jsxs(
8889
+ Dialog$1.Content,
8890
+ {
8891
+ "data-slot": "responsive-panel-sheet",
8892
+ "aria-describedby": void 0,
8893
+ className: cn(
8894
+ "fixed inset-x-0 bottom-0 z-50 flex max-h-[80svh] flex-col overflow-hidden rounded-t-2xl border-t border-border bg-popover pb-[env(safe-area-inset-bottom)] text-popover-foreground shadow-lg",
8895
+ "data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom data-[state=open]:animate-in data-[state=open]:slide-in-from-bottom"
8896
+ ),
8897
+ children: [
8898
+ /* @__PURE__ */ jsx(Dialog$1.Title, { className: "sr-only", children: title }),
8899
+ /* @__PURE__ */ jsx(
8900
+ "div",
8901
+ {
8902
+ "aria-hidden": "true",
8903
+ className: "mx-auto mt-2.5 mb-1 h-1 w-9 shrink-0 rounded-full bg-muted-foreground/30"
8904
+ }
8905
+ ),
8906
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain p-1", children })
8907
+ ]
8908
+ }
8909
+ )
8910
+ ] })
8911
+ ] });
8912
+ }
8679
8913
  function Skeleton({ className, ...props }) {
8680
8914
  return /* @__PURE__ */ jsx(
8681
8915
  "div",
@@ -8847,6 +9081,7 @@ function SkillsEmptyState() {
8847
9081
  ] })
8848
9082
  ] });
8849
9083
  }
9084
+ var TOOLS_ITEM_CLASS = "gap-3 rounded-md px-3 py-2 pointer-coarse:min-h-11 pointer-coarse:text-[15px]";
8850
9085
  function ChatComposer(props) {
8851
9086
  return /* @__PURE__ */ jsx(PromptInputProvider, { children: /* @__PURE__ */ jsx(ChatComposerInner, { ...props }) });
8852
9087
  }
@@ -8944,6 +9179,8 @@ function ChatComposerInner({
8944
9179
  isRunning,
8945
9180
  disabled = false,
8946
9181
  disabledReason,
9182
+ awaitingAnswer = false,
9183
+ placeholder,
8947
9184
  tokenUsage,
8948
9185
  onComposerError,
8949
9186
  showBrowser = false,
@@ -8967,7 +9204,6 @@ function ChatComposerInner({
8967
9204
  onViewModeChange
8968
9205
  }) {
8969
9206
  const { adapter } = useAgentChatAdapterContext();
8970
- const [profileMenuOpen, setProfileMenuOpen] = useState(false);
8971
9207
  const [browserProfiles, setBrowserProfiles] = useState(null);
8972
9208
  const loadBrowserProfiles = useCallback(() => {
8973
9209
  if (!adapter.listBrowserProfiles) return;
@@ -8980,16 +9216,15 @@ function ChatComposerInner({
8980
9216
  loadBrowserProfiles();
8981
9217
  }, [browserProfilesEnabled, browserProfileId, browserProfiles, loadBrowserProfiles]);
8982
9218
  const activeBrowserProfile = (browserProfiles ?? []).find((p) => p.id === browserProfileId) ?? null;
8983
- const profileTriggerLabel = activeBrowserProfile && /* @__PURE__ */ jsx("span", { className: "max-w-24 truncate text-xs", children: activeBrowserProfile.name });
8984
9219
  const { textInput, attachments } = usePromptInputController();
8985
- const status = isRunning ? "streaming" : disabled ? "error" : "ready";
9220
+ const status = isRunning && !awaitingAnswer ? "streaming" : disabled ? "error" : "ready";
8986
9221
  const [adapterCommands, setAdapterCommands] = useState([]);
8987
9222
  const [skillsLoading, setSkillsLoading] = useState(false);
8988
9223
  const [buttonMenuOpen, setButtonMenuOpen] = useState(false);
8989
9224
  const [menuMode, setMenuMode] = useState("all");
8990
9225
  const [addMenuOpen, setAddMenuOpen] = useState(false);
8991
9226
  const [menuDismissed, setMenuDismissed] = useState(false);
8992
- const showSlashMenu = !menuDismissed && (textInput.value.startsWith("/") || buttonMenuOpen);
9227
+ const showSlashMenu = !menuDismissed && !awaitingAnswer && (textInput.value.startsWith("/") || buttonMenuOpen);
8993
9228
  useEffect(() => {
8994
9229
  if (menuDismissed && !textInput.value.startsWith("/") && !buttonMenuOpen) {
8995
9230
  setMenuDismissed(false);
@@ -9185,16 +9420,13 @@ function ChatComposerInner({
9185
9420
  });
9186
9421
  return;
9187
9422
  }
9188
- if (isRunning) {
9189
- await onStop();
9190
- }
9191
9423
  await onSend(
9192
9424
  text,
9193
9425
  images.length > 0 ? images : void 0,
9194
9426
  pending.length > 0 ? pending : void 0
9195
9427
  );
9196
9428
  },
9197
- [onSend, onStop, onComposerError, isRunning, disabled]
9429
+ [onSend, onComposerError, disabled]
9198
9430
  );
9199
9431
  const handlePromptInputError = useCallback(
9200
9432
  (err) => {
@@ -9202,6 +9434,59 @@ function ChatComposerInner({
9202
9434
  },
9203
9435
  [onComposerError]
9204
9436
  );
9437
+ const [contextOpen, setContextOpen] = useState(false);
9438
+ const contextPercentLabel = tokenUsage?.contextWindow ? `${Math.min(100, Math.round(tokenUsage.totalTokens / tokenUsage.contextWindow * 100))}%` : "0%";
9439
+ const canAttach = !disabled && !awaitingAnswer;
9440
+ const showPaneToggles = Boolean(
9441
+ canShowBrowser && onToggleBrowser || canShowWorkspace && onToggleWorkspace
9442
+ );
9443
+ const canPickProfile = Boolean(
9444
+ browserProfilesEnabled && onSelectBrowserProfile && adapter.listBrowserProfiles
9445
+ );
9446
+ const showToolsPanel = canAttach || showPaneToggles || canPickProfile;
9447
+ const viewModeToggle = onViewModeChange ? /* @__PURE__ */ jsxs(
9448
+ ButtonGroup,
9449
+ {
9450
+ "aria-label": "Chat view mode",
9451
+ className: "overflow-hidden rounded-full border border-border bg-muted/40 p-0.5",
9452
+ children: [
9453
+ /* @__PURE__ */ jsx(
9454
+ PromptInputButton,
9455
+ {
9456
+ "aria-label": "Simple view",
9457
+ "aria-pressed": viewMode === "simple",
9458
+ tooltip: "Simple \u2014 just the conversation",
9459
+ variant: viewMode === "simple" ? "secondary" : "ghost",
9460
+ onClick: () => onViewModeChange("simple"),
9461
+ className: cn(
9462
+ // 32px is a comfortable segment for a cursor and a miss for a
9463
+ // thumb, so touch takes both halves to the 44px floor.
9464
+ "size-8 rounded-full pointer-coarse:size-11",
9465
+ viewMode === "simple" && "shadow-sm"
9466
+ ),
9467
+ children: /* @__PURE__ */ jsx(MessageSquareIcon, { className: "size-[18px] pointer-coarse:size-5" })
9468
+ }
9469
+ ),
9470
+ /* @__PURE__ */ jsx(
9471
+ PromptInputButton,
9472
+ {
9473
+ "aria-label": "Advanced view",
9474
+ "aria-pressed": viewMode === "expert",
9475
+ tooltip: "Advanced \u2014 every step the agent took",
9476
+ variant: viewMode === "expert" ? "secondary" : "ghost",
9477
+ onClick: () => onViewModeChange("expert"),
9478
+ className: cn(
9479
+ // 32px is a comfortable segment for a cursor and a miss for a
9480
+ // thumb, so touch takes both halves to the 44px floor.
9481
+ "size-8 rounded-full pointer-coarse:size-11",
9482
+ viewMode === "expert" && "shadow-sm"
9483
+ ),
9484
+ children: /* @__PURE__ */ jsx(ListTreeIcon, { className: "size-[18px] pointer-coarse:size-5" })
9485
+ }
9486
+ )
9487
+ ]
9488
+ }
9489
+ ) : null;
9205
9490
  return /* @__PURE__ */ jsxs(
9206
9491
  Popover,
9207
9492
  {
@@ -9254,154 +9539,132 @@ function ChatComposerInner({
9254
9539
  PromptInputTextarea,
9255
9540
  {
9256
9541
  ref: textareaRef,
9257
- placeholder: disabled ? disabledReason ?? "Session disabled" : "Send a message...",
9542
+ placeholder: disabled ? disabledReason ?? "Session disabled" : placeholder ?? "Send a message...",
9258
9543
  disabled
9259
9544
  }
9260
9545
  ) }),
9261
9546
  /* @__PURE__ */ jsxs(PromptInputFooter, { children: [
9262
9547
  /* @__PURE__ */ jsxs(PromptInputTools, { children: [
9263
- !disabled && /* @__PURE__ */ jsxs(Popover, { open: addMenuOpen, onOpenChange: setAddMenuOpen, children: [
9264
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(PromptInputButton, { "aria-label": "Add", children: /* @__PURE__ */ jsx(PlusIcon, { className: "size-4" }) }) }),
9265
- /* @__PURE__ */ jsx(
9266
- PopoverContent,
9267
- {
9268
- side: "top",
9269
- align: "start",
9270
- className: "w-64 overflow-hidden rounded-xl p-1",
9271
- children: /* @__PURE__ */ jsx(Command, { children: /* @__PURE__ */ jsxs(CommandList, { children: [
9548
+ showToolsPanel && /* @__PURE__ */ jsx(
9549
+ ResponsivePanel,
9550
+ {
9551
+ open: addMenuOpen,
9552
+ onOpenChange: (open) => {
9553
+ setAddMenuOpen(open);
9554
+ if (open && canPickProfile && !browserProfileLocked && browserProfiles === null) {
9555
+ loadBrowserProfiles();
9556
+ }
9557
+ },
9558
+ title: "Composer tools",
9559
+ trigger: /* @__PURE__ */ jsx(
9560
+ PromptInputButton,
9561
+ {
9562
+ "aria-label": "Composer tools",
9563
+ tooltip: "Attach files, panes and browser profile",
9564
+ children: /* @__PURE__ */ jsx(PlusIcon, { className: "size-4" })
9565
+ }
9566
+ ),
9567
+ children: /* @__PURE__ */ jsx(Command, { children: /* @__PURE__ */ jsxs(CommandList, { className: "max-md:max-h-none", children: [
9568
+ canAttach && /* @__PURE__ */ jsxs(CommandGroup, { heading: "Attach", children: [
9272
9569
  /* @__PURE__ */ jsxs(
9273
9570
  CommandItem,
9274
9571
  {
9275
- disabled: true,
9276
- className: "gap-3 rounded-md px-3 py-2 [&>svg:last-child]:hidden",
9572
+ onSelect: () => {
9573
+ setAddMenuOpen(false);
9574
+ attachments.openFileDialog();
9575
+ },
9576
+ className: TOOLS_ITEM_CLASS,
9277
9577
  children: [
9278
- /* @__PURE__ */ jsx(CloudIcon, { className: "size-4 shrink-0 text-sky-500" }),
9279
- "Add from OneDrive",
9280
- /* @__PURE__ */ jsx(ChevronRightIcon, { className: "ml-auto size-4 shrink-0 text-muted-foreground" })
9578
+ /* @__PURE__ */ jsx(PaperclipIcon, { className: "size-4 shrink-0 text-muted-foreground" }),
9579
+ "Add local files"
9281
9580
  ]
9282
9581
  }
9283
9582
  ),
9284
- /* @__PURE__ */ jsxs(
9583
+ /* @__PURE__ */ jsxs(CommandItem, { disabled: true, className: TOOLS_ITEM_CLASS, children: [
9584
+ /* @__PURE__ */ jsx(CloudIcon, { className: "size-4 shrink-0 text-sky-500" }),
9585
+ "Add from OneDrive"
9586
+ ] }),
9587
+ /* @__PURE__ */ jsxs(CommandItem, { disabled: true, className: TOOLS_ITEM_CLASS, children: [
9588
+ /* @__PURE__ */ jsx(HardDriveIcon, { className: "size-4 shrink-0 text-emerald-500" }),
9589
+ "Add from Google Drive"
9590
+ ] })
9591
+ ] }),
9592
+ showPaneToggles && // Panes stay available while a question is pending —
9593
+ // only attachments are suppressed there.
9594
+ /* @__PURE__ */ jsxs(CommandGroup, { heading: "Panes", children: [
9595
+ canShowBrowser && onToggleBrowser && /* @__PURE__ */ jsxs(
9285
9596
  CommandItem,
9286
9597
  {
9287
- disabled: true,
9288
- className: "gap-3 rounded-md px-3 py-2 [&>svg:last-child]:hidden",
9598
+ onSelect: onToggleBrowser,
9599
+ "data-checked": showBrowser || void 0,
9600
+ className: TOOLS_ITEM_CLASS,
9289
9601
  children: [
9290
- /* @__PURE__ */ jsx(HardDriveIcon, { className: "size-4 shrink-0 text-emerald-500" }),
9291
- "Add from Google Drive"
9602
+ /* @__PURE__ */ jsx(GlobeIcon, { className: "size-4 shrink-0 text-muted-foreground" }),
9603
+ "Browser"
9292
9604
  ]
9293
9605
  }
9294
9606
  ),
9295
- /* @__PURE__ */ jsxs(
9607
+ canShowWorkspace && onToggleWorkspace && /* @__PURE__ */ jsxs(
9296
9608
  CommandItem,
9297
9609
  {
9298
- onSelect: () => {
9299
- setAddMenuOpen(false);
9300
- attachments.openFileDialog();
9301
- },
9302
- className: "gap-3 rounded-md px-3 py-2 [&>svg:last-child]:hidden",
9610
+ onSelect: onToggleWorkspace,
9611
+ "data-checked": showWorkspace || void 0,
9612
+ className: TOOLS_ITEM_CLASS,
9303
9613
  children: [
9304
- /* @__PURE__ */ jsx(PaperclipIcon, { className: "size-4 shrink-0 text-muted-foreground" }),
9305
- "Add local files"
9614
+ /* @__PURE__ */ jsx(FolderIcon, { className: "size-4 shrink-0 text-muted-foreground" }),
9615
+ "Workspace"
9306
9616
  ]
9307
9617
  }
9308
9618
  )
9309
- ] }) })
9310
- }
9311
- )
9312
- ] }),
9313
- canShowBrowser && onToggleBrowser && /* @__PURE__ */ jsx(
9314
- PromptInputButton,
9315
- {
9316
- "aria-label": showBrowser ? "Hide browser pane" : "Show browser pane",
9317
- "aria-pressed": showBrowser,
9318
- onClick: onToggleBrowser,
9319
- tooltip: showBrowser ? "Hide browser pane" : "Show browser pane",
9320
- className: showBrowser ? "bg-accent text-foreground" : void 0,
9321
- children: /* @__PURE__ */ jsx(GlobeIcon, { className: "size-4" })
9322
- }
9323
- ),
9324
- browserProfilesEnabled && onSelectBrowserProfile && adapter.listBrowserProfiles && (browserProfileLocked ? (
9325
- // A profile is bound at session creation; once a session is
9326
- // active the choice can't change. Render disabled (via
9327
- // aria-disabled, not the `disabled` attribute, so the
9328
- // tooltip still shows on hover) with an explanation.
9329
- /* @__PURE__ */ jsxs(
9330
- PromptInputButton,
9331
- {
9332
- "aria-label": "Select browser profile",
9333
- "aria-disabled": true,
9334
- "aria-pressed": !!browserProfileId,
9335
- tooltip: activeBrowserProfile ? `Browser profile: ${activeBrowserProfile.name} (locked for this session)` : "A browser profile must be chosen before starting the session",
9336
- className: `cursor-not-allowed opacity-50 ${browserProfileId ? "bg-accent text-foreground" : ""}`,
9337
- children: [
9338
- /* @__PURE__ */ jsx(IdCardIcon, { className: "size-4" }),
9339
- profileTriggerLabel
9340
- ]
9341
- }
9342
- )
9343
- ) : /* @__PURE__ */ jsxs(
9344
- Popover,
9345
- {
9346
- open: profileMenuOpen,
9347
- onOpenChange: (open) => {
9348
- setProfileMenuOpen(open);
9349
- if (open) loadBrowserProfiles();
9350
- },
9351
- children: [
9352
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
9353
- PromptInputButton,
9354
- {
9355
- "aria-label": "Select browser profile",
9356
- "aria-pressed": !!browserProfileId,
9357
- tooltip: activeBrowserProfile ? `Browser profile: ${activeBrowserProfile.name}` : "Select browser profile",
9358
- className: browserProfileId ? "bg-accent text-foreground" : void 0,
9359
- children: [
9360
- /* @__PURE__ */ jsx(IdCardIcon, { className: "size-4" }),
9361
- profileTriggerLabel
9362
- ]
9363
- }
9364
- ) }),
9365
- /* @__PURE__ */ jsx(
9366
- PopoverContent,
9367
- {
9368
- side: "top",
9369
- align: "start",
9370
- className: "w-64 overflow-hidden rounded-xl p-1",
9371
- children: /* @__PURE__ */ jsx(Command, { children: /* @__PURE__ */ jsx(CommandList, { children: [
9372
- { id: null, name: "No profile" },
9373
- ...browserProfiles ?? []
9374
- ].map((p) => /* @__PURE__ */ jsx(
9619
+ ] }),
9620
+ canPickProfile && /* @__PURE__ */ jsx(CommandGroup, { heading: "Browser profile", children: browserProfileLocked ? (
9621
+ // A profile is bound at session creation and cannot
9622
+ // change mid-session. In a menu there is nothing to
9623
+ // hover, so the reason is stated outright rather
9624
+ // than hidden in a tooltip as it was on the button.
9625
+ /* @__PURE__ */ jsxs(Fragment, { children: [
9626
+ /* @__PURE__ */ jsxs(
9375
9627
  CommandItem,
9376
9628
  {
9377
- onSelect: () => {
9378
- setProfileMenuOpen(false);
9379
- onSelectBrowserProfile(p.id);
9380
- },
9381
- "data-checked": (browserProfileId ?? null) === p.id || void 0,
9382
- className: "gap-3 rounded-md px-3 py-2",
9383
- children: p.name
9629
+ disabled: true,
9630
+ "data-checked": !!browserProfileId || void 0,
9631
+ className: TOOLS_ITEM_CLASS,
9632
+ children: [
9633
+ /* @__PURE__ */ jsx(IdCardIcon, { className: "size-4 shrink-0 text-muted-foreground" }),
9634
+ activeBrowserProfile?.name ?? "No profile"
9635
+ ]
9636
+ }
9637
+ ),
9638
+ /* @__PURE__ */ jsx("p", { className: "px-3 pt-0.5 pb-1.5 text-xs text-muted-foreground", children: activeBrowserProfile ? "Locked for this session." : "A profile can only be chosen before the session starts." })
9639
+ ] })
9640
+ ) : (
9641
+ // data-checked lights CommandItem's built-in
9642
+ // trailing check on the selected entry.
9643
+ [
9644
+ { id: null, name: "No profile" },
9645
+ ...browserProfiles ?? []
9646
+ ].map((p) => /* @__PURE__ */ jsxs(
9647
+ CommandItem,
9648
+ {
9649
+ onSelect: () => {
9650
+ setAddMenuOpen(false);
9651
+ onSelectBrowserProfile?.(p.id);
9384
9652
  },
9385
- p.id ?? "none"
9386
- )) }) })
9387
- }
9388
- )
9389
- ]
9390
- }
9391
- )),
9392
- canShowWorkspace && onToggleWorkspace && /* @__PURE__ */ jsx(
9393
- PromptInputButton,
9394
- {
9395
- "aria-label": showWorkspace ? "Hide workspace pane" : "Show workspace pane",
9396
- "aria-pressed": showWorkspace,
9397
- onClick: onToggleWorkspace,
9398
- tooltip: showWorkspace ? "Hide workspace pane" : "Show workspace pane",
9399
- className: showWorkspace ? "bg-accent text-foreground" : void 0,
9400
- children: /* @__PURE__ */ jsx(FolderIcon, { className: "size-4" })
9653
+ "data-checked": (browserProfileId ?? null) === p.id || void 0,
9654
+ className: TOOLS_ITEM_CLASS,
9655
+ children: [
9656
+ /* @__PURE__ */ jsx(IdCardIcon, { className: "size-4 shrink-0 text-muted-foreground" }),
9657
+ p.name
9658
+ ]
9659
+ },
9660
+ p.id ?? "none"
9661
+ ))
9662
+ ) })
9663
+ ] }) })
9401
9664
  }
9402
9665
  ),
9403
- !disabled && tokenUsage && tokenUsage.contextWindow > 0 && /* @__PURE__ */ jsxs(
9404
- Context,
9666
+ !disabled && tokenUsage && tokenUsage.contextWindow > 0 && /* @__PURE__ */ jsx(
9667
+ ContextValueProvider,
9405
9668
  {
9406
9669
  usedTokens: tokenUsage.totalTokens,
9407
9670
  maxTokens: tokenUsage.contextWindow,
@@ -9415,82 +9678,68 @@ function ChatComposerInner({
9415
9678
  inputTokenDetails: void 0,
9416
9679
  outputTokenDetails: void 0
9417
9680
  },
9418
- children: [
9419
- /* @__PURE__ */ jsx(
9420
- ContextTrigger,
9421
- {
9422
- size: "icon-sm",
9423
- className: "rounded-sm border border-input/70 text-foreground hover:bg-accent"
9424
- }
9425
- ),
9426
- /* @__PURE__ */ jsxs(ContextContent, { children: [
9427
- /* @__PURE__ */ jsx(ContextContentHeader, {}),
9428
- /* @__PURE__ */ jsx(ContextContentBody, { children: tokenUsage.totalTokens > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
9429
- /* @__PURE__ */ jsx(ContextInputUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
9430
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Input" }),
9431
- /* @__PURE__ */ jsx("span", { children: tokenUsage.inputTokens.toLocaleString() })
9432
- ] }) }),
9433
- /* @__PURE__ */ jsx(ContextOutputUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
9434
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Output" }),
9435
- /* @__PURE__ */ jsx("span", { children: tokenUsage.outputTokens.toLocaleString() })
9436
- ] }) }),
9437
- tokenUsage.reasoningTokens > 0 && /* @__PURE__ */ jsx(ContextReasoningUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
9438
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Reasoning" }),
9439
- /* @__PURE__ */ jsx("span", { children: tokenUsage.reasoningTokens.toLocaleString() })
9440
- ] }) }),
9441
- tokenUsage.cachedInputTokens > 0 && /* @__PURE__ */ jsx(ContextCacheUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
9442
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Cache" }),
9443
- /* @__PURE__ */ jsx("span", { children: tokenUsage.cachedInputTokens.toLocaleString() })
9444
- ] }) })
9445
- ] }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground text-center py-1", children: "Empty" }) }),
9446
- tokenUsage.totalTokens > 0 && /* @__PURE__ */ jsx("div", { className: "flex w-full items-center justify-end gap-3 bg-secondary p-2", children: /* @__PURE__ */ jsx(
9447
- Button,
9681
+ children: /* @__PURE__ */ jsxs(
9682
+ ResponsivePanel,
9683
+ {
9684
+ open: contextOpen,
9685
+ onOpenChange: setContextOpen,
9686
+ title: "Session context",
9687
+ align: "end",
9688
+ popoverClassName: "min-w-60 divide-y p-0",
9689
+ trigger: /* @__PURE__ */ jsxs(
9690
+ PromptInputButton,
9448
9691
  {
9449
- type: "button",
9450
- size: "xs",
9451
- onClick: () => onSend("/compress"),
9452
- children: "Compress"
9692
+ "aria-label": `Session context: ${contextPercentLabel} of the window used`,
9693
+ tooltip: "Session context",
9694
+ variant: "ghost",
9695
+ className: "h-9 gap-1 rounded-md border-0 bg-transparent px-1.5 text-muted-foreground hover:bg-accent/60 hover:text-foreground pointer-coarse:h-11",
9696
+ children: [
9697
+ /* @__PURE__ */ jsx(ContextIcon, { className: "size-4" }),
9698
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] tabular-nums", children: contextPercentLabel })
9699
+ ]
9453
9700
  }
9454
- ) })
9455
- ] })
9456
- ]
9701
+ ),
9702
+ children: [
9703
+ /* @__PURE__ */ jsx(ContextContentHeader, {}),
9704
+ /* @__PURE__ */ jsx(ContextContentBody, { children: tokenUsage.totalTokens > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
9705
+ /* @__PURE__ */ jsx(ContextInputUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
9706
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Input" }),
9707
+ /* @__PURE__ */ jsx("span", { children: tokenUsage.inputTokens.toLocaleString() })
9708
+ ] }) }),
9709
+ /* @__PURE__ */ jsx(ContextOutputUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
9710
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Output" }),
9711
+ /* @__PURE__ */ jsx("span", { children: tokenUsage.outputTokens.toLocaleString() })
9712
+ ] }) }),
9713
+ tokenUsage.reasoningTokens > 0 && /* @__PURE__ */ jsx(ContextReasoningUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
9714
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Reasoning" }),
9715
+ /* @__PURE__ */ jsx("span", { children: tokenUsage.reasoningTokens.toLocaleString() })
9716
+ ] }) }),
9717
+ tokenUsage.cachedInputTokens > 0 && /* @__PURE__ */ jsx(ContextCacheUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
9718
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Cache" }),
9719
+ /* @__PURE__ */ jsx("span", { children: tokenUsage.cachedInputTokens.toLocaleString() })
9720
+ ] }) })
9721
+ ] }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground text-center py-1", children: "Empty" }) }),
9722
+ tokenUsage.totalTokens > 0 && /* @__PURE__ */ jsx("div", { className: "flex w-full items-center justify-end gap-3 bg-secondary p-2", children: /* @__PURE__ */ jsx(
9723
+ Button,
9724
+ {
9725
+ type: "button",
9726
+ size: "xs",
9727
+ className: "pointer-coarse:h-11 pointer-coarse:px-4 pointer-coarse:text-sm",
9728
+ onClick: () => {
9729
+ setContextOpen(false);
9730
+ onSend("/compress");
9731
+ },
9732
+ children: "Compress"
9733
+ }
9734
+ ) })
9735
+ ]
9736
+ }
9737
+ )
9457
9738
  }
9458
9739
  )
9459
9740
  ] }),
9460
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
9461
- onViewModeChange && /* @__PURE__ */ jsxs(
9462
- ButtonGroup,
9463
- {
9464
- "aria-label": "Chat view mode",
9465
- className: "overflow-hidden rounded-md border border-border",
9466
- children: [
9467
- /* @__PURE__ */ jsx(
9468
- Button,
9469
- {
9470
- type: "button",
9471
- size: "xs",
9472
- className: "capitalize font-normal",
9473
- variant: viewMode === "simple" ? "secondary" : "ghost",
9474
- "aria-pressed": viewMode === "simple",
9475
- onClick: () => onViewModeChange("simple"),
9476
- children: "Simple"
9477
- }
9478
- ),
9479
- /* @__PURE__ */ jsx(
9480
- Button,
9481
- {
9482
- type: "button",
9483
- size: "xs",
9484
- className: "capitalize font-normal",
9485
- variant: viewMode === "expert" ? "secondary" : "ghost",
9486
- "aria-pressed": viewMode === "expert",
9487
- onClick: () => onViewModeChange("expert"),
9488
- children: "Advanced"
9489
- }
9490
- )
9491
- ]
9492
- }
9493
- ),
9741
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: [
9742
+ viewModeToggle,
9494
9743
  !disabled && /* @__PURE__ */ jsx(
9495
9744
  PromptInputSubmit,
9496
9745
  {
@@ -11410,16 +11659,34 @@ var WORKING_ON_IT_DELAY_MS = 250;
11410
11659
  function hasUserFacingAskContent(msg) {
11411
11660
  return !!msg.toolCalls?.some((tc) => tc.toolName === "ask_user_question");
11412
11661
  }
11413
- function isAwaitingUserInput(messages) {
11662
+ function pendingAskCall(messages) {
11414
11663
  for (let i = messages.length - 1; i >= 0; i--) {
11415
11664
  const m = messages[i];
11416
- if (m.role === "user") return false;
11665
+ if (m.role === "user") return null;
11417
11666
  if (m.role !== "assistant") continue;
11418
- return !!m.toolCalls?.some(
11667
+ return m.toolCalls?.find(
11419
11668
  (tc) => tc.toolName === "ask_user_question" && tc.status === "running"
11420
- );
11669
+ ) ?? null;
11421
11670
  }
11422
- return false;
11671
+ return null;
11672
+ }
11673
+ function askComposerPolicy(pending) {
11674
+ if (!pending) return null;
11675
+ const questions = askQuestionsOf(pending);
11676
+ if (!isConversationalAsk(questions)) {
11677
+ return { locked: true, reason: "Answer the questions above to continue." };
11678
+ }
11679
+ const question = questions[0];
11680
+ if (!conversationalAskAcceptsFreeText(question)) {
11681
+ return {
11682
+ locked: true,
11683
+ reason: "Pick one of the options above to continue."
11684
+ };
11685
+ }
11686
+ return {
11687
+ locked: false,
11688
+ placeholder: hasChoices(question) ? "Pick one above, or type your answer\u2026" : "Type your answer\u2026"
11689
+ };
11423
11690
  }
11424
11691
  function messageToEntries(msg, isLast, artifactFallbacks) {
11425
11692
  if (msg.role === "system") {
@@ -11502,7 +11769,14 @@ function messageToEntries(msg, isLast, artifactFallbacks) {
11502
11769
  const parsed = parseTerminalResult(tc.result, tc.args);
11503
11770
  if (parsed && (parsed.exit_code !== 0 || parsed.error)) continue;
11504
11771
  }
11505
- entries.push({ kind: "tool", key: tc.id, tc });
11772
+ entries.push({
11773
+ kind: "tool",
11774
+ key: tc.id,
11775
+ tc,
11776
+ // Only ask_user_question reads this, to suppress a prompt the
11777
+ // agent already spelled out in its message body.
11778
+ assistantContent: tc.toolName === "ask_user_question" ? msg.content : void 0
11779
+ });
11506
11780
  }
11507
11781
  }
11508
11782
  if (effectiveHasContent) {
@@ -11747,6 +12021,8 @@ function TimelineEntryItem({
11747
12021
  {
11748
12022
  tc: entry.tc,
11749
12023
  resolvedArtifactName: entry.resolvedArtifactName,
12024
+ assistantContent: entry.assistantContent,
12025
+ viewMode: "expert",
11750
12026
  onFileSelect
11751
12027
  }
11752
12028
  ),
@@ -12270,7 +12546,14 @@ function IterationGroup({
12270
12546
  (tc) => tc.toolName === "ask_user_question"
12271
12547
  );
12272
12548
  if (askCall) {
12273
- return /* @__PURE__ */ jsx("div", { className: "px-1 py-0.5", children: /* @__PURE__ */ jsx(AskUserQuestionToolBlock, { tc: askCall }) });
12549
+ return /* @__PURE__ */ jsx("div", { className: "px-1 py-0.5", children: /* @__PURE__ */ jsx(
12550
+ AskUserQuestionToolBlock,
12551
+ {
12552
+ tc: askCall,
12553
+ assistantContent: message.content,
12554
+ viewMode: "simple"
12555
+ }
12556
+ ) });
12274
12557
  }
12275
12558
  if (isIterationLive(message)) {
12276
12559
  return /* @__PURE__ */ jsx(
@@ -12613,7 +12896,12 @@ function ChatThread({
12613
12896
  onOpenIntegrations
12614
12897
  }) {
12615
12898
  const groups = useMemo(() => groupMessages(messages), [messages]);
12616
- const awaitingInput = useMemo(() => isAwaitingUserInput(messages), [messages]);
12899
+ const pendingAsk = useMemo(() => pendingAskCall(messages), [messages]);
12900
+ const askPolicy = useMemo(
12901
+ () => askComposerPolicy(pendingAsk),
12902
+ [pendingAsk?.id, pendingAsk?.args]
12903
+ );
12904
+ const awaitingInput = askPolicy !== null;
12617
12905
  const orbActivity = useMemo(
12618
12906
  () => deriveOrbActivity(
12619
12907
  messages,
@@ -12622,8 +12910,9 @@ function ChatThread({
12622
12910
  [messages, viewMode]
12623
12911
  );
12624
12912
  const showWorkingOnIt = useDelayedRunningIndicator(isRunning) && !awaitingInput;
12625
- const composerDisabled = disabled || awaitingInput;
12626
- const composerDisabledReason = awaitingInput ? "Answer the question above to continue." : disabledReason;
12913
+ const composerLocked = askPolicy?.locked ?? false;
12914
+ const composerDisabled = disabled || composerLocked;
12915
+ const composerDisabledReason = composerLocked ? askPolicy?.reason : disabledReason;
12627
12916
  const artifactFallbacks = useMemo(() => {
12628
12917
  const fallbacks = {};
12629
12918
  const createArtifactToolCallIds = [];
@@ -12668,6 +12957,8 @@ function ChatThread({
12668
12957
  isRunning,
12669
12958
  disabled: composerDisabled,
12670
12959
  disabledReason: composerDisabledReason,
12960
+ awaitingAnswer: askPolicy !== null && !askPolicy.locked,
12961
+ placeholder: askPolicy?.placeholder,
12671
12962
  tokenUsage,
12672
12963
  onComposerError,
12673
12964
  showBrowser,
@@ -12700,10 +12991,16 @@ function ChatThread({
12700
12991
  )
12701
12992
  ] });
12702
12993
  if (messages.length === 0 && !isLoadingHistory && !disabled) {
12703
- return /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col items-center justify-center overflow-hidden bg-background px-3 text-base", children: /* @__PURE__ */ jsxs("div", { className: "w-full", children: [
12704
- /* @__PURE__ */ jsx("h2", { className: "mb-8 text-center text-2xl font-bold text-foreground", children: "What should we do ?" }),
12705
- composerArea
12706
- ] }) });
12994
+ return (
12995
+ // A centred hero is a deliberate desktop look. On a phone it leaves the
12996
+ // input floating mid-screen with dead space under it, and the keyboard
12997
+ // then opens across that gap. There the composer keeps its usual place
12998
+ // at the bottom edge and the greeting takes the space above it.
12999
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-end overflow-hidden bg-background px-3 text-base md:justify-center", children: [
13000
+ /* @__PURE__ */ jsx("div", { className: "flex w-full flex-1 flex-col justify-center md:flex-none", children: /* @__PURE__ */ jsx("h2", { className: "mb-6 text-center text-2xl font-bold text-foreground md:mb-8", children: "What should we do ?" }) }),
13001
+ /* @__PURE__ */ jsx("div", { className: "w-full", children: composerArea })
13002
+ ] })
13003
+ );
12707
13004
  }
12708
13005
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col overflow-hidden bg-background text-base", children: [
12709
13006
  /* @__PURE__ */ jsxs(Conversation, { className: "relative flex-1 min-h-0", children: [
@@ -13003,14 +13300,22 @@ function RenderEntries({
13003
13300
  }) {
13004
13301
  return /* @__PURE__ */ jsx(Fragment, { children: entries.map((entry) => {
13005
13302
  if (entry.kind === "dir") {
13006
- return /* @__PURE__ */ jsx(FileTreeFolder, { name: entry.name, path: entry.path, children: entry.children && entry.children.length > 0 && /* @__PURE__ */ jsx(
13007
- RenderEntries,
13303
+ return /* @__PURE__ */ jsx(
13304
+ FileTreeFolder,
13008
13305
  {
13009
- entries: entry.children,
13010
- onDelete,
13011
- downloadUrlFor
13012
- }
13013
- ) }, entry.path);
13306
+ name: entry.name,
13307
+ path: entry.path,
13308
+ children: entry.children && entry.children.length > 0 && /* @__PURE__ */ jsx(
13309
+ RenderEntries,
13310
+ {
13311
+ entries: entry.children,
13312
+ onDelete,
13313
+ downloadUrlFor
13314
+ }
13315
+ )
13316
+ },
13317
+ entry.path
13318
+ );
13014
13319
  }
13015
13320
  const fileName2 = entry.name;
13016
13321
  return /* @__PURE__ */ jsxs(FileTreeFile, { name: entry.name, path: entry.path, children: [
@@ -13069,6 +13374,7 @@ function WorkspacePanel({
13069
13374
  const [deleteTarget, setDeleteTarget] = useState(null);
13070
13375
  const [expandedPaths, setExpandedPaths] = useState(/* @__PURE__ */ new Set());
13071
13376
  const [width, setWidth] = useState(DEFAULT_WIDTH);
13377
+ const narrow = useIsMobile();
13072
13378
  const sessionIdRef = useRef(sessionId);
13073
13379
  const isResizing = useRef(false);
13074
13380
  const startX = useRef(0);
@@ -13218,7 +13524,14 @@ function WorkspacePanel({
13218
13524
  setDeleteTarget(null);
13219
13525
  }
13220
13526
  },
13221
- [adapter, disabled, fetchTree, onSelectedPathChange, selectedPath, sessionId]
13527
+ [
13528
+ adapter,
13529
+ disabled,
13530
+ fetchTree,
13531
+ onSelectedPathChange,
13532
+ selectedPath,
13533
+ sessionId
13534
+ ]
13222
13535
  );
13223
13536
  const onResizeStart = useCallback(
13224
13537
  (event) => {
@@ -13258,11 +13571,11 @@ function WorkspacePanel({
13258
13571
  "data-testid": "workspace-panel",
13259
13572
  className: cn(
13260
13573
  "relative z-10 flex h-full min-h-0 flex-col overflow-hidden border-l border-muted-foreground/20 bg-card",
13261
- fillParent && "w-full"
13574
+ (fillParent || narrow) && "w-full"
13262
13575
  ),
13263
- style: fillParent ? void 0 : { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
13576
+ style: fillParent || narrow ? void 0 : { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
13264
13577
  children: [
13265
- !fillParent && /* @__PURE__ */ jsx(
13578
+ !(fillParent || narrow) && /* @__PURE__ */ jsx(
13266
13579
  "div",
13267
13580
  {
13268
13581
  className: "absolute inset-y-0 left-0 z-20 w-1.5 cursor-col-resize transition-colors hover:bg-primary/20 active:bg-primary/30",
@@ -13435,10 +13748,18 @@ function ConfirmDialog2({
13435
13748
  ] }),
13436
13749
  /* @__PURE__ */ jsxs(DialogFooter, { children: [
13437
13750
  /* @__PURE__ */ jsx(Button, { variant: "outline", disabled: loading, onClick: onCancel, children: "Cancel" }),
13438
- /* @__PURE__ */ jsxs(Button, { variant: "destructive", disabled: loading, onClick: handleConfirm, children: [
13439
- loading && /* @__PURE__ */ jsx(Loader2Icon, { className: "mr-1.5 size-3.5 animate-spin" }),
13440
- confirmLabel
13441
- ] })
13751
+ /* @__PURE__ */ jsxs(
13752
+ Button,
13753
+ {
13754
+ variant: "destructive",
13755
+ disabled: loading,
13756
+ onClick: handleConfirm,
13757
+ children: [
13758
+ loading && /* @__PURE__ */ jsx(Loader2Icon, { className: "mr-1.5 size-3.5 animate-spin" }),
13759
+ confirmLabel
13760
+ ]
13761
+ }
13762
+ )
13442
13763
  ] })
13443
13764
  ] })
13444
13765
  }
@@ -13734,7 +14055,11 @@ function applyAgentChatEvent(state, event) {
13734
14055
  case "ask_user_question.response":
13735
14056
  return withMessages(
13736
14057
  nextState,
13737
- applyAskUserQuestionResponse(nextState.messages, event.data)
14058
+ applyAskUserQuestionResponse(
14059
+ nextState.messages,
14060
+ event.eventId,
14061
+ event.data
14062
+ )
13738
14063
  );
13739
14064
  case "policy.denied":
13740
14065
  return applyPolicyDenied(nextState, event.data);
@@ -14485,34 +14810,59 @@ function applyUserFeedback(messages, data) {
14485
14810
  }
14486
14811
  return messages;
14487
14812
  }
14488
- function applyAskUserQuestionResponse(messages, data) {
14813
+ function applyAskUserQuestionResponse(messages, eventId, data) {
14489
14814
  const targetToolId = stringValue2(data.tool_call_id);
14490
14815
  const responses = Array.isArray(data.responses) ? data.responses : void 0;
14491
14816
  if (!targetToolId || !responses) return messages;
14817
+ const answers = responses.map((response) => {
14818
+ const row = objectValue(response) ?? {};
14819
+ return {
14820
+ question: stringValue2(row.question),
14821
+ answer: stringValue2(row.answer),
14822
+ is_other: Boolean(row.is_other)
14823
+ };
14824
+ });
14492
14825
  const next = [...messages];
14493
14826
  for (let i = next.length - 1; i >= 0; i--) {
14494
14827
  const msg = next[i];
14495
14828
  if (!msg?.toolCalls?.some((tc) => tc.id === targetToolId)) continue;
14829
+ const alreadyAnswered = msg.toolCalls.some(
14830
+ (tc) => tc.id === targetToolId && tc.askUserQuestionAnswers !== void 0
14831
+ );
14832
+ if (alreadyAnswered) return messages;
14496
14833
  next[i] = {
14497
14834
  ...msg,
14498
14835
  toolCalls: msg.toolCalls.map(
14499
- (tc) => tc.id === targetToolId ? {
14500
- ...tc,
14501
- askUserQuestionAnswers: responses.map((response) => {
14502
- const row = objectValue(response) ?? {};
14503
- return {
14504
- question: stringValue2(row.question),
14505
- answer: stringValue2(row.answer),
14506
- is_other: Boolean(row.is_other)
14507
- };
14508
- })
14509
- } : tc
14836
+ (tc) => tc.id === targetToolId ? { ...tc, askUserQuestionAnswers: answers } : tc
14510
14837
  )
14511
14838
  };
14512
- return next;
14839
+ return appendConversationalAnswer(next, i, eventId, answers);
14513
14840
  }
14514
14841
  return messages;
14515
14842
  }
14843
+ function appendConversationalAnswer(messages, askIndex, eventId, answers) {
14844
+ if (answers.length !== 1) return messages;
14845
+ const answer = answers[0].answer.trim();
14846
+ if (!answer) return messages;
14847
+ const id = `evt-${eventId}`;
14848
+ if (messages.some((m) => m.id === id)) return messages;
14849
+ const next = [...messages];
14850
+ for (let i = askIndex + 1; i < next.length; i++) {
14851
+ const msg = next[i];
14852
+ if (msg.role !== "user" || !msg.id.startsWith("local-")) continue;
14853
+ if (msg.status === "error") continue;
14854
+ next[i] = { ...msg, id, content: answer, status: "complete" };
14855
+ return next;
14856
+ }
14857
+ next.push({
14858
+ id,
14859
+ role: "user",
14860
+ content: answer,
14861
+ createdAt: /* @__PURE__ */ new Date(),
14862
+ status: "complete"
14863
+ });
14864
+ return next;
14865
+ }
14516
14866
  function applyPolicyDenied(state, data) {
14517
14867
  const messages = [...state.messages];
14518
14868
  const policyIdx = findLastAssistantIndex(messages);
@@ -15204,6 +15554,11 @@ function parseGoalDefinition(content) {
15204
15554
  var RIGHT_STACK_STYLE = {
15205
15555
  ["--right-stack-w"]: "440px"
15206
15556
  };
15557
+ var MOBILE_PANE_LABELS = {
15558
+ chat: "Chat",
15559
+ browser: "Browser",
15560
+ workspace: "Workspace"
15561
+ };
15207
15562
  function AgentChat({
15208
15563
  adapter,
15209
15564
  agentId,
@@ -15276,7 +15631,14 @@ function AgentChat({
15276
15631
  const handleToggleWorkspace = useCallback(() => {
15277
15632
  setShowWorkspace((prev) => !prev);
15278
15633
  }, []);
15279
- const showMobileToggle = rightStackVisible;
15634
+ const mobilePanes = useMemo(() => {
15635
+ const panes = ["chat"];
15636
+ if (browserVisible) panes.push("browser");
15637
+ if (workspaceVisible) panes.push("workspace");
15638
+ return panes;
15639
+ }, [browserVisible, workspaceVisible]);
15640
+ const activeMobilePane = mobilePanes.includes(mobileView) ? mobileView : "chat";
15641
+ const showMobileToggle = mobilePanes.length > 1;
15280
15642
  return /* @__PURE__ */ jsx(
15281
15643
  AgentChatAdapterProvider,
15282
15644
  {
@@ -15303,43 +15665,36 @@ function AgentChat({
15303
15665
  ),
15304
15666
  style: { direction: "ltr", ...RIGHT_STACK_STYLE },
15305
15667
  children: [
15306
- showMobileToggle && /* @__PURE__ */ jsxs("div", { className: "md:hidden flex shrink-0 border-b border-line bg-card", children: [
15307
- /* @__PURE__ */ jsx(
15308
- "button",
15309
- {
15310
- type: "button",
15311
- onClick: () => setMobileView("chat"),
15312
- "aria-pressed": mobileView === "chat",
15313
- className: cn(
15314
- "flex-1 px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-colors",
15315
- mobileView === "chat" ? "border-primary text-foreground" : "border-transparent text-subtle hover:text-foreground"
15316
- ),
15317
- children: "Chat"
15318
- }
15319
- ),
15320
- /* @__PURE__ */ jsx(
15321
- "button",
15322
- {
15323
- type: "button",
15324
- onClick: () => setMobileView("workspace"),
15325
- "aria-pressed": mobileView === "workspace",
15326
- className: cn(
15327
- "flex-1 px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-colors",
15328
- mobileView === "workspace" ? "border-primary text-foreground" : "border-transparent text-subtle hover:text-foreground"
15329
- ),
15330
- children: "Workspace"
15331
- }
15332
- )
15333
- ] }),
15668
+ showMobileToggle && /* @__PURE__ */ jsx(
15669
+ "div",
15670
+ {
15671
+ "data-testid": "mobile-pane-toggle",
15672
+ className: "md:hidden flex shrink-0 border-b border-line bg-card",
15673
+ children: mobilePanes.map((pane) => /* @__PURE__ */ jsx(
15674
+ "button",
15675
+ {
15676
+ type: "button",
15677
+ onClick: () => setMobileView(pane),
15678
+ "aria-pressed": activeMobilePane === pane,
15679
+ className: cn(
15680
+ "flex-1 min-h-11 px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-colors",
15681
+ activeMobilePane === pane ? "border-primary text-foreground" : "border-transparent text-subtle hover:text-foreground"
15682
+ ),
15683
+ children: MOBILE_PANE_LABELS[pane]
15684
+ },
15685
+ pane
15686
+ ))
15687
+ }
15688
+ ),
15334
15689
  /* @__PURE__ */ jsx(
15335
15690
  "div",
15336
15691
  {
15337
15692
  "data-testid": "chat-panel",
15338
- "data-mobile-view": mobileView,
15693
+ "data-mobile-view": activeMobilePane,
15339
15694
  className: cn(
15340
- // Phone: full width column, hidden when workspace tab active.
15695
+ // Phone: full width column, hidden while another pane is picked.
15341
15696
  "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
15342
- showMobileToggle && "data-[mobile-view=workspace]:hidden md:flex!",
15697
+ showMobileToggle && "data-[mobile-view=browser]:hidden data-[mobile-view=workspace]:hidden md:flex!",
15343
15698
  // md+: positioning depends on whether the browser/workspace
15344
15699
  // right stack is laid out. When the browser pane is shown
15345
15700
  // we pin a fixed-width column on the right and absolutely
@@ -15396,7 +15751,7 @@ function AgentChat({
15396
15751
  "div",
15397
15752
  {
15398
15753
  "data-testid": "right-stack",
15399
- "data-mobile-view": mobileView,
15754
+ "data-mobile-view": activeMobilePane,
15400
15755
  className: cn(
15401
15756
  // Phone: full width column, hidden when chat tab active.
15402
15757
  "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
@@ -15414,9 +15769,13 @@ function AgentChat({
15414
15769
  "div",
15415
15770
  {
15416
15771
  "data-testid": "browser-panel",
15772
+ "data-mobile-view": activeMobilePane,
15417
15773
  className: cn(
15418
15774
  "min-h-0 w-full overflow-hidden",
15419
- bothPanesVisible ? "h-1/2 border-b border-line" : "h-full"
15775
+ // With both panes the desktop splits the column in half.
15776
+ // A phone gives whichever pane is selected the whole
15777
+ // height instead — see the mobile toggle above.
15778
+ bothPanesVisible ? "h-full data-[mobile-view=workspace]:hidden md:block! md:h-1/2 md:border-b md:border-line" : "h-full"
15420
15779
  ),
15421
15780
  children: /* @__PURE__ */ jsx(
15422
15781
  BrowserPane,
@@ -15433,7 +15792,8 @@ function AgentChat({
15433
15792
  "div",
15434
15793
  {
15435
15794
  "data-testid": "workspace-panel-frame",
15436
- className: bothPanesVisible ? "h-1/2 min-h-0 w-full overflow-hidden" : "min-h-0 h-full",
15795
+ "data-mobile-view": activeMobilePane,
15796
+ className: bothPanesVisible ? "h-full min-h-0 w-full overflow-hidden data-[mobile-view=browser]:hidden md:block! md:h-1/2" : "min-h-0 h-full",
15437
15797
  children: /* @__PURE__ */ jsx(
15438
15798
  WorkspacePanel,
15439
15799
  {
@@ -15733,7 +16093,7 @@ function IntegrationsPage({ agentId, adapter, onBack }) {
15733
16093
  {
15734
16094
  type: "button",
15735
16095
  onClick: onBack,
15736
- className: "flex w-fit items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground",
16096
+ className: "-mx-2 inline-flex min-h-11 w-fit items-center gap-1.5 rounded-lg px-2 text-sm text-muted-foreground pointer-fine:mx-0 pointer-fine:min-h-0 pointer-fine:px-0 pointer-fine:hover:text-foreground",
15737
16097
  children: [
15738
16098
  /* @__PURE__ */ jsx(ArrowLeftIcon, { className: "h-4 w-4" }),
15739
16099
  "Back"
@@ -15877,7 +16237,7 @@ function CodingAgentsPanel({ agentId, adapter, onBack }) {
15877
16237
  {
15878
16238
  type: "button",
15879
16239
  onClick: onBack,
15880
- className: "flex w-fit items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground",
16240
+ className: "-mx-2 inline-flex min-h-11 w-fit items-center gap-1.5 rounded-lg px-2 text-sm text-muted-foreground pointer-fine:mx-0 pointer-fine:min-h-0 pointer-fine:px-0 pointer-fine:hover:text-foreground",
15881
16241
  children: [
15882
16242
  /* @__PURE__ */ jsx(ArrowLeftIcon, { className: "h-4 w-4" }),
15883
16243
  "Back"
@@ -16018,7 +16378,7 @@ function ProviderCard({
16018
16378
  type: "button",
16019
16379
  disabled: busy,
16020
16380
  onClick: submit,
16021
- className: "inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs text-foreground hover:bg-accent disabled:opacity-60",
16381
+ className: "inline-flex min-h-11 items-center justify-center gap-1.5 rounded-md border border-border px-3 text-xs text-foreground disabled:opacity-60 pointer-fine:min-h-0 pointer-fine:py-1.5 pointer-fine:hover:bg-accent",
16022
16382
  children: [
16023
16383
  busy && /* @__PURE__ */ jsx(Loader2Icon, { className: "h-3.5 w-3.5 animate-spin" }),
16024
16384
  connected ? "Update" : "Connect"
@@ -16038,6 +16398,63 @@ function ProviderCard({
16038
16398
  ] })
16039
16399
  ] });
16040
16400
  }
16401
+
16402
+ // src/components/inbox/inbox-answers.ts
16403
+ function hasChoices2(question) {
16404
+ return (question.choices?.length ?? 0) > 0;
16405
+ }
16406
+ function parseInboxQuestions(item) {
16407
+ const raw = item.payload.questions;
16408
+ if (!Array.isArray(raw)) return [];
16409
+ return raw.map((entry) => {
16410
+ const question = entry;
16411
+ const choices = Array.isArray(question.choices) ? question.choices.map((choice) => ({
16412
+ label: typeof choice.label === "string" ? choice.label : "",
16413
+ description: typeof choice.description === "string" ? choice.description : void 0
16414
+ })).filter((choice) => choice.label) : void 0;
16415
+ return {
16416
+ prompt: typeof question.prompt === "string" ? question.prompt : "",
16417
+ choices,
16418
+ allowOther: question.allow_other !== false
16419
+ };
16420
+ }).filter((question) => question.prompt);
16421
+ }
16422
+ function isOtherAnswer(question, draft) {
16423
+ return hasChoices2(question) && !!draft?.useOther;
16424
+ }
16425
+ function answerText(question, draft) {
16426
+ const fromMenu = hasChoices2(question) && !draft?.useOther;
16427
+ return (fromMenu ? draft?.picked ?? "" : draft?.typed ?? "").trim();
16428
+ }
16429
+ function buildInboxResponse(question, draft) {
16430
+ return {
16431
+ question: question.prompt,
16432
+ answer: answerText(question, draft),
16433
+ is_other: isOtherAnswer(question, draft)
16434
+ };
16435
+ }
16436
+ var TICK_MS = 3e4;
16437
+ function formatExpiresIn(expiresAtIso, nowMs) {
16438
+ const remainingMs = Date.parse(expiresAtIso) - nowMs;
16439
+ if (remainingMs <= 0) return "Expired";
16440
+ if (remainingMs < 6e4) return "Expires in under a minute";
16441
+ const minutes = Math.round(remainingMs / 6e4);
16442
+ return `Expires in ~${minutes} minute${minutes === 1 ? "" : "s"}`;
16443
+ }
16444
+ function useAnswerWindow(expiresAtIso) {
16445
+ const [now, setNow] = useState(() => Date.now());
16446
+ useEffect(() => {
16447
+ const id = setInterval(() => setNow(Date.now()), TICK_MS);
16448
+ return () => clearInterval(id);
16449
+ }, []);
16450
+ if (!expiresAtIso) {
16451
+ return { expired: false, label: "" };
16452
+ }
16453
+ return {
16454
+ expired: Date.parse(expiresAtIso) <= now,
16455
+ label: formatExpiresIn(expiresAtIso, now)
16456
+ };
16457
+ }
16041
16458
  var DEFAULT_LIMIT = 50;
16042
16459
  function requireInboxAdapter(adapter) {
16043
16460
  if (!adapter.listInbox || !adapter.getInboxItem || !adapter.markInboxItemRead || !adapter.acknowledgeInboxItem || !adapter.respondGovernanceInboxItem || !adapter.openInboxStream) {
@@ -16071,8 +16488,33 @@ function kindIcon(kind) {
16071
16488
  if (kind === "progress_checkin") return TimerIcon;
16072
16489
  return CircleDotIcon;
16073
16490
  }
16491
+ var VIEWS = {
16492
+ active: {
16493
+ statuses: ["pending"],
16494
+ kinds: ["input_required", "action_required", "governance_gate"],
16495
+ empty: "Nothing needs you right now"
16496
+ },
16497
+ updates: {
16498
+ statuses: ["pending"],
16499
+ kinds: ["task_complete", "progress_checkin"],
16500
+ empty: "No news from your agent"
16501
+ },
16502
+ history: {
16503
+ statuses: ["acknowledged", "responded", "expired"],
16504
+ empty: "No history yet"
16505
+ }
16506
+ };
16507
+ var VIEW_NAMES = Object.keys(VIEWS);
16508
+ function belongsToView(item, view) {
16509
+ const kinds = VIEWS[view].kinds;
16510
+ return VIEWS[view].statuses.includes(item.status) && (kinds === void 0 || kinds.includes(item.kind));
16511
+ }
16074
16512
  function sortItems(items) {
16075
- return [...items].sort((a, b) => {
16513
+ const byId = /* @__PURE__ */ new Map();
16514
+ for (const item of items) {
16515
+ if (!byId.has(item.id)) byId.set(item.id, item);
16516
+ }
16517
+ return [...byId.values()].sort((a, b) => {
16076
16518
  if (a.createdAt !== b.createdAt) return a.createdAt > b.createdAt ? -1 : 1;
16077
16519
  return b.id - a.id;
16078
16520
  });
@@ -16086,37 +16528,106 @@ function InboxBody({ body, muted }) {
16086
16528
  }
16087
16529
  );
16088
16530
  }
16531
+ var FIELD_CLASS = "h-9 w-full border border-line bg-background px-2 text-sm text-foreground outline-none focus:border-primary disabled:opacity-50";
16532
+ function errorMessage(error, fallback) {
16533
+ return error instanceof Error && error.message ? error.message : fallback;
16534
+ }
16535
+ function useAction() {
16536
+ const [error, setError] = useState(null);
16537
+ const [busy, setBusy] = useState(false);
16538
+ const run = useCallback(
16539
+ async (fallback, action) => {
16540
+ setBusy(true);
16541
+ setError(null);
16542
+ try {
16543
+ await action();
16544
+ } catch (err) {
16545
+ setError(errorMessage(err, fallback));
16546
+ } finally {
16547
+ setBusy(false);
16548
+ }
16549
+ },
16550
+ []
16551
+ );
16552
+ return { error, busy, run };
16553
+ }
16554
+ function ActionError({ message }) {
16555
+ if (!message) return null;
16556
+ return /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-destructive", children: message });
16557
+ }
16558
+ function ExpiryNote({ label, expired }) {
16559
+ return /* @__PURE__ */ jsx(
16560
+ "p",
16561
+ {
16562
+ className: cn(
16563
+ "text-xs",
16564
+ expired ? "text-muted-foreground" : "text-foreground"
16565
+ ),
16566
+ children: expired ? "The agent stopped waiting for this answer and moved on, so it can no longer be submitted." : `${label}. Deleting it will not answer the agent; it only clears it from your inbox.`
16567
+ }
16568
+ );
16569
+ }
16570
+ var OTHER_OPTION = "other";
16089
16571
  function QuestionInput({
16090
- prompt,
16091
- choices,
16572
+ question,
16573
+ draft,
16092
16574
  disabled,
16093
- value,
16094
- onChange
16575
+ onPickChoice,
16576
+ onChooseOther,
16577
+ onType
16095
16578
  }) {
16579
+ const { prompt, choices, allowOther } = question;
16580
+ const typed = draft?.typed ?? "";
16581
+ const useOther = !!draft?.useOther;
16096
16582
  if (choices && choices.length > 0) {
16097
- return /* @__PURE__ */ jsxs(
16098
- "select",
16099
- {
16100
- "aria-label": prompt,
16101
- className: "h-9 w-full border border-line bg-background px-2 text-sm text-foreground outline-none focus:border-primary disabled:opacity-50",
16102
- disabled,
16103
- value,
16104
- onChange: (event) => onChange(event.target.value),
16105
- children: [
16106
- /* @__PURE__ */ jsx("option", { value: "", children: "Select" }),
16107
- choices.map((choice) => /* @__PURE__ */ jsx("option", { value: choice.label, children: choice.label }, choice.label))
16108
- ]
16109
- }
16583
+ const pickedIndex = choices.findIndex(
16584
+ (choice) => choice.label === draft?.picked
16110
16585
  );
16586
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
16587
+ /* @__PURE__ */ jsxs(
16588
+ "select",
16589
+ {
16590
+ "aria-label": prompt,
16591
+ className: FIELD_CLASS,
16592
+ disabled,
16593
+ value: useOther ? OTHER_OPTION : pickedIndex >= 0 ? String(pickedIndex) : "",
16594
+ onChange: (event) => {
16595
+ const value = event.target.value;
16596
+ if (value === OTHER_OPTION) {
16597
+ onChooseOther();
16598
+ return;
16599
+ }
16600
+ const choice = choices[Number(value)];
16601
+ if (choice) onPickChoice(choice.label);
16602
+ },
16603
+ children: [
16604
+ /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Select" }),
16605
+ choices.map((choice, index) => /* @__PURE__ */ jsx("option", { value: String(index), children: choice.label }, choice.label)),
16606
+ allowOther && /* @__PURE__ */ jsx("option", { value: OTHER_OPTION, children: "Other" })
16607
+ ]
16608
+ }
16609
+ ),
16610
+ useOther && /* @__PURE__ */ jsx(
16611
+ "input",
16612
+ {
16613
+ "aria-label": `${prompt} (other)`,
16614
+ className: FIELD_CLASS,
16615
+ placeholder: "Type your answer\u2026",
16616
+ disabled,
16617
+ value: typed,
16618
+ onChange: (event) => onType(event.target.value)
16619
+ }
16620
+ )
16621
+ ] });
16111
16622
  }
16112
16623
  return /* @__PURE__ */ jsx(
16113
16624
  "input",
16114
16625
  {
16115
16626
  "aria-label": prompt,
16116
- className: "h-9 w-full border border-line bg-background px-2 text-sm text-foreground outline-none focus:border-primary disabled:opacity-50",
16627
+ className: FIELD_CLASS,
16117
16628
  disabled,
16118
- value,
16119
- onChange: (event) => onChange(event.target.value)
16629
+ value: typed,
16630
+ onChange: (event) => onType(event.target.value)
16120
16631
  }
16121
16632
  );
16122
16633
  }
@@ -16127,47 +16638,45 @@ function InboxDetailActions({
16127
16638
  onSessionSelect,
16128
16639
  children
16129
16640
  }) {
16130
- const [deleting, setDeleting] = useState(false);
16641
+ const { error, busy: deleting, run } = useAction();
16131
16642
  async function deleteItem() {
16132
16643
  if (!adapter.deleteInboxItem) return;
16133
- setDeleting(true);
16134
- try {
16135
- await onDeleted(item.id);
16136
- } finally {
16137
- setDeleting(false);
16138
- }
16644
+ await run("Failed to delete", () => onDeleted(item.id));
16139
16645
  }
16140
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
16141
- /* @__PURE__ */ jsxs(
16142
- Button,
16143
- {
16144
- type: "button",
16145
- size: "sm",
16146
- onClick: () => onSessionSelect?.(item.sessionId, item),
16147
- "aria-label": "Open session",
16148
- children: [
16149
- /* @__PURE__ */ jsx(ExternalLinkIcon, { className: "size-3.5" }),
16150
- "Open session"
16151
- ]
16152
- }
16153
- ),
16154
- children,
16155
- adapter.deleteInboxItem && /* @__PURE__ */ jsxs(
16156
- Button,
16157
- {
16158
- type: "button",
16159
- size: "sm",
16160
- variant: "ghost",
16161
- onClick: () => void deleteItem(),
16162
- disabled: deleting,
16163
- "aria-label": "Delete inbox item",
16164
- title: "Delete inbox item",
16165
- children: [
16166
- /* @__PURE__ */ jsx(Trash2Icon, { className: "size-3.5" }),
16167
- deleting ? "Deleting" : "Delete"
16168
- ]
16169
- }
16170
- )
16646
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
16647
+ /* @__PURE__ */ jsx(ActionError, { message: error }),
16648
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
16649
+ /* @__PURE__ */ jsxs(
16650
+ Button,
16651
+ {
16652
+ type: "button",
16653
+ size: "sm",
16654
+ onClick: () => onSessionSelect?.(item.sessionId, item),
16655
+ "aria-label": "Open session",
16656
+ children: [
16657
+ /* @__PURE__ */ jsx(ExternalLinkIcon, { className: "size-3.5" }),
16658
+ "Open session"
16659
+ ]
16660
+ }
16661
+ ),
16662
+ children,
16663
+ adapter.deleteInboxItem && /* @__PURE__ */ jsxs(
16664
+ Button,
16665
+ {
16666
+ type: "button",
16667
+ size: "sm",
16668
+ variant: "ghost",
16669
+ onClick: () => void deleteItem(),
16670
+ disabled: deleting,
16671
+ "aria-label": "Delete inbox item",
16672
+ title: "Delete inbox item",
16673
+ children: [
16674
+ /* @__PURE__ */ jsx(Trash2Icon, { className: "size-3.5" }),
16675
+ deleting ? "Deleting" : "Delete"
16676
+ ]
16677
+ }
16678
+ )
16679
+ ] })
16171
16680
  ] });
16172
16681
  }
16173
16682
  function InputRequiredDetail({
@@ -16177,36 +16686,31 @@ function InputRequiredDetail({
16177
16686
  onDeleted,
16178
16687
  onSessionSelect
16179
16688
  }) {
16180
- const questions = Array.isArray(item.payload.questions) ? item.payload.questions.map((question) => ({
16181
- prompt: typeof question.prompt === "string" ? question.prompt : "",
16182
- choices: Array.isArray(question.choices) ? question.choices.map((choice) => ({
16183
- label: typeof choice.label === "string" ? choice.label : "",
16184
- description: typeof choice.description === "string" ? choice.description : void 0
16185
- })).filter((choice) => choice.label) : void 0
16186
- })).filter((question) => question.prompt) : [];
16187
- const [answers, setAnswers] = useState({});
16188
- const [submitting, setSubmitting] = useState(false);
16189
- const disabled = item.status !== "pending" || submitting;
16190
- const canSubmit = questions.every((question) => answers[question.prompt]?.trim());
16689
+ const questions = useMemo(() => parseInboxQuestions(item), [item]);
16690
+ const [drafts, setDrafts] = useState({});
16691
+ const { error, busy: submitting, run } = useAction();
16692
+ const { expired, label: expiryLabel } = useAnswerWindow(item.expiresAt);
16693
+ const pending = item.status === "pending" && !expired;
16694
+ const disabled = !pending || submitting;
16695
+ const editDraft = (prompt, patch) => setDrafts((current) => ({
16696
+ ...current,
16697
+ [prompt]: { ...current[prompt], ...patch }
16698
+ }));
16699
+ const canSubmit = questions.length > 0 && questions.every((question) => answerText(question, drafts[question.prompt]));
16191
16700
  async function submit() {
16192
16701
  const toolCallId = typeof item.payload.tool_call_id === "string" ? item.payload.tool_call_id : "";
16193
16702
  if (!toolCallId || !canSubmit) return;
16194
- setSubmitting(true);
16195
- try {
16196
- const responses = questions.map((question) => ({
16197
- question: question.prompt,
16198
- answer: answers[question.prompt]?.trim() ?? "",
16199
- is_other: false
16200
- }));
16703
+ await run("Failed to submit your answer", async () => {
16704
+ const responses = questions.map(
16705
+ (question) => buildInboxResponse(question, drafts[question.prompt])
16706
+ );
16201
16707
  await adapter.submitAskUserQuestionResponse({
16202
16708
  sessionId: item.sessionId,
16203
16709
  toolCallId,
16204
16710
  responses
16205
16711
  });
16206
16712
  onUpdated(await adapter.getInboxItem({ itemId: item.id }));
16207
- } finally {
16208
- setSubmitting(false);
16209
- }
16713
+ });
16210
16714
  }
16211
16715
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
16212
16716
  item.body && /* @__PURE__ */ jsx(InboxBody, { body: item.body, muted: true }),
@@ -16215,17 +16719,25 @@ function InputRequiredDetail({
16215
16719
  /* @__PURE__ */ jsx(
16216
16720
  QuestionInput,
16217
16721
  {
16218
- prompt: question.prompt,
16219
- choices: question.choices,
16722
+ question,
16723
+ draft: drafts[question.prompt],
16220
16724
  disabled,
16221
- value: answers[question.prompt] ?? "",
16222
- onChange: (value) => setAnswers((current) => ({
16223
- ...current,
16224
- [question.prompt]: value
16225
- }))
16725
+ onPickChoice: (label) => (
16726
+ // Dropping the typed value keeps an abandoned draft from
16727
+ // resurfacing if the user switches back to Other.
16728
+ editDraft(question.prompt, {
16729
+ picked: label,
16730
+ typed: "",
16731
+ useOther: false
16732
+ })
16733
+ ),
16734
+ onChooseOther: () => editDraft(question.prompt, { useOther: true }),
16735
+ onType: (value) => editDraft(question.prompt, { typed: value })
16226
16736
  }
16227
16737
  )
16228
16738
  ] }, question.prompt)),
16739
+ item.status === "pending" && /* @__PURE__ */ jsx(ExpiryNote, { label: expiryLabel, expired }),
16740
+ /* @__PURE__ */ jsx(ActionError, { message: error }),
16229
16741
  /* @__PURE__ */ jsx(
16230
16742
  InboxDetailActions,
16231
16743
  {
@@ -16255,8 +16767,11 @@ function AckDetail({
16255
16767
  onDeleted,
16256
16768
  onSessionSelect
16257
16769
  }) {
16770
+ const { error, busy, run } = useAction();
16258
16771
  async function acknowledge() {
16259
- onUpdated(await adapter.acknowledgeInboxItem({ itemId: item.id }));
16772
+ await run("Failed to acknowledge", async () => {
16773
+ onUpdated(await adapter.acknowledgeInboxItem({ itemId: item.id }));
16774
+ });
16260
16775
  }
16261
16776
  const outcome = typeof item.payload.outcome === "string" ? item.payload.outcome : "";
16262
16777
  const duration = typeof item.payload.duration_seconds === "number" ? item.payload.duration_seconds : null;
@@ -16271,6 +16786,7 @@ function AckDetail({
16271
16786
  duration,
16272
16787
  " s)"
16273
16788
  ] }),
16789
+ /* @__PURE__ */ jsx(ActionError, { message: error }),
16274
16790
  /* @__PURE__ */ jsx(
16275
16791
  InboxDetailActions,
16276
16792
  {
@@ -16284,10 +16800,11 @@ function AckDetail({
16284
16800
  type: "button",
16285
16801
  size: "sm",
16286
16802
  onClick: () => void acknowledge(),
16803
+ disabled: busy,
16287
16804
  "aria-label": "Acknowledge inbox item",
16288
16805
  children: [
16289
16806
  /* @__PURE__ */ jsx(CheckIcon, { className: "size-3.5" }),
16290
- "Acknowledge"
16807
+ busy ? "Saving" : "Acknowledge"
16291
16808
  ]
16292
16809
  }
16293
16810
  )
@@ -16302,15 +16819,18 @@ function GovernanceDetail({
16302
16819
  onDeleted,
16303
16820
  onSessionSelect
16304
16821
  }) {
16822
+ const { error, busy, run } = useAction();
16305
16823
  async function decide(decision) {
16306
- onUpdated(
16307
- await adapter.respondGovernanceInboxItem({ itemId: item.id, decision })
16308
- );
16824
+ await run(`Failed to ${decision}`, async () => {
16825
+ onUpdated(
16826
+ await adapter.respondGovernanceInboxItem({ itemId: item.id, decision })
16827
+ );
16828
+ });
16309
16829
  }
16310
16830
  const toolName = typeof item.payload.tool_name === "string" ? item.payload.tool_name : "tool";
16311
16831
  const args = typeof item.payload.arguments_excerpt === "string" ? item.payload.arguments_excerpt : "";
16312
16832
  const reason = typeof item.payload.deny_reason === "string" ? item.payload.deny_reason : "";
16313
- const disabled = item.status !== "pending";
16833
+ const disabled = item.status !== "pending" || busy;
16314
16834
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
16315
16835
  /* @__PURE__ */ jsxs("div", { children: [
16316
16836
  /* @__PURE__ */ jsxs("div", { className: "text-base font-semibold text-foreground", children: [
@@ -16321,6 +16841,7 @@ function GovernanceDetail({
16321
16841
  reason && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: reason })
16322
16842
  ] }),
16323
16843
  args && /* @__PURE__ */ jsx("pre", { className: "overflow-x-auto border border-line bg-muted p-3 text-xs", children: args }),
16844
+ /* @__PURE__ */ jsx(ActionError, { message: error }),
16324
16845
  /* @__PURE__ */ jsxs(
16325
16846
  InboxDetailActions,
16326
16847
  {
@@ -16362,23 +16883,22 @@ function ActionRequiredDetail({
16362
16883
  onDeleted,
16363
16884
  onSessionSelect
16364
16885
  }) {
16365
- const [submitting, setSubmitting] = useState(false);
16886
+ const { error, busy: submitting, run } = useAction();
16366
16887
  const actionType = typeof item.payload.action_type === "string" ? item.payload.action_type : "";
16367
16888
  const context = typeof item.payload.context === "string" ? item.payload.context : "";
16368
16889
  const disabled = item.status !== "pending" || submitting || !adapter.respondActionRequiredInboxItem;
16369
16890
  async function complete() {
16370
- if (!adapter.respondActionRequiredInboxItem) return;
16371
- setSubmitting(true);
16372
- try {
16373
- onUpdated(await adapter.respondActionRequiredInboxItem({ itemId: item.id }));
16374
- } finally {
16375
- setSubmitting(false);
16376
- }
16891
+ const respond = adapter.respondActionRequiredInboxItem;
16892
+ if (!respond) return;
16893
+ await run("Failed to mark the action complete", async () => {
16894
+ onUpdated(await respond({ itemId: item.id }));
16895
+ });
16377
16896
  }
16378
16897
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
16379
16898
  item.body && /* @__PURE__ */ jsx(InboxBody, { body: item.body }),
16380
16899
  context && context !== item.body && /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-sm text-muted-foreground", children: context }),
16381
16900
  actionType && /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: actionType }),
16901
+ /* @__PURE__ */ jsx(ActionError, { message: error }),
16382
16902
  /* @__PURE__ */ jsx(
16383
16903
  InboxDetailActions,
16384
16904
  {
@@ -16411,8 +16931,11 @@ function ProgressDetail({
16411
16931
  onDeleted,
16412
16932
  onSessionSelect
16413
16933
  }) {
16934
+ const { error, busy, run } = useAction();
16414
16935
  async function acknowledge() {
16415
- onUpdated(await adapter.acknowledgeInboxItem({ itemId: item.id }));
16936
+ await run("Failed to acknowledge", async () => {
16937
+ onUpdated(await adapter.acknowledgeInboxItem({ itemId: item.id }));
16938
+ });
16416
16939
  }
16417
16940
  const rows = [];
16418
16941
  for (const row of [
@@ -16430,6 +16953,7 @@ function ProgressDetail({
16430
16953
  /* @__PURE__ */ jsx("dt", { className: "w-20 shrink-0", children: String(label) }),
16431
16954
  /* @__PURE__ */ jsx("dd", { className: "min-w-0", children: String(value) })
16432
16955
  ] }, String(label))) }),
16956
+ /* @__PURE__ */ jsx(ActionError, { message: error }),
16433
16957
  /* @__PURE__ */ jsx(
16434
16958
  InboxDetailActions,
16435
16959
  {
@@ -16443,10 +16967,11 @@ function ProgressDetail({
16443
16967
  type: "button",
16444
16968
  size: "sm",
16445
16969
  onClick: () => void acknowledge(),
16970
+ disabled: busy,
16446
16971
  "aria-label": "Acknowledge inbox item",
16447
16972
  children: [
16448
16973
  /* @__PURE__ */ jsx(CheckIcon, { className: "size-3.5" }),
16449
- "Acknowledge"
16974
+ busy ? "Saving" : "Acknowledge"
16450
16975
  ]
16451
16976
  }
16452
16977
  )
@@ -16459,10 +16984,23 @@ function InboxDetail({
16459
16984
  adapter,
16460
16985
  onUpdated,
16461
16986
  onDeleted,
16462
- onSessionSelect
16987
+ onSessionSelect,
16988
+ onBack
16463
16989
  }) {
16464
16990
  const Icon = kindIcon(item.kind);
16465
- return /* @__PURE__ */ jsxs("section", { className: "min-w-0 flex-1 overflow-y-auto p-6", children: [
16991
+ return /* @__PURE__ */ jsxs("section", { className: "min-w-0 flex-1 overflow-y-auto p-4 md:p-6", children: [
16992
+ onBack && /* @__PURE__ */ jsxs(
16993
+ "button",
16994
+ {
16995
+ type: "button",
16996
+ onClick: onBack,
16997
+ className: "-ml-2 mb-2 inline-flex min-h-11 items-center gap-1.5 px-2 text-sm font-medium text-muted-foreground md:hidden",
16998
+ children: [
16999
+ /* @__PURE__ */ jsx(ChevronLeftIcon, { className: "size-4" }),
17000
+ "Inbox"
17001
+ ]
17002
+ }
17003
+ ),
16466
17004
  /* @__PURE__ */ jsxs("div", { className: "mb-5 flex min-w-0 items-start gap-3", children: [
16467
17005
  /* @__PURE__ */ jsx("div", { className: "mt-0.5 flex size-8 shrink-0 items-center justify-center border border-line text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { className: "size-4" }) }),
16468
17006
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
@@ -16534,6 +17072,7 @@ function InboxPanel({
16534
17072
  const [items, setItems] = useState([]);
16535
17073
  const [nextCursor, setNextCursor] = useState(null);
16536
17074
  const [internalSelectedId, setInternalSelectedId] = useState(null);
17075
+ const [view, setView] = useState("active");
16537
17076
  const [loading, setLoading] = useState(false);
16538
17077
  const [error, setError] = useState(null);
16539
17078
  const requestId = useRef(0);
@@ -16542,13 +17081,20 @@ function InboxPanel({
16542
17081
  () => items.find((item) => item.id === selectedItemId) ?? null,
16543
17082
  [items, selectedItemId]
16544
17083
  );
17084
+ const viewRef = useRef(view);
17085
+ viewRef.current = view;
16545
17086
  const applyItem = useCallback((nextItem) => {
16546
- setItems(
16547
- (current) => sortItems(
16548
- current.map((item) => item.id === nextItem.id ? nextItem : item)
16549
- )
16550
- );
17087
+ setItems((current) => {
17088
+ if (current.some((item) => item.id === nextItem.id)) {
17089
+ return sortItems([nextItem, ...current]);
17090
+ }
17091
+ return belongsToView(nextItem, viewRef.current) ? sortItems([nextItem, ...current]) : current;
17092
+ });
16551
17093
  }, []);
17094
+ const clearSelection = useCallback(() => {
17095
+ if (selectedId === void 0) setInternalSelectedId(null);
17096
+ onSelectedIdChange?.(null);
17097
+ }, [onSelectedIdChange, selectedId]);
16552
17098
  const selectItem = useCallback(
16553
17099
  async (itemId) => {
16554
17100
  if (selectedId === void 0) setInternalSelectedId(itemId);
@@ -16565,26 +17111,26 @@ function InboxPanel({
16565
17111
  async (cursor) => {
16566
17112
  const id = ++requestId.current;
16567
17113
  setLoading(true);
16568
- setError(null);
16569
17114
  try {
16570
17115
  const response = await inboxAdapter.listInbox({
17116
+ status: VIEWS[view].statuses,
17117
+ kind: VIEWS[view].kinds,
16571
17118
  cursor: cursor ?? void 0,
16572
17119
  limit
16573
17120
  });
16574
17121
  if (id !== requestId.current) return;
16575
- setItems(
16576
- (current) => sortItems(cursor ? [...current, ...response.items] : response.items)
16577
- );
17122
+ setItems((current) => sortItems([...response.items, ...current]));
16578
17123
  setNextCursor(response.nextCursor);
17124
+ setError(null);
16579
17125
  } catch (err) {
16580
17126
  if (id === requestId.current) {
16581
- setError(err instanceof Error ? err.message : "Failed to load inbox");
17127
+ setError(errorMessage(err, "Failed to load inbox"));
16582
17128
  }
16583
17129
  } finally {
16584
17130
  if (id === requestId.current) setLoading(false);
16585
17131
  }
16586
17132
  },
16587
- [inboxAdapter, limit]
17133
+ [inboxAdapter, limit, view]
16588
17134
  );
16589
17135
  useEffect(() => {
16590
17136
  void load(null);
@@ -16592,15 +17138,33 @@ function InboxPanel({
16592
17138
  useEffect(() => {
16593
17139
  const stream = inboxAdapter.openInboxStream();
16594
17140
  stream.addEventListener("item", (event) => {
16595
- const payload = JSON.parse(event.data);
16596
- if (typeof payload.item_id !== "number") return;
16597
- void inboxAdapter.getInboxItem({ itemId: payload.item_id }).then(applyItem);
17141
+ let itemId;
17142
+ try {
17143
+ itemId = JSON.parse(event.data).item_id;
17144
+ } catch {
17145
+ return;
17146
+ }
17147
+ if (typeof itemId !== "number") return;
17148
+ if (viewRef.current === "history") return;
17149
+ void inboxAdapter.getInboxItem({ itemId }).then(applyItem, () => void 0);
16598
17150
  });
16599
17151
  return () => stream.close();
16600
17152
  }, [applyItem, inboxAdapter]);
16601
17153
  function updateSelectedItem(item) {
16602
17154
  applyItem(item);
16603
17155
  }
17156
+ const selectView = useCallback(
17157
+ (next) => {
17158
+ if (next === view) return;
17159
+ setView(next);
17160
+ setItems([]);
17161
+ setNextCursor(null);
17162
+ setError(null);
17163
+ if (selectedId === void 0) setInternalSelectedId(null);
17164
+ onSelectedIdChange?.(null);
17165
+ },
17166
+ [onSelectedIdChange, selectedId, view]
17167
+ );
16604
17168
  const deleteItem = useCallback(
16605
17169
  async (itemId) => {
16606
17170
  if (!inboxAdapter.deleteInboxItem) return;
@@ -16614,55 +17178,80 @@ function InboxPanel({
16614
17178
  [inboxAdapter, onSelectedIdChange, selectedId, selectedItemId]
16615
17179
  );
16616
17180
  return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 min-w-0 bg-background text-foreground", children: [
16617
- /* @__PURE__ */ jsxs("aside", { className: "flex w-80 min-w-72 max-w-sm shrink-0 flex-col border-r border-line", children: [
16618
- !hideHeader && /* @__PURE__ */ jsxs("div", { className: "flex min-h-14 items-center gap-2 border-b border-line px-4", children: [
16619
- /* @__PURE__ */ jsx(InboxIcon, { className: "size-4 text-muted-foreground" }),
16620
- /* @__PURE__ */ jsx("h1", { className: "font-semibold", children: title })
16621
- ] }),
16622
- /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
16623
- error && /* @__PURE__ */ jsx("div", { className: "px-4 py-3 text-sm text-destructive", children: error }),
16624
- !error && items.length === 0 && !loading && /* @__PURE__ */ jsx("div", { className: "px-4 py-8 text-sm text-muted-foreground", children: "No inbox items" }),
16625
- items.map((item) => {
16626
- const Icon = kindIcon(item.kind);
16627
- const selected = item.id === selectedItemId;
16628
- return /* @__PURE__ */ jsxs(
17181
+ /* @__PURE__ */ jsxs(
17182
+ "aside",
17183
+ {
17184
+ className: cn(
17185
+ "flex min-w-0 flex-col border-line md:w-80 md:min-w-72 md:max-w-sm md:shrink-0 md:border-r",
17186
+ selectedItemId != null ? "hidden md:flex" : "w-full"
17187
+ ),
17188
+ children: [
17189
+ !hideHeader && /* @__PURE__ */ jsxs("div", { className: "flex min-h-14 items-center gap-2 border-b border-line px-4", children: [
17190
+ /* @__PURE__ */ jsx(InboxIcon, { className: "size-4 text-muted-foreground" }),
17191
+ /* @__PURE__ */ jsx("h1", { className: "font-semibold", children: title })
17192
+ ] }),
17193
+ /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b border-line px-2 py-2", children: VIEW_NAMES.map((value) => /* @__PURE__ */ jsx(
16629
17194
  "button",
16630
17195
  {
16631
17196
  type: "button",
16632
- "aria-label": `Open inbox item ${item.title}`,
17197
+ onClick: () => selectView(value),
16633
17198
  className: cn(
16634
- "flex w-full min-w-0 gap-3 border-l-2 px-3 py-3 text-left transition-colors hover:bg-input",
16635
- selected ? "border-l-primary bg-line text-foreground" : "border-l-transparent text-subtle",
16636
- !item.readAt && "font-medium text-foreground"
17199
+ "px-2 py-1 text-xs capitalize transition-colors",
17200
+ view === value ? "bg-line font-medium text-foreground" : "text-muted-foreground hover:text-foreground"
16637
17201
  ),
16638
- onClick: () => void selectItem(item.id),
16639
- children: [
16640
- /* @__PURE__ */ jsx(Icon, { className: "mt-0.5 size-4 shrink-0 text-muted-foreground" }),
16641
- /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
16642
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center justify-between gap-2", children: [
16643
- /* @__PURE__ */ jsx("span", { className: "truncate text-xs uppercase tracking-wide text-muted-foreground", children: kindLabel(item.kind) }),
16644
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: formatRelative(item.createdAt) })
16645
- ] }),
16646
- /* @__PURE__ */ jsx("span", { className: "mt-1 block truncate text-sm", children: item.title }),
16647
- /* @__PURE__ */ jsx("span", { className: "mt-1 block text-xs text-muted-foreground", children: statusLabel(item.status) })
16648
- ] })
16649
- ]
17202
+ children: value
16650
17203
  },
16651
- item.id
16652
- );
16653
- }),
16654
- nextCursor && /* @__PURE__ */ jsx(
16655
- "button",
16656
- {
16657
- type: "button",
16658
- className: "w-full px-3 py-3 text-sm text-muted-foreground hover:bg-input",
16659
- onClick: () => void load(nextCursor),
16660
- disabled: loading,
16661
- children: loading ? "Loading" : "Load more"
16662
- }
16663
- )
16664
- ] })
16665
- ] }),
17204
+ value
17205
+ )) }),
17206
+ /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
17207
+ error && items.length === 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2 px-4 py-3", children: [
17208
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }),
17209
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: () => void load(null), children: "Try again" })
17210
+ ] }),
17211
+ items.length === 0 && !error && !loading && /* @__PURE__ */ jsx("div", { className: "px-4 py-8 text-sm text-muted-foreground", children: VIEWS[view].empty }),
17212
+ items.map((item) => {
17213
+ const Icon = kindIcon(item.kind);
17214
+ const selected = item.id === selectedItemId;
17215
+ return /* @__PURE__ */ jsxs(
17216
+ "button",
17217
+ {
17218
+ type: "button",
17219
+ "aria-label": `Open inbox item ${item.title}`,
17220
+ className: cn(
17221
+ "flex w-full min-w-0 gap-3 border-l-2 px-3 py-3 text-left transition-colors hover:bg-input",
17222
+ selected ? "border-l-primary bg-line text-foreground" : "border-l-transparent text-subtle",
17223
+ !item.readAt && "font-medium text-foreground"
17224
+ ),
17225
+ onClick: () => void selectItem(item.id),
17226
+ children: [
17227
+ /* @__PURE__ */ jsx(Icon, { className: "mt-0.5 size-4 shrink-0 text-muted-foreground" }),
17228
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
17229
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center justify-between gap-2", children: [
17230
+ /* @__PURE__ */ jsx("span", { className: "truncate text-xs uppercase tracking-wide text-muted-foreground", children: kindLabel(item.kind) }),
17231
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: formatRelative(item.createdAt) })
17232
+ ] }),
17233
+ /* @__PURE__ */ jsx("span", { className: "mt-1 block truncate text-sm", children: item.title }),
17234
+ /* @__PURE__ */ jsx("span", { className: "mt-1 block text-xs text-muted-foreground", children: statusLabel(item.status) })
17235
+ ] })
17236
+ ]
17237
+ },
17238
+ item.id
17239
+ );
17240
+ }),
17241
+ nextCursor && /* @__PURE__ */ jsx(
17242
+ "button",
17243
+ {
17244
+ type: "button",
17245
+ className: "w-full px-3 py-3 text-sm text-muted-foreground hover:bg-input",
17246
+ onClick: () => void load(nextCursor),
17247
+ disabled: loading,
17248
+ children: loading ? "Loading" : "Load more"
17249
+ }
17250
+ )
17251
+ ] })
17252
+ ]
17253
+ }
17254
+ ),
16666
17255
  selectedItem ? /* @__PURE__ */ jsx(
16667
17256
  InboxDetail,
16668
17257
  {
@@ -16670,59 +17259,59 @@ function InboxPanel({
16670
17259
  adapter: inboxAdapter,
16671
17260
  onUpdated: updateSelectedItem,
16672
17261
  onDeleted: deleteItem,
16673
- onSessionSelect
16674
- }
16675
- ) : /* @__PURE__ */ jsx("main", { className: "flex min-w-0 flex-1 items-center justify-center px-4 text-sm text-muted-foreground", children: "Select an item" })
17262
+ onSessionSelect,
17263
+ onBack: clearSelection
17264
+ },
17265
+ selectedItem.id
17266
+ ) : /* @__PURE__ */ jsx("main", { className: "hidden min-w-0 flex-1 items-center justify-center px-4 text-sm text-muted-foreground md:flex", children: "Select an item" })
16676
17267
  ] });
16677
17268
  }
16678
17269
  function useInboxUnreadCount(adapter) {
16679
17270
  const [unreadCount, setUnreadCount] = useState(0);
16680
17271
  const [hasLoaded, setHasLoaded] = useState(false);
16681
17272
  const [error, setError] = useState(null);
17273
+ const requestId = useRef(0);
17274
+ const listInbox = adapter.listInbox;
17275
+ const openInboxStream = adapter.openInboxStream;
17276
+ const refetch = useCallback(async () => {
17277
+ if (!listInbox) return;
17278
+ const id = ++requestId.current;
17279
+ try {
17280
+ const response = await listInbox({ status: "pending", limit: 200 });
17281
+ if (id !== requestId.current) return;
17282
+ setUnreadCount(response.items.filter((item) => !item.readAt).length);
17283
+ setError(null);
17284
+ } catch (err) {
17285
+ if (id !== requestId.current) return;
17286
+ setError(err instanceof Error ? err.message : "Failed to load inbox");
17287
+ } finally {
17288
+ if (id === requestId.current) setHasLoaded(true);
17289
+ }
17290
+ }, [listInbox]);
16682
17291
  useEffect(() => {
16683
- let cancelled = false;
16684
- let stream = null;
16685
- if (!adapter.listInbox || !adapter.openInboxStream) {
17292
+ if (!listInbox || !openInboxStream) {
16686
17293
  setError("Inbox is not supported by this adapter.");
16687
17294
  setHasLoaded(true);
16688
- return () => {
16689
- cancelled = true;
16690
- };
17295
+ return;
16691
17296
  }
16692
- adapter.listInbox({ status: "pending", limit: 200 }).then((response) => {
16693
- if (cancelled) return;
16694
- setUnreadCount(response.items.filter((item) => !item.readAt).length);
16695
- setHasLoaded(true);
16696
- }).catch((err) => {
16697
- if (cancelled) return;
16698
- setError(err instanceof Error ? err.message : "Failed to load inbox");
16699
- setHasLoaded(true);
16700
- });
17297
+ void refetch();
17298
+ let stream = null;
16701
17299
  try {
16702
- stream = adapter.openInboxStream();
16703
- stream.addEventListener("snapshot", (event) => {
16704
- if (cancelled) return;
16705
- const payload = JSON.parse(event.data);
16706
- const ids = Array.isArray(payload.unread_ids) ? payload.unread_ids : [];
16707
- setUnreadCount(ids.length);
16708
- setHasLoaded(true);
16709
- });
16710
- stream.addEventListener("item", () => {
16711
- if (cancelled) return;
16712
- setUnreadCount((count) => count + 1);
16713
- setHasLoaded(true);
16714
- });
16715
- stream.onerror = () => {
16716
- if (!cancelled) setError("Inbox stream disconnected.");
16717
- };
17300
+ stream = openInboxStream();
17301
+ const onChange = () => void refetch();
17302
+ stream.addEventListener("snapshot", onChange);
17303
+ stream.addEventListener("item", onChange);
17304
+ stream.onerror = () => setError("Inbox stream disconnected.");
16718
17305
  } catch (err) {
16719
- setError(err instanceof Error ? err.message : "Failed to open inbox stream");
17306
+ setError(
17307
+ err instanceof Error ? err.message : "Failed to open inbox stream"
17308
+ );
16720
17309
  }
16721
17310
  return () => {
16722
- cancelled = true;
17311
+ requestId.current += 1;
16723
17312
  stream?.close();
16724
17313
  };
16725
- }, [adapter]);
17314
+ }, [listInbox, openInboxStream, refetch]);
16726
17315
  return { unreadCount, hasLoaded, error };
16727
17316
  }
16728
17317
  function Card({