@open-mercato/ui 0.6.1-develop.3246.1.dbef9d7392 → 0.6.1-develop.3256.1.fe3dec2464

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.
@@ -0,0 +1,186 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { ChevronDown, Cpu } from "lucide-react";
5
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ import { cn } from "@open-mercato/shared/lib/utils";
7
+ import { Button } from "../primitives/button.js";
8
+ function ModelPicker({
9
+ agentId,
10
+ value,
11
+ onChange,
12
+ availableProviders,
13
+ disabled,
14
+ compact = false,
15
+ defaultLabel,
16
+ className
17
+ }) {
18
+ const t = useT();
19
+ const [open, setOpen] = React.useState(false);
20
+ const containerRef = React.useRef(null);
21
+ const triggerRef = React.useRef(null);
22
+ const selectedLabel = React.useMemo(() => {
23
+ if (!value) {
24
+ return defaultLabel ? t("ai_assistant.modelPicker.defaultWithModelLabel", "Default: {{model}}").replace("{{model}}", defaultLabel) : t("ai_assistant.modelPicker.defaultLabel", "Model: Default");
25
+ }
26
+ const provider = availableProviders.find((p) => p.id === value.providerId);
27
+ const model = provider?.models.find((m) => m.id === value.modelId);
28
+ const modelLabel = model?.name ?? value.modelId;
29
+ const providerLabel = provider?.name ?? value.providerId;
30
+ return `${providerLabel} / ${modelLabel}`;
31
+ }, [value, availableProviders, defaultLabel, t]);
32
+ const handleToggle = React.useCallback(() => {
33
+ if (disabled) return;
34
+ setOpen((prev) => !prev);
35
+ }, [disabled]);
36
+ const handleSelect = React.useCallback(
37
+ (providerId, modelId) => {
38
+ onChange({ providerId, modelId });
39
+ setOpen(false);
40
+ triggerRef.current?.focus();
41
+ },
42
+ [onChange]
43
+ );
44
+ const handleClearDefault = React.useCallback(() => {
45
+ onChange(null);
46
+ setOpen(false);
47
+ triggerRef.current?.focus();
48
+ }, [onChange]);
49
+ React.useEffect(() => {
50
+ if (!open) return;
51
+ const handleKeyDown = (event) => {
52
+ if (event.key === "Escape") {
53
+ setOpen(false);
54
+ triggerRef.current?.focus();
55
+ }
56
+ };
57
+ document.addEventListener("keydown", handleKeyDown);
58
+ return () => document.removeEventListener("keydown", handleKeyDown);
59
+ }, [open]);
60
+ React.useEffect(() => {
61
+ if (!open) return;
62
+ const handleClickOutside = (event) => {
63
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
64
+ setOpen(false);
65
+ }
66
+ };
67
+ document.addEventListener("mousedown", handleClickOutside);
68
+ return () => document.removeEventListener("mousedown", handleClickOutside);
69
+ }, [open]);
70
+ if (availableProviders.length === 0) return null;
71
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: cn("relative", className), "data-ai-model-picker": agentId, children: [
72
+ /* @__PURE__ */ jsxs(
73
+ Button,
74
+ {
75
+ ref: triggerRef,
76
+ type: "button",
77
+ variant: "outline",
78
+ size: "sm",
79
+ disabled,
80
+ onClick: handleToggle,
81
+ "aria-haspopup": "listbox",
82
+ "aria-expanded": open,
83
+ "aria-label": t("ai_assistant.modelPicker.triggerAriaLabel", "Select AI model"),
84
+ title: selectedLabel,
85
+ "data-ai-model-picker-trigger": true,
86
+ className: cn(
87
+ "font-normal",
88
+ compact ? "w-8 gap-0 px-0" : "gap-1.5 px-2.5"
89
+ ),
90
+ children: [
91
+ /* @__PURE__ */ jsx(Cpu, { className: "size-3.5 shrink-0", "aria-hidden": true }),
92
+ !compact ? /* @__PURE__ */ jsx("span", { className: "max-w-36 truncate text-xs", children: selectedLabel }) : null,
93
+ /* @__PURE__ */ jsx(
94
+ ChevronDown,
95
+ {
96
+ className: cn(
97
+ "size-3 shrink-0 transition-transform",
98
+ compact && "hidden",
99
+ open && "rotate-180"
100
+ ),
101
+ "aria-hidden": true
102
+ }
103
+ )
104
+ ]
105
+ }
106
+ ),
107
+ open ? /* @__PURE__ */ jsxs(
108
+ "div",
109
+ {
110
+ role: "listbox",
111
+ "aria-label": t("ai_assistant.modelPicker.listAriaLabel", "Available models"),
112
+ className: cn(
113
+ "absolute bottom-full left-0 z-50 mb-1 max-h-72 min-w-56 overflow-y-auto",
114
+ "rounded-md border border-border bg-popover shadow-md"
115
+ ),
116
+ "data-ai-model-picker-dropdown": true,
117
+ children: [
118
+ /* @__PURE__ */ jsxs(
119
+ "div",
120
+ {
121
+ role: "option",
122
+ "aria-selected": value === null,
123
+ tabIndex: 0,
124
+ className: cn(
125
+ "cursor-pointer px-3 py-2 text-xs hover:bg-accent hover:text-accent-foreground",
126
+ "flex items-center gap-2",
127
+ value === null && "bg-accent/50"
128
+ ),
129
+ onClick: handleClearDefault,
130
+ onKeyDown: (event) => {
131
+ if (event.key === "Enter" || event.key === " ") {
132
+ event.preventDefault();
133
+ handleClearDefault();
134
+ }
135
+ },
136
+ "data-ai-model-picker-default-option": true,
137
+ children: [
138
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: defaultLabel ? t("ai_assistant.modelPicker.useDefaultWithModel", "Use agent default: {{model}}").replace("{{model}}", defaultLabel) : t("ai_assistant.modelPicker.useDefault", "Use agent default") }),
139
+ value === null ? /* @__PURE__ */ jsx("span", { className: "rounded bg-primary/10 px-1 py-0.5 text-overline font-medium text-primary", children: t("ai_assistant.modelPicker.activeBadge", "active") }) : null
140
+ ]
141
+ }
142
+ ),
143
+ availableProviders.map((provider) => /* @__PURE__ */ jsxs("div", { "data-ai-model-picker-provider": provider.id, children: [
144
+ /* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 text-overline font-semibold uppercase tracking-wider text-muted-foreground", children: provider.name }),
145
+ provider.models.map((model) => {
146
+ const isSelected = value?.providerId === provider.id && value?.modelId === model.id;
147
+ return /* @__PURE__ */ jsxs(
148
+ "div",
149
+ {
150
+ role: "option",
151
+ "aria-selected": isSelected,
152
+ tabIndex: 0,
153
+ className: cn(
154
+ "cursor-pointer px-3 py-2 text-xs hover:bg-accent hover:text-accent-foreground",
155
+ "flex items-center gap-2 pl-5",
156
+ isSelected && "bg-accent/50"
157
+ ),
158
+ onClick: () => handleSelect(provider.id, model.id),
159
+ onKeyDown: (event) => {
160
+ if (event.key === "Enter" || event.key === " ") {
161
+ event.preventDefault();
162
+ handleSelect(provider.id, model.id);
163
+ }
164
+ },
165
+ "data-ai-model-picker-model": `${provider.id}:${model.id}`,
166
+ children: [
167
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: model.name }),
168
+ model.isDefault ? /* @__PURE__ */ jsx("span", { className: "shrink-0 rounded bg-muted px-1 py-0.5 text-overline text-muted-foreground", children: t("ai_assistant.modelPicker.defaultBadge", "default") }) : null,
169
+ isSelected ? /* @__PURE__ */ jsx("span", { className: "shrink-0 rounded bg-primary/10 px-1 py-0.5 text-overline font-medium text-primary", children: t("ai_assistant.modelPicker.activeBadge", "active") }) : null
170
+ ]
171
+ },
172
+ model.id
173
+ );
174
+ })
175
+ ] }, provider.id))
176
+ ]
177
+ }
178
+ ) : null
179
+ ] });
180
+ }
181
+ var ModelPicker_default = ModelPicker;
182
+ export {
183
+ ModelPicker,
184
+ ModelPicker_default as default
185
+ };
186
+ //# sourceMappingURL=ModelPicker.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ai/ModelPicker.tsx"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { ChevronDown, Cpu } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Button } from '../primitives/button'\n\nexport interface ModelPickerProviderModel {\n id: string\n name: string\n contextWindow?: number | null\n tags?: string[]\n isDefault: boolean\n}\n\nexport interface ModelPickerProvider {\n id: string\n name: string\n isDefault: boolean\n models: ModelPickerProviderModel[]\n}\n\nexport interface ModelPickerValue {\n providerId: string\n modelId: string\n baseURL?: string | null\n}\n\nexport interface ModelPickerProps {\n agentId: string\n value: ModelPickerValue | null\n onChange: (value: ModelPickerValue | null) => void\n availableProviders: ModelPickerProvider[]\n disabled?: boolean\n compact?: boolean\n defaultLabel?: string | null\n className?: string\n}\n\n/**\n * Stateless dropdown for selecting a provider + model override in the chat UI.\n *\n * - Renders provider name + model id + \"(default)\" badge for the agent's resolved default.\n * - Keyboard-accessible: trigger opens a floating list, arrow keys navigate, Enter selects,\n * Escape closes.\n * - Does NOT expose a baseURL input (R6 mitigation: only provider+model selection from the\n * curated catalog is exposed to end users).\n * - localStorage persistence keyed by agentId is handled by the consumer (AiChat / 4b.2).\n * - When allowRuntimeModelOverride is false the consumer should not render this component\n * at all; this component itself does no gating.\n */\nexport function ModelPicker({\n agentId,\n value,\n onChange,\n availableProviders,\n disabled,\n compact = false,\n defaultLabel,\n className,\n}: ModelPickerProps) {\n const t = useT()\n const [open, setOpen] = React.useState(false)\n const containerRef = React.useRef<HTMLDivElement>(null)\n const triggerRef = React.useRef<HTMLButtonElement>(null)\n\n const selectedLabel = React.useMemo(() => {\n if (!value) {\n return defaultLabel\n ? t('ai_assistant.modelPicker.defaultWithModelLabel', 'Default: {{model}}').replace('{{model}}', defaultLabel)\n : t('ai_assistant.modelPicker.defaultLabel', 'Model: Default')\n }\n const provider = availableProviders.find((p) => p.id === value.providerId)\n const model = provider?.models.find((m) => m.id === value.modelId)\n const modelLabel = model?.name ?? value.modelId\n const providerLabel = provider?.name ?? value.providerId\n return `${providerLabel} / ${modelLabel}`\n }, [value, availableProviders, defaultLabel, t])\n\n const handleToggle = React.useCallback(() => {\n if (disabled) return\n setOpen((prev) => !prev)\n }, [disabled])\n\n const handleSelect = React.useCallback(\n (providerId: string, modelId: string) => {\n onChange({ providerId, modelId })\n setOpen(false)\n triggerRef.current?.focus()\n },\n [onChange],\n )\n\n const handleClearDefault = React.useCallback(() => {\n onChange(null)\n setOpen(false)\n triggerRef.current?.focus()\n }, [onChange])\n\n React.useEffect(() => {\n if (!open) return\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n setOpen(false)\n triggerRef.current?.focus()\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n const handleClickOutside = (event: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(event.target as Node)) {\n setOpen(false)\n }\n }\n document.addEventListener('mousedown', handleClickOutside)\n return () => document.removeEventListener('mousedown', handleClickOutside)\n }, [open])\n\n if (availableProviders.length === 0) return null\n\n return (\n <div ref={containerRef} className={cn('relative', className)} data-ai-model-picker={agentId}>\n <Button\n ref={triggerRef}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n disabled={disabled}\n onClick={handleToggle}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-label={t('ai_assistant.modelPicker.triggerAriaLabel', 'Select AI model')}\n title={selectedLabel}\n data-ai-model-picker-trigger\n className={cn(\n 'font-normal',\n compact ? 'w-8 gap-0 px-0' : 'gap-1.5 px-2.5',\n )}\n >\n <Cpu className=\"size-3.5 shrink-0\" aria-hidden />\n {!compact ? (\n <span className=\"max-w-36 truncate text-xs\">{selectedLabel}</span>\n ) : null}\n <ChevronDown\n className={cn(\n 'size-3 shrink-0 transition-transform',\n compact && 'hidden',\n open && 'rotate-180',\n )}\n aria-hidden\n />\n </Button>\n\n {open ? (\n <div\n role=\"listbox\"\n aria-label={t('ai_assistant.modelPicker.listAriaLabel', 'Available models')}\n className={cn(\n 'absolute bottom-full left-0 z-50 mb-1 max-h-72 min-w-56 overflow-y-auto',\n 'rounded-md border border-border bg-popover shadow-md',\n )}\n data-ai-model-picker-dropdown\n >\n <div\n role=\"option\"\n aria-selected={value === null}\n tabIndex={0}\n className={cn(\n 'cursor-pointer px-3 py-2 text-xs hover:bg-accent hover:text-accent-foreground',\n 'flex items-center gap-2',\n value === null && 'bg-accent/50',\n )}\n onClick={handleClearDefault}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n handleClearDefault()\n }\n }}\n data-ai-model-picker-default-option\n >\n <span className=\"flex-1\">\n {defaultLabel\n ? t('ai_assistant.modelPicker.useDefaultWithModel', 'Use agent default: {{model}}').replace('{{model}}', defaultLabel)\n : t('ai_assistant.modelPicker.useDefault', 'Use agent default')}\n </span>\n {value === null ? (\n <span className=\"rounded bg-primary/10 px-1 py-0.5 text-overline font-medium text-primary\">\n {t('ai_assistant.modelPicker.activeBadge', 'active')}\n </span>\n ) : null}\n </div>\n {availableProviders.map((provider) => (\n <div key={provider.id} data-ai-model-picker-provider={provider.id}>\n <div className=\"px-3 py-1.5 text-overline font-semibold uppercase tracking-wider text-muted-foreground\">\n {provider.name}\n </div>\n {provider.models.map((model) => {\n const isSelected =\n value?.providerId === provider.id && value?.modelId === model.id\n return (\n <div\n key={model.id}\n role=\"option\"\n aria-selected={isSelected}\n tabIndex={0}\n className={cn(\n 'cursor-pointer px-3 py-2 text-xs hover:bg-accent hover:text-accent-foreground',\n 'flex items-center gap-2 pl-5',\n isSelected && 'bg-accent/50',\n )}\n onClick={() => handleSelect(provider.id, model.id)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n handleSelect(provider.id, model.id)\n }\n }}\n data-ai-model-picker-model={`${provider.id}:${model.id}`}\n >\n <span className=\"flex-1 truncate\">{model.name}</span>\n {model.isDefault ? (\n <span className=\"shrink-0 rounded bg-muted px-1 py-0.5 text-overline text-muted-foreground\">\n {t('ai_assistant.modelPicker.defaultBadge', 'default')}\n </span>\n ) : null}\n {isSelected ? (\n <span className=\"shrink-0 rounded bg-primary/10 px-1 py-0.5 text-overline font-medium text-primary\">\n {t('ai_assistant.modelPicker.activeBadge', 'active')}\n </span>\n ) : null}\n </div>\n )\n })}\n </div>\n ))}\n </div>\n ) : null}\n </div>\n )\n}\n\nexport default ModelPicker\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/ai/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AiChat } from "./AiChat.js";
2
2
  import {
3
- AiAssistantLauncher
3
+ AiAssistantLauncher,
4
+ AI_ASSISTANT_LAUNCHER_OPEN_EVENT
4
5
  } from "./AiAssistantLauncher.js";
5
6
  import {
6
7
  AiDockProvider,
@@ -66,6 +67,7 @@ import {
66
67
  useAiChatUpload
67
68
  } from "./useAiChatUpload.js";
68
69
  export {
70
+ AI_ASSISTANT_LAUNCHER_OPEN_EVENT,
69
71
  AI_MUTATION_APPROVAL_CARDS,
70
72
  ActivityCard,
71
73
  AiAssistantLauncher,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ai/index.ts"],
4
- "sourcesContent": ["export { AiChat, type AiChatProps, type AiChatDebugTool, type AiChatDebugPromptSection } from './AiChat'\nexport {\n AiAssistantLauncher,\n type AiAssistantLauncherAgent,\n type AiAssistantLauncherProps,\n} from './AiAssistantLauncher'\nexport {\n AiDockProvider,\n useAiDock,\n type AiDockedAssistant,\n} from './AiDock'\nexport {\n AiChatSessionsProvider,\n useAiChatSessions,\n defaultSessionLabel,\n type AiChatSession,\n} from './AiChatSessions'\nexport { ChatPaneTabs } from './ChatPaneTabs'\nexport {\n useAiChat,\n type AiChatMessage,\n type AiChatToolCallSnapshot,\n type AiChatMessageFile,\n type AiChatErrorEnvelope,\n type UseAiChatInput,\n type UseAiChatResult,\n} from './useAiChat'\nexport {\n useAiShortcuts,\n type UseAiShortcutsOptions,\n type UseAiShortcutsResult,\n} from './useAiShortcuts'\nexport {\n registerAiUiPart,\n resolveAiUiPart,\n unregisterAiUiPart,\n resetAiUiPartRegistryForTests,\n listAiUiParts,\n createAiUiPartRegistry,\n defaultAiUiPartRegistry,\n RESERVED_AI_UI_PART_IDS,\n isReservedAiUiPartId,\n type AiUiPartComponent,\n type AiUiPartComponentId,\n type AiUiPartProps,\n type AiUiPartRegistry,\n type AiUiPartRegistryEntry,\n type CreateAiUiPartRegistryOptions,\n type ReservedAiUiPartId,\n} from './ui-part-registry'\nexport { PendingPhase3Placeholder } from './ui-parts/pending-phase3-placeholder'\nexport {\n MutationPreviewCard,\n FieldDiffCard,\n ConfirmationCard,\n MutationResultCard,\n AI_MUTATION_APPROVAL_CARDS,\n useAiPendingActionPolling,\n confirmPendingAction,\n cancelPendingAction,\n type UseAiPendingActionPollingOptions,\n type UseAiPendingActionPollingResult,\n type PendingActionMutationOk,\n type PendingActionMutationError,\n type PendingActionMutationResult,\n type AiPendingActionCardAction,\n type AiPendingActionCardStatus,\n type AiPendingActionCardFieldDiff,\n type AiPendingActionCardRecordDiff,\n type AiPendingActionCardFailedRecord,\n type AiPendingActionCardExecutionResult,\n} from './parts'\nexport {\n uploadAttachmentsForChat,\n type UploadAttachmentsForChatOptions,\n type UploadAttachmentsForChatResult,\n type UploadedAttachment,\n type UploadFailure,\n type UploadFailureReason,\n} from './upload-adapter'\nexport {\n RecordCard,\n DealCard,\n PersonCard,\n CompanyCard,\n ProductCard,\n ActivityCard,\n RecordCardShell,\n KeyValueList,\n TagRow,\n statusToTagVariant,\n type RecordCardProps,\n type RecordCardShellProps,\n type KeyValueListItem,\n type DealCardProps,\n type PersonCardProps,\n type CompanyCardProps,\n type ProductCardProps,\n type ActivityCardProps,\n type RecordCardKind,\n type RecordCardPayload,\n type RecordCardBaseProps,\n type DealRecordPayload,\n type PersonRecordPayload,\n type CompanyRecordPayload,\n type ProductRecordPayload,\n type ActivityRecordPayload,\n registerRecordCardUiParts,\n RECORD_CARD_COMPONENT_IDS,\n type RecordCardComponentId,\n} from './records'\nexport {\n AiMessageContent,\n parseAiContentSegments,\n RECORD_CARD_FENCE_INFO_PREFIX,\n type AiMessageContentSegment,\n type AiMessageContentProps,\n} from './AiMessageContent'\nexport {\n useAiChatUpload,\n type UseAiChatUploadOptions,\n type UseAiChatUploadState,\n type AiChatUploadFileState,\n type AiChatUploadFileStatus,\n} from './useAiChatUpload'\n"],
5
- "mappings": "AAAA,SAAS,cAAqF;AAC9F;AAAA,EACE;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,OAOK;AACP;AAAA,EACE;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAQK;AACP,SAAS,gCAAgC;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAYK;AACP;AAAA,EACE;AAAA,OAMK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAiBA;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,OAKK;",
4
+ "sourcesContent": ["export { AiChat, type AiChatProps, type AiChatDebugTool, type AiChatDebugPromptSection } from './AiChat'\nexport {\n AiAssistantLauncher,\n AI_ASSISTANT_LAUNCHER_OPEN_EVENT,\n type AiAssistantLauncherAgent,\n type AiAssistantLauncherProps,\n} from './AiAssistantLauncher'\nexport {\n AiDockProvider,\n useAiDock,\n type AiDockedAssistant,\n} from './AiDock'\nexport {\n AiChatSessionsProvider,\n useAiChatSessions,\n defaultSessionLabel,\n type AiChatSession,\n} from './AiChatSessions'\nexport { ChatPaneTabs } from './ChatPaneTabs'\nexport {\n useAiChat,\n type AiChatMessage,\n type AiChatToolCallSnapshot,\n type AiChatMessageFile,\n type AiChatErrorEnvelope,\n type UseAiChatInput,\n type UseAiChatResult,\n} from './useAiChat'\nexport {\n useAiShortcuts,\n type UseAiShortcutsOptions,\n type UseAiShortcutsResult,\n} from './useAiShortcuts'\nexport {\n registerAiUiPart,\n resolveAiUiPart,\n unregisterAiUiPart,\n resetAiUiPartRegistryForTests,\n listAiUiParts,\n createAiUiPartRegistry,\n defaultAiUiPartRegistry,\n RESERVED_AI_UI_PART_IDS,\n isReservedAiUiPartId,\n type AiUiPartComponent,\n type AiUiPartComponentId,\n type AiUiPartProps,\n type AiUiPartRegistry,\n type AiUiPartRegistryEntry,\n type CreateAiUiPartRegistryOptions,\n type ReservedAiUiPartId,\n} from './ui-part-registry'\nexport { PendingPhase3Placeholder } from './ui-parts/pending-phase3-placeholder'\nexport {\n MutationPreviewCard,\n FieldDiffCard,\n ConfirmationCard,\n MutationResultCard,\n AI_MUTATION_APPROVAL_CARDS,\n useAiPendingActionPolling,\n confirmPendingAction,\n cancelPendingAction,\n type UseAiPendingActionPollingOptions,\n type UseAiPendingActionPollingResult,\n type PendingActionMutationOk,\n type PendingActionMutationError,\n type PendingActionMutationResult,\n type AiPendingActionCardAction,\n type AiPendingActionCardStatus,\n type AiPendingActionCardFieldDiff,\n type AiPendingActionCardRecordDiff,\n type AiPendingActionCardFailedRecord,\n type AiPendingActionCardExecutionResult,\n} from './parts'\nexport {\n uploadAttachmentsForChat,\n type UploadAttachmentsForChatOptions,\n type UploadAttachmentsForChatResult,\n type UploadedAttachment,\n type UploadFailure,\n type UploadFailureReason,\n} from './upload-adapter'\nexport {\n RecordCard,\n DealCard,\n PersonCard,\n CompanyCard,\n ProductCard,\n ActivityCard,\n RecordCardShell,\n KeyValueList,\n TagRow,\n statusToTagVariant,\n type RecordCardProps,\n type RecordCardShellProps,\n type KeyValueListItem,\n type DealCardProps,\n type PersonCardProps,\n type CompanyCardProps,\n type ProductCardProps,\n type ActivityCardProps,\n type RecordCardKind,\n type RecordCardPayload,\n type RecordCardBaseProps,\n type DealRecordPayload,\n type PersonRecordPayload,\n type CompanyRecordPayload,\n type ProductRecordPayload,\n type ActivityRecordPayload,\n registerRecordCardUiParts,\n RECORD_CARD_COMPONENT_IDS,\n type RecordCardComponentId,\n} from './records'\nexport {\n AiMessageContent,\n parseAiContentSegments,\n RECORD_CARD_FENCE_INFO_PREFIX,\n type AiMessageContentSegment,\n type AiMessageContentProps,\n} from './AiMessageContent'\nexport {\n useAiChatUpload,\n type UseAiChatUploadOptions,\n type UseAiChatUploadState,\n type AiChatUploadFileState,\n type AiChatUploadFileStatus,\n} from './useAiChatUpload'\n"],
5
+ "mappings": "AAAA,SAAS,cAAqF;AAC9F;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,OAOK;AACP;AAAA,EACE;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAQK;AACP,SAAS,gCAAgC;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAYK;AACP;AAAA,EACE;AAAA,OAMK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAiBA;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,OAKK;",
6
6
  "names": []
7
7
  }
@@ -95,12 +95,19 @@ function clearPersistedSession(agent, conversationId) {
95
95
  } catch {
96
96
  }
97
97
  }
98
- function getTransportEndpoint(agent, apiPath) {
98
+ function getTransportEndpoint(agent, apiPath, providerOverride, modelOverride) {
99
99
  const transport = createAiAgentTransport({ agentId: agent, endpoint: apiPath });
100
100
  void transport;
101
101
  const base = apiPath && apiPath.length > 0 ? apiPath : "/api/ai_assistant/ai/chat";
102
102
  const separator = base.includes("?") ? "&" : "?";
103
- return `${base}${separator}agent=${encodeURIComponent(agent)}`;
103
+ let url = `${base}${separator}agent=${encodeURIComponent(agent)}`;
104
+ if (providerOverride) {
105
+ url += `&provider=${encodeURIComponent(providerOverride)}`;
106
+ }
107
+ if (modelOverride) {
108
+ url += `&model=${encodeURIComponent(modelOverride)}`;
109
+ }
110
+ return url;
104
111
  }
105
112
  function createBuilder() {
106
113
  return { text: "", reasoning: "", reasoningStreaming: false, toolCalls: [], uiParts: [] };
@@ -304,7 +311,7 @@ async function readErrorEnvelope(response) {
304
311
  return { message: text || `Agent dispatch failed (${response.status}).` };
305
312
  }
306
313
  function useAiChat(input) {
307
- const { agent, apiPath, pageContext, attachmentIds, debug, initialMessages, onError, conversationId: conversationIdInput } = input;
314
+ const { agent, apiPath, pageContext, attachmentIds, debug, initialMessages, onError, conversationId: conversationIdInput, providerOverride, modelOverride } = input;
308
315
  const persistedRef = React.useRef("unread");
309
316
  if (persistedRef.current === "unread") {
310
317
  persistedRef.current = typeof conversationIdInput === "string" && conversationIdInput.length > 0 ? readPersistedSession(agent, conversationIdInput) : readPersistedSession(agent);
@@ -458,7 +465,7 @@ function useAiChat(input) {
458
465
  writeSessionStreamState(sessionStorageKey, true);
459
466
  const controller = new AbortController();
460
467
  abortRef.current = controller;
461
- const url = getTransportEndpoint(agent, apiPath);
468
+ const url = getTransportEndpoint(agent, apiPath, providerOverride, modelOverride);
462
469
  const body = {
463
470
  messages: outgoingHistory.map((message) => ({
464
471
  role: message.role,
@@ -627,18 +634,7 @@ function useAiChat(input) {
627
634
  setStatus("idle");
628
635
  }
629
636
  },
630
- [
631
- agent,
632
- apiPath,
633
- attachmentIds,
634
- debug,
635
- effectiveConversationId,
636
- emitError,
637
- pageContext,
638
- sessionStorageKey,
639
- setStatus,
640
- updateMessages
641
- ]
637
+ [agent, apiPath, attachmentIds, debug, effectiveConversationId, emitError, messages, modelOverride, pageContext, providerOverride]
642
638
  );
643
639
  React.useEffect(() => {
644
640
  if (typeof window === "undefined") return;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ai/useAiChat.ts"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { createAiAgentTransport } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/agent-transport'\nimport { apiFetch } from '../backend/utils/api'\n\n/**\n * Chat message shape used by {@link AiChat}. Kept intentionally minimal so the\n * component stays independent of the AI SDK's evolving `UIMessage` type. The\n * dispatcher route (`POST /api/ai_assistant/ai/chat`) accepts exactly this\n * shape for `messages`.\n */\nexport interface AiChatMessageFile {\n name: string\n type: string\n previewUrl?: string\n}\n\nexport interface AiChatToolCallSnapshot {\n id: string\n toolName: string\n state: 'pending' | 'complete' | 'error'\n input?: unknown\n output?: unknown\n errorMessage?: string\n}\n\nexport interface AiChatMessageUiPart {\n componentId: string\n payload?: unknown\n pendingActionId?: string\n /** Stable id used as React key when rendering. */\n key: string\n}\n\nexport interface AiChatMessage {\n id: string\n role: 'user' | 'assistant'\n content: string\n files?: AiChatMessageFile[]\n reasoning?: string\n reasoningStreaming?: boolean\n toolCalls?: AiChatToolCallSnapshot[]\n /**\n * UI parts emitted by the agent during this message's lifecycle. Today\n * the only producer is `prepareMutation` (mutation approval flow):\n * the dispatcher's mutation tool returns an `awaiting-confirmation`\n * envelope, useAiChat parses it and attaches a `mutation-preview-card`\n * part here so AiChat can render the approval card inline. Phase 3\n * WS-C wiring \u2014 without this, the `MutationPreviewCard` registered in\n * the UI-part registry never surfaces.\n */\n uiParts?: AiChatMessageUiPart[]\n}\n\nexport interface UseAiChatInput {\n agent: string\n apiPath?: string\n pageContext?: Record<string, unknown>\n attachmentIds?: string[]\n debug?: boolean\n initialMessages?: Array<Pick<AiChatMessage, 'role' | 'content'>>\n onError?: (err: { code?: string; message: string }) => void\n /**\n * Optional stable conversation id. When provided, the same id is forwarded\n * to the dispatcher on every turn so `prepareMutation`'s idempotency hash\n * (Step 5.6) stays stable across mutation preview / confirm / retry cycles.\n * When omitted, the hook mints a fresh random id once on mount and reuses\n * it for the lifetime of the component \u2014 callers can still override via\n * props at any time to reset the conversation.\n */\n conversationId?: string\n}\n\nexport interface AiChatErrorEnvelope {\n code?: string\n message: string\n}\n\nexport interface UseAiChatResult {\n messages: AiChatMessage[]\n status: 'idle' | 'submitting' | 'streaming'\n error: AiChatErrorEnvelope | null\n lastRequestDebug: { url: string; body: unknown } | null\n lastResponseDebug: { status: number; text: string } | null\n /**\n * The conversation id currently in use for this chat instance. Equal to\n * the caller-provided `conversationId` input when one is supplied;\n * otherwise the random id minted on mount. Stable across re-renders for a\n * given mount (Phase 3 WS-D contract with `prepareMutation`).\n */\n conversationId: string\n sendMessage: (input: string, files?: AiChatMessageFile[]) => Promise<void>\n cancel: () => void\n reset: () => void\n}\n\nfunction makeMessageId(): string {\n const random = Math.random().toString(36).slice(2, 10)\n const time = Date.now().toString(36)\n return `msg_${time}_${random}`\n}\n\nfunction makeConversationId(): string {\n // Use crypto.randomUUID() when the browser exposes it (all evergreen\n // runtimes do), otherwise fall back to a low-entropy token that is still\n // unique enough for the idempotency-hash use case.\n const g = globalThis as unknown as { crypto?: { randomUUID?: () => string } }\n if (g.crypto && typeof g.crypto.randomUUID === 'function') {\n try {\n return g.crypto.randomUUID()\n } catch {\n // fall through to the random fallback\n }\n }\n const rand = () => Math.random().toString(16).slice(2, 10)\n return `conv_${Date.now().toString(16)}_${rand()}${rand()}`\n}\n\nconst SESSION_STORAGE_PREFIX = 'om-ai-chat:'\nconst SESSION_STORAGE_VERSION = 1\nconst SESSION_UPDATED_EVENT = 'om-ai-chat-session-updated'\nconst SESSION_STREAM_STATE_EVENT = 'om-ai-chat-session-stream-state'\nconst activeSessionStreams = new Set<string>()\n\ninterface PersistedAiChatSession {\n v: number\n conversationId: string\n messages: AiChatMessage[]\n}\n\nfunction getSessionStorageKey(agent: string, conversationId?: string | null): string {\n // When the caller pins a `conversationId` (e.g. via the AiChatSessions\n // provider's tabs), namespace the persisted slot per session so multiple\n // open conversations for the same agent don't overwrite each other. The\n // legacy single-session-per-agent layout (no externally-supplied id) is\n // kept for backward compatibility with code that still relies on it.\n if (typeof conversationId === 'string' && conversationId.length > 0) {\n return `${SESSION_STORAGE_PREFIX}${agent}:${conversationId}`\n }\n return `${SESSION_STORAGE_PREFIX}${agent}`\n}\n\nfunction writeSessionStreamState(storageKey: string, active: boolean): void {\n if (active) {\n activeSessionStreams.add(storageKey)\n } else {\n activeSessionStreams.delete(storageKey)\n }\n if (typeof window === 'undefined') return\n try {\n window.dispatchEvent(\n new CustomEvent(SESSION_STREAM_STATE_EVENT, {\n detail: { key: storageKey, active },\n }),\n )\n } catch {\n // ignore event dispatch failures in non-browser test environments\n }\n}\n\nfunction isSessionStreamActive(storageKey: string): boolean {\n return activeSessionStreams.has(storageKey)\n}\n\nfunction readPersistedSession(\n agent: string,\n conversationId?: string | null,\n): PersistedAiChatSession | null {\n if (typeof window === 'undefined') return null\n try {\n const raw = window.localStorage.getItem(getSessionStorageKey(agent, conversationId))\n if (!raw) return null\n const parsed = JSON.parse(raw) as PersistedAiChatSession | null\n if (!parsed || parsed.v !== SESSION_STORAGE_VERSION) return null\n if (typeof parsed.conversationId !== 'string') return null\n if (!Array.isArray(parsed.messages)) return null\n const messages = parsed.messages.filter((entry): entry is AiChatMessage => {\n return (\n !!entry &&\n typeof entry === 'object' &&\n typeof (entry as AiChatMessage).id === 'string' &&\n typeof (entry as AiChatMessage).content === 'string' &&\n ((entry as AiChatMessage).role === 'user' || (entry as AiChatMessage).role === 'assistant')\n )\n })\n return { v: SESSION_STORAGE_VERSION, conversationId: parsed.conversationId, messages }\n } catch {\n return null\n }\n}\n\nfunction writePersistedSession(\n agent: string,\n session: PersistedAiChatSession,\n conversationId?: string | null,\n options?: { notify?: boolean },\n): void {\n if (typeof window === 'undefined') return\n try {\n // Strip transient blob/object preview URLs before persisting (they would\n // not survive a reload). Self-contained `data:` URLs are kept so image\n // previews come back unchanged after the chat is reopened \u2014 public\n // attachment URLs are intentionally not used because the LLM provider\n // cannot reach a localhost origin and we want a single durable shape\n // that works for both transport and reload.\n const messages = session.messages.map((message) => {\n if (!message.files || message.files.length === 0) return message\n const safeFiles = message.files.map(({ name, type, previewUrl }) => {\n const durable =\n typeof previewUrl === 'string' && previewUrl.startsWith('data:')\n ? previewUrl\n : undefined\n return durable ? { name, type, previewUrl: durable } : { name, type }\n })\n return { ...message, files: safeFiles }\n })\n const storageKey = getSessionStorageKey(agent, conversationId)\n window.localStorage.setItem(storageKey, JSON.stringify({ ...session, messages }))\n if (options?.notify) {\n window.dispatchEvent(\n new CustomEvent(SESSION_UPDATED_EVENT, {\n detail: { key: storageKey, agent, conversationId: session.conversationId },\n }),\n )\n }\n } catch {\n // Quota exceeded / privacy mode \u2014 silently drop persistence.\n }\n}\n\nfunction clearPersistedSession(agent: string, conversationId?: string | null): void {\n if (typeof window === 'undefined') return\n try {\n window.localStorage.removeItem(getSessionStorageKey(agent, conversationId))\n } catch {\n // ignore\n }\n}\n\nfunction getTransportEndpoint(agent: string, apiPath?: string): string {\n // Reuse the transport factory so UI consumers share the dispatcher URL\n // convention with server-side callers (e.g. runAiAgentText / Playwright\n // fixtures). The factory returns a ChatTransport<UI_MESSAGE> whose internal\n // endpoint we do not directly read \u2014 instead we reconstruct the same URL\n // shape here so downstream error handling stays deterministic.\n //\n // When the AI SDK exposes a public endpoint getter (or the stream format\n // switches from plain text to UIMessageChunk) we can call\n // transport.sendMessages(...) directly.\n const transport = createAiAgentTransport({ agentId: agent, endpoint: apiPath })\n void transport\n const base = apiPath && apiPath.length > 0 ? apiPath : '/api/ai_assistant/ai/chat'\n const separator = base.includes('?') ? '&' : '?'\n return `${base}${separator}agent=${encodeURIComponent(agent)}`\n}\n\ninterface AssistantBuilderState {\n text: string\n reasoning: string\n reasoningStreaming: boolean\n toolCalls: AiChatToolCallSnapshot[]\n uiParts: AiChatMessageUiPart[]\n}\n\nfunction createBuilder(): AssistantBuilderState {\n return { text: '', reasoning: '', reasoningStreaming: false, toolCalls: [], uiParts: [] }\n}\n\n/**\n * Generic extractor for UI parts emitted by tool outputs. A tool can\n * surface inline UI to the chat by returning JSON in any of these\n * shapes \u2014 each tool call produces zero or more UI parts:\n *\n * 1. The dispatcher's mutation envelope:\n * `{ status: 'awaiting-confirmation', pendingActionId, expiresAt,\n * agent, toolName, message }`\n * \u2192 synthesizes a `mutation-preview-card` part (the registered\n * card fetches the live diff via `useAiPendingActionPolling`).\n *\n * 2. A single explicit UI part:\n * `{ uiPart: { componentId, payload?, pendingActionId? } }`\n *\n * 3. Multiple explicit UI parts:\n * `{ uiParts: [{ componentId, payload? }, ...] }`\n *\n * Tool authors only need to JSON-encode an object whose `uiPart` /\n * `uiParts` reference component ids that the host has registered on\n * `defaultAiUiPartRegistry` (or a scoped registry passed through\n * `<AiChat registry={...}/>`). Unknown component ids fall back to the\n * `UnknownUiPartPlaceholder` so an unregistered id never blows up the\n * transcript.\n */\nfunction extractUiPartsFromOutput(\n output: unknown,\n toolCallId: string,\n): AiChatMessageUiPart[] {\n let parsed: unknown = output\n if (typeof output === 'string') {\n const trimmed = output.trim()\n if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return []\n try {\n parsed = JSON.parse(trimmed)\n } catch {\n return []\n }\n }\n if (!parsed || typeof parsed !== 'object') return []\n const value = parsed as Record<string, unknown>\n const parts: AiChatMessageUiPart[] = []\n\n // (1) Mutation approval envelope. The dispatcher's `prepareMutation`\n // interceptor in `agent-tools.ts` formats the result via\n // `formatPendingActionToolResult` as\n // { status: 'pending-confirmation', agentId, toolName, pendingActionId,\n // expiresAt, message }\n // (NOTE: status is `pending-confirmation` and the field is `agentId`,\n // not `agent`). We also accept `awaiting-confirmation` / `agent` for\n // forward compat with older / alternative dispatchers.\n if (value.status === 'pending-confirmation' || value.status === 'awaiting-confirmation') {\n const pendingActionId =\n typeof value.pendingActionId === 'string' && value.pendingActionId.length > 0\n ? value.pendingActionId\n : null\n if (pendingActionId) {\n const agentId =\n typeof value.agentId === 'string'\n ? value.agentId\n : typeof value.agent === 'string'\n ? value.agent\n : undefined\n parts.push({\n componentId: 'mutation-preview-card',\n pendingActionId,\n payload: {\n pendingActionId,\n expiresAt: typeof value.expiresAt === 'string' ? value.expiresAt : undefined,\n agentId,\n toolName: typeof value.toolName === 'string' ? value.toolName : undefined,\n },\n key: `${toolCallId}:mutation-preview-card`,\n })\n }\n }\n\n // (2) Explicit single UI part.\n if (value.uiPart && typeof value.uiPart === 'object') {\n const part = value.uiPart as Record<string, unknown>\n if (typeof part.componentId === 'string' && part.componentId.length > 0) {\n parts.push({\n componentId: part.componentId,\n payload: part.payload,\n pendingActionId:\n typeof part.pendingActionId === 'string' ? part.pendingActionId : undefined,\n key: `${toolCallId}:${part.componentId}`,\n })\n }\n }\n\n // (3) Explicit list of UI parts.\n if (Array.isArray(value.uiParts)) {\n value.uiParts.forEach((entry, index) => {\n if (!entry || typeof entry !== 'object') return\n const part = entry as Record<string, unknown>\n if (typeof part.componentId !== 'string' || part.componentId.length === 0) return\n parts.push({\n componentId: part.componentId,\n payload: part.payload,\n pendingActionId:\n typeof part.pendingActionId === 'string' ? part.pendingActionId : undefined,\n key: `${toolCallId}:${index}:${part.componentId}`,\n })\n })\n }\n\n return parts\n}\n\nfunction updateToolCall(\n state: AssistantBuilderState,\n id: string,\n patch: Partial<AiChatToolCallSnapshot> & { toolName?: string },\n): AssistantBuilderState {\n if (!id) return state\n const idx = state.toolCalls.findIndex((entry) => entry.id === id)\n if (idx === -1) {\n const next: AiChatToolCallSnapshot = {\n id,\n toolName: patch.toolName ?? 'tool',\n state: patch.state ?? 'pending',\n input: patch.input,\n output: patch.output,\n errorMessage: patch.errorMessage,\n }\n return { ...state, toolCalls: [...state.toolCalls, next] }\n }\n const current = state.toolCalls[idx]\n const merged: AiChatToolCallSnapshot = {\n ...current,\n toolName: patch.toolName ?? current.toolName,\n state: patch.state ?? current.state,\n input: patch.input !== undefined ? patch.input : current.input,\n output: patch.output !== undefined ? patch.output : current.output,\n errorMessage: patch.errorMessage ?? current.errorMessage,\n }\n const nextCalls = state.toolCalls.slice()\n nextCalls[idx] = merged\n return { ...state, toolCalls: nextCalls }\n}\n\nfunction applyChunk(\n state: AssistantBuilderState,\n chunk: { type: string; [key: string]: unknown },\n): AssistantBuilderState {\n switch (chunk.type) {\n case 'text-delta':\n return {\n ...state,\n text: state.text + (typeof chunk.delta === 'string' ? chunk.delta : ''),\n }\n case 'reasoning-start':\n return { ...state, reasoningStreaming: true }\n case 'reasoning-delta':\n return {\n ...state,\n reasoning:\n state.reasoning + (typeof chunk.delta === 'string' ? chunk.delta : ''),\n reasoningStreaming: true,\n }\n case 'reasoning-end':\n return { ...state, reasoningStreaming: false }\n case 'tool-input-start':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n toolName: typeof chunk.toolName === 'string' ? chunk.toolName : undefined,\n state: 'pending',\n })\n case 'tool-input-available':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n toolName: typeof chunk.toolName === 'string' ? chunk.toolName : undefined,\n input: chunk.input,\n state: 'pending',\n })\n case 'tool-output-available': {\n const toolCallId = String(chunk.toolCallId ?? '')\n const next = updateToolCall(state, toolCallId, {\n output: chunk.output,\n state: 'complete',\n })\n // Phase 3 WS-C \u2014 surface ANY UI parts the tool output advertises:\n // the legacy `awaiting-confirmation` mutation envelope plus the\n // generic `{ uiPart }` / `{ uiParts: [...] }` shapes. This lets\n // module authors define their own dynamic cards (stats panels,\n // record summaries, charts\u2026) without touching the dispatcher or\n // the chat client.\n const newParts = extractUiPartsFromOutput(chunk.output, toolCallId)\n if (newParts.length === 0) return next\n const seen = new Set(next.uiParts.map((entry) => entry.key))\n const merged = [...next.uiParts]\n for (const part of newParts) {\n if (seen.has(part.key)) continue\n seen.add(part.key)\n merged.push(part)\n }\n if (merged.length === next.uiParts.length) return next\n return { ...next, uiParts: merged }\n }\n case 'tool-output-error':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n state: 'error',\n errorMessage:\n typeof chunk.errorText === 'string' ? chunk.errorText : 'Tool error',\n })\n case 'tool-input-error':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n toolName: typeof chunk.toolName === 'string' ? chunk.toolName : undefined,\n input: chunk.input,\n state: 'error',\n errorMessage:\n typeof chunk.errorText === 'string' ? chunk.errorText : 'Tool error',\n })\n default:\n return state\n }\n}\n\nfunction mergeAssistantMessage(\n current: AiChatMessage,\n state: AssistantBuilderState,\n): AiChatMessage {\n return {\n ...current,\n content: state.text,\n reasoning: state.reasoning ? state.reasoning : undefined,\n reasoningStreaming: state.reasoning ? state.reasoningStreaming : undefined,\n toolCalls: state.toolCalls.length > 0 ? state.toolCalls : undefined,\n uiParts: state.uiParts.length > 0 ? state.uiParts : undefined,\n }\n}\n\nfunction parseSseLines(buffer: string): { events: string[]; rest: string } {\n const events: string[] = []\n let rest = buffer\n for (;;) {\n const idx = rest.indexOf('\\n\\n')\n if (idx === -1) break\n events.push(rest.slice(0, idx))\n rest = rest.slice(idx + 2)\n }\n return { events, rest }\n}\n\nfunction extractDataPayload(eventBlock: string): string | null {\n const lines = eventBlock.split('\\n')\n const dataLines: string[] = []\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n dataLines.push(line.slice(6))\n } else if (line.startsWith('data:')) {\n dataLines.push(line.slice(5))\n }\n }\n if (dataLines.length === 0) return null\n return dataLines.join('\\n')\n}\n\nasync function readErrorEnvelope(response: Response): Promise<AiChatErrorEnvelope> {\n try {\n const data = (await response.clone().json()) as\n | { error?: unknown; code?: unknown; message?: unknown }\n | null\n if (data && typeof data === 'object') {\n const rawMessage =\n (typeof data.error === 'string' && data.error) ||\n (typeof data.message === 'string' && data.message) ||\n ''\n const rawCode = typeof data.code === 'string' ? data.code : undefined\n if (rawMessage || rawCode) {\n return {\n code: rawCode,\n message: rawMessage || 'Agent dispatch failed.',\n }\n }\n }\n } catch {\n // Fall through to text fallback\n }\n const text = await response.text().catch(() => '')\n return { message: text || `Agent dispatch failed (${response.status}).` }\n}\n\nexport function useAiChat(input: UseAiChatInput): UseAiChatResult {\n const { agent, apiPath, pageContext, attachmentIds, debug, initialMessages, onError, conversationId: conversationIdInput } = input\n\n // Minted once on mount when the caller does not supply a conversationId.\n // The ref keeps the id stable across re-renders and is reused for every\n // turn so the Phase 3 WS-C `prepareMutation` idempotency hash stays\n // stable within the same chat. When the agent has a persisted session in\n // localStorage we re-hydrate the conversationId from it so re-opening the\n // chat continues the previous turn instead of starting fresh.\n const persistedRef = React.useRef<PersistedAiChatSession | null | 'unread'>('unread')\n if (persistedRef.current === 'unread') {\n // When the caller pins a `conversationId` (multi-tab session mode) we\n // read ONLY from that per-conversation slot. Falling back to the\n // legacy agent-only slot here would make every brand-new tab inherit\n // the previous tab's messages \u2014 the \"+ shows the same chat\" bug \u2014 so\n // unknown conversationIds always start clean. Without a pinned id we\n // keep the legacy single-session-per-agent layout for backward\n // compatibility.\n persistedRef.current =\n typeof conversationIdInput === 'string' && conversationIdInput.length > 0\n ? readPersistedSession(agent, conversationIdInput)\n : readPersistedSession(agent)\n }\n const persisted = persistedRef.current\n\n const mintedConversationIdRef = React.useRef<string | null>(null)\n if (mintedConversationIdRef.current === null) {\n mintedConversationIdRef.current = persisted?.conversationId ?? makeConversationId()\n }\n const effectiveConversationId =\n typeof conversationIdInput === 'string' && conversationIdInput.length > 0\n ? conversationIdInput\n : mintedConversationIdRef.current\n const persistKey =\n typeof conversationIdInput === 'string' && conversationIdInput.length > 0\n ? conversationIdInput\n : null\n const sessionStorageKey = getSessionStorageKey(agent, persistKey)\n\n const [messages, setMessages] = React.useState<AiChatMessage[]>(() => {\n if (persisted && persisted.messages.length > 0) {\n return persisted.messages\n }\n return (initialMessages ?? []).map((entry) => ({\n id: makeMessageId(),\n role: entry.role,\n content: entry.content,\n }))\n })\n\n // Persist messages + conversationId on every change. Skip during in-flight\n // streaming so we do not write the same growing string on every chunk \u2014\n // the next idle tick captures the final assistant content.\n const [status, setStatusInternal] = React.useState<'idle' | 'submitting' | 'streaming'>(() =>\n isSessionStreamActive(sessionStorageKey) ? 'streaming' : 'idle',\n )\n // Refs mirror the latest persist state so the unmount cleanup can flush\n // an in-flight assistant message to localStorage even though the streaming\n // skip above (and React's stale-closure semantics) would otherwise lose it.\n // Without this, closing the dock or switching agents while the assistant\n // is \"Thinking\" abandons the partial reply (issue #1816).\n const latestMessagesRef = React.useRef<AiChatMessage[]>(messages)\n const persistKeyRef = React.useRef<string | null>(null)\n const agentRef = React.useRef<string>(agent)\n const effectiveConversationIdRef = React.useRef<string>(effectiveConversationId)\n const sessionStorageKeyRef = React.useRef<string>(sessionStorageKey)\n const mountedRef = React.useRef(true)\n React.useEffect(() => {\n latestMessagesRef.current = messages\n }, [messages])\n React.useEffect(() => {\n persistKeyRef.current =\n typeof conversationIdInput === 'string' && conversationIdInput.length > 0\n ? conversationIdInput\n : null\n }, [conversationIdInput])\n React.useEffect(() => {\n agentRef.current = agent\n }, [agent])\n React.useEffect(() => {\n effectiveConversationIdRef.current = effectiveConversationId\n }, [effectiveConversationId])\n React.useEffect(() => {\n sessionStorageKeyRef.current = sessionStorageKey\n }, [sessionStorageKey])\n React.useEffect(() => {\n if (status !== 'idle') return\n if (messages.length === 0) {\n clearPersistedSession(agent, persistKey)\n return\n }\n writePersistedSession(\n agent,\n {\n v: SESSION_STORAGE_VERSION,\n conversationId: effectiveConversationId,\n messages,\n },\n persistKey,\n )\n }, [agent, effectiveConversationId, messages, persistKey, status])\n const persistSnapshot = React.useCallback((snapshot: AiChatMessage[], notify = false) => {\n if (snapshot.length === 0) return\n writePersistedSession(\n agentRef.current,\n {\n v: SESSION_STORAGE_VERSION,\n conversationId: effectiveConversationIdRef.current,\n messages: snapshot,\n },\n persistKeyRef.current,\n { notify },\n )\n }, [])\n\n const updateMessages = React.useCallback(\n (\n updater:\n | AiChatMessage[]\n | ((current: AiChatMessage[]) => AiChatMessage[]),\n options?: { persistWhenUnmounted?: boolean },\n ) => {\n const current = latestMessagesRef.current\n const next = typeof updater === 'function' ? updater(current) : updater\n latestMessagesRef.current = next\n if (mountedRef.current) {\n setMessages(next)\n } else if (options?.persistWhenUnmounted) {\n persistSnapshot(next, true)\n }\n },\n [persistSnapshot],\n )\n\n const setStatus = React.useCallback((next: 'idle' | 'submitting' | 'streaming') => {\n if (mountedRef.current) {\n setStatusInternal(next)\n }\n }, [])\n const [error, setError] = React.useState<AiChatErrorEnvelope | null>(null)\n const [lastRequestDebug, setLastRequestDebug] = React.useState<\n { url: string; body: unknown } | null\n >(null)\n const [lastResponseDebug, setLastResponseDebug] = React.useState<\n { status: number; text: string } | null\n >(null)\n\n const abortRef = React.useRef<AbortController | null>(null)\n const onErrorRef = React.useRef(onError)\n React.useEffect(() => {\n onErrorRef.current = onError\n }, [onError])\n\n const emitError = React.useCallback((envelope: AiChatErrorEnvelope) => {\n if (mountedRef.current) {\n setError(envelope)\n try {\n onErrorRef.current?.(envelope)\n } catch {\n // UI layer must never throw because a caller-supplied error handler\n // misbehaved.\n }\n }\n }, [])\n\n const cancel = React.useCallback(() => {\n if (abortRef.current) {\n abortRef.current.abort()\n abortRef.current = null\n }\n writeSessionStreamState(sessionStorageKeyRef.current, false)\n setStatus('idle')\n }, [setStatus])\n\n const reset = React.useCallback(() => {\n cancel()\n updateMessages([])\n setError(null)\n setLastRequestDebug(null)\n setLastResponseDebug(null)\n clearPersistedSession(agent, persistKey)\n mintedConversationIdRef.current = makeConversationId()\n }, [agent, cancel, persistKey, updateMessages])\n\n const sendMessage = React.useCallback(\n async (textInput: string, files?: AiChatMessageFile[]) => {\n const trimmed = textInput.trim()\n if (!trimmed) return\n if (abortRef.current) {\n abortRef.current.abort()\n }\n\n setError(null)\n const userMessage: AiChatMessage = {\n id: makeMessageId(),\n role: 'user',\n content: trimmed,\n files: files && files.length > 0 ? files : undefined,\n }\n const assistantMessage: AiChatMessage = {\n id: makeMessageId(),\n role: 'assistant',\n content: '',\n }\n const assistantId = assistantMessage.id\n // Snapshot prior messages for request payload so the dispatcher sees the\n // full turn history including the just-added user message.\n const outgoingHistory = [...latestMessagesRef.current, userMessage]\n updateMessages([...outgoingHistory, assistantMessage], { persistWhenUnmounted: true })\n setStatus('submitting')\n writeSessionStreamState(sessionStorageKey, true)\n\n const controller = new AbortController()\n abortRef.current = controller\n\n const url = getTransportEndpoint(agent, apiPath)\n const body = {\n messages: outgoingHistory.map((message) => ({\n role: message.role,\n content: message.content,\n })),\n pageContext,\n attachmentIds,\n debug,\n conversationId: effectiveConversationId,\n }\n if (mountedRef.current) {\n setLastRequestDebug({ url, body })\n }\n\n let response: Response\n try {\n response = await apiFetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'text/event-stream, text/plain, application/json',\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n })\n } catch (requestError) {\n if ((requestError as { name?: string })?.name === 'AbortError') {\n writeSessionStreamState(sessionStorageKey, false)\n setStatus('idle')\n abortRef.current = null\n return\n }\n const message =\n requestError instanceof Error\n ? requestError.message\n : 'Network request failed.'\n emitError({ message })\n writeSessionStreamState(sessionStorageKey, false)\n setStatus('idle')\n abortRef.current = null\n return\n }\n\n if (!response.ok) {\n const envelope = await readErrorEnvelope(response)\n if (mountedRef.current) {\n setLastResponseDebug({ status: response.status, text: envelope.message })\n }\n emitError(envelope)\n writeSessionStreamState(sessionStorageKey, false)\n setStatus('idle')\n updateMessages((current) => current.filter((entry) => entry.id !== assistantId), {\n persistWhenUnmounted: true,\n })\n abortRef.current = null\n return\n }\n\n const bodyStream = response.body\n if (!bodyStream) {\n if (mountedRef.current) {\n setLastResponseDebug({ status: response.status, text: '' })\n }\n writeSessionStreamState(sessionStorageKey, false)\n setStatus('idle')\n abortRef.current = null\n return\n }\n\n const headerGet = (name: string): string | null => {\n const headers = (response as { headers?: { get?: (k: string) => string | null } })\n .headers\n if (!headers || typeof headers.get !== 'function') return null\n try {\n return headers.get(name)\n } catch {\n return null\n }\n }\n const isUiMessageStream =\n headerGet('x-vercel-ai-ui-message-stream') !== null ||\n (headerGet('content-type') ?? '').includes('event-stream')\n\n setStatus('streaming')\n const reader = bodyStream.getReader()\n const decoder = new TextDecoder()\n let streamedRaw = ''\n let builder = createBuilder()\n let sseBuffer = ''\n const flushUiMessageBuffer = (extra?: string) => {\n if (extra) sseBuffer += extra\n const { events, rest } = parseSseLines(sseBuffer)\n sseBuffer = rest\n for (const block of events) {\n const data = extractDataPayload(block)\n if (!data) continue\n if (data === '[DONE]') continue\n try {\n const parsed = JSON.parse(data) as { type?: string }\n if (parsed && typeof parsed.type === 'string') {\n builder = applyChunk(builder, parsed as { type: string })\n }\n } catch {\n // Tolerate malformed events / SSE comments.\n }\n }\n }\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n if (!value) continue\n const piece = decoder.decode(value, { stream: true })\n if (!piece) continue\n streamedRaw += piece\n\n if (isUiMessageStream) {\n flushUiMessageBuffer(piece)\n } else {\n // Plain text fallback (legacy `toTextStreamResponse`).\n builder = { ...builder, text: streamedRaw }\n }\n const snapshotBuilder = builder\n updateMessages(\n (current) =>\n current.map((entry) =>\n entry.id === assistantId\n ? mergeAssistantMessage(entry, snapshotBuilder)\n : entry,\n ),\n { persistWhenUnmounted: true },\n )\n }\n const tail = decoder.decode()\n if (tail) {\n streamedRaw += tail\n if (isUiMessageStream) {\n flushUiMessageBuffer(tail)\n } else {\n builder = { ...builder, text: streamedRaw }\n }\n }\n if (isUiMessageStream && sseBuffer.length > 0) {\n flushUiMessageBuffer('\\n\\n')\n }\n builder = { ...builder, reasoningStreaming: false }\n const finalSnapshot = builder\n updateMessages(\n (current) =>\n current.map((entry) =>\n entry.id === assistantId\n ? mergeAssistantMessage(entry, finalSnapshot)\n : entry,\n ),\n { persistWhenUnmounted: true },\n )\n if (mountedRef.current) {\n setLastResponseDebug({ status: response.status, text: streamedRaw })\n }\n const isEmpty =\n !builder.text.trim() && builder.toolCalls.length === 0 && !builder.reasoning\n if (isEmpty) {\n emitError({\n code: 'empty_response',\n message:\n 'The AI agent returned an empty response. This usually means the LLM provider rejected the request (invalid API key, rate limit, or model error). Check your server logs for details.',\n })\n updateMessages((current) => current.filter((entry) => entry.id !== assistantId), {\n persistWhenUnmounted: true,\n })\n }\n } catch (streamError) {\n if ((streamError as { name?: string })?.name === 'AbortError') {\n // Cancelled by the user \u2014 keep whatever we have so far and exit\n // quietly.\n } else {\n const rawMessage =\n streamError instanceof Error\n ? streamError.message\n : 'Stream interrupted.'\n // LLM provider errors (auth failures, rate limits, invalid tool\n // schemas) surface as stream read errors. Include a hint so the\n // operator can check server logs for the full stack trace.\n const message = rawMessage.includes('API')\n ? rawMessage\n : `${rawMessage} \u2014 check server logs for LLM provider details.`\n emitError({ code: 'stream_error', message })\n // Remove the empty assistant placeholder so the error alert is\n // the only visible feedback.\n updateMessages((current) => current.filter((entry) => entry.id !== assistantId), {\n persistWhenUnmounted: true,\n })\n }\n } finally {\n reader.releaseLock()\n if (abortRef.current === controller) {\n abortRef.current = null\n }\n writeSessionStreamState(sessionStorageKey, false)\n setStatus('idle')\n }\n },\n [\n agent,\n apiPath,\n attachmentIds,\n debug,\n effectiveConversationId,\n emitError,\n pageContext,\n sessionStorageKey,\n setStatus,\n updateMessages,\n ],\n )\n\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n const handleSessionUpdate = (event: Event) => {\n const detail = (event as CustomEvent<{ key?: string }>).detail\n if (!detail || detail.key !== sessionStorageKey) return\n const next = readPersistedSession(agent, persistKey)\n if (!next) return\n if (next.conversationId !== effectiveConversationId) return\n updateMessages(next.messages)\n }\n window.addEventListener(SESSION_UPDATED_EVENT, handleSessionUpdate)\n return () => {\n window.removeEventListener(SESSION_UPDATED_EVENT, handleSessionUpdate)\n }\n }, [agent, effectiveConversationId, persistKey, sessionStorageKey, updateMessages])\n\n React.useEffect(() => {\n if (isSessionStreamActive(sessionStorageKey)) {\n setStatusInternal('streaming')\n }\n if (typeof window === 'undefined') return\n const handleStreamState = (event: Event) => {\n const detail = (event as CustomEvent<{ key?: string; active?: boolean }>).detail\n if (!detail || detail.key !== sessionStorageKey) return\n setStatusInternal(detail.active ? 'streaming' : 'idle')\n }\n window.addEventListener(SESSION_STREAM_STATE_EVENT, handleStreamState)\n return () => {\n window.removeEventListener(SESSION_STREAM_STATE_EVENT, handleStreamState)\n }\n }, [sessionStorageKey])\n\n React.useEffect(() => {\n mountedRef.current = true\n return () => {\n // Flush the latest snapshot \u2014 including any in-flight assistant\n // content \u2014 and let the request keep running in the background.\n // Background stream updates continue writing to this same storage slot\n // and notify any reopened chat for the conversation (issue #1816).\n const finalMessages = latestMessagesRef.current\n if (finalMessages.length > 0) {\n persistSnapshot(finalMessages, true)\n }\n mountedRef.current = false\n }\n }, [persistSnapshot])\n\n return {\n messages,\n status,\n error,\n lastRequestDebug,\n lastResponseDebug,\n conversationId: effectiveConversationId,\n sendMessage,\n cancel,\n reset,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { createAiAgentTransport } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/agent-transport'\nimport { apiFetch } from '../backend/utils/api'\n\n/**\n * Chat message shape used by {@link AiChat}. Kept intentionally minimal so the\n * component stays independent of the AI SDK's evolving `UIMessage` type. The\n * dispatcher route (`POST /api/ai_assistant/ai/chat`) accepts exactly this\n * shape for `messages`.\n */\nexport interface AiChatMessageFile {\n name: string\n type: string\n previewUrl?: string\n}\n\nexport interface AiChatToolCallSnapshot {\n id: string\n toolName: string\n state: 'pending' | 'complete' | 'error'\n input?: unknown\n output?: unknown\n errorMessage?: string\n}\n\nexport interface AiChatMessageUiPart {\n componentId: string\n payload?: unknown\n pendingActionId?: string\n /** Stable id used as React key when rendering. */\n key: string\n}\n\nexport interface AiChatMessage {\n id: string\n role: 'user' | 'assistant'\n content: string\n files?: AiChatMessageFile[]\n reasoning?: string\n reasoningStreaming?: boolean\n toolCalls?: AiChatToolCallSnapshot[]\n /**\n * UI parts emitted by the agent during this message's lifecycle. Today\n * the only producer is `prepareMutation` (mutation approval flow):\n * the dispatcher's mutation tool returns an `awaiting-confirmation`\n * envelope, useAiChat parses it and attaches a `mutation-preview-card`\n * part here so AiChat can render the approval card inline. Phase 3\n * WS-C wiring \u2014 without this, the `MutationPreviewCard` registered in\n * the UI-part registry never surfaces.\n */\n uiParts?: AiChatMessageUiPart[]\n}\n\nexport interface UseAiChatInput {\n agent: string\n apiPath?: string\n pageContext?: Record<string, unknown>\n attachmentIds?: string[]\n debug?: boolean\n initialMessages?: Array<Pick<AiChatMessage, 'role' | 'content'>>\n onError?: (err: { code?: string; message: string }) => void\n /**\n * Optional stable conversation id. When provided, the same id is forwarded\n * to the dispatcher on every turn so `prepareMutation`'s idempotency hash\n * (Step 5.6) stays stable across mutation preview / confirm / retry cycles.\n * When omitted, the hook mints a fresh random id once on mount and reuses\n * it for the lifetime of the component \u2014 callers can still override via\n * props at any time to reset the conversation.\n */\n conversationId?: string\n /**\n * Runtime provider override (4b.2). Forwarded as `?provider=` query param\n * on every POST to the dispatcher. Undefined/null means use the agent's\n * configured default (no override sent).\n */\n providerOverride?: string | null\n /**\n * Runtime model override (4b.2). Forwarded as `?model=` query param\n * on every POST to the dispatcher. Undefined/null means use the agent's\n * configured default (no override sent).\n */\n modelOverride?: string | null\n}\n\nexport interface AiChatErrorEnvelope {\n code?: string\n message: string\n}\n\nexport interface UseAiChatResult {\n messages: AiChatMessage[]\n status: 'idle' | 'submitting' | 'streaming'\n error: AiChatErrorEnvelope | null\n lastRequestDebug: { url: string; body: unknown } | null\n lastResponseDebug: { status: number; text: string } | null\n /**\n * The conversation id currently in use for this chat instance. Equal to\n * the caller-provided `conversationId` input when one is supplied;\n * otherwise the random id minted on mount. Stable across re-renders for a\n * given mount (Phase 3 WS-D contract with `prepareMutation`).\n */\n conversationId: string\n sendMessage: (input: string, files?: AiChatMessageFile[]) => Promise<void>\n cancel: () => void\n reset: () => void\n}\n\nfunction makeMessageId(): string {\n const random = Math.random().toString(36).slice(2, 10)\n const time = Date.now().toString(36)\n return `msg_${time}_${random}`\n}\n\nfunction makeConversationId(): string {\n // Use crypto.randomUUID() when the browser exposes it (all evergreen\n // runtimes do), otherwise fall back to a low-entropy token that is still\n // unique enough for the idempotency-hash use case.\n const g = globalThis as unknown as { crypto?: { randomUUID?: () => string } }\n if (g.crypto && typeof g.crypto.randomUUID === 'function') {\n try {\n return g.crypto.randomUUID()\n } catch {\n // fall through to the random fallback\n }\n }\n const rand = () => Math.random().toString(16).slice(2, 10)\n return `conv_${Date.now().toString(16)}_${rand()}${rand()}`\n}\n\nconst SESSION_STORAGE_PREFIX = 'om-ai-chat:'\nconst SESSION_STORAGE_VERSION = 1\nconst SESSION_UPDATED_EVENT = 'om-ai-chat-session-updated'\nconst SESSION_STREAM_STATE_EVENT = 'om-ai-chat-session-stream-state'\nconst activeSessionStreams = new Set<string>()\n\ninterface PersistedAiChatSession {\n v: number\n conversationId: string\n messages: AiChatMessage[]\n}\n\nfunction getSessionStorageKey(agent: string, conversationId?: string | null): string {\n // When the caller pins a `conversationId` (e.g. via the AiChatSessions\n // provider's tabs), namespace the persisted slot per session so multiple\n // open conversations for the same agent don't overwrite each other. The\n // legacy single-session-per-agent layout (no externally-supplied id) is\n // kept for backward compatibility with code that still relies on it.\n if (typeof conversationId === 'string' && conversationId.length > 0) {\n return `${SESSION_STORAGE_PREFIX}${agent}:${conversationId}`\n }\n return `${SESSION_STORAGE_PREFIX}${agent}`\n}\n\nfunction writeSessionStreamState(storageKey: string, active: boolean): void {\n if (active) {\n activeSessionStreams.add(storageKey)\n } else {\n activeSessionStreams.delete(storageKey)\n }\n if (typeof window === 'undefined') return\n try {\n window.dispatchEvent(\n new CustomEvent(SESSION_STREAM_STATE_EVENT, {\n detail: { key: storageKey, active },\n }),\n )\n } catch {\n // ignore event dispatch failures in non-browser test environments\n }\n}\n\nfunction isSessionStreamActive(storageKey: string): boolean {\n return activeSessionStreams.has(storageKey)\n}\n\nfunction readPersistedSession(\n agent: string,\n conversationId?: string | null,\n): PersistedAiChatSession | null {\n if (typeof window === 'undefined') return null\n try {\n const raw = window.localStorage.getItem(getSessionStorageKey(agent, conversationId))\n if (!raw) return null\n const parsed = JSON.parse(raw) as PersistedAiChatSession | null\n if (!parsed || parsed.v !== SESSION_STORAGE_VERSION) return null\n if (typeof parsed.conversationId !== 'string') return null\n if (!Array.isArray(parsed.messages)) return null\n const messages = parsed.messages.filter((entry): entry is AiChatMessage => {\n return (\n !!entry &&\n typeof entry === 'object' &&\n typeof (entry as AiChatMessage).id === 'string' &&\n typeof (entry as AiChatMessage).content === 'string' &&\n ((entry as AiChatMessage).role === 'user' || (entry as AiChatMessage).role === 'assistant')\n )\n })\n return { v: SESSION_STORAGE_VERSION, conversationId: parsed.conversationId, messages }\n } catch {\n return null\n }\n}\n\nfunction writePersistedSession(\n agent: string,\n session: PersistedAiChatSession,\n conversationId?: string | null,\n options?: { notify?: boolean },\n): void {\n if (typeof window === 'undefined') return\n try {\n // Strip transient blob/object preview URLs before persisting (they would\n // not survive a reload). Self-contained `data:` URLs are kept so image\n // previews come back unchanged after the chat is reopened \u2014 public\n // attachment URLs are intentionally not used because the LLM provider\n // cannot reach a localhost origin and we want a single durable shape\n // that works for both transport and reload.\n const messages = session.messages.map((message) => {\n if (!message.files || message.files.length === 0) return message\n const safeFiles = message.files.map(({ name, type, previewUrl }) => {\n const durable =\n typeof previewUrl === 'string' && previewUrl.startsWith('data:')\n ? previewUrl\n : undefined\n return durable ? { name, type, previewUrl: durable } : { name, type }\n })\n return { ...message, files: safeFiles }\n })\n const storageKey = getSessionStorageKey(agent, conversationId)\n window.localStorage.setItem(storageKey, JSON.stringify({ ...session, messages }))\n if (options?.notify) {\n window.dispatchEvent(\n new CustomEvent(SESSION_UPDATED_EVENT, {\n detail: { key: storageKey, agent, conversationId: session.conversationId },\n }),\n )\n }\n } catch {\n // Quota exceeded / privacy mode \u2014 silently drop persistence.\n }\n}\n\nfunction clearPersistedSession(agent: string, conversationId?: string | null): void {\n if (typeof window === 'undefined') return\n try {\n window.localStorage.removeItem(getSessionStorageKey(agent, conversationId))\n } catch {\n // ignore\n }\n}\n\nfunction getTransportEndpoint(\n agent: string,\n apiPath?: string,\n providerOverride?: string | null,\n modelOverride?: string | null,\n): string {\n // Reuse the transport factory so UI consumers share the dispatcher URL\n // convention with server-side callers (e.g. runAiAgentText / Playwright\n // fixtures). The factory returns a ChatTransport<UI_MESSAGE> whose internal\n // endpoint we do not directly read \u2014 instead we reconstruct the same URL\n // shape here so downstream error handling stays deterministic.\n //\n // When the AI SDK exposes a public endpoint getter (or the stream format\n // switches from plain text to UIMessageChunk) we can call\n // transport.sendMessages(...) directly.\n const transport = createAiAgentTransport({ agentId: agent, endpoint: apiPath })\n void transport\n const base = apiPath && apiPath.length > 0 ? apiPath : '/api/ai_assistant/ai/chat'\n const separator = base.includes('?') ? '&' : '?'\n let url = `${base}${separator}agent=${encodeURIComponent(agent)}`\n if (providerOverride) {\n url += `&provider=${encodeURIComponent(providerOverride)}`\n }\n if (modelOverride) {\n url += `&model=${encodeURIComponent(modelOverride)}`\n }\n return url\n}\n\ninterface AssistantBuilderState {\n text: string\n reasoning: string\n reasoningStreaming: boolean\n toolCalls: AiChatToolCallSnapshot[]\n uiParts: AiChatMessageUiPart[]\n}\n\nfunction createBuilder(): AssistantBuilderState {\n return { text: '', reasoning: '', reasoningStreaming: false, toolCalls: [], uiParts: [] }\n}\n\n/**\n * Generic extractor for UI parts emitted by tool outputs. A tool can\n * surface inline UI to the chat by returning JSON in any of these\n * shapes \u2014 each tool call produces zero or more UI parts:\n *\n * 1. The dispatcher's mutation envelope:\n * `{ status: 'awaiting-confirmation', pendingActionId, expiresAt,\n * agent, toolName, message }`\n * \u2192 synthesizes a `mutation-preview-card` part (the registered\n * card fetches the live diff via `useAiPendingActionPolling`).\n *\n * 2. A single explicit UI part:\n * `{ uiPart: { componentId, payload?, pendingActionId? } }`\n *\n * 3. Multiple explicit UI parts:\n * `{ uiParts: [{ componentId, payload? }, ...] }`\n *\n * Tool authors only need to JSON-encode an object whose `uiPart` /\n * `uiParts` reference component ids that the host has registered on\n * `defaultAiUiPartRegistry` (or a scoped registry passed through\n * `<AiChat registry={...}/>`). Unknown component ids fall back to the\n * `UnknownUiPartPlaceholder` so an unregistered id never blows up the\n * transcript.\n */\nfunction extractUiPartsFromOutput(\n output: unknown,\n toolCallId: string,\n): AiChatMessageUiPart[] {\n let parsed: unknown = output\n if (typeof output === 'string') {\n const trimmed = output.trim()\n if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return []\n try {\n parsed = JSON.parse(trimmed)\n } catch {\n return []\n }\n }\n if (!parsed || typeof parsed !== 'object') return []\n const value = parsed as Record<string, unknown>\n const parts: AiChatMessageUiPart[] = []\n\n // (1) Mutation approval envelope. The dispatcher's `prepareMutation`\n // interceptor in `agent-tools.ts` formats the result via\n // `formatPendingActionToolResult` as\n // { status: 'pending-confirmation', agentId, toolName, pendingActionId,\n // expiresAt, message }\n // (NOTE: status is `pending-confirmation` and the field is `agentId`,\n // not `agent`). We also accept `awaiting-confirmation` / `agent` for\n // forward compat with older / alternative dispatchers.\n if (value.status === 'pending-confirmation' || value.status === 'awaiting-confirmation') {\n const pendingActionId =\n typeof value.pendingActionId === 'string' && value.pendingActionId.length > 0\n ? value.pendingActionId\n : null\n if (pendingActionId) {\n const agentId =\n typeof value.agentId === 'string'\n ? value.agentId\n : typeof value.agent === 'string'\n ? value.agent\n : undefined\n parts.push({\n componentId: 'mutation-preview-card',\n pendingActionId,\n payload: {\n pendingActionId,\n expiresAt: typeof value.expiresAt === 'string' ? value.expiresAt : undefined,\n agentId,\n toolName: typeof value.toolName === 'string' ? value.toolName : undefined,\n },\n key: `${toolCallId}:mutation-preview-card`,\n })\n }\n }\n\n // (2) Explicit single UI part.\n if (value.uiPart && typeof value.uiPart === 'object') {\n const part = value.uiPart as Record<string, unknown>\n if (typeof part.componentId === 'string' && part.componentId.length > 0) {\n parts.push({\n componentId: part.componentId,\n payload: part.payload,\n pendingActionId:\n typeof part.pendingActionId === 'string' ? part.pendingActionId : undefined,\n key: `${toolCallId}:${part.componentId}`,\n })\n }\n }\n\n // (3) Explicit list of UI parts.\n if (Array.isArray(value.uiParts)) {\n value.uiParts.forEach((entry, index) => {\n if (!entry || typeof entry !== 'object') return\n const part = entry as Record<string, unknown>\n if (typeof part.componentId !== 'string' || part.componentId.length === 0) return\n parts.push({\n componentId: part.componentId,\n payload: part.payload,\n pendingActionId:\n typeof part.pendingActionId === 'string' ? part.pendingActionId : undefined,\n key: `${toolCallId}:${index}:${part.componentId}`,\n })\n })\n }\n\n return parts\n}\n\nfunction updateToolCall(\n state: AssistantBuilderState,\n id: string,\n patch: Partial<AiChatToolCallSnapshot> & { toolName?: string },\n): AssistantBuilderState {\n if (!id) return state\n const idx = state.toolCalls.findIndex((entry) => entry.id === id)\n if (idx === -1) {\n const next: AiChatToolCallSnapshot = {\n id,\n toolName: patch.toolName ?? 'tool',\n state: patch.state ?? 'pending',\n input: patch.input,\n output: patch.output,\n errorMessage: patch.errorMessage,\n }\n return { ...state, toolCalls: [...state.toolCalls, next] }\n }\n const current = state.toolCalls[idx]\n const merged: AiChatToolCallSnapshot = {\n ...current,\n toolName: patch.toolName ?? current.toolName,\n state: patch.state ?? current.state,\n input: patch.input !== undefined ? patch.input : current.input,\n output: patch.output !== undefined ? patch.output : current.output,\n errorMessage: patch.errorMessage ?? current.errorMessage,\n }\n const nextCalls = state.toolCalls.slice()\n nextCalls[idx] = merged\n return { ...state, toolCalls: nextCalls }\n}\n\nfunction applyChunk(\n state: AssistantBuilderState,\n chunk: { type: string; [key: string]: unknown },\n): AssistantBuilderState {\n switch (chunk.type) {\n case 'text-delta':\n return {\n ...state,\n text: state.text + (typeof chunk.delta === 'string' ? chunk.delta : ''),\n }\n case 'reasoning-start':\n return { ...state, reasoningStreaming: true }\n case 'reasoning-delta':\n return {\n ...state,\n reasoning:\n state.reasoning + (typeof chunk.delta === 'string' ? chunk.delta : ''),\n reasoningStreaming: true,\n }\n case 'reasoning-end':\n return { ...state, reasoningStreaming: false }\n case 'tool-input-start':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n toolName: typeof chunk.toolName === 'string' ? chunk.toolName : undefined,\n state: 'pending',\n })\n case 'tool-input-available':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n toolName: typeof chunk.toolName === 'string' ? chunk.toolName : undefined,\n input: chunk.input,\n state: 'pending',\n })\n case 'tool-output-available': {\n const toolCallId = String(chunk.toolCallId ?? '')\n const next = updateToolCall(state, toolCallId, {\n output: chunk.output,\n state: 'complete',\n })\n // Phase 3 WS-C \u2014 surface ANY UI parts the tool output advertises:\n // the legacy `awaiting-confirmation` mutation envelope plus the\n // generic `{ uiPart }` / `{ uiParts: [...] }` shapes. This lets\n // module authors define their own dynamic cards (stats panels,\n // record summaries, charts\u2026) without touching the dispatcher or\n // the chat client.\n const newParts = extractUiPartsFromOutput(chunk.output, toolCallId)\n if (newParts.length === 0) return next\n const seen = new Set(next.uiParts.map((entry) => entry.key))\n const merged = [...next.uiParts]\n for (const part of newParts) {\n if (seen.has(part.key)) continue\n seen.add(part.key)\n merged.push(part)\n }\n if (merged.length === next.uiParts.length) return next\n return { ...next, uiParts: merged }\n }\n case 'tool-output-error':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n state: 'error',\n errorMessage:\n typeof chunk.errorText === 'string' ? chunk.errorText : 'Tool error',\n })\n case 'tool-input-error':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n toolName: typeof chunk.toolName === 'string' ? chunk.toolName : undefined,\n input: chunk.input,\n state: 'error',\n errorMessage:\n typeof chunk.errorText === 'string' ? chunk.errorText : 'Tool error',\n })\n default:\n return state\n }\n}\n\nfunction mergeAssistantMessage(\n current: AiChatMessage,\n state: AssistantBuilderState,\n): AiChatMessage {\n return {\n ...current,\n content: state.text,\n reasoning: state.reasoning ? state.reasoning : undefined,\n reasoningStreaming: state.reasoning ? state.reasoningStreaming : undefined,\n toolCalls: state.toolCalls.length > 0 ? state.toolCalls : undefined,\n uiParts: state.uiParts.length > 0 ? state.uiParts : undefined,\n }\n}\n\nfunction parseSseLines(buffer: string): { events: string[]; rest: string } {\n const events: string[] = []\n let rest = buffer\n for (;;) {\n const idx = rest.indexOf('\\n\\n')\n if (idx === -1) break\n events.push(rest.slice(0, idx))\n rest = rest.slice(idx + 2)\n }\n return { events, rest }\n}\n\nfunction extractDataPayload(eventBlock: string): string | null {\n const lines = eventBlock.split('\\n')\n const dataLines: string[] = []\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n dataLines.push(line.slice(6))\n } else if (line.startsWith('data:')) {\n dataLines.push(line.slice(5))\n }\n }\n if (dataLines.length === 0) return null\n return dataLines.join('\\n')\n}\n\nasync function readErrorEnvelope(response: Response): Promise<AiChatErrorEnvelope> {\n try {\n const data = (await response.clone().json()) as\n | { error?: unknown; code?: unknown; message?: unknown }\n | null\n if (data && typeof data === 'object') {\n const rawMessage =\n (typeof data.error === 'string' && data.error) ||\n (typeof data.message === 'string' && data.message) ||\n ''\n const rawCode = typeof data.code === 'string' ? data.code : undefined\n if (rawMessage || rawCode) {\n return {\n code: rawCode,\n message: rawMessage || 'Agent dispatch failed.',\n }\n }\n }\n } catch {\n // Fall through to text fallback\n }\n const text = await response.text().catch(() => '')\n return { message: text || `Agent dispatch failed (${response.status}).` }\n}\n\nexport function useAiChat(input: UseAiChatInput): UseAiChatResult {\n const { agent, apiPath, pageContext, attachmentIds, debug, initialMessages, onError, conversationId: conversationIdInput, providerOverride, modelOverride } = input\n\n // Minted once on mount when the caller does not supply a conversationId.\n // The ref keeps the id stable across re-renders and is reused for every\n // turn so the Phase 3 WS-C `prepareMutation` idempotency hash stays\n // stable within the same chat. When the agent has a persisted session in\n // localStorage we re-hydrate the conversationId from it so re-opening the\n // chat continues the previous turn instead of starting fresh.\n const persistedRef = React.useRef<PersistedAiChatSession | null | 'unread'>('unread')\n if (persistedRef.current === 'unread') {\n // When the caller pins a `conversationId` (multi-tab session mode) we\n // read ONLY from that per-conversation slot. Falling back to the\n // legacy agent-only slot here would make every brand-new tab inherit\n // the previous tab's messages \u2014 the \"+ shows the same chat\" bug \u2014 so\n // unknown conversationIds always start clean. Without a pinned id we\n // keep the legacy single-session-per-agent layout for backward\n // compatibility.\n persistedRef.current =\n typeof conversationIdInput === 'string' && conversationIdInput.length > 0\n ? readPersistedSession(agent, conversationIdInput)\n : readPersistedSession(agent)\n }\n const persisted = persistedRef.current\n\n const mintedConversationIdRef = React.useRef<string | null>(null)\n if (mintedConversationIdRef.current === null) {\n mintedConversationIdRef.current = persisted?.conversationId ?? makeConversationId()\n }\n const effectiveConversationId =\n typeof conversationIdInput === 'string' && conversationIdInput.length > 0\n ? conversationIdInput\n : mintedConversationIdRef.current\n const persistKey =\n typeof conversationIdInput === 'string' && conversationIdInput.length > 0\n ? conversationIdInput\n : null\n const sessionStorageKey = getSessionStorageKey(agent, persistKey)\n\n const [messages, setMessages] = React.useState<AiChatMessage[]>(() => {\n if (persisted && persisted.messages.length > 0) {\n return persisted.messages\n }\n return (initialMessages ?? []).map((entry) => ({\n id: makeMessageId(),\n role: entry.role,\n content: entry.content,\n }))\n })\n\n // Persist messages + conversationId on every change. Skip during in-flight\n // streaming so we do not write the same growing string on every chunk \u2014\n // the next idle tick captures the final assistant content.\n const [status, setStatusInternal] = React.useState<'idle' | 'submitting' | 'streaming'>(() =>\n isSessionStreamActive(sessionStorageKey) ? 'streaming' : 'idle',\n )\n // Refs mirror the latest persist state so the unmount cleanup can flush\n // an in-flight assistant message to localStorage even though the streaming\n // skip above (and React's stale-closure semantics) would otherwise lose it.\n // Without this, closing the dock or switching agents while the assistant\n // is \"Thinking\" abandons the partial reply (issue #1816).\n const latestMessagesRef = React.useRef<AiChatMessage[]>(messages)\n const persistKeyRef = React.useRef<string | null>(null)\n const agentRef = React.useRef<string>(agent)\n const effectiveConversationIdRef = React.useRef<string>(effectiveConversationId)\n const sessionStorageKeyRef = React.useRef<string>(sessionStorageKey)\n const mountedRef = React.useRef(true)\n React.useEffect(() => {\n latestMessagesRef.current = messages\n }, [messages])\n React.useEffect(() => {\n persistKeyRef.current =\n typeof conversationIdInput === 'string' && conversationIdInput.length > 0\n ? conversationIdInput\n : null\n }, [conversationIdInput])\n React.useEffect(() => {\n agentRef.current = agent\n }, [agent])\n React.useEffect(() => {\n effectiveConversationIdRef.current = effectiveConversationId\n }, [effectiveConversationId])\n React.useEffect(() => {\n sessionStorageKeyRef.current = sessionStorageKey\n }, [sessionStorageKey])\n React.useEffect(() => {\n if (status !== 'idle') return\n if (messages.length === 0) {\n clearPersistedSession(agent, persistKey)\n return\n }\n writePersistedSession(\n agent,\n {\n v: SESSION_STORAGE_VERSION,\n conversationId: effectiveConversationId,\n messages,\n },\n persistKey,\n )\n }, [agent, effectiveConversationId, messages, persistKey, status])\n const persistSnapshot = React.useCallback((snapshot: AiChatMessage[], notify = false) => {\n if (snapshot.length === 0) return\n writePersistedSession(\n agentRef.current,\n {\n v: SESSION_STORAGE_VERSION,\n conversationId: effectiveConversationIdRef.current,\n messages: snapshot,\n },\n persistKeyRef.current,\n { notify },\n )\n }, [])\n\n const updateMessages = React.useCallback(\n (\n updater:\n | AiChatMessage[]\n | ((current: AiChatMessage[]) => AiChatMessage[]),\n options?: { persistWhenUnmounted?: boolean },\n ) => {\n const current = latestMessagesRef.current\n const next = typeof updater === 'function' ? updater(current) : updater\n latestMessagesRef.current = next\n if (mountedRef.current) {\n setMessages(next)\n } else if (options?.persistWhenUnmounted) {\n persistSnapshot(next, true)\n }\n },\n [persistSnapshot],\n )\n\n const setStatus = React.useCallback((next: 'idle' | 'submitting' | 'streaming') => {\n if (mountedRef.current) {\n setStatusInternal(next)\n }\n }, [])\n const [error, setError] = React.useState<AiChatErrorEnvelope | null>(null)\n const [lastRequestDebug, setLastRequestDebug] = React.useState<\n { url: string; body: unknown } | null\n >(null)\n const [lastResponseDebug, setLastResponseDebug] = React.useState<\n { status: number; text: string } | null\n >(null)\n\n const abortRef = React.useRef<AbortController | null>(null)\n const onErrorRef = React.useRef(onError)\n React.useEffect(() => {\n onErrorRef.current = onError\n }, [onError])\n\n const emitError = React.useCallback((envelope: AiChatErrorEnvelope) => {\n if (mountedRef.current) {\n setError(envelope)\n try {\n onErrorRef.current?.(envelope)\n } catch {\n // UI layer must never throw because a caller-supplied error handler\n // misbehaved.\n }\n }\n }, [])\n\n const cancel = React.useCallback(() => {\n if (abortRef.current) {\n abortRef.current.abort()\n abortRef.current = null\n }\n writeSessionStreamState(sessionStorageKeyRef.current, false)\n setStatus('idle')\n }, [setStatus])\n\n const reset = React.useCallback(() => {\n cancel()\n updateMessages([])\n setError(null)\n setLastRequestDebug(null)\n setLastResponseDebug(null)\n clearPersistedSession(agent, persistKey)\n mintedConversationIdRef.current = makeConversationId()\n }, [agent, cancel, persistKey, updateMessages])\n\n const sendMessage = React.useCallback(\n async (textInput: string, files?: AiChatMessageFile[]) => {\n const trimmed = textInput.trim()\n if (!trimmed) return\n if (abortRef.current) {\n abortRef.current.abort()\n }\n\n setError(null)\n const userMessage: AiChatMessage = {\n id: makeMessageId(),\n role: 'user',\n content: trimmed,\n files: files && files.length > 0 ? files : undefined,\n }\n const assistantMessage: AiChatMessage = {\n id: makeMessageId(),\n role: 'assistant',\n content: '',\n }\n const assistantId = assistantMessage.id\n // Snapshot prior messages for request payload so the dispatcher sees the\n // full turn history including the just-added user message.\n const outgoingHistory = [...latestMessagesRef.current, userMessage]\n updateMessages([...outgoingHistory, assistantMessage], { persistWhenUnmounted: true })\n setStatus('submitting')\n writeSessionStreamState(sessionStorageKey, true)\n\n const controller = new AbortController()\n abortRef.current = controller\n\n const url = getTransportEndpoint(agent, apiPath, providerOverride, modelOverride)\n const body = {\n messages: outgoingHistory.map((message) => ({\n role: message.role,\n content: message.content,\n })),\n pageContext,\n attachmentIds,\n debug,\n conversationId: effectiveConversationId,\n }\n if (mountedRef.current) {\n setLastRequestDebug({ url, body })\n }\n\n let response: Response\n try {\n response = await apiFetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'text/event-stream, text/plain, application/json',\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n })\n } catch (requestError) {\n if ((requestError as { name?: string })?.name === 'AbortError') {\n writeSessionStreamState(sessionStorageKey, false)\n setStatus('idle')\n abortRef.current = null\n return\n }\n const message =\n requestError instanceof Error\n ? requestError.message\n : 'Network request failed.'\n emitError({ message })\n writeSessionStreamState(sessionStorageKey, false)\n setStatus('idle')\n abortRef.current = null\n return\n }\n\n if (!response.ok) {\n const envelope = await readErrorEnvelope(response)\n if (mountedRef.current) {\n setLastResponseDebug({ status: response.status, text: envelope.message })\n }\n emitError(envelope)\n writeSessionStreamState(sessionStorageKey, false)\n setStatus('idle')\n updateMessages((current) => current.filter((entry) => entry.id !== assistantId), {\n persistWhenUnmounted: true,\n })\n abortRef.current = null\n return\n }\n\n const bodyStream = response.body\n if (!bodyStream) {\n if (mountedRef.current) {\n setLastResponseDebug({ status: response.status, text: '' })\n }\n writeSessionStreamState(sessionStorageKey, false)\n setStatus('idle')\n abortRef.current = null\n return\n }\n\n const headerGet = (name: string): string | null => {\n const headers = (response as { headers?: { get?: (k: string) => string | null } })\n .headers\n if (!headers || typeof headers.get !== 'function') return null\n try {\n return headers.get(name)\n } catch {\n return null\n }\n }\n const isUiMessageStream =\n headerGet('x-vercel-ai-ui-message-stream') !== null ||\n (headerGet('content-type') ?? '').includes('event-stream')\n\n setStatus('streaming')\n const reader = bodyStream.getReader()\n const decoder = new TextDecoder()\n let streamedRaw = ''\n let builder = createBuilder()\n let sseBuffer = ''\n const flushUiMessageBuffer = (extra?: string) => {\n if (extra) sseBuffer += extra\n const { events, rest } = parseSseLines(sseBuffer)\n sseBuffer = rest\n for (const block of events) {\n const data = extractDataPayload(block)\n if (!data) continue\n if (data === '[DONE]') continue\n try {\n const parsed = JSON.parse(data) as { type?: string }\n if (parsed && typeof parsed.type === 'string') {\n builder = applyChunk(builder, parsed as { type: string })\n }\n } catch {\n // Tolerate malformed events / SSE comments.\n }\n }\n }\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n if (!value) continue\n const piece = decoder.decode(value, { stream: true })\n if (!piece) continue\n streamedRaw += piece\n\n if (isUiMessageStream) {\n flushUiMessageBuffer(piece)\n } else {\n // Plain text fallback (legacy `toTextStreamResponse`).\n builder = { ...builder, text: streamedRaw }\n }\n const snapshotBuilder = builder\n updateMessages(\n (current) =>\n current.map((entry) =>\n entry.id === assistantId\n ? mergeAssistantMessage(entry, snapshotBuilder)\n : entry,\n ),\n { persistWhenUnmounted: true },\n )\n }\n const tail = decoder.decode()\n if (tail) {\n streamedRaw += tail\n if (isUiMessageStream) {\n flushUiMessageBuffer(tail)\n } else {\n builder = { ...builder, text: streamedRaw }\n }\n }\n if (isUiMessageStream && sseBuffer.length > 0) {\n flushUiMessageBuffer('\\n\\n')\n }\n builder = { ...builder, reasoningStreaming: false }\n const finalSnapshot = builder\n updateMessages(\n (current) =>\n current.map((entry) =>\n entry.id === assistantId\n ? mergeAssistantMessage(entry, finalSnapshot)\n : entry,\n ),\n { persistWhenUnmounted: true },\n )\n if (mountedRef.current) {\n setLastResponseDebug({ status: response.status, text: streamedRaw })\n }\n const isEmpty =\n !builder.text.trim() && builder.toolCalls.length === 0 && !builder.reasoning\n if (isEmpty) {\n emitError({\n code: 'empty_response',\n message:\n 'The AI agent returned an empty response. This usually means the LLM provider rejected the request (invalid API key, rate limit, or model error). Check your server logs for details.',\n })\n updateMessages((current) => current.filter((entry) => entry.id !== assistantId), {\n persistWhenUnmounted: true,\n })\n }\n } catch (streamError) {\n if ((streamError as { name?: string })?.name === 'AbortError') {\n // Cancelled by the user \u2014 keep whatever we have so far and exit\n // quietly.\n } else {\n const rawMessage =\n streamError instanceof Error\n ? streamError.message\n : 'Stream interrupted.'\n // LLM provider errors (auth failures, rate limits, invalid tool\n // schemas) surface as stream read errors. Include a hint so the\n // operator can check server logs for the full stack trace.\n const message = rawMessage.includes('API')\n ? rawMessage\n : `${rawMessage} \u2014 check server logs for LLM provider details.`\n emitError({ code: 'stream_error', message })\n // Remove the empty assistant placeholder so the error alert is\n // the only visible feedback.\n updateMessages((current) => current.filter((entry) => entry.id !== assistantId), {\n persistWhenUnmounted: true,\n })\n }\n } finally {\n reader.releaseLock()\n if (abortRef.current === controller) {\n abortRef.current = null\n }\n writeSessionStreamState(sessionStorageKey, false)\n setStatus('idle')\n }\n },\n [agent, apiPath, attachmentIds, debug, effectiveConversationId, emitError, messages, modelOverride, pageContext, providerOverride],\n )\n\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n const handleSessionUpdate = (event: Event) => {\n const detail = (event as CustomEvent<{ key?: string }>).detail\n if (!detail || detail.key !== sessionStorageKey) return\n const next = readPersistedSession(agent, persistKey)\n if (!next) return\n if (next.conversationId !== effectiveConversationId) return\n updateMessages(next.messages)\n }\n window.addEventListener(SESSION_UPDATED_EVENT, handleSessionUpdate)\n return () => {\n window.removeEventListener(SESSION_UPDATED_EVENT, handleSessionUpdate)\n }\n }, [agent, effectiveConversationId, persistKey, sessionStorageKey, updateMessages])\n\n React.useEffect(() => {\n if (isSessionStreamActive(sessionStorageKey)) {\n setStatusInternal('streaming')\n }\n if (typeof window === 'undefined') return\n const handleStreamState = (event: Event) => {\n const detail = (event as CustomEvent<{ key?: string; active?: boolean }>).detail\n if (!detail || detail.key !== sessionStorageKey) return\n setStatusInternal(detail.active ? 'streaming' : 'idle')\n }\n window.addEventListener(SESSION_STREAM_STATE_EVENT, handleStreamState)\n return () => {\n window.removeEventListener(SESSION_STREAM_STATE_EVENT, handleStreamState)\n }\n }, [sessionStorageKey])\n\n React.useEffect(() => {\n mountedRef.current = true\n return () => {\n // Flush the latest snapshot \u2014 including any in-flight assistant\n // content \u2014 and let the request keep running in the background.\n // Background stream updates continue writing to this same storage slot\n // and notify any reopened chat for the conversation (issue #1816).\n const finalMessages = latestMessagesRef.current\n if (finalMessages.length > 0) {\n persistSnapshot(finalMessages, true)\n }\n mountedRef.current = false\n }\n }, [persistSnapshot])\n\n return {\n messages,\n status,\n error,\n lastRequestDebug,\n lastResponseDebug,\n conversationId: effectiveConversationId,\n sendMessage,\n cancel,\n reset,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }