@gravity-rail/sdk 0.47.0 → 0.50.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 +117 -9
  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-Bqj3Vbst.d.cts → chat-interaction-CmGKKlnL.d.ts} +244 -37
  9. package/dist/{chat-interaction-DiZVtDRE.d.ts → chat-interaction-THDGF4ko.d.cts} +244 -37
  10. package/dist/{chunk-72SWNLVD.js → chunk-MMDSZ25F.js} +1 -1
  11. package/dist/chunk-MMDSZ25F.js.map +1 -0
  12. package/dist/{chunk-N4GQWNGU.js → chunk-QRZVXPR4.js} +204 -50
  13. package/dist/chunk-QRZVXPR4.js.map +1 -0
  14. package/dist/{chunk-67QXTQVL.js → chunk-TGL72KPS.js} +59 -118
  15. package/dist/chunk-TGL72KPS.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 +262 -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 +262 -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-ZC4qP3CS.d.cts → path-BQgkft9c.d.ts} +64 -3
  35. package/dist/{revision-state-DaVz2528.d.ts → path-DZbDwvpS.d.cts} +64 -3
  36. package/dist/schemas.cjs +59 -16
  37. package/dist/schemas.cjs.map +1 -1
  38. package/dist/schemas.d.cts +22 -3
  39. package/dist/schemas.d.ts +22 -3
  40. package/dist/schemas.js +59 -17
  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-N4GQWNGU.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,7 +365,9 @@ const operators = await client.getLiveOperators(workspaceId);
352
365
 
353
366
  ### Sites & Portals
354
367
 
355
- 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.
368
+ Site settings accept `allowedScopes: ['records:read', 'records:write', 'records:admin']` for Manage Records. This opt-in exposes only record scopes the signed-in Member’s current role already allows, within the Site workspace. It permits existing record administration, including other Members’ records, admin-only fields, and deletion. The default is `[]`; read/write opt-ins remain independent. Unrelated administrative scopes stay suppressed.
369
+
370
+ Site record write methods may return a successful value-free receipt: `fieldValues` is empty and `memberId` / `memberName` / `externalId` are null unless Reading Records and the row read policy both allow values. This includes partial updates, resubmissions and upserts. The write still succeeds; use a permitted record read to load stored values.
356
371
 
357
372
  ```typescript
358
373
  // Build customer-facing portals with AI chat built in
@@ -374,13 +389,42 @@ await client.crawlSite(workspaceId, site.id, { url: 'https://docs.example.com' }
374
389
 
375
390
  ### Workspace Management
376
391
 
392
+ Workspace and organization directories return `{ items, page, pageSize, total, totalPages }`. Read
393
+ `items` instead of treating the response as an array. Pass `page`, `pageSize` (at
394
+ most 100), and `search` to request a page. This applies to `getWorkspaces`,
395
+ `getDeviceAdmissibleWorkspaces`, `getPublicWorkspaces`, `getMyOrganizations`, and
396
+ `getOrganizationWorkspaceSummaries`.
397
+ Organization member details no longer embed workspace lists; request organization
398
+ workspace summary pages instead.
399
+ `getMyOrganizations` also accepts `organizationSlug` for exact host resolution
400
+ and `organizationUuids` (at most 100 UUIDs) for a bounded membership lookup. These
401
+ filters only narrow the current account's memberships.
402
+ Account and device workspace totals count eligible global membership candidates;
403
+ workspace admission can make a page sparse or empty. Use pagination metadata to
404
+ reach later candidates.
405
+
406
+ `getOrgWorkspaceUsage(orgUuid, { page, pageSize, search })` returns a bounded
407
+ `workspaces` list with `page`, `pageSize`, `total`, and `totalPages` metadata.
408
+ Organization usage totals remain global and can be null when measurement coverage
409
+ is incomplete. Do not sum the visible cards to calculate an organization total.
410
+
377
411
  ```typescript
378
412
  // Workspaces this credential can reach
379
- const workspaces = await client.getWorkspaces();
413
+ const workspaces = await client.getWorkspaces({ page: 1, pageSize: 20, search: 'clinic' });
414
+ // Read workspaces.items; request the next page explicitly when needed.
380
415
 
381
416
  // Companion-device picker (Lantern): compact rows for only the
382
417
  // workspaces a paired device could enter — no device credential needed
383
- const deviceWorkspaces = await client.getDeviceAdmissibleWorkspaces();
418
+ const deviceWorkspaces = await client.getDeviceAdmissibleWorkspaces({
419
+ page: 1, pageSize: 20, search: 'clinic',
420
+ });
421
+ // Read deviceWorkspaces.items; a sparse page can still have later candidates.
422
+
423
+ // Public workspace directory
424
+ const publicWorkspaces = await client.getPublicWorkspaces({
425
+ page: 1, pageSize: 20, search: 'clinic',
426
+ });
427
+ // Read publicWorkspaces.items and use total/page/pageSize for pagination.
384
428
 
385
429
  // Export/import full workspace configuration
386
430
  const config = await client.exportWorkspace(workspaceId);
@@ -395,6 +439,70 @@ const clientWs = await client.createClientWorkspace(workspaceId, {
395
439
  await client.provisionClientWorkspace(workspaceId, clientWs.id);
396
440
  ```
397
441
 
442
+ ## Export Workflow revisions
443
+
444
+ Workspace JSON and ZIP exports default to active snapshots. Request
445
+ `revisionMode: 'all'` to include historical revisions and drafts. The version `2.0` archive carries isolated
446
+ `workflowRevisions` graphs and requires a revision-aware server for import. Import
447
+ it as new Workflows to preserve active and draft roles; an existing Workflow match
448
+ is refused before writes. Legacy version `1.0` imports keep their existing behavior.
449
+
450
+ ```typescript
451
+ const archive = await client.exportWorkspace(workspaceUuid, undefined, undefined, undefined, {
452
+ revisionMode: 'all',
453
+ });
454
+ const selected = await client.exportWorkspace(workspaceUuid, workflowId, undefined, undefined, {
455
+ revisionId: 87,
456
+ });
457
+ const active = await client.exportWorkspace(workspaceUuid, undefined, undefined, undefined, {
458
+ revisionMode: 'active',
459
+ });
460
+ ```
461
+
462
+ The same options apply to `exportWorkspaceZip`. A selected revision requires a
463
+ Workflow ID and cannot be combined with active mode. Selected and active snapshots
464
+ import into an editable draft and do not change an existing active revision.
465
+
466
+ The public workspace export API supports only the `channels` exclusion in every
467
+ revision mode. All-revision archives preserve that exclusion. Other public
468
+ exclusion values are rejected.
469
+
470
+ The SDK throws when a legacy server returns an active-only snapshot for an
471
+ all-revision or selected export.
472
+ JSON validation checks selection metadata and internal Workflow/revision identities.
473
+ ZIP validation checks provenance headers generated from the actual exported data,
474
+ before reading the archive. It adds no archive size limit or permission requirement.
475
+ Deploy a server with ZIP provenance headers before releasing this client update;
476
+ servers with revision-aware JSON alone still fail the ZIP guard. Explicit
477
+ `revisionMode: 'active'` exports remain compatible with legacy servers.
478
+
479
+ ZIP provenance uses `X-Gravityrail-Export-Version` (`2.0` for all revisions,
480
+ `1.0` for active and selected snapshots) and `X-Gravityrail-Export-Selection`
481
+ (`active`, `all`, or `selected`). A selected ZIP also includes nonempty
482
+ `X-Gravityrail-Export-Revision-Uuid` and `X-Gravityrail-Export-Workflow-Uuid`.
483
+ These values come from the exported artifact. Explicit active exports do not
484
+ require these headers.
485
+
486
+ Shared dependency definitions are current exported Forms, files, and integrations,
487
+ not historical snapshots of those resources.
488
+
489
+ ## Chat history (`@gravity-rail/sdk/agui`)
490
+
491
+ `mergeChatHistoryWindow(existing, fetched)` merges an authoritative chronological
492
+ history window into an existing transcript. If the first fetched ID appears in
493
+ the existing transcript, the helper retains only the earlier prefix and replaces
494
+ the covered tail. It removes prefix entries whose IDs also appear in the fetched
495
+ window; it does not remove repeated IDs within the retained prefix. If the first
496
+ ID is unknown, or the fetched window is empty, the result is the fetched window alone.
497
+ Message text does not identify a message. Fetch the needed history pages before
498
+ calling this helper; it does not fetch or filter messages.
499
+
500
+ ```ts
501
+ import { mergeChatHistoryWindow } from '@gravity-rail/sdk/agui';
502
+
503
+ const messages = mergeChatHistoryWindow(existingMessages, fetchedMessages);
504
+ ```
505
+
398
506
  ## Helpers (`@gravity-rail/sdk/helpers`)
399
507
 
400
508
  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":[]}