@open-mercato/ui 0.6.1-develop.3291.1.6fad645fd0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +2 -0
- package/dist/ai/AiAssistantLauncher.js +8 -8
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChat.js +92 -93
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiIcon.js +17 -0
- package/dist/ai/AiIcon.js.map +7 -0
- package/dist/ai/LoopDisabledBanner.js +28 -0
- package/dist/ai/LoopDisabledBanner.js.map +7 -0
- package/dist/ai/LoopTracePanel.js +123 -0
- package/dist/ai/LoopTracePanel.js.map +7 -0
- package/dist/ai/ModelPicker.js.map +1 -1
- package/dist/ai/index.js +8 -0
- package/dist/ai/index.js.map +2 -2
- package/dist/ai/parts/ConfirmationCard.js +1 -5
- package/dist/ai/parts/ConfirmationCard.js.map +2 -2
- package/dist/ai/parts/FieldDiffCard.js +8 -15
- package/dist/ai/parts/FieldDiffCard.js.map +2 -2
- package/dist/ai/parts/MutationResultCard.js +1 -4
- package/dist/ai/parts/MutationResultCard.js.map +2 -2
- package/dist/ai/ui-parts/pending-phase3-placeholder.js +0 -2
- package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +2 -2
- package/dist/ai/useAiChat.js +18 -4
- package/dist/ai/useAiChat.js.map +2 -2
- package/dist/backend/CrudForm.js +24 -2
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +2 -2
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +102 -96
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/detail/CustomDataSection.js +22 -3
- package/dist/backend/detail/CustomDataSection.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterBuilder.js +2 -2
- package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
- package/dist/backend/injection/ComponentOverrideProvider.js +1 -1
- package/dist/backend/injection/ComponentOverrideProvider.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +25 -7
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/progress/useProgressPoll.js +58 -3
- package/dist/backend/progress/useProgressPoll.js.map +2 -2
- package/dist/backend/progress/useProgressSse.js +15 -2
- package/dist/backend/progress/useProgressSse.js.map +2 -2
- package/dist/backend/utils/bulkDelete.js +76 -0
- package/dist/backend/utils/bulkDelete.js.map +2 -2
- package/package.json +11 -6
- package/src/ai/AiAssistantLauncher.tsx +8 -8
- package/src/ai/AiChat.tsx +44 -38
- package/src/ai/AiIcon.tsx +12 -0
- package/src/ai/LoopDisabledBanner.tsx +38 -0
- package/src/ai/LoopTracePanel.tsx +184 -0
- package/src/ai/ModelPicker.tsx +1 -1
- package/src/ai/__tests__/AiChat.test.tsx +174 -5
- package/src/ai/index.ts +8 -0
- package/src/ai/parts/ConfirmationCard.tsx +1 -5
- package/src/ai/parts/FieldDiffCard.tsx +0 -3
- package/src/ai/parts/MutationResultCard.tsx +1 -4
- package/src/ai/parts/__tests__/MutationResultCard.test.tsx +5 -0
- package/src/ai/ui-parts/pending-phase3-placeholder.tsx +0 -2
- package/src/ai/useAiChat.ts +35 -5
- package/src/backend/CrudForm.tsx +30 -3
- package/src/backend/DataTable.tsx +2 -2
- package/src/backend/__tests__/CrudForm.custom-fields.test.tsx +72 -0
- package/src/backend/__tests__/CustomDataSection.test.tsx +76 -0
- package/src/backend/__tests__/DataTable.render.test.tsx +36 -0
- package/src/backend/__tests__/component-replacement.test.tsx +44 -0
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +101 -91
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +19 -0
- package/src/backend/detail/CustomDataSection.tsx +26 -3
- package/src/backend/filters/AdvancedFilterBuilder.tsx +6 -6
- package/src/backend/injection/ComponentOverrideProvider.tsx +2 -1
- package/src/backend/inputs/ComboboxInput.tsx +29 -7
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +33 -2
- package/src/backend/progress/useProgressPoll.ts +68 -3
- package/src/backend/progress/useProgressSse.ts +16 -2
- package/src/backend/utils/__tests__/bulkDelete.test.ts +42 -0
- package/src/backend/utils/bulkDelete.ts +93 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/ai/parts/MutationResultCard.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { AlertTriangle, CheckCircle2, Wand2, XCircle } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Alert, AlertDescription, AlertTitle } from '../../primitives/alert'\nimport { Button } from '../../primitives/button'\nimport type { AiUiPartProps } from '../ui-part-registry'\nimport { useAiPendingActionPolling } from './useAiPendingActionPolling'\nimport type { AiPendingActionCardAction, AiPendingActionCardStatus } from './types'\n\n/** Custom DOM event the failure card dispatches when the operator clicks\n * \"Fix with AI\". `<AiChat>` listens for this and sends a follow-up user\n * message asking the agent to diagnose and retry the failed call. */\nexport const AI_CHAT_FIX_REQUEST_EVENT = 'om-ai-chat-fix-request'\n\nexport interface AiChatFixRequestDetail {\n message: string\n toolName?: string\n pendingActionId?: string\n}\n\nfunction dispatchFixRequest(detail: AiChatFixRequestDetail): void {\n if (typeof window === 'undefined') return\n window.dispatchEvent(new CustomEvent<AiChatFixRequestDetail>(AI_CHAT_FIX_REQUEST_EVENT, { detail }))\n}\n\n/**\n * Terminal-state card that renders the `executionResult` of a pending\n * action. Success \u2192 `Alert variant=\"success\"` with a record link; partial\n * success (batch `failedRecords[]`) \u2192 `variant=\"warning\"` with the list;\n * failure \u2192 `variant=\"destructive\"` with the error code + message.\n *\n * Reads the pending action via the shared polling hook so page reloads\n * still recover state. The hook short-circuits once the row is terminal\n * (spec's reconnect behavior), which is always the case for this card.\n */\nexport interface MutationResultCardPayload {\n /** Server-serialized pending action snapshot (optional \u2014 the hook refetches). */\n pendingAction?: AiPendingActionCardAction\n /** Optional link target for the success record. */\n recordHref?: string\n}\n\nexport interface MutationResultCardProps extends AiUiPartProps {\n /** Optional injected action for tests \u2014 bypasses the polling fetch. */\n initialAction?: AiPendingActionCardAction\n /** Poll endpoint override for tests. */\n endpoint?: string\n}\n\nfunction isSuccessStatus(status: AiPendingActionCardStatus | null): boolean {\n return status === 'confirmed'\n}\n\nfunction isFailureStatus(status: AiPendingActionCardStatus | null): boolean {\n return status === 'failed' || status === 'cancelled' || status === 'expired'\n}\n\nexport function MutationResultCard(props: MutationResultCardProps) {\n const t = useT()\n const pendingActionId = props.pendingActionId ?? ''\n const payload = (props.payload as MutationResultCardPayload | undefined) ?? {}\n const injected = props.initialAction ?? payload.pendingAction ?? null\n\n const { action: polled } = useAiPendingActionPolling({\n pendingActionId,\n endpoint: props.endpoint,\n disabled: !pendingActionId || Boolean(injected),\n })\n const action = injected ?? polled\n const status = action?.status ?? null\n const failedRecords = action?.failedRecords ?? null\n const result = action?.executionResult ?? null\n\n if (!action) {\n return null\n }\n\n if (isSuccessStatus(status) && failedRecords && failedRecords.length > 0) {\n return (\n <Alert variant=\"warning\" data-ai-mutation-result=\"partial\">\n <AlertTriangle className=\"size-4\" aria-hidden />\n <AlertTitle>\n {t(\n 'ai_assistant.chat.mutation_cards.result.partialTitle',\n 'Action applied with failures',\n )}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <p>\n {t(\n 'ai_assistant.chat.mutation_cards.result.partialBody',\n 'Some records could not be updated.',\n )}\n </p>\n <ul\n className=\"mt-2 list-disc space-y-1 pl-5 text-xs\"\n data-ai-mutation-failed-records\n >\n {failedRecords.map((record) => (\n <li key={record.recordId} data-ai-mutation-failed-record={record.recordId}>\n <span className=\"font-mono\">{record.recordId}</span>\n <span className=\"mx-1 text-muted-foreground\">\u2022</span>\n <span className=\"font-mono\">{record.error.code}</span>\n <span className=\"mx-1 text-muted-foreground\">\u2014</span>\n <span>{record.error.message}</span>\n </li>\n ))}\n </ul>\n </div>\n </Alert>\n )\n }\n\n if (isSuccessStatus(status)) {\n const recordId = result?.recordId ?? action.targetRecordId ?? null\n const href = payload.recordHref ?? null\n return (\n <Alert variant=\"success\" data-ai-mutation-result=\"success\">\n <CheckCircle2 className=\"size-4\" aria-hidden />\n <AlertTitle>\n {t('ai_assistant.chat.mutation_cards.result.successTitle', 'Action applied')}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <p>\n {result?.commandName\n ? t(\n 'ai_assistant.chat.mutation_cards.result.successWithCommand',\n 'Completed',\n ) + `: ${result.commandName}`\n : t(\n 'ai_assistant.chat.mutation_cards.result.successBody',\n 'The mutation completed successfully.',\n )}\n </p>\n {recordId ? (\n <p className=\"mt-1 text-xs\">\n {href ? (\n <a\n className=\"font-mono text-primary underline\"\n href={href}\n data-ai-mutation-result-link\n >\n {t(\n 'ai_assistant.chat.mutation_cards.result.viewRecord',\n 'View record',\n )}\n : {recordId}\n </a>\n ) : (\n <span className=\"font-mono\" data-ai-mutation-result-record-id>\n {recordId}\n </span>\n )}\n </p>\n ) : null}\n </div>\n </Alert>\n )\n }\n\n // Render the failure alert when the action is in a failure status OR when\n // the dispatcher already captured an `executionResult.error` (the row may\n // not yet have transitioned out of `executing` in the polling snapshot,\n // but the handler error is authoritative \u2014 surface it immediately so the\n // operator never sees a stuck \"applying\u2026\" state silently masking a real\n // failure).\n if (isFailureStatus(status) || result?.error) {\n const code = result?.error?.code ?? status ?? 'failed'\n const message =\n result?.error?.message ??\n t(\n 'ai_assistant.chat.mutation_cards.result.failureBody',\n 'The mutation could not be applied.',\n )\n const errorObj = result?.error\n const errorDetails = errorObj?.details\n const errorInput = errorObj?.input\n const errorName = errorObj?.name\n const onFixWithAi = () => {\n // Build a structured prompt that gives the agent enough context to\n // diagnose and retry without copy/paste from the operator. Keeping\n // it explicit (\"retry with corrected arguments\") nudges the model\n // away from re-issuing the same args (which would just hit the\n // same error). The repository's idempotency check only dedupes\n // active `pending` rows, so a fresh prepareMutation call after a\n // terminal failure always produces a new pending action \u2014 the\n // retry is never silently collapsed.\n //\n // The prompt now embeds the full structured failure context the\n // server captured (Zod issues / fieldErrors, original arguments,\n // failedRecords for batch tools, error name + cause). Without this\n // the operator routinely saw \"Invalid input\" with no field path \u2014\n // the model literally could not fix what it could not see.\n const promptLines: string[] = [\n `The previous call to tool \"${action.toolName}\" failed.`,\n `Error: ${code} \u2014 ${message}.`,\n ]\n if (errorName && errorName !== 'Error') {\n promptLines.push(`Error class: ${errorName}.`)\n }\n if (action.targetEntityType || action.targetRecordId) {\n promptLines.push(\n `Target: ${action.targetEntityType ?? '?'}${action.targetRecordId ? ' / ' + action.targetRecordId : ''}.`,\n )\n }\n\n // Field-level validation issues (Zod, custom). Render as a bulleted\n // list of `path: message` so the model can locate the offender by\n // schema path instead of guessing from a generic message.\n const issues = errorDetails?.issues ?? []\n if (Array.isArray(issues) && issues.length > 0) {\n promptLines.push('', 'Validation issues:')\n for (const issue of issues) {\n const path = Array.isArray(issue?.path) && issue.path.length > 0\n ? issue.path.join('.')\n : '(root)'\n const msg = issue?.message ?? '(no message)'\n const codeHint = issue?.code ? ` [${issue.code}]` : ''\n const expHint =\n issue?.expected || issue?.received\n ? ` (expected ${issue?.expected ?? '?'}, got ${issue?.received ?? '?'})`\n : ''\n promptLines.push(`- ${path}: ${msg}${codeHint}${expHint}`)\n }\n } else if (errorDetails?.fieldErrors && typeof errorDetails.fieldErrors === 'object') {\n const entries = Object.entries(errorDetails.fieldErrors)\n if (entries.length > 0) {\n promptLines.push('', 'Field errors:')\n for (const [path, msgs] of entries) {\n const list = Array.isArray(msgs) ? msgs.join('; ') : String(msgs)\n promptLines.push(`- ${path}: ${list}`)\n }\n }\n }\n\n // Echo the arguments the handler was invoked with so the model can\n // see exactly what it sent and change at least one parameter on\n // retry. JSON-stringified inline for compactness; non-serializable\n // values are dropped on the server side already.\n if (errorInput !== undefined) {\n try {\n const json = JSON.stringify(errorInput, null, 2)\n if (json && json !== '{}' && json.length <= 4000) {\n promptLines.push('', 'Arguments you sent:', '```json', json, '```')\n } else if (json && json.length > 4000) {\n promptLines.push(\n '',\n 'Arguments you sent (truncated):',\n '```json',\n json.slice(0, 4000) + '\\n\u2026 [truncated]',\n '```',\n )\n }\n } catch {\n // ignore\n }\n }\n\n // Surface root-cause when the handler nested another error inside.\n if (errorDetails?.cause !== undefined) {\n try {\n const causeJson = JSON.stringify(errorDetails.cause, null, 2)\n if (causeJson && causeJson !== '{}' && causeJson.length <= 1500) {\n promptLines.push('', 'Underlying cause:', '```json', causeJson, '```')\n }\n } catch {\n // ignore\n }\n }\n\n // Per-record failures from batch tools (Step 5.14). These usually\n // carry the most actionable information for partial-success cases.\n if (failedRecords && failedRecords.length > 0) {\n promptLines.push('', 'Records that failed:')\n for (const rec of failedRecords) {\n promptLines.push(\n `- ${rec.recordId} \u2192 ${rec.error.code}: ${rec.error.message}`,\n )\n }\n }\n\n promptLines.push(\n '',\n 'Diagnose what went wrong using the validation issues / cause / arguments above, correct the arguments, and call the tool again. If the failure indicates missing prerequisites (e.g. a deal needs a linked person/company before commenting), tell me what to fix on the platform side instead of retrying blindly. Do not repeat the exact same arguments \u2014 you must change at least one parameter or stop and explain.',\n )\n dispatchFixRequest({\n message: promptLines.join('\\n'),\n toolName: action.toolName,\n pendingActionId: action.id,\n })\n }\n const visibleIssues = Array.isArray(errorDetails?.issues)\n ? errorDetails!.issues!.filter((entry) => entry && (entry.message || entry.path))\n : []\n return (\n <Alert variant=\"destructive\" data-ai-mutation-result=\"failure\">\n <XCircle className=\"size-4\" aria-hidden />\n <AlertTitle>\n {t(\n 'ai_assistant.chat.mutation_cards.result.failureTitle',\n 'Action failed',\n )}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <div>\n <span className=\"mr-2 font-mono text-xs\" data-ai-mutation-result-code>\n {code}\n </span>\n <span>{message}</span>\n </div>\n {visibleIssues.length > 0 ? (\n <ul\n className=\"mt-2 list-disc space-y-0.5 pl-5 text-xs\"\n data-ai-mutation-result-issues\n >\n {visibleIssues.map((issue, index) => {\n const path =\n Array.isArray(issue?.path) && issue.path.length > 0\n ? issue.path.join('.')\n : null\n return (\n <li key={index}>\n {path ? (\n <span className=\"font-mono\">{path}</span>\n ) : null}\n {path && issue?.message ? <span className=\"mx-1\">\u2014</span> : null}\n {issue?.message ? <span>{issue.message}</span> : null}\n </li>\n )\n })}\n </ul>\n ) : null}\n <div className=\"mt-2 flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={onFixWithAi}\n data-ai-mutation-result-fix\n >\n <Wand2 className=\"size-4\" aria-hidden />\n <span>\n {t(\n 'ai_assistant.chat.mutation_cards.result.fixWithAi',\n 'Fix with AI',\n )}\n </span>\n </Button>\n </div>\n </div>\n </Alert>\n )\n }\n\n return null\n}\n\nexport default MutationResultCard\n"],
|
|
5
|
-
"mappings": ";AAkFQ,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Wand2 } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Alert, AlertDescription, AlertTitle } from '../../primitives/alert'\nimport { Button } from '../../primitives/button'\nimport type { AiUiPartProps } from '../ui-part-registry'\nimport { useAiPendingActionPolling } from './useAiPendingActionPolling'\nimport type { AiPendingActionCardAction, AiPendingActionCardStatus } from './types'\n\n/** Custom DOM event the failure card dispatches when the operator clicks\n * \"Fix with AI\". `<AiChat>` listens for this and sends a follow-up user\n * message asking the agent to diagnose and retry the failed call. */\nexport const AI_CHAT_FIX_REQUEST_EVENT = 'om-ai-chat-fix-request'\n\nexport interface AiChatFixRequestDetail {\n message: string\n toolName?: string\n pendingActionId?: string\n}\n\nfunction dispatchFixRequest(detail: AiChatFixRequestDetail): void {\n if (typeof window === 'undefined') return\n window.dispatchEvent(new CustomEvent<AiChatFixRequestDetail>(AI_CHAT_FIX_REQUEST_EVENT, { detail }))\n}\n\n/**\n * Terminal-state card that renders the `executionResult` of a pending\n * action. Success \u2192 `Alert variant=\"success\"` with a record link; partial\n * success (batch `failedRecords[]`) \u2192 `variant=\"warning\"` with the list;\n * failure \u2192 `variant=\"destructive\"` with the error code + message.\n *\n * Reads the pending action via the shared polling hook so page reloads\n * still recover state. The hook short-circuits once the row is terminal\n * (spec's reconnect behavior), which is always the case for this card.\n */\nexport interface MutationResultCardPayload {\n /** Server-serialized pending action snapshot (optional \u2014 the hook refetches). */\n pendingAction?: AiPendingActionCardAction\n /** Optional link target for the success record. */\n recordHref?: string\n}\n\nexport interface MutationResultCardProps extends AiUiPartProps {\n /** Optional injected action for tests \u2014 bypasses the polling fetch. */\n initialAction?: AiPendingActionCardAction\n /** Poll endpoint override for tests. */\n endpoint?: string\n}\n\nfunction isSuccessStatus(status: AiPendingActionCardStatus | null): boolean {\n return status === 'confirmed'\n}\n\nfunction isFailureStatus(status: AiPendingActionCardStatus | null): boolean {\n return status === 'failed' || status === 'cancelled' || status === 'expired'\n}\n\nexport function MutationResultCard(props: MutationResultCardProps) {\n const t = useT()\n const pendingActionId = props.pendingActionId ?? ''\n const payload = (props.payload as MutationResultCardPayload | undefined) ?? {}\n const injected = props.initialAction ?? payload.pendingAction ?? null\n\n const { action: polled } = useAiPendingActionPolling({\n pendingActionId,\n endpoint: props.endpoint,\n disabled: !pendingActionId || Boolean(injected),\n })\n const action = injected ?? polled\n const status = action?.status ?? null\n const failedRecords = action?.failedRecords ?? null\n const result = action?.executionResult ?? null\n\n if (!action) {\n return null\n }\n\n if (isSuccessStatus(status) && failedRecords && failedRecords.length > 0) {\n return (\n <Alert variant=\"warning\" data-ai-mutation-result=\"partial\">\n <AlertTitle>\n {t(\n 'ai_assistant.chat.mutation_cards.result.partialTitle',\n 'Action applied with failures',\n )}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <p>\n {t(\n 'ai_assistant.chat.mutation_cards.result.partialBody',\n 'Some records could not be updated.',\n )}\n </p>\n <ul\n className=\"mt-2 list-disc space-y-1 pl-5 text-xs\"\n data-ai-mutation-failed-records\n >\n {failedRecords.map((record) => (\n <li key={record.recordId} data-ai-mutation-failed-record={record.recordId}>\n <span className=\"font-mono\">{record.recordId}</span>\n <span className=\"mx-1 text-muted-foreground\">\u2022</span>\n <span className=\"font-mono\">{record.error.code}</span>\n <span className=\"mx-1 text-muted-foreground\">\u2014</span>\n <span>{record.error.message}</span>\n </li>\n ))}\n </ul>\n </div>\n </Alert>\n )\n }\n\n if (isSuccessStatus(status)) {\n const recordId = result?.recordId ?? action.targetRecordId ?? null\n const href = payload.recordHref ?? null\n return (\n <Alert variant=\"success\" data-ai-mutation-result=\"success\">\n <AlertTitle>\n {t('ai_assistant.chat.mutation_cards.result.successTitle', 'Action applied')}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <p>\n {result?.commandName\n ? t(\n 'ai_assistant.chat.mutation_cards.result.successWithCommand',\n 'Completed',\n ) + `: ${result.commandName}`\n : t(\n 'ai_assistant.chat.mutation_cards.result.successBody',\n 'The mutation completed successfully.',\n )}\n </p>\n {recordId ? (\n <p className=\"mt-1 text-xs\">\n {href ? (\n <a\n className=\"font-mono text-primary underline\"\n href={href}\n data-ai-mutation-result-link\n >\n {t(\n 'ai_assistant.chat.mutation_cards.result.viewRecord',\n 'View record',\n )}\n : {recordId}\n </a>\n ) : (\n <span className=\"font-mono\" data-ai-mutation-result-record-id>\n {recordId}\n </span>\n )}\n </p>\n ) : null}\n </div>\n </Alert>\n )\n }\n\n // Render the failure alert when the action is in a failure status OR when\n // the dispatcher already captured an `executionResult.error` (the row may\n // not yet have transitioned out of `executing` in the polling snapshot,\n // but the handler error is authoritative \u2014 surface it immediately so the\n // operator never sees a stuck \"applying\u2026\" state silently masking a real\n // failure).\n if (isFailureStatus(status) || result?.error) {\n const code = result?.error?.code ?? status ?? 'failed'\n const message =\n result?.error?.message ??\n t(\n 'ai_assistant.chat.mutation_cards.result.failureBody',\n 'The mutation could not be applied.',\n )\n const errorObj = result?.error\n const errorDetails = errorObj?.details\n const errorInput = errorObj?.input\n const errorName = errorObj?.name\n const onFixWithAi = () => {\n // Build a structured prompt that gives the agent enough context to\n // diagnose and retry without copy/paste from the operator. Keeping\n // it explicit (\"retry with corrected arguments\") nudges the model\n // away from re-issuing the same args (which would just hit the\n // same error). The repository's idempotency check only dedupes\n // active `pending` rows, so a fresh prepareMutation call after a\n // terminal failure always produces a new pending action \u2014 the\n // retry is never silently collapsed.\n //\n // The prompt now embeds the full structured failure context the\n // server captured (Zod issues / fieldErrors, original arguments,\n // failedRecords for batch tools, error name + cause). Without this\n // the operator routinely saw \"Invalid input\" with no field path \u2014\n // the model literally could not fix what it could not see.\n const promptLines: string[] = [\n `The previous call to tool \"${action.toolName}\" failed.`,\n `Error: ${code} \u2014 ${message}.`,\n ]\n if (errorName && errorName !== 'Error') {\n promptLines.push(`Error class: ${errorName}.`)\n }\n if (action.targetEntityType || action.targetRecordId) {\n promptLines.push(\n `Target: ${action.targetEntityType ?? '?'}${action.targetRecordId ? ' / ' + action.targetRecordId : ''}.`,\n )\n }\n\n // Field-level validation issues (Zod, custom). Render as a bulleted\n // list of `path: message` so the model can locate the offender by\n // schema path instead of guessing from a generic message.\n const issues = errorDetails?.issues ?? []\n if (Array.isArray(issues) && issues.length > 0) {\n promptLines.push('', 'Validation issues:')\n for (const issue of issues) {\n const path = Array.isArray(issue?.path) && issue.path.length > 0\n ? issue.path.join('.')\n : '(root)'\n const msg = issue?.message ?? '(no message)'\n const codeHint = issue?.code ? ` [${issue.code}]` : ''\n const expHint =\n issue?.expected || issue?.received\n ? ` (expected ${issue?.expected ?? '?'}, got ${issue?.received ?? '?'})`\n : ''\n promptLines.push(`- ${path}: ${msg}${codeHint}${expHint}`)\n }\n } else if (errorDetails?.fieldErrors && typeof errorDetails.fieldErrors === 'object') {\n const entries = Object.entries(errorDetails.fieldErrors)\n if (entries.length > 0) {\n promptLines.push('', 'Field errors:')\n for (const [path, msgs] of entries) {\n const list = Array.isArray(msgs) ? msgs.join('; ') : String(msgs)\n promptLines.push(`- ${path}: ${list}`)\n }\n }\n }\n\n // Echo the arguments the handler was invoked with so the model can\n // see exactly what it sent and change at least one parameter on\n // retry. JSON-stringified inline for compactness; non-serializable\n // values are dropped on the server side already.\n if (errorInput !== undefined) {\n try {\n const json = JSON.stringify(errorInput, null, 2)\n if (json && json !== '{}' && json.length <= 4000) {\n promptLines.push('', 'Arguments you sent:', '```json', json, '```')\n } else if (json && json.length > 4000) {\n promptLines.push(\n '',\n 'Arguments you sent (truncated):',\n '```json',\n json.slice(0, 4000) + '\\n\u2026 [truncated]',\n '```',\n )\n }\n } catch {\n // ignore\n }\n }\n\n // Surface root-cause when the handler nested another error inside.\n if (errorDetails?.cause !== undefined) {\n try {\n const causeJson = JSON.stringify(errorDetails.cause, null, 2)\n if (causeJson && causeJson !== '{}' && causeJson.length <= 1500) {\n promptLines.push('', 'Underlying cause:', '```json', causeJson, '```')\n }\n } catch {\n // ignore\n }\n }\n\n // Per-record failures from batch tools (Step 5.14). These usually\n // carry the most actionable information for partial-success cases.\n if (failedRecords && failedRecords.length > 0) {\n promptLines.push('', 'Records that failed:')\n for (const rec of failedRecords) {\n promptLines.push(\n `- ${rec.recordId} \u2192 ${rec.error.code}: ${rec.error.message}`,\n )\n }\n }\n\n promptLines.push(\n '',\n 'Diagnose what went wrong using the validation issues / cause / arguments above, correct the arguments, and call the tool again. If the failure indicates missing prerequisites (e.g. a deal needs a linked person/company before commenting), tell me what to fix on the platform side instead of retrying blindly. Do not repeat the exact same arguments \u2014 you must change at least one parameter or stop and explain.',\n )\n dispatchFixRequest({\n message: promptLines.join('\\n'),\n toolName: action.toolName,\n pendingActionId: action.id,\n })\n }\n const visibleIssues = Array.isArray(errorDetails?.issues)\n ? errorDetails!.issues!.filter((entry) => entry && (entry.message || entry.path))\n : []\n return (\n <Alert variant=\"destructive\" data-ai-mutation-result=\"failure\">\n <AlertTitle>\n {t(\n 'ai_assistant.chat.mutation_cards.result.failureTitle',\n 'Action failed',\n )}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <div>\n <span className=\"mr-2 font-mono text-xs\" data-ai-mutation-result-code>\n {code}\n </span>\n <span>{message}</span>\n </div>\n {visibleIssues.length > 0 ? (\n <ul\n className=\"mt-2 list-disc space-y-0.5 pl-5 text-xs\"\n data-ai-mutation-result-issues\n >\n {visibleIssues.map((issue, index) => {\n const path =\n Array.isArray(issue?.path) && issue.path.length > 0\n ? issue.path.join('.')\n : null\n return (\n <li key={index}>\n {path ? (\n <span className=\"font-mono\">{path}</span>\n ) : null}\n {path && issue?.message ? <span className=\"mx-1\">\u2014</span> : null}\n {issue?.message ? <span>{issue.message}</span> : null}\n </li>\n )\n })}\n </ul>\n ) : null}\n <div className=\"mt-2 flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={onFixWithAi}\n data-ai-mutation-result-fix\n >\n <Wand2 className=\"size-4\" aria-hidden />\n <span>\n {t(\n 'ai_assistant.chat.mutation_cards.result.fixWithAi',\n 'Fix with AI',\n )}\n </span>\n </Button>\n </div>\n </div>\n </Alert>\n )\n }\n\n return null\n}\n\nexport default MutationResultCard\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Info } from "lucide-react";
|
|
4
3
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
4
|
import { Alert, AlertDescription, AlertTitle } from "../../primitives/alert.js";
|
|
6
5
|
function PendingPhase3Placeholder({ componentId }) {
|
|
@@ -11,7 +10,6 @@ function PendingPhase3Placeholder({ componentId }) {
|
|
|
11
10
|
variant: "info",
|
|
12
11
|
"data-ai-ui-part-pending-phase3": componentId,
|
|
13
12
|
children: [
|
|
14
|
-
/* @__PURE__ */ jsx(Info, { className: "size-4", "aria-hidden": true }),
|
|
15
13
|
/* @__PURE__ */ jsx(AlertTitle, { children: t(
|
|
16
14
|
"ai_assistant.chat.pending_phase3.title",
|
|
17
15
|
"Mutation approval card pending"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/ai/ui-parts/pending-phase3-placeholder.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Alert, AlertDescription, AlertTitle } from '../../primitives/alert'\nimport type { AiUiPartProps } from '../ui-part-registry'\n\n/**\n * Default placeholder for the four Phase 3 reserved slot ids.\n *\n * Consumers that forget to register the real mutation-approval cards still\n * see a humane \"Phase 3 pending\" state instead of the neutral debug chip the\n * `<AiChat>` fallback uses for genuinely unknown component ids. Uses the\n * shared DS `Alert` primitive with `variant=\"info\"` \u2014 no hardcoded colors.\n *\n * When Step 5.10 lands the real cards, app bootstrappers will call\n * `registerAiUiPart('mutation-preview-card', MutationPreviewCard)` (etc.)\n * which overwrites this placeholder. The unit tests in\n * `__tests__/ui-part-registry.test.ts` pin that replacement behavior so the\n * Phase 3 hand-off stays deterministic.\n */\nexport function PendingPhase3Placeholder({ componentId }: AiUiPartProps) {\n const t = useT()\n return (\n <Alert\n variant=\"info\"\n data-ai-ui-part-pending-phase3={componentId}\n >\n <AlertTitle>\n {t(\n 'ai_assistant.chat.pending_phase3.title',\n 'Mutation approval card pending',\n )}\n </AlertTitle>\n <AlertDescription>\n <span>\n {t(\n 'ai_assistant.chat.pending_phase3.body',\n 'This interactive card will land in Phase 3 of the unified AI framework.',\n )}\n </span>\n <span className=\"ml-1 font-mono text-xs\">{componentId}</span>\n </AlertDescription>\n </Alert>\n )\n}\n\nexport default PendingPhase3Placeholder\n"],
|
|
5
|
+
"mappings": ";AA4BM,cAMA,YANA;AAzBN,SAAS,YAAY;AACrB,SAAS,OAAO,kBAAkB,kBAAkB;AAiB7C,SAAS,yBAAyB,EAAE,YAAY,GAAkB;AACvE,QAAM,IAAI,KAAK;AACf,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,kCAAgC;AAAA,MAEhC;AAAA,4BAAC,cACE;AAAA,UACC;AAAA,UACA;AAAA,QACF,GACF;AAAA,QACA,qBAAC,oBACC;AAAA,8BAAC,UACE;AAAA,YACC;AAAA,YACA;AAAA,UACF,GACF;AAAA,UACA,oBAAC,UAAK,WAAU,0BAA0B,uBAAY;AAAA,WACxD;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,qCAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/ai/useAiChat.js
CHANGED
|
@@ -196,6 +196,10 @@ function updateToolCall(state, id, patch) {
|
|
|
196
196
|
nextCalls[idx] = merged;
|
|
197
197
|
return { ...state, toolCalls: nextCalls };
|
|
198
198
|
}
|
|
199
|
+
function displayToolName(toolName) {
|
|
200
|
+
if (typeof toolName !== "string") return void 0;
|
|
201
|
+
return toolName.replace(/__/g, ".");
|
|
202
|
+
}
|
|
199
203
|
function applyChunk(state, chunk) {
|
|
200
204
|
switch (chunk.type) {
|
|
201
205
|
case "text-delta":
|
|
@@ -215,12 +219,12 @@ function applyChunk(state, chunk) {
|
|
|
215
219
|
return { ...state, reasoningStreaming: false };
|
|
216
220
|
case "tool-input-start":
|
|
217
221
|
return updateToolCall(state, String(chunk.toolCallId ?? ""), {
|
|
218
|
-
toolName:
|
|
222
|
+
toolName: displayToolName(chunk.toolName),
|
|
219
223
|
state: "pending"
|
|
220
224
|
});
|
|
221
225
|
case "tool-input-available":
|
|
222
226
|
return updateToolCall(state, String(chunk.toolCallId ?? ""), {
|
|
223
|
-
toolName:
|
|
227
|
+
toolName: displayToolName(chunk.toolName),
|
|
224
228
|
input: chunk.input,
|
|
225
229
|
state: "pending"
|
|
226
230
|
});
|
|
@@ -249,7 +253,7 @@ function applyChunk(state, chunk) {
|
|
|
249
253
|
});
|
|
250
254
|
case "tool-input-error":
|
|
251
255
|
return updateToolCall(state, String(chunk.toolCallId ?? ""), {
|
|
252
|
-
toolName:
|
|
256
|
+
toolName: displayToolName(chunk.toolName),
|
|
253
257
|
input: chunk.input,
|
|
254
258
|
state: "error",
|
|
255
259
|
errorMessage: typeof chunk.errorText === "string" ? chunk.errorText : "Tool error"
|
|
@@ -408,6 +412,7 @@ function useAiChat(input) {
|
|
|
408
412
|
const [error, setError] = React.useState(null);
|
|
409
413
|
const [lastRequestDebug, setLastRequestDebug] = React.useState(null);
|
|
410
414
|
const [lastResponseDebug, setLastResponseDebug] = React.useState(null);
|
|
415
|
+
const [lastLoopTrace, setLastLoopTrace] = React.useState(null);
|
|
411
416
|
const abortRef = React.useRef(null);
|
|
412
417
|
const onErrorRef = React.useRef(onError);
|
|
413
418
|
React.useEffect(() => {
|
|
@@ -436,6 +441,7 @@ function useAiChat(input) {
|
|
|
436
441
|
setError(null);
|
|
437
442
|
setLastRequestDebug(null);
|
|
438
443
|
setLastResponseDebug(null);
|
|
444
|
+
setLastLoopTrace(null);
|
|
439
445
|
clearPersistedSession(agent, persistKey);
|
|
440
446
|
mintedConversationIdRef.current = makeConversationId();
|
|
441
447
|
}, [agent, cancel, persistKey, updateMessages]);
|
|
@@ -528,6 +534,7 @@ function useAiChat(input) {
|
|
|
528
534
|
abortRef.current = null;
|
|
529
535
|
return;
|
|
530
536
|
}
|
|
537
|
+
let pendingLoopTrace = null;
|
|
531
538
|
const headerGet = (name) => {
|
|
532
539
|
const headers = response.headers;
|
|
533
540
|
if (!headers || typeof headers.get !== "function") return null;
|
|
@@ -554,7 +561,10 @@ function useAiChat(input) {
|
|
|
554
561
|
if (data === "[DONE]") continue;
|
|
555
562
|
try {
|
|
556
563
|
const parsed = JSON.parse(data);
|
|
557
|
-
if (parsed
|
|
564
|
+
if (!parsed || typeof parsed.type !== "string") continue;
|
|
565
|
+
if (parsed.type === "loop-finish") {
|
|
566
|
+
pendingLoopTrace = parsed.trace ?? null;
|
|
567
|
+
} else {
|
|
558
568
|
builder = applyChunk(builder, parsed);
|
|
559
569
|
}
|
|
560
570
|
} catch {
|
|
@@ -604,6 +614,9 @@ function useAiChat(input) {
|
|
|
604
614
|
);
|
|
605
615
|
if (mountedRef.current) {
|
|
606
616
|
setLastResponseDebug({ status: response.status, text: streamedRaw });
|
|
617
|
+
if (pendingLoopTrace !== null) {
|
|
618
|
+
setLastLoopTrace(pendingLoopTrace);
|
|
619
|
+
}
|
|
607
620
|
}
|
|
608
621
|
const isEmpty = !builder.text.trim() && builder.toolCalls.length === 0 && !builder.reasoning;
|
|
609
622
|
if (isEmpty) {
|
|
@@ -683,6 +696,7 @@ function useAiChat(input) {
|
|
|
683
696
|
lastRequestDebug,
|
|
684
697
|
lastResponseDebug,
|
|
685
698
|
conversationId: effectiveConversationId,
|
|
699
|
+
lastLoopTrace,
|
|
686
700
|
sendMessage,
|
|
687
701
|
cancel,
|
|
688
702
|
reset
|
package/dist/ai/useAiChat.js.map
CHANGED
|
@@ -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 * 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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",
|
|
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'\nimport type { LoopTracePanelTrace } from './LoopTracePanel'\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 /**\n * Loop trace from the last completed turn. Populated when the dispatcher\n * emits a `loop-finish` SSE event at the end of the response stream.\n * `null` until the first turn completes or when the dispatcher does not\n * emit trace events (non-debug deployments may omit them).\n *\n * Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n lastLoopTrace: LoopTracePanelTrace | null\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 displayToolName(toolName: unknown): string | undefined {\n if (typeof toolName !== 'string') return undefined\n return toolName.replace(/__/g, '.')\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: displayToolName(chunk.toolName),\n state: 'pending',\n })\n case 'tool-input-available':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n toolName: displayToolName(chunk.toolName),\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: displayToolName(chunk.toolName),\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 [lastLoopTrace, setLastLoopTrace] = React.useState<LoopTracePanelTrace | null>(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 setLastLoopTrace(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 // Per-turn loop trace collected from the `loop-finish` SSE event emitted\n // by the dispatcher at the end of the stream (Phase 4).\n let pendingLoopTrace: LoopTracePanelTrace | null = null\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; trace?: unknown }\n if (!parsed || typeof parsed.type !== 'string') continue\n if (parsed.type === 'loop-finish') {\n // Capture the loop trace for post-stream state update.\n pendingLoopTrace = parsed.trace as LoopTracePanelTrace ?? null\n } else {\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 if (pendingLoopTrace !== null) {\n setLastLoopTrace(pendingLoopTrace)\n }\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 lastLoopTrace,\n sendMessage,\n cancel,\n reset,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/backend/CrudForm.js
CHANGED
|
@@ -175,6 +175,18 @@ function readInitialCustomFieldValue(source, key) {
|
|
|
175
175
|
}
|
|
176
176
|
return void 0;
|
|
177
177
|
}
|
|
178
|
+
function readRenderedFieldValue(source, fieldId) {
|
|
179
|
+
if (Object.prototype.hasOwnProperty.call(source, fieldId)) {
|
|
180
|
+
return source[fieldId];
|
|
181
|
+
}
|
|
182
|
+
if (fieldId.startsWith("cf_")) {
|
|
183
|
+
return readInitialCustomFieldValue(source, fieldId.slice(3));
|
|
184
|
+
}
|
|
185
|
+
if (fieldId.startsWith("cf:")) {
|
|
186
|
+
return readInitialCustomFieldValue(source, fieldId.slice(3));
|
|
187
|
+
}
|
|
188
|
+
return void 0;
|
|
189
|
+
}
|
|
178
190
|
function serializeIssuePath(path) {
|
|
179
191
|
if (!Array.isArray(path) || path.length === 0) return null;
|
|
180
192
|
const segments = path.map((segment) => {
|
|
@@ -528,9 +540,18 @@ function CrudForm({
|
|
|
528
540
|
if (!target || navigationPromptBypassRef.current || submitNavigationBypassRef.current) {
|
|
529
541
|
return original(data, unused, url);
|
|
530
542
|
}
|
|
543
|
+
if (!isDirtyRef.current) {
|
|
544
|
+
return original(data, unused, url);
|
|
545
|
+
}
|
|
531
546
|
if (shouldBypassUnsavedChangesGuardRef.current?.(target)) {
|
|
532
547
|
return original(data, unused, url);
|
|
533
548
|
}
|
|
549
|
+
const baselineSnapshot = dirtyBaselineSnapshotRef.current;
|
|
550
|
+
if (baselineSnapshot && JSON.stringify(valuesRef.current) === baselineSnapshot) {
|
|
551
|
+
isDirtyRef.current = false;
|
|
552
|
+
setHasUnsavedChanges(false);
|
|
553
|
+
return original(data, unused, url);
|
|
554
|
+
}
|
|
534
555
|
if (navigationConfirmPendingRef.current) {
|
|
535
556
|
return void 0;
|
|
536
557
|
}
|
|
@@ -2105,7 +2126,7 @@ function CrudForm({
|
|
|
2105
2126
|
FieldControl,
|
|
2106
2127
|
{
|
|
2107
2128
|
field: f,
|
|
2108
|
-
value: values
|
|
2129
|
+
value: readRenderedFieldValue(values, f.id),
|
|
2109
2130
|
error: errors[f.id],
|
|
2110
2131
|
options: fieldOptionsById.get(f.id) || EMPTY_OPTIONS,
|
|
2111
2132
|
setValue,
|
|
@@ -3314,7 +3335,8 @@ const FieldControl = React.memo(
|
|
|
3314
3335
|
value: Array.isArray(value) ? String(value[0] ?? "") : value == null ? "" : String(value),
|
|
3315
3336
|
onValueChange: (next) => {
|
|
3316
3337
|
if (!next || next === SELECT_CLEAR_SENTINEL) {
|
|
3317
|
-
|
|
3338
|
+
const isCustomField = field.id.startsWith("cf_") || field.id.startsWith("cf:");
|
|
3339
|
+
setValue(field.id, isCustomField ? null : void 0);
|
|
3318
3340
|
return;
|
|
3319
3341
|
}
|
|
3320
3342
|
setValue(field.id, next);
|