@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.
Files changed (77) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +2 -0
  3. package/dist/ai/AiAssistantLauncher.js +8 -8
  4. package/dist/ai/AiAssistantLauncher.js.map +2 -2
  5. package/dist/ai/AiChat.js +92 -93
  6. package/dist/ai/AiChat.js.map +2 -2
  7. package/dist/ai/AiIcon.js +17 -0
  8. package/dist/ai/AiIcon.js.map +7 -0
  9. package/dist/ai/LoopDisabledBanner.js +28 -0
  10. package/dist/ai/LoopDisabledBanner.js.map +7 -0
  11. package/dist/ai/LoopTracePanel.js +123 -0
  12. package/dist/ai/LoopTracePanel.js.map +7 -0
  13. package/dist/ai/ModelPicker.js.map +1 -1
  14. package/dist/ai/index.js +8 -0
  15. package/dist/ai/index.js.map +2 -2
  16. package/dist/ai/parts/ConfirmationCard.js +1 -5
  17. package/dist/ai/parts/ConfirmationCard.js.map +2 -2
  18. package/dist/ai/parts/FieldDiffCard.js +8 -15
  19. package/dist/ai/parts/FieldDiffCard.js.map +2 -2
  20. package/dist/ai/parts/MutationResultCard.js +1 -4
  21. package/dist/ai/parts/MutationResultCard.js.map +2 -2
  22. package/dist/ai/ui-parts/pending-phase3-placeholder.js +0 -2
  23. package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +2 -2
  24. package/dist/ai/useAiChat.js +18 -4
  25. package/dist/ai/useAiChat.js.map +2 -2
  26. package/dist/backend/CrudForm.js +24 -2
  27. package/dist/backend/CrudForm.js.map +2 -2
  28. package/dist/backend/DataTable.js +2 -2
  29. package/dist/backend/DataTable.js.map +2 -2
  30. package/dist/backend/confirm-dialog/ConfirmDialog.js +102 -96
  31. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  32. package/dist/backend/detail/CustomDataSection.js +22 -3
  33. package/dist/backend/detail/CustomDataSection.js.map +2 -2
  34. package/dist/backend/filters/AdvancedFilterBuilder.js +2 -2
  35. package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
  36. package/dist/backend/injection/ComponentOverrideProvider.js +1 -1
  37. package/dist/backend/injection/ComponentOverrideProvider.js.map +2 -2
  38. package/dist/backend/inputs/ComboboxInput.js +25 -7
  39. package/dist/backend/inputs/ComboboxInput.js.map +2 -2
  40. package/dist/backend/progress/useProgressPoll.js +58 -3
  41. package/dist/backend/progress/useProgressPoll.js.map +2 -2
  42. package/dist/backend/progress/useProgressSse.js +15 -2
  43. package/dist/backend/progress/useProgressSse.js.map +2 -2
  44. package/dist/backend/utils/bulkDelete.js +76 -0
  45. package/dist/backend/utils/bulkDelete.js.map +2 -2
  46. package/package.json +11 -6
  47. package/src/ai/AiAssistantLauncher.tsx +8 -8
  48. package/src/ai/AiChat.tsx +44 -38
  49. package/src/ai/AiIcon.tsx +12 -0
  50. package/src/ai/LoopDisabledBanner.tsx +38 -0
  51. package/src/ai/LoopTracePanel.tsx +184 -0
  52. package/src/ai/ModelPicker.tsx +1 -1
  53. package/src/ai/__tests__/AiChat.test.tsx +174 -5
  54. package/src/ai/index.ts +8 -0
  55. package/src/ai/parts/ConfirmationCard.tsx +1 -5
  56. package/src/ai/parts/FieldDiffCard.tsx +0 -3
  57. package/src/ai/parts/MutationResultCard.tsx +1 -4
  58. package/src/ai/parts/__tests__/MutationResultCard.test.tsx +5 -0
  59. package/src/ai/ui-parts/pending-phase3-placeholder.tsx +0 -2
  60. package/src/ai/useAiChat.ts +35 -5
  61. package/src/backend/CrudForm.tsx +30 -3
  62. package/src/backend/DataTable.tsx +2 -2
  63. package/src/backend/__tests__/CrudForm.custom-fields.test.tsx +72 -0
  64. package/src/backend/__tests__/CustomDataSection.test.tsx +76 -0
  65. package/src/backend/__tests__/DataTable.render.test.tsx +36 -0
  66. package/src/backend/__tests__/component-replacement.test.tsx +44 -0
  67. package/src/backend/confirm-dialog/ConfirmDialog.tsx +101 -91
  68. package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +19 -0
  69. package/src/backend/detail/CustomDataSection.tsx +26 -3
  70. package/src/backend/filters/AdvancedFilterBuilder.tsx +6 -6
  71. package/src/backend/injection/ComponentOverrideProvider.tsx +2 -1
  72. package/src/backend/inputs/ComboboxInput.tsx +29 -7
  73. package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +33 -2
  74. package/src/backend/progress/useProgressPoll.ts +68 -3
  75. package/src/backend/progress/useProgressSse.ts +16 -2
  76. package/src/backend/utils/__tests__/bulkDelete.test.ts +42 -0
  77. 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": "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",
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 { Info } from 'lucide-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 <Info className=\"size-4\" aria-hidden />\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": ";AA6BM,cAOA,YAPA;AA1BN,SAAS,YAAY;AACrB,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,QAAK,WAAU,UAAS,eAAW,MAAC;AAAA,QACrC,oBAAC,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;",
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
  }
@@ -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: typeof chunk.toolName === "string" ? chunk.toolName : void 0,
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: typeof chunk.toolName === "string" ? chunk.toolName : void 0,
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: typeof chunk.toolName === "string" ? chunk.toolName : void 0,
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 && typeof parsed.type === "string") {
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
@@ -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
  }
@@ -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[f.id],
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
- setValue(field.id, void 0);
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);