@gravity-rail/sdk 0.46.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +116 -8
  2. package/dist/agui.cjs +17 -2
  3. package/dist/agui.cjs.map +1 -1
  4. package/dist/agui.d.cts +9 -1
  5. package/dist/agui.d.ts +9 -1
  6. package/dist/agui.js +15 -1
  7. package/dist/agui.js.map +1 -1
  8. package/dist/{chat-interaction-0MSzmdxz.d.cts → chat-interaction-D4Edo92W.d.ts} +273 -48
  9. package/dist/{chat-interaction-CO9ecTSn.d.ts → chat-interaction-e5vHKt5j.d.cts} +273 -48
  10. package/dist/{chunk-72SWNLVD.js → chunk-MMDSZ25F.js} +1 -1
  11. package/dist/chunk-MMDSZ25F.js.map +1 -0
  12. package/dist/{chunk-67QXTQVL.js → chunk-TGL72KPS.js} +59 -118
  13. package/dist/chunk-TGL72KPS.js.map +1 -0
  14. package/dist/{chunk-I7BMDNPO.js → chunk-XQTPXOBU.js} +249 -50
  15. package/dist/chunk-XQTPXOBU.js.map +1 -0
  16. package/dist/helpers.cjs.map +1 -1
  17. package/dist/helpers.d.cts +4 -4
  18. package/dist/helpers.d.ts +4 -4
  19. package/dist/helpers.js +1 -1
  20. package/dist/index.cjs +307 -164
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +7 -6
  23. package/dist/index.d.ts +7 -6
  24. package/dist/index.js +9 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/internal.cjs +307 -164
  27. package/dist/internal.cjs.map +1 -1
  28. package/dist/internal.d.cts +148 -6
  29. package/dist/internal.d.ts +148 -6
  30. package/dist/internal.js +9 -3
  31. package/dist/internal.js.map +1 -1
  32. package/dist/{member-import-D7U4kDIy.d.cts → member-import-9zHkUjcn.d.cts} +1 -1
  33. package/dist/{member-import-C2Xt_o5d.d.ts → member-import-ocJ_K474.d.ts} +1 -1
  34. package/dist/{revision-state-Cc3-CIi5.d.ts → path-CQBykGoj.d.cts} +64 -3
  35. package/dist/{revision-state-m6Cjyr4M.d.cts → path-DVWLvQS-.d.ts} +64 -3
  36. package/dist/schemas.cjs +60 -15
  37. package/dist/schemas.cjs.map +1 -1
  38. package/dist/schemas.d.cts +34 -3
  39. package/dist/schemas.d.ts +34 -3
  40. package/dist/schemas.js +60 -16
  41. package/dist/schemas.js.map +1 -1
  42. package/dist/{tasks-Ca6yfsj2.d.cts → tasks-DsNsbco5.d.cts} +66 -14
  43. package/dist/{tasks-Ca6yfsj2.d.ts → tasks-DsNsbco5.d.ts} +66 -14
  44. package/package.json +2 -2
  45. package/dist/chunk-67QXTQVL.js.map +0 -1
  46. package/dist/chunk-72SWNLVD.js.map +0 -1
  47. package/dist/chunk-I7BMDNPO.js.map +0 -1
package/README.md CHANGED
@@ -35,9 +35,10 @@ const client = new GravityRailClient(
35
35
  'https://api.gravityrail.com'
36
36
  );
37
37
 
38
- const workspaces = await client.getWorkspaces();
39
- if (workspaces.length === 0) throw new Error('No workspace available');
40
- const workspaceId = workspaces[0].id;
38
+ const workspaces = await client.getWorkspaces({ page: 1, pageSize: 20, search: 'clinic' });
39
+ // Read workspaces.items; request the next page explicitly when needed.
40
+ if (workspaces.items.length === 0) throw new Error('No workspace on this page; search or request another page');
41
+ const workspaceId = workspaces.items[0].uuid;
41
42
  const workflows = await client.getWorkflows(workspaceId);
42
43
  const chats = await client.getChats(workspaceId);
43
44
  ```
@@ -96,17 +97,29 @@ Select the required scopes and expiry using the [granular scope system](#scopes-
96
97
 
97
98
  ### OAuth2 with PKCE
98
99
 
99
- For browser-based applications:
100
+ For browser-based applications on your own origin, complete the PKCE flow and pass the access token it returns as the client's credential — it is sent as `Authorization: Bearer`:
100
101
 
101
102
  ```typescript
102
- const client = new GravityRailClient(undefined, 'https://api.gravityrail.com');
103
+ const client = new GravityRailClient(accessToken, 'https://api.gravityrail.com');
103
104
 
104
105
  // Handle step-up authentication when the API requires it
105
106
  client.setEnhancedAuthHandler(async (authInfo) => {
106
- // Redirect to login or show auth modal
107
+ // Redirect to login or show auth modal; resolve with the new token
107
108
  });
108
109
  ```
109
110
 
111
+ ### Same-origin session
112
+
113
+ A client built **without** a credential is a same-origin session client: it sends no `Authorization` header and fetches with `credentials: 'same-origin'`, so the browser presents the httpOnly session cookie of the origin the page is on, and only to that origin. It never reads a cookie to build a credential. This is how Gravity Rail's own web app calls the API (it serves `/api/v2` on its own origin); point `baseUrl` at the page's origin:
114
+
115
+ ```typescript
116
+ const client = new GravityRailClient(undefined, window.location.origin);
117
+ ```
118
+
119
+ A cross-origin `baseUrl` with no credential makes unauthenticated calls.
120
+
121
+ When an organization requires SSO, a 401 carrying `WWW-Authenticate: Bearer realm="org"` calls your `setOrgAuthHandler` handler once. A bearer client's handler must hand back the org session with `client.setOrgSession(session, orgUuid)` (sent as `X-Org-Session`); a same-origin session client is retried once after the handler resolves, carrying the httpOnly org-session cookie the sign-in left.
122
+
110
123
  ## Core Concepts
111
124
 
112
125
  Every Gravity Rail workspace is a self-contained environment with isolated data, workflows, and configuration. The SDK organizes its 500+ methods by domain:
@@ -352,6 +365,8 @@ const operators = await client.getLiveOperators(workspaceId);
352
365
 
353
366
  ### Sites & Portals
354
367
 
368
+ Site record write methods may return a successful value-free receipt: `fieldValues` is empty and `memberId` / `memberName` / `externalId` are null unless Reading Records, level-2 sign-in verification, and the row read policy all allow values. This includes partial updates, resubmissions and upserts. The write still succeeds; use a permitted record read to load stored values.
369
+
355
370
  ```typescript
356
371
  // Build customer-facing portals with AI chat built in
357
372
  const site = await client.createSite(workspaceId, {
@@ -372,13 +387,42 @@ await client.crawlSite(workspaceId, site.id, { url: 'https://docs.example.com' }
372
387
 
373
388
  ### Workspace Management
374
389
 
390
+ Workspace and organization directories return `{ items, page, pageSize, total, totalPages }`. Read
391
+ `items` instead of treating the response as an array. Pass `page`, `pageSize` (at
392
+ most 100), and `search` to request a page. This applies to `getWorkspaces`,
393
+ `getDeviceAdmissibleWorkspaces`, `getPublicWorkspaces`, `getMyOrganizations`, and
394
+ `getOrganizationWorkspaceSummaries`.
395
+ Organization member details no longer embed workspace lists; request organization
396
+ workspace summary pages instead.
397
+ `getMyOrganizations` also accepts `organizationSlug` for exact host resolution
398
+ and `organizationUuids` (at most 100 UUIDs) for a bounded membership lookup. These
399
+ filters only narrow the current account's memberships.
400
+ Account and device workspace totals count eligible global membership candidates;
401
+ workspace admission can make a page sparse or empty. Use pagination metadata to
402
+ reach later candidates.
403
+
404
+ `getOrgWorkspaceUsage(orgUuid, { page, pageSize, search })` returns a bounded
405
+ `workspaces` list with `page`, `pageSize`, `total`, and `totalPages` metadata.
406
+ Organization usage totals remain global and can be null when measurement coverage
407
+ is incomplete. Do not sum the visible cards to calculate an organization total.
408
+
375
409
  ```typescript
376
410
  // Workspaces this credential can reach
377
- const workspaces = await client.getWorkspaces();
411
+ const workspaces = await client.getWorkspaces({ page: 1, pageSize: 20, search: 'clinic' });
412
+ // Read workspaces.items; request the next page explicitly when needed.
378
413
 
379
414
  // Companion-device picker (Lantern): compact rows for only the
380
415
  // workspaces a paired device could enter — no device credential needed
381
- const deviceWorkspaces = await client.getDeviceAdmissibleWorkspaces();
416
+ const deviceWorkspaces = await client.getDeviceAdmissibleWorkspaces({
417
+ page: 1, pageSize: 20, search: 'clinic',
418
+ });
419
+ // Read deviceWorkspaces.items; a sparse page can still have later candidates.
420
+
421
+ // Public workspace directory
422
+ const publicWorkspaces = await client.getPublicWorkspaces({
423
+ page: 1, pageSize: 20, search: 'clinic',
424
+ });
425
+ // Read publicWorkspaces.items and use total/page/pageSize for pagination.
382
426
 
383
427
  // Export/import full workspace configuration
384
428
  const config = await client.exportWorkspace(workspaceId);
@@ -393,6 +437,70 @@ const clientWs = await client.createClientWorkspace(workspaceId, {
393
437
  await client.provisionClientWorkspace(workspaceId, clientWs.id);
394
438
  ```
395
439
 
440
+ ## Export Workflow revisions
441
+
442
+ Workspace JSON and ZIP exports default to active snapshots. Request
443
+ `revisionMode: 'all'` to include historical revisions and drafts. The version `2.0` archive carries isolated
444
+ `workflowRevisions` graphs and requires a revision-aware server for import. Import
445
+ it as new Workflows to preserve active and draft roles; an existing Workflow match
446
+ is refused before writes. Legacy version `1.0` imports keep their existing behavior.
447
+
448
+ ```typescript
449
+ const archive = await client.exportWorkspace(workspaceUuid, undefined, undefined, undefined, {
450
+ revisionMode: 'all',
451
+ });
452
+ const selected = await client.exportWorkspace(workspaceUuid, workflowId, undefined, undefined, {
453
+ revisionId: 87,
454
+ });
455
+ const active = await client.exportWorkspace(workspaceUuid, undefined, undefined, undefined, {
456
+ revisionMode: 'active',
457
+ });
458
+ ```
459
+
460
+ The same options apply to `exportWorkspaceZip`. A selected revision requires a
461
+ Workflow ID and cannot be combined with active mode. Selected and active snapshots
462
+ import into an editable draft and do not change an existing active revision.
463
+
464
+ The public workspace export API supports only the `channels` exclusion in every
465
+ revision mode. All-revision archives preserve that exclusion. Other public
466
+ exclusion values are rejected.
467
+
468
+ The SDK throws when a legacy server returns an active-only snapshot for an
469
+ all-revision or selected export.
470
+ JSON validation checks selection metadata and internal Workflow/revision identities.
471
+ ZIP validation checks provenance headers generated from the actual exported data,
472
+ before reading the archive. It adds no archive size limit or permission requirement.
473
+ Deploy a server with ZIP provenance headers before releasing this client update;
474
+ servers with revision-aware JSON alone still fail the ZIP guard. Explicit
475
+ `revisionMode: 'active'` exports remain compatible with legacy servers.
476
+
477
+ ZIP provenance uses `X-Gravityrail-Export-Version` (`2.0` for all revisions,
478
+ `1.0` for active and selected snapshots) and `X-Gravityrail-Export-Selection`
479
+ (`active`, `all`, or `selected`). A selected ZIP also includes nonempty
480
+ `X-Gravityrail-Export-Revision-Uuid` and `X-Gravityrail-Export-Workflow-Uuid`.
481
+ These values come from the exported artifact. Explicit active exports do not
482
+ require these headers.
483
+
484
+ Shared dependency definitions are current exported Forms, files, and integrations,
485
+ not historical snapshots of those resources.
486
+
487
+ ## Chat history (`@gravity-rail/sdk/agui`)
488
+
489
+ `mergeChatHistoryWindow(existing, fetched)` merges an authoritative chronological
490
+ history window into an existing transcript. If the first fetched ID appears in
491
+ the existing transcript, the helper retains only the earlier prefix and replaces
492
+ the covered tail. It removes prefix entries whose IDs also appear in the fetched
493
+ window; it does not remove repeated IDs within the retained prefix. If the first
494
+ ID is unknown, or the fetched window is empty, the result is the fetched window alone.
495
+ Message text does not identify a message. Fetch the needed history pages before
496
+ calling this helper; it does not fetch or filter messages.
497
+
498
+ ```ts
499
+ import { mergeChatHistoryWindow } from '@gravity-rail/sdk/agui';
500
+
501
+ const messages = mergeChatHistoryWindow(existingMessages, fetchedMessages);
502
+ ```
503
+
396
504
  ## Helpers (`@gravity-rail/sdk/helpers`)
397
505
 
398
506
  Small compound operations built on the client methods, for the common
package/dist/agui.cjs CHANGED
@@ -24,7 +24,8 @@ __export(agui_exports, {
24
24
  getAguiTextMessageContent: () => getAguiTextMessageContent,
25
25
  getAguiTextMessageId: () => getAguiTextMessageId,
26
26
  getAguiTextMessageMemberId: () => getAguiTextMessageMemberId,
27
- getAguiTextMessageRole: () => getAguiTextMessageRole
27
+ getAguiTextMessageRole: () => getAguiTextMessageRole,
28
+ mergeChatHistoryWindow: () => mergeChatHistoryWindow
28
29
  });
29
30
  module.exports = __toCommonJS(agui_exports);
30
31
 
@@ -79,12 +80,26 @@ function applyAguiTextMessageEvent(messages, event) {
79
80
  }
80
81
  return false;
81
82
  }
83
+
84
+ // src/agui/chat-history.ts
85
+ function mergeChatHistoryWindow(existingMessages, fetchedMessages) {
86
+ const joinsAt = fetchedMessages.length > 0 ? existingMessages.findIndex((msg) => msg.id === fetchedMessages[0].id) : -1;
87
+ if (joinsAt > 0) {
88
+ const inPage = new Set(fetchedMessages.map((msg) => msg.id));
89
+ return [
90
+ ...existingMessages.slice(0, joinsAt).filter((msg) => !inPage.has(msg.id)),
91
+ ...fetchedMessages
92
+ ];
93
+ }
94
+ return fetchedMessages;
95
+ }
82
96
  // Annotate the CommonJS export names for ESM import in node:
83
97
  0 && (module.exports = {
84
98
  applyAguiTextMessageEvent,
85
99
  getAguiTextMessageContent,
86
100
  getAguiTextMessageId,
87
101
  getAguiTextMessageMemberId,
88
- getAguiTextMessageRole
102
+ getAguiTextMessageRole,
103
+ mergeChatHistoryWindow
89
104
  });
90
105
  //# sourceMappingURL=agui.cjs.map
package/dist/agui.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/agui/index.ts","../src/agui/text-messages.ts"],"sourcesContent":["export {\n applyAguiTextMessageEvent,\n getAguiTextMessageContent,\n getAguiTextMessageId,\n getAguiTextMessageMemberId,\n getAguiTextMessageRole,\n type TranscriptMessage,\n} from './text-messages';\n","import type { ChannelEvent } from '../realtime/types';\n\nexport interface TranscriptMessage {\n id: string;\n role: string;\n content: string;\n /** Authoring member id when TEXT_MESSAGE_START carried it (Gravity extension). */\n memberId?: number;\n}\n\nexport function getAguiTextMessageId(event: ChannelEvent): string | null {\n const raw = event.messageId ?? event.message_id;\n return typeof raw === 'string' && raw.length > 0 ? raw : null;\n}\n\nexport function getAguiTextMessageContent(event: ChannelEvent): string | null {\n const raw = event.content ?? event.text ?? event.delta;\n return typeof raw === 'string' ? raw : null;\n}\n\nexport function getAguiTextMessageRole(event: ChannelEvent, fallback = 'assistant'): string {\n const raw = event.role;\n return typeof raw === 'string' && raw.length > 0 ? raw : fallback;\n}\n\n/** Gravity extension: authoring member on TEXT_MESSAGE_START (camel or snake). */\nexport function getAguiTextMessageMemberId(event: ChannelEvent): number | undefined {\n const raw = event.memberId ?? event.member_id;\n return typeof raw === 'number' && Number.isFinite(raw) ? raw : undefined;\n}\n\nexport function applyAguiTextMessageEvent(\n messages: Map<string, TranscriptMessage>,\n event: ChannelEvent\n): boolean {\n const type = event.type;\n if (typeof type !== 'string') return false;\n\n if (type === 'TEXT_MESSAGE_START') {\n const messageId = getAguiTextMessageId(event);\n if (!messageId) return false;\n const role = getAguiTextMessageRole(event);\n const memberId = getAguiTextMessageMemberId(event);\n messages.set(messageId, {\n id: messageId,\n role,\n content: '',\n ...(memberId !== undefined ? { memberId } : {}),\n });\n return true;\n }\n\n if (type === 'TEXT_MESSAGE_CONTENT') {\n const messageId = getAguiTextMessageId(event);\n const content = getAguiTextMessageContent(event);\n if (!messageId || content === null) return false;\n\n // CONTENT does not carry memberId per protocol — only START does.\n // Synthesize a bare message if START was missed; leave memberId unset.\n const existing =\n messages.get(messageId) ??\n ({\n id: messageId,\n role: getAguiTextMessageRole(event),\n content: '',\n } satisfies TranscriptMessage);\n\n existing.content = content;\n messages.set(messageId, existing);\n return true;\n }\n\n if (type === 'TEXT_MESSAGE_END') {\n return Boolean(getAguiTextMessageId(event));\n }\n\n return false;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACUO,SAAS,qBAAqB,OAAoC;AACtE,QAAM,MAAM,MAAM,aAAa,MAAM;AACrC,SAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,MAAM;AAC5D;AAEO,SAAS,0BAA0B,OAAoC;AAC3E,QAAM,MAAM,MAAM,WAAW,MAAM,QAAQ,MAAM;AACjD,SAAO,OAAO,QAAQ,WAAW,MAAM;AAC1C;AAEO,SAAS,uBAAuB,OAAqB,WAAW,aAAqB;AACzF,QAAM,MAAM,MAAM;AAClB,SAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,MAAM;AAC5D;AAGO,SAAS,2BAA2B,OAAyC;AACjF,QAAM,MAAM,MAAM,YAAY,MAAM;AACpC,SAAO,OAAO,QAAQ,YAAY,OAAO,SAAS,GAAG,IAAI,MAAM;AAClE;AAEO,SAAS,0BACb,UACA,OACQ;AACR,QAAM,OAAO,MAAM;AACnB,MAAI,OAAO,SAAS,SAAU,QAAO;AAErC,MAAI,SAAS,sBAAsB;AAChC,UAAM,YAAY,qBAAqB,KAAK;AAC5C,QAAI,CAAC,UAAW,QAAO;AACvB,UAAM,OAAO,uBAAuB,KAAK;AACzC,UAAM,WAAW,2BAA2B,KAAK;AACjD,aAAS,IAAI,WAAW;AAAA,MACrB,IAAI;AAAA,MACJ;AAAA,MACA,SAAS;AAAA,MACT,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAAA,IAChD,CAAC;AACD,WAAO;AAAA,EACV;AAEA,MAAI,SAAS,wBAAwB;AAClC,UAAM,YAAY,qBAAqB,KAAK;AAC5C,UAAM,UAAU,0BAA0B,KAAK;AAC/C,QAAI,CAAC,aAAa,YAAY,KAAM,QAAO;AAI3C,UAAM,WACH,SAAS,IAAI,SAAS,KACrB;AAAA,MACE,IAAI;AAAA,MACJ,MAAM,uBAAuB,KAAK;AAAA,MAClC,SAAS;AAAA,IACZ;AAEH,aAAS,UAAU;AACnB,aAAS,IAAI,WAAW,QAAQ;AAChC,WAAO;AAAA,EACV;AAEA,MAAI,SAAS,oBAAoB;AAC9B,WAAO,QAAQ,qBAAqB,KAAK,CAAC;AAAA,EAC7C;AAEA,SAAO;AACV;","names":[]}
1
+ {"version":3,"sources":["../src/agui/index.ts","../src/agui/text-messages.ts","../src/agui/chat-history.ts"],"sourcesContent":["export {\n applyAguiTextMessageEvent,\n getAguiTextMessageContent,\n getAguiTextMessageId,\n getAguiTextMessageMemberId,\n getAguiTextMessageRole,\n type TranscriptMessage,\n} from './text-messages';\nexport { mergeChatHistoryWindow } from './chat-history';\n","import type { ChannelEvent } from '../realtime/types';\n\nexport interface TranscriptMessage {\n id: string;\n role: string;\n content: string;\n /** Authoring member id when TEXT_MESSAGE_START carried it (Gravity extension). */\n memberId?: number;\n}\n\nexport function getAguiTextMessageId(event: ChannelEvent): string | null {\n const raw = event.messageId ?? event.message_id;\n return typeof raw === 'string' && raw.length > 0 ? raw : null;\n}\n\nexport function getAguiTextMessageContent(event: ChannelEvent): string | null {\n const raw = event.content ?? event.text ?? event.delta;\n return typeof raw === 'string' ? raw : null;\n}\n\nexport function getAguiTextMessageRole(event: ChannelEvent, fallback = 'assistant'): string {\n const raw = event.role;\n return typeof raw === 'string' && raw.length > 0 ? raw : fallback;\n}\n\n/** Gravity extension: authoring member on TEXT_MESSAGE_START (camel or snake). */\nexport function getAguiTextMessageMemberId(event: ChannelEvent): number | undefined {\n const raw = event.memberId ?? event.member_id;\n return typeof raw === 'number' && Number.isFinite(raw) ? raw : undefined;\n}\n\nexport function applyAguiTextMessageEvent(\n messages: Map<string, TranscriptMessage>,\n event: ChannelEvent\n): boolean {\n const type = event.type;\n if (typeof type !== 'string') return false;\n\n if (type === 'TEXT_MESSAGE_START') {\n const messageId = getAguiTextMessageId(event);\n if (!messageId) return false;\n const role = getAguiTextMessageRole(event);\n const memberId = getAguiTextMessageMemberId(event);\n messages.set(messageId, {\n id: messageId,\n role,\n content: '',\n ...(memberId !== undefined ? { memberId } : {}),\n });\n return true;\n }\n\n if (type === 'TEXT_MESSAGE_CONTENT') {\n const messageId = getAguiTextMessageId(event);\n const content = getAguiTextMessageContent(event);\n if (!messageId || content === null) return false;\n\n // CONTENT does not carry memberId per protocol — only START does.\n // Synthesize a bare message if START was missed; leave memberId unset.\n const existing =\n messages.get(messageId) ??\n ({\n id: messageId,\n role: getAguiTextMessageRole(event),\n content: '',\n } satisfies TranscriptMessage);\n\n existing.content = content;\n messages.set(messageId, existing);\n return true;\n }\n\n if (type === 'TEXT_MESSAGE_END') {\n return Boolean(getAguiTextMessageId(event));\n }\n\n return false;\n}\n","/** Keep loaded messages before a fetched window; the fetched tail is authoritative.\n * With no shared first message, the windows do not join and the fetched page stands alone.\n * Extracted from the main app's useChatMessages cache merge.\n */\nexport function mergeChatHistoryWindow<T extends { id: string }>(\n existingMessages: T[],\n fetchedMessages: T[]\n): T[] {\n const joinsAt =\n fetchedMessages.length > 0\n ? existingMessages.findIndex((msg) => msg.id === fetchedMessages[0].id)\n : -1;\n if (joinsAt > 0) {\n const inPage = new Set(fetchedMessages.map((msg) => msg.id));\n return [\n ...existingMessages.slice(0, joinsAt).filter((msg) => !inPage.has(msg.id)),\n ...fetchedMessages,\n ];\n }\n return fetchedMessages;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACUO,SAAS,qBAAqB,OAAoC;AACtE,QAAM,MAAM,MAAM,aAAa,MAAM;AACrC,SAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,MAAM;AAC5D;AAEO,SAAS,0BAA0B,OAAoC;AAC3E,QAAM,MAAM,MAAM,WAAW,MAAM,QAAQ,MAAM;AACjD,SAAO,OAAO,QAAQ,WAAW,MAAM;AAC1C;AAEO,SAAS,uBAAuB,OAAqB,WAAW,aAAqB;AACzF,QAAM,MAAM,MAAM;AAClB,SAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,MAAM;AAC5D;AAGO,SAAS,2BAA2B,OAAyC;AACjF,QAAM,MAAM,MAAM,YAAY,MAAM;AACpC,SAAO,OAAO,QAAQ,YAAY,OAAO,SAAS,GAAG,IAAI,MAAM;AAClE;AAEO,SAAS,0BACb,UACA,OACQ;AACR,QAAM,OAAO,MAAM;AACnB,MAAI,OAAO,SAAS,SAAU,QAAO;AAErC,MAAI,SAAS,sBAAsB;AAChC,UAAM,YAAY,qBAAqB,KAAK;AAC5C,QAAI,CAAC,UAAW,QAAO;AACvB,UAAM,OAAO,uBAAuB,KAAK;AACzC,UAAM,WAAW,2BAA2B,KAAK;AACjD,aAAS,IAAI,WAAW;AAAA,MACrB,IAAI;AAAA,MACJ;AAAA,MACA,SAAS;AAAA,MACT,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAAA,IAChD,CAAC;AACD,WAAO;AAAA,EACV;AAEA,MAAI,SAAS,wBAAwB;AAClC,UAAM,YAAY,qBAAqB,KAAK;AAC5C,UAAM,UAAU,0BAA0B,KAAK;AAC/C,QAAI,CAAC,aAAa,YAAY,KAAM,QAAO;AAI3C,UAAM,WACH,SAAS,IAAI,SAAS,KACrB;AAAA,MACE,IAAI;AAAA,MACJ,MAAM,uBAAuB,KAAK;AAAA,MAClC,SAAS;AAAA,IACZ;AAEH,aAAS,UAAU;AACnB,aAAS,IAAI,WAAW,QAAQ;AAChC,WAAO;AAAA,EACV;AAEA,MAAI,SAAS,oBAAoB;AAC9B,WAAO,QAAQ,qBAAqB,KAAK,CAAC;AAAA,EAC7C;AAEA,SAAO;AACV;;;ACzEO,SAAS,uBACb,kBACA,iBACI;AACJ,QAAM,UACH,gBAAgB,SAAS,IACpB,iBAAiB,UAAU,CAAC,QAAQ,IAAI,OAAO,gBAAgB,CAAC,EAAE,EAAE,IACpE;AACR,MAAI,UAAU,GAAG;AACd,UAAM,SAAS,IAAI,IAAI,gBAAgB,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;AAC3D,WAAO;AAAA,MACJ,GAAG,iBAAiB,MAAM,GAAG,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;AAAA,MACzE,GAAG;AAAA,IACN;AAAA,EACH;AACA,SAAO;AACV;","names":[]}
package/dist/agui.d.cts CHANGED
@@ -14,4 +14,12 @@ declare function getAguiTextMessageRole(event: ChannelEvent, fallback?: string):
14
14
  declare function getAguiTextMessageMemberId(event: ChannelEvent): number | undefined;
15
15
  declare function applyAguiTextMessageEvent(messages: Map<string, TranscriptMessage>, event: ChannelEvent): boolean;
16
16
 
17
- export { type TranscriptMessage, applyAguiTextMessageEvent, getAguiTextMessageContent, getAguiTextMessageId, getAguiTextMessageMemberId, getAguiTextMessageRole };
17
+ /** Keep loaded messages before a fetched window; the fetched tail is authoritative.
18
+ * With no shared first message, the windows do not join and the fetched page stands alone.
19
+ * Extracted from the main app's useChatMessages cache merge.
20
+ */
21
+ declare function mergeChatHistoryWindow<T extends {
22
+ id: string;
23
+ }>(existingMessages: T[], fetchedMessages: T[]): T[];
24
+
25
+ export { type TranscriptMessage, applyAguiTextMessageEvent, getAguiTextMessageContent, getAguiTextMessageId, getAguiTextMessageMemberId, getAguiTextMessageRole, mergeChatHistoryWindow };
package/dist/agui.d.ts CHANGED
@@ -14,4 +14,12 @@ declare function getAguiTextMessageRole(event: ChannelEvent, fallback?: string):
14
14
  declare function getAguiTextMessageMemberId(event: ChannelEvent): number | undefined;
15
15
  declare function applyAguiTextMessageEvent(messages: Map<string, TranscriptMessage>, event: ChannelEvent): boolean;
16
16
 
17
- export { type TranscriptMessage, applyAguiTextMessageEvent, getAguiTextMessageContent, getAguiTextMessageId, getAguiTextMessageMemberId, getAguiTextMessageRole };
17
+ /** Keep loaded messages before a fetched window; the fetched tail is authoritative.
18
+ * With no shared first message, the windows do not join and the fetched page stands alone.
19
+ * Extracted from the main app's useChatMessages cache merge.
20
+ */
21
+ declare function mergeChatHistoryWindow<T extends {
22
+ id: string;
23
+ }>(existingMessages: T[], fetchedMessages: T[]): T[];
24
+
25
+ export { type TranscriptMessage, applyAguiTextMessageEvent, getAguiTextMessageContent, getAguiTextMessageId, getAguiTextMessageMemberId, getAguiTextMessageRole, mergeChatHistoryWindow };
package/dist/agui.js CHANGED
@@ -49,11 +49,25 @@ function applyAguiTextMessageEvent(messages, event) {
49
49
  }
50
50
  return false;
51
51
  }
52
+
53
+ // src/agui/chat-history.ts
54
+ function mergeChatHistoryWindow(existingMessages, fetchedMessages) {
55
+ const joinsAt = fetchedMessages.length > 0 ? existingMessages.findIndex((msg) => msg.id === fetchedMessages[0].id) : -1;
56
+ if (joinsAt > 0) {
57
+ const inPage = new Set(fetchedMessages.map((msg) => msg.id));
58
+ return [
59
+ ...existingMessages.slice(0, joinsAt).filter((msg) => !inPage.has(msg.id)),
60
+ ...fetchedMessages
61
+ ];
62
+ }
63
+ return fetchedMessages;
64
+ }
52
65
  export {
53
66
  applyAguiTextMessageEvent,
54
67
  getAguiTextMessageContent,
55
68
  getAguiTextMessageId,
56
69
  getAguiTextMessageMemberId,
57
- getAguiTextMessageRole
70
+ getAguiTextMessageRole,
71
+ mergeChatHistoryWindow
58
72
  };
59
73
  //# sourceMappingURL=agui.js.map
package/dist/agui.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/agui/text-messages.ts"],"sourcesContent":["import type { ChannelEvent } from '../realtime/types';\n\nexport interface TranscriptMessage {\n id: string;\n role: string;\n content: string;\n /** Authoring member id when TEXT_MESSAGE_START carried it (Gravity extension). */\n memberId?: number;\n}\n\nexport function getAguiTextMessageId(event: ChannelEvent): string | null {\n const raw = event.messageId ?? event.message_id;\n return typeof raw === 'string' && raw.length > 0 ? raw : null;\n}\n\nexport function getAguiTextMessageContent(event: ChannelEvent): string | null {\n const raw = event.content ?? event.text ?? event.delta;\n return typeof raw === 'string' ? raw : null;\n}\n\nexport function getAguiTextMessageRole(event: ChannelEvent, fallback = 'assistant'): string {\n const raw = event.role;\n return typeof raw === 'string' && raw.length > 0 ? raw : fallback;\n}\n\n/** Gravity extension: authoring member on TEXT_MESSAGE_START (camel or snake). */\nexport function getAguiTextMessageMemberId(event: ChannelEvent): number | undefined {\n const raw = event.memberId ?? event.member_id;\n return typeof raw === 'number' && Number.isFinite(raw) ? raw : undefined;\n}\n\nexport function applyAguiTextMessageEvent(\n messages: Map<string, TranscriptMessage>,\n event: ChannelEvent\n): boolean {\n const type = event.type;\n if (typeof type !== 'string') return false;\n\n if (type === 'TEXT_MESSAGE_START') {\n const messageId = getAguiTextMessageId(event);\n if (!messageId) return false;\n const role = getAguiTextMessageRole(event);\n const memberId = getAguiTextMessageMemberId(event);\n messages.set(messageId, {\n id: messageId,\n role,\n content: '',\n ...(memberId !== undefined ? { memberId } : {}),\n });\n return true;\n }\n\n if (type === 'TEXT_MESSAGE_CONTENT') {\n const messageId = getAguiTextMessageId(event);\n const content = getAguiTextMessageContent(event);\n if (!messageId || content === null) return false;\n\n // CONTENT does not carry memberId per protocol — only START does.\n // Synthesize a bare message if START was missed; leave memberId unset.\n const existing =\n messages.get(messageId) ??\n ({\n id: messageId,\n role: getAguiTextMessageRole(event),\n content: '',\n } satisfies TranscriptMessage);\n\n existing.content = content;\n messages.set(messageId, existing);\n return true;\n }\n\n if (type === 'TEXT_MESSAGE_END') {\n return Boolean(getAguiTextMessageId(event));\n }\n\n return false;\n}\n"],"mappings":";AAUO,SAAS,qBAAqB,OAAoC;AACtE,QAAM,MAAM,MAAM,aAAa,MAAM;AACrC,SAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,MAAM;AAC5D;AAEO,SAAS,0BAA0B,OAAoC;AAC3E,QAAM,MAAM,MAAM,WAAW,MAAM,QAAQ,MAAM;AACjD,SAAO,OAAO,QAAQ,WAAW,MAAM;AAC1C;AAEO,SAAS,uBAAuB,OAAqB,WAAW,aAAqB;AACzF,QAAM,MAAM,MAAM;AAClB,SAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,MAAM;AAC5D;AAGO,SAAS,2BAA2B,OAAyC;AACjF,QAAM,MAAM,MAAM,YAAY,MAAM;AACpC,SAAO,OAAO,QAAQ,YAAY,OAAO,SAAS,GAAG,IAAI,MAAM;AAClE;AAEO,SAAS,0BACb,UACA,OACQ;AACR,QAAM,OAAO,MAAM;AACnB,MAAI,OAAO,SAAS,SAAU,QAAO;AAErC,MAAI,SAAS,sBAAsB;AAChC,UAAM,YAAY,qBAAqB,KAAK;AAC5C,QAAI,CAAC,UAAW,QAAO;AACvB,UAAM,OAAO,uBAAuB,KAAK;AACzC,UAAM,WAAW,2BAA2B,KAAK;AACjD,aAAS,IAAI,WAAW;AAAA,MACrB,IAAI;AAAA,MACJ;AAAA,MACA,SAAS;AAAA,MACT,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAAA,IAChD,CAAC;AACD,WAAO;AAAA,EACV;AAEA,MAAI,SAAS,wBAAwB;AAClC,UAAM,YAAY,qBAAqB,KAAK;AAC5C,UAAM,UAAU,0BAA0B,KAAK;AAC/C,QAAI,CAAC,aAAa,YAAY,KAAM,QAAO;AAI3C,UAAM,WACH,SAAS,IAAI,SAAS,KACrB;AAAA,MACE,IAAI;AAAA,MACJ,MAAM,uBAAuB,KAAK;AAAA,MAClC,SAAS;AAAA,IACZ;AAEH,aAAS,UAAU;AACnB,aAAS,IAAI,WAAW,QAAQ;AAChC,WAAO;AAAA,EACV;AAEA,MAAI,SAAS,oBAAoB;AAC9B,WAAO,QAAQ,qBAAqB,KAAK,CAAC;AAAA,EAC7C;AAEA,SAAO;AACV;","names":[]}
1
+ {"version":3,"sources":["../src/agui/text-messages.ts","../src/agui/chat-history.ts"],"sourcesContent":["import type { ChannelEvent } from '../realtime/types';\n\nexport interface TranscriptMessage {\n id: string;\n role: string;\n content: string;\n /** Authoring member id when TEXT_MESSAGE_START carried it (Gravity extension). */\n memberId?: number;\n}\n\nexport function getAguiTextMessageId(event: ChannelEvent): string | null {\n const raw = event.messageId ?? event.message_id;\n return typeof raw === 'string' && raw.length > 0 ? raw : null;\n}\n\nexport function getAguiTextMessageContent(event: ChannelEvent): string | null {\n const raw = event.content ?? event.text ?? event.delta;\n return typeof raw === 'string' ? raw : null;\n}\n\nexport function getAguiTextMessageRole(event: ChannelEvent, fallback = 'assistant'): string {\n const raw = event.role;\n return typeof raw === 'string' && raw.length > 0 ? raw : fallback;\n}\n\n/** Gravity extension: authoring member on TEXT_MESSAGE_START (camel or snake). */\nexport function getAguiTextMessageMemberId(event: ChannelEvent): number | undefined {\n const raw = event.memberId ?? event.member_id;\n return typeof raw === 'number' && Number.isFinite(raw) ? raw : undefined;\n}\n\nexport function applyAguiTextMessageEvent(\n messages: Map<string, TranscriptMessage>,\n event: ChannelEvent\n): boolean {\n const type = event.type;\n if (typeof type !== 'string') return false;\n\n if (type === 'TEXT_MESSAGE_START') {\n const messageId = getAguiTextMessageId(event);\n if (!messageId) return false;\n const role = getAguiTextMessageRole(event);\n const memberId = getAguiTextMessageMemberId(event);\n messages.set(messageId, {\n id: messageId,\n role,\n content: '',\n ...(memberId !== undefined ? { memberId } : {}),\n });\n return true;\n }\n\n if (type === 'TEXT_MESSAGE_CONTENT') {\n const messageId = getAguiTextMessageId(event);\n const content = getAguiTextMessageContent(event);\n if (!messageId || content === null) return false;\n\n // CONTENT does not carry memberId per protocol — only START does.\n // Synthesize a bare message if START was missed; leave memberId unset.\n const existing =\n messages.get(messageId) ??\n ({\n id: messageId,\n role: getAguiTextMessageRole(event),\n content: '',\n } satisfies TranscriptMessage);\n\n existing.content = content;\n messages.set(messageId, existing);\n return true;\n }\n\n if (type === 'TEXT_MESSAGE_END') {\n return Boolean(getAguiTextMessageId(event));\n }\n\n return false;\n}\n","/** Keep loaded messages before a fetched window; the fetched tail is authoritative.\n * With no shared first message, the windows do not join and the fetched page stands alone.\n * Extracted from the main app's useChatMessages cache merge.\n */\nexport function mergeChatHistoryWindow<T extends { id: string }>(\n existingMessages: T[],\n fetchedMessages: T[]\n): T[] {\n const joinsAt =\n fetchedMessages.length > 0\n ? existingMessages.findIndex((msg) => msg.id === fetchedMessages[0].id)\n : -1;\n if (joinsAt > 0) {\n const inPage = new Set(fetchedMessages.map((msg) => msg.id));\n return [\n ...existingMessages.slice(0, joinsAt).filter((msg) => !inPage.has(msg.id)),\n ...fetchedMessages,\n ];\n }\n return fetchedMessages;\n}\n"],"mappings":";AAUO,SAAS,qBAAqB,OAAoC;AACtE,QAAM,MAAM,MAAM,aAAa,MAAM;AACrC,SAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,MAAM;AAC5D;AAEO,SAAS,0BAA0B,OAAoC;AAC3E,QAAM,MAAM,MAAM,WAAW,MAAM,QAAQ,MAAM;AACjD,SAAO,OAAO,QAAQ,WAAW,MAAM;AAC1C;AAEO,SAAS,uBAAuB,OAAqB,WAAW,aAAqB;AACzF,QAAM,MAAM,MAAM;AAClB,SAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,MAAM;AAC5D;AAGO,SAAS,2BAA2B,OAAyC;AACjF,QAAM,MAAM,MAAM,YAAY,MAAM;AACpC,SAAO,OAAO,QAAQ,YAAY,OAAO,SAAS,GAAG,IAAI,MAAM;AAClE;AAEO,SAAS,0BACb,UACA,OACQ;AACR,QAAM,OAAO,MAAM;AACnB,MAAI,OAAO,SAAS,SAAU,QAAO;AAErC,MAAI,SAAS,sBAAsB;AAChC,UAAM,YAAY,qBAAqB,KAAK;AAC5C,QAAI,CAAC,UAAW,QAAO;AACvB,UAAM,OAAO,uBAAuB,KAAK;AACzC,UAAM,WAAW,2BAA2B,KAAK;AACjD,aAAS,IAAI,WAAW;AAAA,MACrB,IAAI;AAAA,MACJ;AAAA,MACA,SAAS;AAAA,MACT,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAAA,IAChD,CAAC;AACD,WAAO;AAAA,EACV;AAEA,MAAI,SAAS,wBAAwB;AAClC,UAAM,YAAY,qBAAqB,KAAK;AAC5C,UAAM,UAAU,0BAA0B,KAAK;AAC/C,QAAI,CAAC,aAAa,YAAY,KAAM,QAAO;AAI3C,UAAM,WACH,SAAS,IAAI,SAAS,KACrB;AAAA,MACE,IAAI;AAAA,MACJ,MAAM,uBAAuB,KAAK;AAAA,MAClC,SAAS;AAAA,IACZ;AAEH,aAAS,UAAU;AACnB,aAAS,IAAI,WAAW,QAAQ;AAChC,WAAO;AAAA,EACV;AAEA,MAAI,SAAS,oBAAoB;AAC9B,WAAO,QAAQ,qBAAqB,KAAK,CAAC;AAAA,EAC7C;AAEA,SAAO;AACV;;;ACzEO,SAAS,uBACb,kBACA,iBACI;AACJ,QAAM,UACH,gBAAgB,SAAS,IACpB,iBAAiB,UAAU,CAAC,QAAQ,IAAI,OAAO,gBAAgB,CAAC,EAAE,EAAE,IACpE;AACR,MAAI,UAAU,GAAG;AACd,UAAM,SAAS,IAAI,IAAI,gBAAgB,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;AAC3D,WAAO;AAAA,MACJ,GAAG,iBAAiB,MAAM,GAAG,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;AAAA,MACzE,GAAG;AAAA,IACN;AAAA,EACH;AACA,SAAO;AACV;","names":[]}