@open-mercato/ui 0.6.1-develop.3227.1.2b7b8ab258 → 0.6.1-develop.3256.1.fe3dec2464

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/__integration__/TC-AI-UI-003-aichat-registry.spec.tsx +13 -0
  3. package/dist/ai/AiAssistantLauncher.js +8 -0
  4. package/dist/ai/AiAssistantLauncher.js.map +2 -2
  5. package/dist/ai/AiChat.js +226 -58
  6. package/dist/ai/AiChat.js.map +2 -2
  7. package/dist/ai/AiDock.js +3 -2
  8. package/dist/ai/AiDock.js.map +2 -2
  9. package/dist/ai/ModelPicker.js +186 -0
  10. package/dist/ai/ModelPicker.js.map +7 -0
  11. package/dist/ai/index.js +3 -1
  12. package/dist/ai/index.js.map +2 -2
  13. package/dist/ai/useAiChat.js +12 -16
  14. package/dist/ai/useAiChat.js.map +2 -2
  15. package/dist/backend/operations/LastOperationBanner.js +15 -9
  16. package/dist/backend/operations/LastOperationBanner.js.map +2 -2
  17. package/dist/backend/operations/store.js +64 -8
  18. package/dist/backend/operations/store.js.map +2 -2
  19. package/dist/backend/utils/bulkDelete.js +46 -0
  20. package/dist/backend/utils/bulkDelete.js.map +7 -0
  21. package/package.json +3 -3
  22. package/src/ai/AiAssistantLauncher.tsx +8 -0
  23. package/src/ai/AiChat.tsx +229 -11
  24. package/src/ai/AiDock.tsx +3 -2
  25. package/src/ai/ModelPicker.tsx +248 -0
  26. package/src/ai/__tests__/AiAssistantLauncher.test.tsx +58 -0
  27. package/src/ai/__tests__/AiChat.conversation.test.tsx +16 -0
  28. package/src/ai/__tests__/AiChat.debug.test.tsx +16 -0
  29. package/src/ai/__tests__/AiChat.registry.test.tsx +16 -0
  30. package/src/ai/__tests__/AiChat.test.tsx +180 -0
  31. package/src/ai/__tests__/AiChat.thinking-unmount.test.tsx +16 -0
  32. package/src/ai/__tests__/AiDock.test.tsx +12 -1
  33. package/src/ai/__tests__/ModelPicker.test.tsx +252 -0
  34. package/src/ai/index.ts +1 -0
  35. package/src/ai/useAiChat.ts +29 -16
  36. package/src/backend/operations/LastOperationBanner.tsx +19 -11
  37. package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +46 -1
  38. package/src/backend/operations/__tests__/store.test.ts +142 -0
  39. package/src/backend/operations/store.ts +76 -7
  40. package/src/backend/utils/__tests__/bulkDelete.test.ts +103 -0
  41. package/src/backend/utils/bulkDelete.ts +77 -0
@@ -21,18 +21,23 @@ function LastOperationBanner() {
21
21
  async function handleUndo() {
22
22
  const undoToken = operation?.undoToken;
23
23
  if (!undoToken || isPending) return;
24
+ const tokens = operation.bulkUndoTokens && operation.bulkUndoTokens.length > 0 ? operation.bulkUndoTokens : [undoToken];
24
25
  setPendingToken(undoToken);
26
+ const completed = [];
25
27
  try {
26
- const call = await apiCall("/api/audit_logs/audit-logs/actions/undo", {
27
- method: "POST",
28
- headers: { "Content-Type": "application/json" },
29
- body: JSON.stringify({ undoToken })
30
- });
31
- if (!call.ok) {
32
- const message = call.result && typeof call.result.error === "string" && call.result.error || "";
33
- throw new Error(message || t("audit_logs.banner.undo_failed", "Failed to undo"));
28
+ for (const token of tokens.slice().reverse()) {
29
+ const call = await apiCall("/api/audit_logs/audit-logs/actions/undo", {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({ undoToken: token })
33
+ });
34
+ if (!call.ok) {
35
+ const message = call.result && typeof call.result.error === "string" && call.result.error || "";
36
+ throw new Error(message || t("audit_logs.banner.undo_failed", "Failed to undo"));
37
+ }
38
+ completed.push(token);
34
39
  }
35
- markUndoSuccess(undoToken);
40
+ markUndoSuccess(tokens);
36
41
  flash(t("audit_logs.banner.undo_success"), "success");
37
42
  router.refresh();
38
43
  if (typeof window !== "undefined") {
@@ -45,6 +50,7 @@ function LastOperationBanner() {
45
50
  }
46
51
  }
47
52
  } catch (err) {
53
+ if (completed.length > 0) markUndoSuccess(completed);
48
54
  const message = err instanceof Error && err.message ? err.message : t("audit_logs.banner.undo_error");
49
55
  flash(message, "error");
50
56
  } finally {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/operations/LastOperationBanner.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Undo2 } from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { Button } from '../../primitives/button'\nimport { apiCall } from '../utils/apiCall'\nimport { flash } from '../FlashMessages'\nimport { useLastOperation, markUndoSuccess } from './store'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport function LastOperationBanner() {\n const t = useT()\n const operation = useLastOperation()\n const [pendingToken, setPendingToken] = React.useState<string | null>(null)\n const router = useRouter()\n\n if (!operation) return null\n\n const rawLabel = operation.actionLabel ?? operation.commandId\n const translatedLabel = t(rawLabel)\n const label = translatedLabel === rawLabel ? rawLabel : translatedLabel\n const isPending = pendingToken === operation.undoToken\n\n async function handleUndo() {\n const undoToken = operation?.undoToken\n if (!undoToken || isPending) return\n setPendingToken(undoToken)\n try {\n const call = await apiCall<Record<string, unknown>>('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ undoToken }),\n })\n if (!call.ok) {\n const message =\n (call.result && typeof call.result.error === 'string' && call.result.error) ||\n ''\n throw new Error(message || t('audit_logs.banner.undo_failed', 'Failed to undo'))\n }\n markUndoSuccess(undoToken)\n flash(t('audit_logs.banner.undo_success'), 'success')\n router.refresh()\n if (typeof window !== 'undefined') {\n try {\n const isJSDOM = typeof navigator !== 'undefined' && typeof navigator.userAgent === 'string'\n ? navigator.userAgent.toLowerCase().includes('jsdom')\n : false\n if (!isJSDOM && typeof window.location?.reload === 'function') {\n window.location.reload()\n }\n } catch {\n // noop in non-browser or jsdom environments\n }\n }\n } catch (err) {\n const message = err instanceof Error && err.message ? err.message : t('audit_logs.banner.undo_error')\n flash(message, 'error')\n } finally {\n setPendingToken(null)\n }\n }\n\n return (\n <div className=\"mb-4 flex items-center justify-between gap-3 rounded-md border border-amber-200/80 bg-amber-50/95 pl-3 pr-2 py-2 text-sm text-amber-900 shadow-xs sm:pr-3\">\n <div className=\"min-w-0 truncate\">\n <span className=\"font-medium text-amber-950\">\n {t('audit_logs.banner.last_operation')}\n </span>\n <span className=\"ml-2 truncate text-amber-900\">\n {label}\n </span>\n </div>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => { void handleUndo() }}\n disabled={isPending}\n className=\"border-amber-200/80 bg-amber-50 text-amber-900 hover:bg-amber-100 hover:text-amber-900 px-2.5 sm:px-3\"\n >\n <Undo2 className=\"mr-1 size-4\" aria-hidden=\"true\" />\n {isPending ? t('audit_logs.actions.undoing') : t('audit_logs.banner.undo')}\n </Button>\n </div>\n )\n}\n"],
5
- "mappings": ";AAgEM,SACE,KADF;AA/DN,YAAY,WAAW;AACvB,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAC1B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,SAAS,kBAAkB,uBAAuB;AAClD,SAAS,YAAY;AAEd,SAAS,sBAAsB;AACpC,QAAM,IAAI,KAAK;AACf,QAAM,YAAY,iBAAiB;AACnC,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAwB,IAAI;AAC1E,QAAM,SAAS,UAAU;AAEzB,MAAI,CAAC,UAAW,QAAO;AAEvB,QAAM,WAAW,UAAU,eAAe,UAAU;AACpD,QAAM,kBAAkB,EAAE,QAAQ;AAClC,QAAM,QAAQ,oBAAoB,WAAW,WAAW;AACxD,QAAM,YAAY,iBAAiB,UAAU;AAE7C,iBAAe,aAAa;AAC1B,UAAM,YAAY,WAAW;AAC7B,QAAI,CAAC,aAAa,UAAW;AAC7B,oBAAgB,SAAS;AACzB,QAAI;AACF,YAAM,OAAO,MAAM,QAAiC,2CAA2C;AAAA,QAC7F,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,CAAC;AAAA,MACpC,CAAC;AACD,UAAI,CAAC,KAAK,IAAI;AACZ,cAAM,UACH,KAAK,UAAU,OAAO,KAAK,OAAO,UAAU,YAAY,KAAK,OAAO,SACrE;AACF,cAAM,IAAI,MAAM,WAAW,EAAE,iCAAiC,gBAAgB,CAAC;AAAA,MACjF;AACA,sBAAgB,SAAS;AACzB,YAAM,EAAE,gCAAgC,GAAG,SAAS;AACpD,aAAO,QAAQ;AACf,UAAI,OAAO,WAAW,aAAa;AACjC,YAAI;AACF,gBAAM,UAAU,OAAO,cAAc,eAAe,OAAO,UAAU,cAAc,WAC/E,UAAU,UAAU,YAAY,EAAE,SAAS,OAAO,IAClD;AACJ,cAAI,CAAC,WAAW,OAAO,OAAO,UAAU,WAAW,YAAY;AAC7D,mBAAO,SAAS,OAAO;AAAA,UACzB;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,UAAU,eAAe,SAAS,IAAI,UAAU,IAAI,UAAU,EAAE,8BAA8B;AACpG,YAAM,SAAS,OAAO;AAAA,IACxB,UAAE;AACA,sBAAgB,IAAI;AAAA,IACtB;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,WAAU,6JACb;AAAA,yBAAC,SAAI,WAAU,oBACb;AAAA,0BAAC,UAAK,WAAU,8BACb,YAAE,kCAAkC,GACvC;AAAA,MACA,oBAAC,UAAK,WAAU,gCACb,iBACH;AAAA,OACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS,MAAM;AAAE,eAAK,WAAW;AAAA,QAAE;AAAA,QACnC,UAAU;AAAA,QACV,WAAU;AAAA,QAEV;AAAA,8BAAC,SAAM,WAAU,eAAc,eAAY,QAAO;AAAA,UACjD,YAAY,EAAE,4BAA4B,IAAI,EAAE,wBAAwB;AAAA;AAAA;AAAA,IAC3E;AAAA,KACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Undo2 } from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { Button } from '../../primitives/button'\nimport { apiCall } from '../utils/apiCall'\nimport { flash } from '../FlashMessages'\nimport { useLastOperation, markUndoSuccess } from './store'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport function LastOperationBanner() {\n const t = useT()\n const operation = useLastOperation()\n const [pendingToken, setPendingToken] = React.useState<string | null>(null)\n const router = useRouter()\n\n if (!operation) return null\n\n const rawLabel = operation.actionLabel ?? operation.commandId\n const translatedLabel = t(rawLabel)\n const label = translatedLabel === rawLabel ? rawLabel : translatedLabel\n const isPending = pendingToken === operation.undoToken\n\n async function handleUndo() {\n const undoToken = operation?.undoToken\n if (!undoToken || isPending) return\n const tokens = operation.bulkUndoTokens && operation.bulkUndoTokens.length > 0\n ? operation.bulkUndoTokens\n : [undoToken]\n setPendingToken(undoToken)\n const completed: string[] = []\n try {\n for (const token of tokens.slice().reverse()) {\n const call = await apiCall<Record<string, unknown>>('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ undoToken: token }),\n })\n if (!call.ok) {\n const message =\n (call.result && typeof call.result.error === 'string' && call.result.error) ||\n ''\n throw new Error(message || t('audit_logs.banner.undo_failed', 'Failed to undo'))\n }\n completed.push(token)\n }\n markUndoSuccess(tokens)\n flash(t('audit_logs.banner.undo_success'), 'success')\n router.refresh()\n if (typeof window !== 'undefined') {\n try {\n const isJSDOM = typeof navigator !== 'undefined' && typeof navigator.userAgent === 'string'\n ? navigator.userAgent.toLowerCase().includes('jsdom')\n : false\n if (!isJSDOM && typeof window.location?.reload === 'function') {\n window.location.reload()\n }\n } catch {\n // noop in non-browser or jsdom environments\n }\n }\n } catch (err) {\n if (completed.length > 0) markUndoSuccess(completed)\n const message = err instanceof Error && err.message ? err.message : t('audit_logs.banner.undo_error')\n flash(message, 'error')\n } finally {\n setPendingToken(null)\n }\n }\n\n return (\n <div className=\"mb-4 flex items-center justify-between gap-3 rounded-md border border-amber-200/80 bg-amber-50/95 pl-3 pr-2 py-2 text-sm text-amber-900 shadow-xs sm:pr-3\">\n <div className=\"min-w-0 truncate\">\n <span className=\"font-medium text-amber-950\">\n {t('audit_logs.banner.last_operation')}\n </span>\n <span className=\"ml-2 truncate text-amber-900\">\n {label}\n </span>\n </div>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => { void handleUndo() }}\n disabled={isPending}\n className=\"border-amber-200/80 bg-amber-50 text-amber-900 hover:bg-amber-100 hover:text-amber-900 px-2.5 sm:px-3\"\n >\n <Undo2 className=\"mr-1 size-4\" aria-hidden=\"true\" />\n {isPending ? t('audit_logs.actions.undoing') : t('audit_logs.banner.undo')}\n </Button>\n </div>\n )\n}\n"],
5
+ "mappings": ";AAwEM,SACE,KADF;AAvEN,YAAY,WAAW;AACvB,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAC1B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,SAAS,kBAAkB,uBAAuB;AAClD,SAAS,YAAY;AAEd,SAAS,sBAAsB;AACpC,QAAM,IAAI,KAAK;AACf,QAAM,YAAY,iBAAiB;AACnC,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAwB,IAAI;AAC1E,QAAM,SAAS,UAAU;AAEzB,MAAI,CAAC,UAAW,QAAO;AAEvB,QAAM,WAAW,UAAU,eAAe,UAAU;AACpD,QAAM,kBAAkB,EAAE,QAAQ;AAClC,QAAM,QAAQ,oBAAoB,WAAW,WAAW;AACxD,QAAM,YAAY,iBAAiB,UAAU;AAE7C,iBAAe,aAAa;AAC1B,UAAM,YAAY,WAAW;AAC7B,QAAI,CAAC,aAAa,UAAW;AAC7B,UAAM,SAAS,UAAU,kBAAkB,UAAU,eAAe,SAAS,IACzE,UAAU,iBACV,CAAC,SAAS;AACd,oBAAgB,SAAS;AACzB,UAAM,YAAsB,CAAC;AAC7B,QAAI;AACF,iBAAW,SAAS,OAAO,MAAM,EAAE,QAAQ,GAAG;AAC5C,cAAM,OAAO,MAAM,QAAiC,2CAA2C;AAAA,UAC7F,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,WAAW,MAAM,CAAC;AAAA,QAC3C,CAAC;AACD,YAAI,CAAC,KAAK,IAAI;AACZ,gBAAM,UACH,KAAK,UAAU,OAAO,KAAK,OAAO,UAAU,YAAY,KAAK,OAAO,SACrE;AACF,gBAAM,IAAI,MAAM,WAAW,EAAE,iCAAiC,gBAAgB,CAAC;AAAA,QACjF;AACA,kBAAU,KAAK,KAAK;AAAA,MACtB;AACA,sBAAgB,MAAM;AACtB,YAAM,EAAE,gCAAgC,GAAG,SAAS;AACpD,aAAO,QAAQ;AACf,UAAI,OAAO,WAAW,aAAa;AACjC,YAAI;AACF,gBAAM,UAAU,OAAO,cAAc,eAAe,OAAO,UAAU,cAAc,WAC/E,UAAU,UAAU,YAAY,EAAE,SAAS,OAAO,IAClD;AACJ,cAAI,CAAC,WAAW,OAAO,OAAO,UAAU,WAAW,YAAY;AAC7D,mBAAO,SAAS,OAAO;AAAA,UACzB;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,UAAU,SAAS,EAAG,iBAAgB,SAAS;AACnD,YAAM,UAAU,eAAe,SAAS,IAAI,UAAU,IAAI,UAAU,EAAE,8BAA8B;AACpG,YAAM,SAAS,OAAO;AAAA,IACxB,UAAE;AACA,sBAAgB,IAAI;AAAA,IACtB;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,WAAU,6JACb;AAAA,yBAAC,SAAI,WAAU,oBACb;AAAA,0BAAC,UAAK,WAAU,8BACb,YAAE,kCAAkC,GACvC;AAAA,MACA,oBAAC,UAAK,WAAU,gCACb,iBACH;AAAA,OACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS,MAAM;AAAE,eAAK,WAAW;AAAA,QAAE;AAAA,QACnC,UAAU;AAAA,QACV,WAAU;AAAA,QAEV;AAAA,8BAAC,SAAM,WAAU,eAAc,eAAY,QAAO;AAAA,UACjD,YAAY,EAAE,4BAA4B,IAAI,EAAE,wBAAwB;AAAA;AAAA;AAAA,IAC3E;AAAA,KACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -38,6 +38,8 @@ function isValidEntry(entry) {
38
38
  }
39
39
  function hydrateEntry(entry) {
40
40
  const source = entry;
41
+ const bulkTokens = Array.isArray(source.bulkUndoTokens) ? source.bulkUndoTokens.filter((t) => typeof t === "string" && t.length > 0) : void 0;
42
+ const bulkCount = typeof source.bulkCount === "number" && Number.isFinite(source.bulkCount) ? source.bulkCount : void 0;
41
43
  return {
42
44
  id: String(source.id),
43
45
  undoToken: String(source.undoToken),
@@ -46,7 +48,9 @@ function hydrateEntry(entry) {
46
48
  resourceKind: typeof source.resourceKind === "string" ? source.resourceKind : null,
47
49
  resourceId: typeof source.resourceId === "string" ? source.resourceId : null,
48
50
  executedAt: typeof source.executedAt === "string" ? source.executedAt : new Date(source.receivedAt || now()).toISOString(),
49
- receivedAt: typeof source.receivedAt === "number" ? source.receivedAt : now()
51
+ receivedAt: typeof source.receivedAt === "number" ? source.receivedAt : now(),
52
+ ...bulkTokens && bulkTokens.length > 0 ? { bulkUndoTokens: bulkTokens } : {},
53
+ ...bulkCount && bulkCount > 0 ? { bulkCount } : {}
50
54
  };
51
55
  }
52
56
  function persist(state) {
@@ -104,19 +108,70 @@ function pushOperation(meta) {
104
108
  return { stack, undone: [] };
105
109
  });
106
110
  }
107
- function markUndoSuccess(undoToken) {
111
+ function markUndoSuccess(undoTokens) {
108
112
  if (typeof window === "undefined") return;
113
+ const tokenSet = new Set(Array.isArray(undoTokens) ? undoTokens : [undoTokens]);
114
+ if (tokenSet.size === 0) return;
109
115
  const removed = [];
110
116
  updateState((prev) => {
111
- const stack = prev.stack.filter((entry) => {
112
- if (entry.undoToken === undoToken) {
117
+ const nextStack = [];
118
+ for (const entry of prev.stack) {
119
+ const bulk = entry.bulkUndoTokens && entry.bulkUndoTokens.length > 0 ? entry.bulkUndoTokens : null;
120
+ if (!bulk) {
121
+ if (tokenSet.has(entry.undoToken)) removed.push(entry);
122
+ else nextStack.push(entry);
123
+ continue;
124
+ }
125
+ const consumed = [];
126
+ const remaining = [];
127
+ for (const token of bulk) {
128
+ if (tokenSet.has(token)) consumed.push(token);
129
+ else remaining.push(token);
130
+ }
131
+ if (consumed.length === 0) {
132
+ nextStack.push(entry);
133
+ } else if (remaining.length === 0) {
113
134
  removed.push(entry);
114
- return false;
135
+ } else {
136
+ removed.push({ ...entry, bulkUndoTokens: consumed, bulkCount: consumed.length });
137
+ nextStack.push({ ...entry, bulkUndoTokens: remaining, bulkCount: remaining.length });
115
138
  }
116
- return true;
117
- });
139
+ }
118
140
  const undone = removed.length ? [...prev.undone, ...removed.map((entry) => ({ ...entry, undoneAt: now() }))] : prev.undone;
119
- return { stack, undone };
141
+ return { stack: nextStack, undone };
142
+ });
143
+ }
144
+ function generateBulkId(seed) {
145
+ const cryptoRef = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
146
+ if (cryptoRef && typeof cryptoRef.randomUUID === "function") {
147
+ return `bulk:${cryptoRef.randomUUID()}`;
148
+ }
149
+ return `bulk:${seed}:${now()}`;
150
+ }
151
+ function coalesceLastOperations(count, options = {}) {
152
+ if (typeof window === "undefined" || count <= 1) return;
153
+ updateState((prev) => {
154
+ if (prev.stack.length < count) return prev;
155
+ const tail = prev.stack.slice(-count);
156
+ if (options.commandId && !tail.every((entry) => entry.commandId === options.commandId)) {
157
+ return prev;
158
+ }
159
+ const head = prev.stack.slice(0, prev.stack.length - count);
160
+ const last = tail[tail.length - 1];
161
+ const tokens = tail.map((entry) => entry.undoToken);
162
+ const bulkId = generateBulkId(last.id);
163
+ const synthetic = {
164
+ ...last,
165
+ id: bulkId,
166
+ undoToken: bulkId,
167
+ actionLabel: options.actionLabel ?? last.actionLabel,
168
+ resourceKind: options.resourceKind ?? last.resourceKind,
169
+ resourceId: null,
170
+ bulkUndoTokens: tokens,
171
+ bulkCount: tail.length,
172
+ receivedAt: now()
173
+ };
174
+ return { stack: [...head, synthetic], undone: prev.undone };
120
175
  });
121
176
  }
122
177
  function markRedoConsumed(logId) {
@@ -170,6 +225,7 @@ const operationStackConstants = {
170
225
  };
171
226
  export {
172
227
  clearAllOperations,
228
+ coalesceLastOperations,
173
229
  getLastOperation,
174
230
  hasRedoCandidate,
175
231
  markRedoConsumed,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/operations/store.ts"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { OperationMetadataPayload } from '@open-mercato/shared/lib/commands/operationMetadata'\n\nexport type OperationEntry = OperationMetadataPayload & {\n receivedAt: number\n}\n\nexport type UndoneEntry = OperationEntry & {\n undoneAt: number\n}\n\ntype OperationStoreState = {\n stack: OperationEntry[]\n undone: UndoneEntry[]\n}\n\nconst DEFAULT_STATE: OperationStoreState = { stack: [], undone: [] }\n\nconst STORAGE_KEY = 'om:last-operations:v1'\nconst STACK_LIMIT = 20\nconst LAST_OPERATION_TTL_MS = 60_000\nconst STACK_RETENTION_MS = 10 * 60_000\n\nlet internalState: OperationStoreState = DEFAULT_STATE\n\nif (typeof window !== 'undefined') {\n internalState = loadState()\n}\n\nconst emitter = new EventTarget()\n\nfunction now() {\n return typeof performance !== 'undefined' && performance.now\n ? Math.round(performance.timeOrigin + performance.now())\n : Date.now()\n}\n\nfunction loadState(): OperationStoreState {\n try {\n const raw = window.localStorage.getItem(STORAGE_KEY)\n if (!raw) return DEFAULT_STATE\n const parsed = JSON.parse(raw)\n if (!parsed || typeof parsed !== 'object') return DEFAULT_STATE\n const stack = Array.isArray(parsed.stack) ? parsed.stack.filter(isValidEntry).map(hydrateEntry) : []\n const undone = Array.isArray(parsed.undone)\n ? parsed.undone.filter(isValidEntry).map((raw: unknown) => {\n const hydrated = hydrateEntry(raw)\n const candidate = raw as { undoneAt?: unknown }\n const undoneAt = typeof candidate.undoneAt === 'number' ? candidate.undoneAt : now()\n return { ...hydrated, undoneAt }\n })\n : []\n return pruneState({ stack, undone })\n } catch {\n return DEFAULT_STATE\n }\n}\n\nfunction isValidEntry(entry: unknown): entry is OperationEntry {\n if (entry == null || typeof entry !== 'object') return false\n const candidate = entry as Record<string, unknown>\n return (\n typeof candidate.id === 'string'\n && typeof candidate.undoToken === 'string'\n && typeof candidate.commandId === 'string'\n && typeof candidate.receivedAt === 'number'\n && typeof candidate.executedAt === 'string'\n )\n}\n\nfunction hydrateEntry(entry: unknown): OperationEntry {\n const source = entry as Partial<OperationEntry> & Record<string, unknown>\n return {\n id: String(source.id),\n undoToken: String(source.undoToken),\n commandId: String(source.commandId),\n actionLabel: typeof source.actionLabel === 'string' ? source.actionLabel : null,\n resourceKind: typeof source.resourceKind === 'string' ? source.resourceKind : null,\n resourceId: typeof source.resourceId === 'string' ? source.resourceId : null,\n executedAt: typeof source.executedAt === 'string' ? source.executedAt : new Date((source.receivedAt as number | undefined) || now()).toISOString(),\n receivedAt: typeof source.receivedAt === 'number' ? source.receivedAt : now(),\n }\n}\n\nfunction persist(state: OperationStoreState) {\n if (typeof window === 'undefined') return\n try {\n window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state))\n } catch {\n // ignore storage quota errors\n }\n}\n\nfunction pruneState(state: OperationStoreState): OperationStoreState {\n const timestamp = now()\n const stack = state.stack\n .filter((entry, index, arr) => {\n // Deduplicate by id/undoToken keeping latest\n const duplicateIndex = arr.findIndex((candidate) => candidate.id === entry.id || candidate.undoToken === entry.undoToken)\n if (duplicateIndex !== index) return false\n return timestamp - entry.receivedAt <= STACK_RETENTION_MS\n })\n .sort((a, b) => a.receivedAt - b.receivedAt)\n .slice(-STACK_LIMIT)\n const undone = state.undone\n .filter((entry) => timestamp - entry.undoneAt <= STACK_RETENTION_MS)\n .sort((a, b) => a.undoneAt - b.undoneAt)\n .slice(-STACK_LIMIT)\n const next = { stack, undone }\n return next\n}\n\nfunction emit() {\n emitter.dispatchEvent(new Event('change'))\n}\n\nfunction updateState(updater: (prev: OperationStoreState) => OperationStoreState) {\n const next = pruneState(updater(internalState))\n internalState = next\n persist(next)\n emit()\n}\n\nfunction subscribe(listener: () => void) {\n const wrapped = () => listener()\n emitter.addEventListener('change', wrapped)\n return () => emitter.removeEventListener('change', wrapped)\n}\n\nfunction getClientSnapshot(): OperationStoreState {\n internalState = pruneState(internalState)\n return internalState\n}\n\nexport function useOperationStore<T>(selector: (state: OperationStoreState) => T): T {\n return React.useSyncExternalStore(\n subscribe,\n () => selector(getClientSnapshot()),\n () => selector(DEFAULT_STATE),\n )\n}\n\nexport function pushOperation(meta: OperationMetadataPayload) {\n if (typeof window === 'undefined') return\n updateState((prev) => {\n const entry: OperationEntry = {\n ...meta,\n receivedAt: now(),\n }\n const stack = prev.stack.filter((item) => item.id !== entry.id && item.undoToken !== entry.undoToken)\n stack.push(entry)\n return { stack, undone: [] }\n })\n}\n\nexport function markUndoSuccess(undoToken: string) {\n if (typeof window === 'undefined') return\n const removed: OperationEntry[] = []\n updateState((prev) => {\n const stack = prev.stack.filter((entry) => {\n if (entry.undoToken === undoToken) {\n removed.push(entry)\n return false\n }\n return true\n })\n const undone = removed.length\n ? [...prev.undone, ...removed.map((entry) => ({ ...entry, undoneAt: now() }))]\n : prev.undone\n return { stack, undone }\n })\n}\n\nexport function markRedoConsumed(logId: string) {\n if (typeof window === 'undefined') return\n updateState((prev) => ({\n stack: prev.stack,\n undone: prev.undone.filter((entry) => entry.id !== logId),\n }))\n}\n\nexport function getLastOperation(): OperationEntry | null {\n const state = getClientSnapshot()\n if (!state.stack.length) return null\n const last = state.stack[state.stack.length - 1]\n const lastExecuted = Date.parse(last.executedAt)\n const cutoff = now() - LAST_OPERATION_TTL_MS\n if (Number.isFinite(lastExecuted) && lastExecuted < cutoff) return null\n if (!Number.isFinite(lastExecuted) && last.receivedAt < cutoff) return null\n return last\n}\n\nexport function useLastOperation(): OperationEntry | null {\n return useOperationStore(getLastOperationFromState)\n}\n\nfunction getLastOperationFromState(state: OperationStoreState): OperationEntry | null {\n if (!state.stack.length) return null\n const last = state.stack[state.stack.length - 1]\n const timestamp = now()\n const executedAt = Date.parse(last.executedAt)\n const cutoff = timestamp - LAST_OPERATION_TTL_MS\n if (Number.isFinite(executedAt)) {\n return executedAt >= cutoff ? last : null\n }\n return last.receivedAt >= cutoff ? last : null\n}\n\nexport function useRedoCandidate(): UndoneEntry | null {\n return useOperationStore((state) => (state.undone.length ? state.undone[state.undone.length - 1] : null))\n}\n\nexport function hasRedoCandidate(logId: string): boolean {\n const state = getClientSnapshot()\n if (!state.undone.length) return false\n const top = state.undone[state.undone.length - 1]\n return top.id === logId\n}\n\nexport function clearAllOperations() {\n if (typeof window === 'undefined') return\n internalState = DEFAULT_STATE\n persist(internalState)\n emit()\n}\n\nexport const operationStackConstants = {\n LAST_OPERATION_TTL_MS,\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { OperationMetadataPayload } from '@open-mercato/shared/lib/commands/operationMetadata'\n\nexport type OperationEntry = OperationMetadataPayload & {\n receivedAt: number\n bulkUndoTokens?: string[]\n bulkCount?: number\n}\n\nexport type UndoneEntry = OperationEntry & {\n undoneAt: number\n}\n\ntype OperationStoreState = {\n stack: OperationEntry[]\n undone: UndoneEntry[]\n}\n\nconst DEFAULT_STATE: OperationStoreState = { stack: [], undone: [] }\n\nconst STORAGE_KEY = 'om:last-operations:v1'\nconst STACK_LIMIT = 20\nconst LAST_OPERATION_TTL_MS = 60_000\nconst STACK_RETENTION_MS = 10 * 60_000\n\nlet internalState: OperationStoreState = DEFAULT_STATE\n\nif (typeof window !== 'undefined') {\n internalState = loadState()\n}\n\nconst emitter = new EventTarget()\n\nfunction now() {\n return typeof performance !== 'undefined' && performance.now\n ? Math.round(performance.timeOrigin + performance.now())\n : Date.now()\n}\n\nfunction loadState(): OperationStoreState {\n try {\n const raw = window.localStorage.getItem(STORAGE_KEY)\n if (!raw) return DEFAULT_STATE\n const parsed = JSON.parse(raw)\n if (!parsed || typeof parsed !== 'object') return DEFAULT_STATE\n const stack = Array.isArray(parsed.stack) ? parsed.stack.filter(isValidEntry).map(hydrateEntry) : []\n const undone = Array.isArray(parsed.undone)\n ? parsed.undone.filter(isValidEntry).map((raw: unknown) => {\n const hydrated = hydrateEntry(raw)\n const candidate = raw as { undoneAt?: unknown }\n const undoneAt = typeof candidate.undoneAt === 'number' ? candidate.undoneAt : now()\n return { ...hydrated, undoneAt }\n })\n : []\n return pruneState({ stack, undone })\n } catch {\n return DEFAULT_STATE\n }\n}\n\nfunction isValidEntry(entry: unknown): entry is OperationEntry {\n if (entry == null || typeof entry !== 'object') return false\n const candidate = entry as Record<string, unknown>\n return (\n typeof candidate.id === 'string'\n && typeof candidate.undoToken === 'string'\n && typeof candidate.commandId === 'string'\n && typeof candidate.receivedAt === 'number'\n && typeof candidate.executedAt === 'string'\n )\n}\n\nfunction hydrateEntry(entry: unknown): OperationEntry {\n const source = entry as Partial<OperationEntry> & Record<string, unknown>\n const bulkTokens = Array.isArray(source.bulkUndoTokens)\n ? source.bulkUndoTokens.filter((t): t is string => typeof t === 'string' && t.length > 0)\n : undefined\n const bulkCount = typeof source.bulkCount === 'number' && Number.isFinite(source.bulkCount)\n ? source.bulkCount\n : undefined\n return {\n id: String(source.id),\n undoToken: String(source.undoToken),\n commandId: String(source.commandId),\n actionLabel: typeof source.actionLabel === 'string' ? source.actionLabel : null,\n resourceKind: typeof source.resourceKind === 'string' ? source.resourceKind : null,\n resourceId: typeof source.resourceId === 'string' ? source.resourceId : null,\n executedAt: typeof source.executedAt === 'string' ? source.executedAt : new Date((source.receivedAt as number | undefined) || now()).toISOString(),\n receivedAt: typeof source.receivedAt === 'number' ? source.receivedAt : now(),\n ...(bulkTokens && bulkTokens.length > 0 ? { bulkUndoTokens: bulkTokens } : {}),\n ...(bulkCount && bulkCount > 0 ? { bulkCount } : {}),\n }\n}\n\nfunction persist(state: OperationStoreState) {\n if (typeof window === 'undefined') return\n try {\n window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state))\n } catch {\n // ignore storage quota errors\n }\n}\n\nfunction pruneState(state: OperationStoreState): OperationStoreState {\n const timestamp = now()\n const stack = state.stack\n .filter((entry, index, arr) => {\n // Deduplicate by id/undoToken keeping latest\n const duplicateIndex = arr.findIndex((candidate) => candidate.id === entry.id || candidate.undoToken === entry.undoToken)\n if (duplicateIndex !== index) return false\n return timestamp - entry.receivedAt <= STACK_RETENTION_MS\n })\n .sort((a, b) => a.receivedAt - b.receivedAt)\n .slice(-STACK_LIMIT)\n const undone = state.undone\n .filter((entry) => timestamp - entry.undoneAt <= STACK_RETENTION_MS)\n .sort((a, b) => a.undoneAt - b.undoneAt)\n .slice(-STACK_LIMIT)\n const next = { stack, undone }\n return next\n}\n\nfunction emit() {\n emitter.dispatchEvent(new Event('change'))\n}\n\nfunction updateState(updater: (prev: OperationStoreState) => OperationStoreState) {\n const next = pruneState(updater(internalState))\n internalState = next\n persist(next)\n emit()\n}\n\nfunction subscribe(listener: () => void) {\n const wrapped = () => listener()\n emitter.addEventListener('change', wrapped)\n return () => emitter.removeEventListener('change', wrapped)\n}\n\nfunction getClientSnapshot(): OperationStoreState {\n internalState = pruneState(internalState)\n return internalState\n}\n\nexport function useOperationStore<T>(selector: (state: OperationStoreState) => T): T {\n return React.useSyncExternalStore(\n subscribe,\n () => selector(getClientSnapshot()),\n () => selector(DEFAULT_STATE),\n )\n}\n\nexport function pushOperation(meta: OperationMetadataPayload) {\n if (typeof window === 'undefined') return\n updateState((prev) => {\n const entry: OperationEntry = {\n ...meta,\n receivedAt: now(),\n }\n const stack = prev.stack.filter((item) => item.id !== entry.id && item.undoToken !== entry.undoToken)\n stack.push(entry)\n return { stack, undone: [] }\n })\n}\n\nexport function markUndoSuccess(undoTokens: string | string[]) {\n if (typeof window === 'undefined') return\n const tokenSet = new Set(Array.isArray(undoTokens) ? undoTokens : [undoTokens])\n if (tokenSet.size === 0) return\n const removed: OperationEntry[] = []\n updateState((prev) => {\n const nextStack: OperationEntry[] = []\n for (const entry of prev.stack) {\n const bulk = entry.bulkUndoTokens && entry.bulkUndoTokens.length > 0 ? entry.bulkUndoTokens : null\n if (!bulk) {\n if (tokenSet.has(entry.undoToken)) removed.push(entry)\n else nextStack.push(entry)\n continue\n }\n const consumed: string[] = []\n const remaining: string[] = []\n for (const token of bulk) {\n if (tokenSet.has(token)) consumed.push(token)\n else remaining.push(token)\n }\n if (consumed.length === 0) {\n nextStack.push(entry)\n } else if (remaining.length === 0) {\n removed.push(entry)\n } else {\n removed.push({ ...entry, bulkUndoTokens: consumed, bulkCount: consumed.length })\n nextStack.push({ ...entry, bulkUndoTokens: remaining, bulkCount: remaining.length })\n }\n }\n const undone = removed.length\n ? [...prev.undone, ...removed.map((entry) => ({ ...entry, undoneAt: now() }))]\n : prev.undone\n return { stack: nextStack, undone }\n })\n}\n\nexport type CoalesceOptions = {\n commandId?: string\n actionLabel?: string | null\n resourceKind?: string | null\n}\n\nfunction generateBulkId(seed: string): string {\n const cryptoRef = typeof globalThis !== 'undefined' ? (globalThis as { crypto?: { randomUUID?: () => string } }).crypto : undefined\n if (cryptoRef && typeof cryptoRef.randomUUID === 'function') {\n return `bulk:${cryptoRef.randomUUID()}`\n }\n return `bulk:${seed}:${now()}`\n}\n\nexport function coalesceLastOperations(count: number, options: CoalesceOptions = {}): void {\n if (typeof window === 'undefined' || count <= 1) return\n updateState((prev) => {\n if (prev.stack.length < count) return prev\n const tail = prev.stack.slice(-count)\n if (options.commandId && !tail.every((entry) => entry.commandId === options.commandId)) {\n return prev\n }\n const head = prev.stack.slice(0, prev.stack.length - count)\n const last = tail[tail.length - 1]\n const tokens = tail.map((entry) => entry.undoToken)\n const bulkId = generateBulkId(last.id)\n const synthetic: OperationEntry = {\n ...last,\n id: bulkId,\n undoToken: bulkId,\n actionLabel: options.actionLabel ?? last.actionLabel,\n resourceKind: options.resourceKind ?? last.resourceKind,\n resourceId: null,\n bulkUndoTokens: tokens,\n bulkCount: tail.length,\n receivedAt: now(),\n }\n return { stack: [...head, synthetic], undone: prev.undone }\n })\n}\n\nexport function markRedoConsumed(logId: string) {\n if (typeof window === 'undefined') return\n updateState((prev) => ({\n stack: prev.stack,\n undone: prev.undone.filter((entry) => entry.id !== logId),\n }))\n}\n\nexport function getLastOperation(): OperationEntry | null {\n const state = getClientSnapshot()\n if (!state.stack.length) return null\n const last = state.stack[state.stack.length - 1]\n const lastExecuted = Date.parse(last.executedAt)\n const cutoff = now() - LAST_OPERATION_TTL_MS\n if (Number.isFinite(lastExecuted) && lastExecuted < cutoff) return null\n if (!Number.isFinite(lastExecuted) && last.receivedAt < cutoff) return null\n return last\n}\n\nexport function useLastOperation(): OperationEntry | null {\n return useOperationStore(getLastOperationFromState)\n}\n\nfunction getLastOperationFromState(state: OperationStoreState): OperationEntry | null {\n if (!state.stack.length) return null\n const last = state.stack[state.stack.length - 1]\n const timestamp = now()\n const executedAt = Date.parse(last.executedAt)\n const cutoff = timestamp - LAST_OPERATION_TTL_MS\n if (Number.isFinite(executedAt)) {\n return executedAt >= cutoff ? last : null\n }\n return last.receivedAt >= cutoff ? last : null\n}\n\nexport function useRedoCandidate(): UndoneEntry | null {\n return useOperationStore((state) => (state.undone.length ? state.undone[state.undone.length - 1] : null))\n}\n\nexport function hasRedoCandidate(logId: string): boolean {\n const state = getClientSnapshot()\n if (!state.undone.length) return false\n const top = state.undone[state.undone.length - 1]\n return top.id === logId\n}\n\nexport function clearAllOperations() {\n if (typeof window === 'undefined') return\n internalState = DEFAULT_STATE\n persist(internalState)\n emit()\n}\n\nexport const operationStackConstants = {\n LAST_OPERATION_TTL_MS,\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["raw"]
7
7
  }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ async function runBulkDelete(rows, deleteOne, options) {
3
+ const fallback = options?.fallbackErrorMessage ?? "Delete failed";
4
+ const logTag = options?.logTag;
5
+ const succeeded = [];
6
+ const failures = [];
7
+ for (const row of rows) {
8
+ try {
9
+ await deleteOne(row);
10
+ succeeded.push(row);
11
+ } catch (err) {
12
+ const message = err instanceof Error && err.message.trim().length > 0 ? err.message : fallback;
13
+ const rawCode = err && typeof err === "object" ? err.code : void 0;
14
+ const code = typeof rawCode === "string" && rawCode.length > 0 ? rawCode : null;
15
+ failures.push({ id: row.id, code, message });
16
+ if (logTag && typeof console !== "undefined" && typeof console.warn === "function") {
17
+ console.warn(`[${logTag}] bulk delete failed`, { id: row.id, code, message, err });
18
+ }
19
+ }
20
+ }
21
+ return { succeeded, failures };
22
+ }
23
+ function groupBulkDeleteFailures(failures) {
24
+ const buckets = /* @__PURE__ */ new Map();
25
+ for (const failure of failures) {
26
+ const key = failure.code || failure.message || "unknown";
27
+ const existing = buckets.get(key);
28
+ if (existing) {
29
+ existing.count += 1;
30
+ existing.ids.push(failure.id);
31
+ continue;
32
+ }
33
+ buckets.set(key, {
34
+ key,
35
+ count: 1,
36
+ sampleMessage: failure.message,
37
+ ids: [failure.id]
38
+ });
39
+ }
40
+ return Array.from(buckets.values());
41
+ }
42
+ export {
43
+ groupBulkDeleteFailures,
44
+ runBulkDelete
45
+ };
46
+ //# sourceMappingURL=bulkDelete.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/utils/bulkDelete.ts"],
4
+ "sourcesContent": ["\"use client\"\n\nexport type BulkDeleteFailure = {\n id: string\n code: string | null\n message: string\n}\n\nexport type BulkDeleteOutcome<T extends { id: string }> = {\n succeeded: T[]\n failures: BulkDeleteFailure[]\n}\n\nexport type BulkDeleteOptions = {\n fallbackErrorMessage?: string\n logTag?: string\n}\n\nexport async function runBulkDelete<T extends { id: string }>(\n rows: T[],\n deleteOne: (row: T) => Promise<void>,\n options?: BulkDeleteOptions,\n): Promise<BulkDeleteOutcome<T>> {\n const fallback = options?.fallbackErrorMessage ?? 'Delete failed'\n const logTag = options?.logTag\n const succeeded: T[] = []\n const failures: BulkDeleteFailure[] = []\n for (const row of rows) {\n try {\n await deleteOne(row)\n succeeded.push(row)\n } catch (err) {\n const message =\n err instanceof Error && err.message.trim().length > 0 ? err.message : fallback\n const rawCode =\n err && typeof err === 'object' ? (err as { code?: unknown }).code : undefined\n const code = typeof rawCode === 'string' && rawCode.length > 0 ? rawCode : null\n failures.push({ id: row.id, code, message })\n if (logTag && typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn(`[${logTag}] bulk delete failed`, { id: row.id, code, message, err })\n }\n }\n }\n return { succeeded, failures }\n}\n\nexport type BulkDeleteFailureGroup = {\n key: string\n count: number\n sampleMessage: string\n ids: string[]\n}\n\n// Keyed by `code` when present so semantically identical failures collapse into\n// one toast even if their message text varies (e.g. row-specific dependency\n// counts). Falls back to the raw message when the server did not surface a code.\nexport function groupBulkDeleteFailures(\n failures: BulkDeleteFailure[],\n): BulkDeleteFailureGroup[] {\n const buckets = new Map<string, BulkDeleteFailureGroup>()\n for (const failure of failures) {\n const key = failure.code || failure.message || 'unknown'\n const existing = buckets.get(key)\n if (existing) {\n existing.count += 1\n existing.ids.push(failure.id)\n continue\n }\n buckets.set(key, {\n key,\n count: 1,\n sampleMessage: failure.message,\n ids: [failure.id],\n })\n }\n return Array.from(buckets.values())\n}\n"],
5
+ "mappings": ";AAkBA,eAAsB,cACpB,MACA,WACA,SAC+B;AAC/B,QAAM,WAAW,SAAS,wBAAwB;AAClD,QAAM,SAAS,SAAS;AACxB,QAAM,YAAiB,CAAC;AACxB,QAAM,WAAgC,CAAC;AACvC,aAAW,OAAO,MAAM;AACtB,QAAI;AACF,YAAM,UAAU,GAAG;AACnB,gBAAU,KAAK,GAAG;AAAA,IACpB,SAAS,KAAK;AACZ,YAAM,UACJ,eAAe,SAAS,IAAI,QAAQ,KAAK,EAAE,SAAS,IAAI,IAAI,UAAU;AACxE,YAAM,UACJ,OAAO,OAAO,QAAQ,WAAY,IAA2B,OAAO;AACtE,YAAM,OAAO,OAAO,YAAY,YAAY,QAAQ,SAAS,IAAI,UAAU;AAC3E,eAAS,KAAK,EAAE,IAAI,IAAI,IAAI,MAAM,QAAQ,CAAC;AAC3C,UAAI,UAAU,OAAO,YAAY,eAAe,OAAO,QAAQ,SAAS,YAAY;AAClF,gBAAQ,KAAK,IAAI,MAAM,wBAAwB,EAAE,IAAI,IAAI,IAAI,MAAM,SAAS,IAAI,CAAC;AAAA,MACnF;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,WAAW,SAAS;AAC/B;AAYO,SAAS,wBACd,UAC0B;AAC1B,QAAM,UAAU,oBAAI,IAAoC;AACxD,aAAW,WAAW,UAAU;AAC9B,UAAM,MAAM,QAAQ,QAAQ,QAAQ,WAAW;AAC/C,UAAM,WAAW,QAAQ,IAAI,GAAG;AAChC,QAAI,UAAU;AACZ,eAAS,SAAS;AAClB,eAAS,IAAI,KAAK,QAAQ,EAAE;AAC5B;AAAA,IACF;AACA,YAAQ,IAAI,KAAK;AAAA,MACf;AAAA,MACA,OAAO;AAAA,MACP,eAAe,QAAQ;AAAA,MACvB,KAAK,CAAC,QAAQ,EAAE;AAAA,IAClB,CAAC;AAAA,EACH;AACA,SAAO,MAAM,KAAK,QAAQ,OAAO,CAAC;AACpC;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.1-develop.3227.1.2b7b8ab258",
3
+ "version": "0.6.1-develop.3256.1.fe3dec2464",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -140,12 +140,12 @@
140
140
  "remark-gfm": "^4.0.1"
141
141
  },
142
142
  "peerDependencies": {
143
- "@open-mercato/shared": "0.6.1-develop.3227.1.2b7b8ab258",
143
+ "@open-mercato/shared": "0.6.1-develop.3256.1.fe3dec2464",
144
144
  "react": ">=18.0.0",
145
145
  "react-dom": ">=18.0.0"
146
146
  },
147
147
  "devDependencies": {
148
- "@open-mercato/shared": "0.6.1-develop.3227.1.2b7b8ab258",
148
+ "@open-mercato/shared": "0.6.1-develop.3256.1.fe3dec2464",
149
149
  "@testing-library/dom": "^10.4.1",
150
150
  "@testing-library/jest-dom": "^6.9.1",
151
151
  "@testing-library/react": "^16.3.1",
@@ -131,6 +131,7 @@ const DEFAULT_AGENTS_ENDPOINT = '/api/ai_assistant/ai/agents'
131
131
  const DEFAULT_HEALTH_ENDPOINT = '/api/ai_assistant/health'
132
132
  const AI_ASSISTANT_DOCS_URL = 'https://docs.openmercato.com/framework/ai-assistant/overview'
133
133
  const AI_ASSISTANT_SETTINGS_DOCS_URL = 'https://docs.openmercato.com/framework/ai-assistant/settings'
134
+ export const AI_ASSISTANT_LAUNCHER_OPEN_EVENT = 'om:open-ai-assistant-launcher'
134
135
 
135
136
  function isMutationCapable(policy: string | null | undefined): boolean {
136
137
  return policy === 'confirm-required' || policy === 'destructive-confirm-required'
@@ -319,6 +320,13 @@ export function AiAssistantLauncher({
319
320
  setPickerOpen(true)
320
321
  }, [])
321
322
 
323
+ React.useEffect(() => {
324
+ if (typeof window === 'undefined') return
325
+ const listener = () => openPicker()
326
+ window.addEventListener(AI_ASSISTANT_LAUNCHER_OPEN_EVENT, listener)
327
+ return () => window.removeEventListener(AI_ASSISTANT_LAUNCHER_OPEN_EVENT, listener)
328
+ }, [openPicker])
329
+
322
330
  const handleSelectAgent = React.useCallback((agent: AiAssistantLauncherAgent) => {
323
331
  if (dock.state.assistant?.agent === agent.id) {
324
332
  dock.dock(dock.state.assistant)