@athenaintel/react 0.21.2 → 0.22.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 (61) hide show
  1. package/README.md +63 -0
  2. package/dist/chat/StatewireApprovalCard.cjs +70 -10
  3. package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
  4. package/dist/chat/StatewireApprovalCard.d.ts +12 -1
  5. package/dist/chat/StatewireApprovalCard.js +73 -13
  6. package/dist/chat/StatewireApprovalCard.js.map +1 -1
  7. package/dist/chat/use-statewire-approval-request.cjs +22 -1
  8. package/dist/chat/use-statewire-approval-request.cjs.map +1 -1
  9. package/dist/chat/use-statewire-approval-request.d.ts +41 -0
  10. package/dist/chat/use-statewire-approval-request.js +24 -3
  11. package/dist/chat/use-statewire-approval-request.js.map +1 -1
  12. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs +0 -1
  13. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs.map +1 -1
  14. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js +0 -1
  15. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js.map +1 -1
  16. package/dist/collab/client.cjs +4 -9
  17. package/dist/collab/client.cjs.map +1 -1
  18. package/dist/collab/client.js +2 -7
  19. package/dist/collab/client.js.map +1 -1
  20. package/dist/collab/index.cjs +23 -0
  21. package/dist/collab/index.cjs.map +1 -1
  22. package/dist/collab/index.d.ts +17 -2
  23. package/dist/collab/index.js +24 -1
  24. package/dist/collab/index.js.map +1 -1
  25. package/dist/collab/mint.cjs +18 -13
  26. package/dist/collab/mint.cjs.map +1 -1
  27. package/dist/collab/mint.d.ts +10 -0
  28. package/dist/collab/mint.js +18 -13
  29. package/dist/collab/mint.js.map +1 -1
  30. package/dist/collab/presence-client.cjs +211 -0
  31. package/dist/collab/presence-client.cjs.map +1 -0
  32. package/dist/collab/presence-client.d.ts +60 -0
  33. package/dist/collab/presence-client.js +194 -0
  34. package/dist/collab/presence-client.js.map +1 -0
  35. package/dist/collab/presence-mint.cjs +68 -0
  36. package/dist/collab/presence-mint.cjs.map +1 -0
  37. package/dist/collab/presence-mint.d.ts +87 -0
  38. package/dist/collab/presence-mint.js +68 -0
  39. package/dist/collab/presence-mint.js.map +1 -0
  40. package/dist/collab/presence-react.cjs +114 -0
  41. package/dist/collab/presence-react.cjs.map +1 -0
  42. package/dist/collab/presence-react.d.ts +47 -0
  43. package/dist/collab/presence-react.js +114 -0
  44. package/dist/collab/presence-react.js.map +1 -0
  45. package/dist/collab/presence-slots.cjs +220 -0
  46. package/dist/collab/presence-slots.cjs.map +1 -0
  47. package/dist/collab/presence-slots.d.ts +119 -0
  48. package/dist/collab/presence-slots.js +220 -0
  49. package/dist/collab/presence-slots.js.map +1 -0
  50. package/dist/collab/token-refresh.cjs +13 -0
  51. package/dist/collab/token-refresh.cjs.map +1 -0
  52. package/dist/collab/token-refresh.d.ts +20 -0
  53. package/dist/collab/token-refresh.js +13 -0
  54. package/dist/collab/token-refresh.js.map +1 -0
  55. package/dist/packages/agent-runtime/src/interrupts.cjs +40 -1
  56. package/dist/packages/agent-runtime/src/interrupts.cjs.map +1 -1
  57. package/dist/packages/agent-runtime/src/interrupts.js +40 -1
  58. package/dist/packages/agent-runtime/src/interrupts.js.map +1 -1
  59. package/dist/packages/react/package.json.cjs +1 -1
  60. package/dist/packages/react/package.json.js +1 -1
  61. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"use-statewire-approval-request.cjs","sources":["../../src/chat/use-statewire-approval-request.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport {\n useStatewireRuns,\n useStatewireSendCommand,\n useStatewireState,\n} from '@assistant-ui/react-statewire';\nimport {\n type AbandonedInputRequest,\n asHitlApproval,\n findAbandonedInterrupt,\n HITL_PENDING_RESUME_GRACE_MS,\n type HitlApprovalPayload,\n isApprovalCardInterrupt,\n type PendingActionRequest,\n pendingActionRequests,\n readAbandonedInputRequests,\n useApprovalResumeLock,\n} from '@athenaintel/agent-runtime';\nimport { useMemo } from 'react';\nimport { useAthenaRunLive } from './use-athena-run-live';\n\n/** Selects which pending interrupt an approval surface answers. */\nexport type StatewireInterruptPredicate = (\n request: StatewireThread.InputRequestState,\n) => request is StatewireThread.CustomInputRequest;\n\nexport interface UseStatewireApprovalRequestOptions {\n /**\n * Which pending interrupt to answer. Defaults to `isApprovalCardInterrupt`\n * (client-tool parks excluded — the client-tool bridge owns those, and a\n * generic resume would corrupt the parked tool call).\n */\n interruptPredicate?: StatewireInterruptPredicate;\n /**\n * How long a sent decision keeps the actions locked when no resume run ever\n * starts. Defaults to {@link HITL_PENDING_RESUME_GRACE_MS}.\n */\n pendingResumeGraceMs?: number;\n /**\n * The host refused the `run/input` (or the transport lost it): the\n * optimistic fold rolls back, the request is on screen again with its\n * actions re-enabled, and this is the surface's one chance to say why.\n */\n onResumeRejected?: (error: unknown, requestId: string) => void;\n}\n\nexport interface StatewireApprovalRequest<TValue> {\n /** The pending interrupt this surface answers, if any. */\n request: StatewireThread.CustomInputRequest | undefined;\n requestId: string | undefined;\n /** The payload narrowed to the Athena HITL approval shape, or `null`. */\n hitl: HitlApprovalPayload | null;\n /** Every gated call the approval covers, in decision order (empty for non-HITL parks). */\n actionRequests: PendingActionRequest[];\n /** An interrupt a non-resume dispatch dropped — the \"approval was cancelled\" notice. */\n abandonedInterrupt: AbandonedInputRequest | undefined;\n /** True after a resume until the request identity changes (or recovery fires). */\n pending: boolean;\n /**\n * Answer the request with `{ type: 'run/input', requestId, response: { type: 'resume', value } }`.\n * Returns the dispatch promise (settles when the host accepts, rejects when\n * it refuses), or `undefined` when the lock dropped the send.\n */\n resume: (value: TValue) => Promise<unknown> | undefined;\n}\n\n/**\n * The headless half of every statewire approval surface: selects the pending\n * interrupt from the runs projection, narrows its payload, reads the\n * host-published abandoned interrupts, and wires the shared optimistic resume\n * lock to the thread's `run/input` command. `StatewireApprovalCard` renders\n * over it; hosts with their own decision UI (per-lane subagent cards) build\n * on it directly so the selection and lock semantics cannot drift.\n *\n * Requires a statewire aui scope (throws outside one, like every\n * `useStatewire*` hook) — gate the mount with `hasStatewireThreadExtras`.\n */\nexport function useStatewireApprovalRequest<\n TValue extends StatewireThread.InputResponse = Record<string, string>,\n>(options: UseStatewireApprovalRequestOptions = {}): StatewireApprovalRequest<TValue> {\n const {\n interruptPredicate = isApprovalCardInterrupt,\n pendingResumeGraceMs = HITL_PENDING_RESUME_GRACE_MS,\n onResumeRejected,\n } = options;\n const sendCommand = useStatewireSendCommand();\n // The runs projection is memoized by the lib, so this derivation is\n // reference-stable between state updates.\n const request = useStatewireRuns().inputRequests?.find(interruptPredicate);\n const requestId = request?.id;\n\n // The host publishes the interrupts a non-resume dispatch dropped. Select\n // the raw array reference (pre-stream-safe) and derive inline; a `.find`\n // selector would return a fresh object every update.\n const abandoned = useStatewireState(readAbandonedInputRequests);\n const abandonedInterrupt = findAbandonedInterrupt(abandoned);\n\n const hitl = asHitlApproval(request?.payload);\n const actionRequests = useMemo(() => pendingActionRequests(hitl), [hitl]);\n\n const isRunning = useAthenaRunLive();\n const { pending, resume } = useApprovalResumeLock<TValue>({\n requestId,\n isRunning,\n pendingResumeGraceMs,\n onResumeRejected,\n // The dispatch promise is handed to the lock: a refusal releases it and\n // reaches `onResumeRejected`.\n sendResume: (id, value) =>\n sendCommand({\n type: 'run/input',\n requestId: id,\n response: { type: 'resume', value },\n }),\n });\n\n return { request, requestId, hitl, actionRequests, abandonedInterrupt, pending, resume };\n}\n"],"names":["isApprovalCardInterrupt","HITL_PENDING_RESUME_GRACE_MS","useStatewireSendCommand","useStatewireRuns","useStatewireState","readAbandonedInputRequests","findAbandonedInterrupt","asHitlApproval","useMemo","pendingActionRequests","useAthenaRunLive","useApprovalResumeLock"],"mappings":";;;;;;;;;;;;;;;;;;;AA6EO,SAAS,4BAEd,UAA8C,IAAsC;;AACpF,QAAM;AAAA,IACJ,qBAAqBA,WAAAA;AAAAA,IACrB,uBAAuBC,WAAAA;AAAAA,IACvB;AAAA,EAAA,IACE;AACJ,QAAM,cAAcC,eAAAA,wBAAA;AAGpB,QAAM,WAAUC,oBAAAA,iBAAA,EAAmB,kBAAnBA,mBAAkC,KAAK;AACvD,QAAM,YAAY,mCAAS;AAK3B,QAAM,YAAYC,eAAAA,kBAAkBC,qCAA0B;AAC9D,QAAM,qBAAqBC,WAAAA,uBAAuB,SAAS;AAE3D,QAAM,OAAOC,WAAAA,eAAe,mCAAS,OAAO;AAC5C,QAAM,iBAAiBC,MAAAA,QAAQ,MAAMC,WAAAA,sBAAsB,IAAI,GAAG,CAAC,IAAI,CAAC;AAExE,QAAM,YAAYC,iBAAAA,iBAAA;AAClB,QAAM,EAAE,SAAS,OAAA,IAAWC,iCAA8B;AAAA,IACxD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA,YAAY,CAAC,IAAI,UACf,YAAY;AAAA,MACV,MAAM;AAAA,MACN,WAAW;AAAA,MACX,UAAU,EAAE,MAAM,UAAU,MAAA;AAAA,IAAM,CACnC;AAAA,EAAA,CACJ;AAED,SAAO,EAAE,SAAS,WAAW,MAAM,gBAAgB,oBAAoB,SAAS,OAAA;AAClF;;"}
1
+ {"version":3,"file":"use-statewire-approval-request.cjs","sources":["../../src/chat/use-statewire-approval-request.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport {\n useStatewireRuns,\n useStatewireSendCommand,\n useStatewireState,\n} from '@assistant-ui/react-statewire';\nimport {\n type AbandonedInputRequest,\n asHitlApproval,\n findAbandonedInterrupt,\n HITL_PENDING_RESUME_GRACE_MS,\n type HitlApprovalPayload,\n interruptMirrorPayload,\n isApprovalCardInterrupt,\n type PendingActionRequest,\n pendingActionRequests,\n readAbandonedInputRequests,\n readInterruptMirror,\n readQueuedLaneDecisions,\n useApprovalResumeLock,\n} from '@athenaintel/agent-runtime';\nimport { useCallback, useMemo } from 'react';\nimport { useAthenaRunLive } from './use-athena-run-live';\n\ndeclare module '@assistant-ui/core' {\n namespace Assistant {\n interface Commands {\n /**\n * Decide per-dispatch subagent lanes while the parent run is still\n * streaming (`DeepAgentThread.run_lane_input`). Keys are the lanes'\n * langgraph interrupt ids; the host matches them against the lanes its\n * mirrored interrupt currently parks, answers at once when the run is\n * already parked, and otherwise holds them until it parks — stale or\n * unknown ids are ignored, never carried to a later interrupt.\n */\n runLaneInput: {\n readonly type: 'run/lane-input';\n readonly laneDecisions: Readonly<Record<string, StatewireThread.InputResponse>>;\n };\n }\n }\n}\n\nconst NO_QUEUED_LANE_DECISIONS: Readonly<Record<string, unknown>> = Object.freeze({});\n\n/** Selects which pending interrupt an approval surface answers. */\nexport type StatewireInterruptPredicate = (\n request: StatewireThread.InputRequestState,\n) => request is StatewireThread.CustomInputRequest;\n\nexport interface UseStatewireApprovalRequestOptions {\n /**\n * Which pending interrupt to answer. Defaults to `isApprovalCardInterrupt`\n * (client-tool parks excluded — the client-tool bridge owns those, and a\n * generic resume would corrupt the parked tool call).\n */\n interruptPredicate?: StatewireInterruptPredicate;\n /**\n * How long a sent decision keeps the actions locked when no resume run ever\n * starts. Defaults to {@link HITL_PENDING_RESUME_GRACE_MS}.\n */\n pendingResumeGraceMs?: number;\n /**\n * The host refused the `run/input` (or the transport lost it): the\n * optimistic fold rolls back, the request is on screen again with its\n * actions re-enabled, and this is the surface's one chance to say why.\n */\n onResumeRejected?: (error: unknown, requestId: string) => void;\n}\n\nexport interface StatewireApprovalRequest<TValue> {\n /** The pending interrupt this surface answers, if any. */\n request: StatewireThread.CustomInputRequest | undefined;\n requestId: string | undefined;\n /** The payload narrowed to the Athena HITL approval shape, or `null`. */\n hitl: HitlApprovalPayload | null;\n /** Every gated call the approval covers, in decision order (empty for non-HITL parks). */\n actionRequests: PendingActionRequest[];\n /** An interrupt a non-resume dispatch dropped — the \"approval was cancelled\" notice. */\n abandonedInterrupt: AbandonedInputRequest | undefined;\n /**\n * The mirrored interrupt's HITL payload while the run is still live and no\n * input request is answerable: per-dispatch `task` lanes parked on their\n * gates while sibling lanes stream. `null` once the run parks (the same\n * lanes are then `hitl` / `actionRequests`) and when nothing is parked.\n */\n liveHitl: HitlApprovalPayload | null;\n /** Every gated call `liveHitl` covers (empty when it is `null`). */\n liveActionRequests: PendingActionRequest[];\n /**\n * The decisions the host already holds from a `run/lane-input`, keyed by\n * langgraph interrupt id, to be answered when the run parks — a queued lane\n * reads as decided on every attached client, not just the one that sent it.\n */\n queuedLaneDecisions: Readonly<Record<string, unknown>>;\n /** True after a resume until the request identity changes (or recovery fires). */\n pending: boolean;\n /**\n * Answer the request with `{ type: 'run/input', requestId, response: { type: 'resume', value } }`.\n * Returns the dispatch promise (settles when the host accepts, rejects when\n * it refuses), or `undefined` when the lock dropped the send.\n */\n resume: (value: TValue) => Promise<unknown> | undefined;\n /**\n * Decide per-dispatch lanes by langgraph interrupt id. With a pending\n * request this is the native `lane_decisions` resume through the lock;\n * without one (the run is still streaming siblings) it is `run/lane-input`,\n * which the host queues and answers on the park. Returns the dispatch\n * promise, or `undefined` when the lock dropped a resume.\n */\n sendLaneDecisions: (\n lanes: Readonly<Record<string, StatewireThread.InputResponse>>,\n ) => Promise<unknown> | undefined;\n}\n\n/**\n * The headless half of every statewire approval surface: selects the pending\n * interrupt from the runs projection, narrows its payload, reads the\n * host-published abandoned interrupts, and wires the shared optimistic resume\n * lock to the thread's `run/input` command. `StatewireApprovalCard` renders\n * over it; hosts with their own decision UI (per-lane subagent cards) build\n * on it directly so the selection and lock semantics cannot drift.\n *\n * Requires a statewire aui scope (throws outside one, like every\n * `useStatewire*` hook) — gate the mount with `hasStatewireThreadExtras`.\n */\nexport function useStatewireApprovalRequest<\n TValue extends StatewireThread.InputResponse = Record<string, string>,\n>(options: UseStatewireApprovalRequestOptions = {}): StatewireApprovalRequest<TValue> {\n const {\n interruptPredicate = isApprovalCardInterrupt,\n pendingResumeGraceMs = HITL_PENDING_RESUME_GRACE_MS,\n onResumeRejected,\n } = options;\n const sendCommand = useStatewireSendCommand();\n // The runs projection is memoized by the lib, so this derivation is\n // reference-stable between state updates.\n const request = useStatewireRuns().inputRequests?.find(interruptPredicate);\n const requestId = request?.id;\n\n // The host publishes the interrupts a non-resume dispatch dropped. Select\n // the raw array reference (pre-stream-safe) and derive inline; a `.find`\n // selector would return a fresh object every update.\n const abandoned = useStatewireState(readAbandonedInputRequests);\n const abandonedInterrupt = findAbandonedInterrupt(abandoned);\n\n const hitl = asHitlApproval(request?.payload);\n const actionRequests = useMemo(() => pendingActionRequests(hitl), [hitl]);\n\n const isRunning = useAthenaRunLive();\n // The host mirrors the interrupt into state as soon as a lane parks — while\n // the run manager still reports `running` (siblings streaming) and has no\n // request to answer. Raw reference selectors; derive inline.\n const mirror = useStatewireState(readInterruptMirror);\n const liveHitl =\n request === undefined && isRunning ? asHitlApproval(interruptMirrorPayload(mirror)) : null;\n const liveActionRequests = useMemo(() => pendingActionRequests(liveHitl), [liveHitl]);\n const queuedLaneDecisions =\n useStatewireState(readQueuedLaneDecisions) ?? NO_QUEUED_LANE_DECISIONS;\n\n const { pending, resume } = useApprovalResumeLock<TValue>({\n requestId,\n isRunning,\n pendingResumeGraceMs,\n onResumeRejected,\n // The dispatch promise is handed to the lock: a refusal releases it and\n // reaches `onResumeRejected`.\n sendResume: (id, value) =>\n sendCommand({\n type: 'run/input',\n requestId: id,\n response: { type: 'resume', value },\n }),\n });\n\n const sendLaneDecisions = useCallback(\n (lanes: Readonly<Record<string, StatewireThread.InputResponse>>) =>\n requestId !== undefined\n ? resume({ lane_decisions: lanes } as unknown as TValue)\n : sendCommand({ type: 'run/lane-input', laneDecisions: lanes }),\n [requestId, resume, sendCommand],\n );\n\n return {\n request,\n requestId,\n hitl,\n actionRequests,\n liveHitl,\n liveActionRequests,\n queuedLaneDecisions,\n abandonedInterrupt,\n pending,\n resume,\n sendLaneDecisions,\n };\n}\n"],"names":["isApprovalCardInterrupt","HITL_PENDING_RESUME_GRACE_MS","useStatewireSendCommand","useStatewireRuns","useStatewireState","readAbandonedInputRequests","findAbandonedInterrupt","asHitlApproval","useMemo","pendingActionRequests","useAthenaRunLive","readInterruptMirror","interruptMirrorPayload","readQueuedLaneDecisions","useApprovalResumeLock","useCallback"],"mappings":";;;;;;;;;;;;;;;;;;;AA2CA,MAAM,2BAA8D,OAAO,OAAO,EAAE;AAmF7E,SAAS,4BAEd,UAA8C,IAAsC;;AACpF,QAAM;AAAA,IACJ,qBAAqBA,WAAAA;AAAAA,IACrB,uBAAuBC,WAAAA;AAAAA,IACvB;AAAA,EAAA,IACE;AACJ,QAAM,cAAcC,eAAAA,wBAAA;AAGpB,QAAM,WAAUC,oBAAAA,iBAAA,EAAmB,kBAAnBA,mBAAkC,KAAK;AACvD,QAAM,YAAY,mCAAS;AAK3B,QAAM,YAAYC,eAAAA,kBAAkBC,qCAA0B;AAC9D,QAAM,qBAAqBC,WAAAA,uBAAuB,SAAS;AAE3D,QAAM,OAAOC,WAAAA,eAAe,mCAAS,OAAO;AAC5C,QAAM,iBAAiBC,MAAAA,QAAQ,MAAMC,WAAAA,sBAAsB,IAAI,GAAG,CAAC,IAAI,CAAC;AAExE,QAAM,YAAYC,iBAAAA,iBAAA;AAIlB,QAAM,SAASN,eAAAA,kBAAkBO,8BAAmB;AACpD,QAAM,WACJ,YAAY,UAAa,YAAYJ,WAAAA,eAAeK,WAAAA,uBAAuB,MAAM,CAAC,IAAI;AACxF,QAAM,qBAAqBJ,MAAAA,QAAQ,MAAMC,WAAAA,sBAAsB,QAAQ,GAAG,CAAC,QAAQ,CAAC;AACpF,QAAM,sBACJL,eAAAA,kBAAkBS,WAAAA,uBAAuB,KAAK;AAEhD,QAAM,EAAE,SAAS,OAAA,IAAWC,iCAA8B;AAAA,IACxD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA,YAAY,CAAC,IAAI,UACf,YAAY;AAAA,MACV,MAAM;AAAA,MACN,WAAW;AAAA,MACX,UAAU,EAAE,MAAM,UAAU,MAAA;AAAA,IAAM,CACnC;AAAA,EAAA,CACJ;AAED,QAAM,oBAAoBC,MAAAA;AAAAA,IACxB,CAAC,UACC,cAAc,SACV,OAAO,EAAE,gBAAgB,MAAA,CAA4B,IACrD,YAAY,EAAE,MAAM,kBAAkB,eAAe,OAAO;AAAA,IAClE,CAAC,WAAW,QAAQ,WAAW;AAAA,EAAA;AAGjC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;;"}
@@ -1,5 +1,23 @@
1
1
  import type { StatewireThread } from '@assistant-ui/react-statewire';
2
2
  import { type AbandonedInputRequest, type HitlApprovalPayload, type PendingActionRequest } from '@athenaintel/agent-runtime';
3
+ declare module '@assistant-ui/core' {
4
+ namespace Assistant {
5
+ interface Commands {
6
+ /**
7
+ * Decide per-dispatch subagent lanes while the parent run is still
8
+ * streaming (`DeepAgentThread.run_lane_input`). Keys are the lanes'
9
+ * langgraph interrupt ids; the host matches them against the lanes its
10
+ * mirrored interrupt currently parks, answers at once when the run is
11
+ * already parked, and otherwise holds them until it parks — stale or
12
+ * unknown ids are ignored, never carried to a later interrupt.
13
+ */
14
+ runLaneInput: {
15
+ readonly type: 'run/lane-input';
16
+ readonly laneDecisions: Readonly<Record<string, StatewireThread.InputResponse>>;
17
+ };
18
+ }
19
+ }
20
+ }
3
21
  /** Selects which pending interrupt an approval surface answers. */
4
22
  export type StatewireInterruptPredicate = (request: StatewireThread.InputRequestState) => request is StatewireThread.CustomInputRequest;
5
23
  export interface UseStatewireApprovalRequestOptions {
@@ -31,6 +49,21 @@ export interface StatewireApprovalRequest<TValue> {
31
49
  actionRequests: PendingActionRequest[];
32
50
  /** An interrupt a non-resume dispatch dropped — the "approval was cancelled" notice. */
33
51
  abandonedInterrupt: AbandonedInputRequest | undefined;
52
+ /**
53
+ * The mirrored interrupt's HITL payload while the run is still live and no
54
+ * input request is answerable: per-dispatch `task` lanes parked on their
55
+ * gates while sibling lanes stream. `null` once the run parks (the same
56
+ * lanes are then `hitl` / `actionRequests`) and when nothing is parked.
57
+ */
58
+ liveHitl: HitlApprovalPayload | null;
59
+ /** Every gated call `liveHitl` covers (empty when it is `null`). */
60
+ liveActionRequests: PendingActionRequest[];
61
+ /**
62
+ * The decisions the host already holds from a `run/lane-input`, keyed by
63
+ * langgraph interrupt id, to be answered when the run parks — a queued lane
64
+ * reads as decided on every attached client, not just the one that sent it.
65
+ */
66
+ queuedLaneDecisions: Readonly<Record<string, unknown>>;
34
67
  /** True after a resume until the request identity changes (or recovery fires). */
35
68
  pending: boolean;
36
69
  /**
@@ -39,6 +72,14 @@ export interface StatewireApprovalRequest<TValue> {
39
72
  * it refuses), or `undefined` when the lock dropped the send.
40
73
  */
41
74
  resume: (value: TValue) => Promise<unknown> | undefined;
75
+ /**
76
+ * Decide per-dispatch lanes by langgraph interrupt id. With a pending
77
+ * request this is the native `lane_decisions` resume through the lock;
78
+ * without one (the run is still streaming siblings) it is `run/lane-input`,
79
+ * which the host queues and answers on the park. Returns the dispatch
80
+ * promise, or `undefined` when the lock dropped a resume.
81
+ */
82
+ sendLaneDecisions: (lanes: Readonly<Record<string, StatewireThread.InputResponse>>) => Promise<unknown> | undefined;
42
83
  }
43
84
  /**
44
85
  * The headless half of every statewire approval surface: selects the pending
@@ -1,11 +1,11 @@
1
1
  import { useStatewireSendCommand, useStatewireRuns, useStatewireState } from "@assistant-ui/react-statewire";
2
2
  import "../packages/agent-runtime/src/generated/runconfig-contract.js";
3
- import { useMemo } from "react";
3
+ import { useMemo, useCallback } from "react";
4
4
  import "@assistant-ui/tap";
5
5
  import "statewire";
6
6
  import "../packages/agent-runtime/src/statewire-suspended.js";
7
7
  import "@tanstack/react-query";
8
- import { isApprovalCardInterrupt, HITL_PENDING_RESUME_GRACE_MS, readAbandonedInputRequests, findAbandonedInterrupt, asHitlApproval, pendingActionRequests, useApprovalResumeLock } from "../packages/agent-runtime/src/interrupts.js";
8
+ import { isApprovalCardInterrupt, HITL_PENDING_RESUME_GRACE_MS, readAbandonedInputRequests, findAbandonedInterrupt, asHitlApproval, pendingActionRequests, readInterruptMirror, interruptMirrorPayload, readQueuedLaneDecisions, useApprovalResumeLock } from "../packages/agent-runtime/src/interrupts.js";
9
9
  import "../packages/agent-runtime/src/messages.js";
10
10
  import "../packages/agent-runtime/src/statewire-state.js";
11
11
  import "@assistant-ui/store";
@@ -15,6 +15,7 @@ import "../packages/agent-runtime/src/thread-status.js";
15
15
  import "@assistant-ui/core";
16
16
  import "../packages/agent-runtime/src/thread-handoff.js";
17
17
  import { useAthenaRunLive } from "./use-athena-run-live.js";
18
+ const NO_QUEUED_LANE_DECISIONS = Object.freeze({});
18
19
  function useStatewireApprovalRequest(options = {}) {
19
20
  var _a;
20
21
  const {
@@ -30,6 +31,10 @@ function useStatewireApprovalRequest(options = {}) {
30
31
  const hitl = asHitlApproval(request == null ? void 0 : request.payload);
31
32
  const actionRequests = useMemo(() => pendingActionRequests(hitl), [hitl]);
32
33
  const isRunning = useAthenaRunLive();
34
+ const mirror = useStatewireState(readInterruptMirror);
35
+ const liveHitl = request === void 0 && isRunning ? asHitlApproval(interruptMirrorPayload(mirror)) : null;
36
+ const liveActionRequests = useMemo(() => pendingActionRequests(liveHitl), [liveHitl]);
37
+ const queuedLaneDecisions = useStatewireState(readQueuedLaneDecisions) ?? NO_QUEUED_LANE_DECISIONS;
33
38
  const { pending, resume } = useApprovalResumeLock({
34
39
  requestId,
35
40
  isRunning,
@@ -43,7 +48,23 @@ function useStatewireApprovalRequest(options = {}) {
43
48
  response: { type: "resume", value }
44
49
  })
45
50
  });
46
- return { request, requestId, hitl, actionRequests, abandonedInterrupt, pending, resume };
51
+ const sendLaneDecisions = useCallback(
52
+ (lanes) => requestId !== void 0 ? resume({ lane_decisions: lanes }) : sendCommand({ type: "run/lane-input", laneDecisions: lanes }),
53
+ [requestId, resume, sendCommand]
54
+ );
55
+ return {
56
+ request,
57
+ requestId,
58
+ hitl,
59
+ actionRequests,
60
+ liveHitl,
61
+ liveActionRequests,
62
+ queuedLaneDecisions,
63
+ abandonedInterrupt,
64
+ pending,
65
+ resume,
66
+ sendLaneDecisions
67
+ };
47
68
  }
48
69
  export {
49
70
  useStatewireApprovalRequest
@@ -1 +1 @@
1
- {"version":3,"file":"use-statewire-approval-request.js","sources":["../../src/chat/use-statewire-approval-request.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport {\n useStatewireRuns,\n useStatewireSendCommand,\n useStatewireState,\n} from '@assistant-ui/react-statewire';\nimport {\n type AbandonedInputRequest,\n asHitlApproval,\n findAbandonedInterrupt,\n HITL_PENDING_RESUME_GRACE_MS,\n type HitlApprovalPayload,\n isApprovalCardInterrupt,\n type PendingActionRequest,\n pendingActionRequests,\n readAbandonedInputRequests,\n useApprovalResumeLock,\n} from '@athenaintel/agent-runtime';\nimport { useMemo } from 'react';\nimport { useAthenaRunLive } from './use-athena-run-live';\n\n/** Selects which pending interrupt an approval surface answers. */\nexport type StatewireInterruptPredicate = (\n request: StatewireThread.InputRequestState,\n) => request is StatewireThread.CustomInputRequest;\n\nexport interface UseStatewireApprovalRequestOptions {\n /**\n * Which pending interrupt to answer. Defaults to `isApprovalCardInterrupt`\n * (client-tool parks excluded — the client-tool bridge owns those, and a\n * generic resume would corrupt the parked tool call).\n */\n interruptPredicate?: StatewireInterruptPredicate;\n /**\n * How long a sent decision keeps the actions locked when no resume run ever\n * starts. Defaults to {@link HITL_PENDING_RESUME_GRACE_MS}.\n */\n pendingResumeGraceMs?: number;\n /**\n * The host refused the `run/input` (or the transport lost it): the\n * optimistic fold rolls back, the request is on screen again with its\n * actions re-enabled, and this is the surface's one chance to say why.\n */\n onResumeRejected?: (error: unknown, requestId: string) => void;\n}\n\nexport interface StatewireApprovalRequest<TValue> {\n /** The pending interrupt this surface answers, if any. */\n request: StatewireThread.CustomInputRequest | undefined;\n requestId: string | undefined;\n /** The payload narrowed to the Athena HITL approval shape, or `null`. */\n hitl: HitlApprovalPayload | null;\n /** Every gated call the approval covers, in decision order (empty for non-HITL parks). */\n actionRequests: PendingActionRequest[];\n /** An interrupt a non-resume dispatch dropped — the \"approval was cancelled\" notice. */\n abandonedInterrupt: AbandonedInputRequest | undefined;\n /** True after a resume until the request identity changes (or recovery fires). */\n pending: boolean;\n /**\n * Answer the request with `{ type: 'run/input', requestId, response: { type: 'resume', value } }`.\n * Returns the dispatch promise (settles when the host accepts, rejects when\n * it refuses), or `undefined` when the lock dropped the send.\n */\n resume: (value: TValue) => Promise<unknown> | undefined;\n}\n\n/**\n * The headless half of every statewire approval surface: selects the pending\n * interrupt from the runs projection, narrows its payload, reads the\n * host-published abandoned interrupts, and wires the shared optimistic resume\n * lock to the thread's `run/input` command. `StatewireApprovalCard` renders\n * over it; hosts with their own decision UI (per-lane subagent cards) build\n * on it directly so the selection and lock semantics cannot drift.\n *\n * Requires a statewire aui scope (throws outside one, like every\n * `useStatewire*` hook) — gate the mount with `hasStatewireThreadExtras`.\n */\nexport function useStatewireApprovalRequest<\n TValue extends StatewireThread.InputResponse = Record<string, string>,\n>(options: UseStatewireApprovalRequestOptions = {}): StatewireApprovalRequest<TValue> {\n const {\n interruptPredicate = isApprovalCardInterrupt,\n pendingResumeGraceMs = HITL_PENDING_RESUME_GRACE_MS,\n onResumeRejected,\n } = options;\n const sendCommand = useStatewireSendCommand();\n // The runs projection is memoized by the lib, so this derivation is\n // reference-stable between state updates.\n const request = useStatewireRuns().inputRequests?.find(interruptPredicate);\n const requestId = request?.id;\n\n // The host publishes the interrupts a non-resume dispatch dropped. Select\n // the raw array reference (pre-stream-safe) and derive inline; a `.find`\n // selector would return a fresh object every update.\n const abandoned = useStatewireState(readAbandonedInputRequests);\n const abandonedInterrupt = findAbandonedInterrupt(abandoned);\n\n const hitl = asHitlApproval(request?.payload);\n const actionRequests = useMemo(() => pendingActionRequests(hitl), [hitl]);\n\n const isRunning = useAthenaRunLive();\n const { pending, resume } = useApprovalResumeLock<TValue>({\n requestId,\n isRunning,\n pendingResumeGraceMs,\n onResumeRejected,\n // The dispatch promise is handed to the lock: a refusal releases it and\n // reaches `onResumeRejected`.\n sendResume: (id, value) =>\n sendCommand({\n type: 'run/input',\n requestId: id,\n response: { type: 'resume', value },\n }),\n });\n\n return { request, requestId, hitl, actionRequests, abandonedInterrupt, pending, resume };\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AA6EO,SAAS,4BAEd,UAA8C,IAAsC;;AACpF,QAAM;AAAA,IACJ,qBAAqB;AAAA,IACrB,uBAAuB;AAAA,IACvB;AAAA,EAAA,IACE;AACJ,QAAM,cAAc,wBAAA;AAGpB,QAAM,WAAU,sBAAA,EAAmB,kBAAnB,mBAAkC,KAAK;AACvD,QAAM,YAAY,mCAAS;AAK3B,QAAM,YAAY,kBAAkB,0BAA0B;AAC9D,QAAM,qBAAqB,uBAAuB,SAAS;AAE3D,QAAM,OAAO,eAAe,mCAAS,OAAO;AAC5C,QAAM,iBAAiB,QAAQ,MAAM,sBAAsB,IAAI,GAAG,CAAC,IAAI,CAAC;AAExE,QAAM,YAAY,iBAAA;AAClB,QAAM,EAAE,SAAS,OAAA,IAAW,sBAA8B;AAAA,IACxD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA,YAAY,CAAC,IAAI,UACf,YAAY;AAAA,MACV,MAAM;AAAA,MACN,WAAW;AAAA,MACX,UAAU,EAAE,MAAM,UAAU,MAAA;AAAA,IAAM,CACnC;AAAA,EAAA,CACJ;AAED,SAAO,EAAE,SAAS,WAAW,MAAM,gBAAgB,oBAAoB,SAAS,OAAA;AAClF;"}
1
+ {"version":3,"file":"use-statewire-approval-request.js","sources":["../../src/chat/use-statewire-approval-request.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport {\n useStatewireRuns,\n useStatewireSendCommand,\n useStatewireState,\n} from '@assistant-ui/react-statewire';\nimport {\n type AbandonedInputRequest,\n asHitlApproval,\n findAbandonedInterrupt,\n HITL_PENDING_RESUME_GRACE_MS,\n type HitlApprovalPayload,\n interruptMirrorPayload,\n isApprovalCardInterrupt,\n type PendingActionRequest,\n pendingActionRequests,\n readAbandonedInputRequests,\n readInterruptMirror,\n readQueuedLaneDecisions,\n useApprovalResumeLock,\n} from '@athenaintel/agent-runtime';\nimport { useCallback, useMemo } from 'react';\nimport { useAthenaRunLive } from './use-athena-run-live';\n\ndeclare module '@assistant-ui/core' {\n namespace Assistant {\n interface Commands {\n /**\n * Decide per-dispatch subagent lanes while the parent run is still\n * streaming (`DeepAgentThread.run_lane_input`). Keys are the lanes'\n * langgraph interrupt ids; the host matches them against the lanes its\n * mirrored interrupt currently parks, answers at once when the run is\n * already parked, and otherwise holds them until it parks — stale or\n * unknown ids are ignored, never carried to a later interrupt.\n */\n runLaneInput: {\n readonly type: 'run/lane-input';\n readonly laneDecisions: Readonly<Record<string, StatewireThread.InputResponse>>;\n };\n }\n }\n}\n\nconst NO_QUEUED_LANE_DECISIONS: Readonly<Record<string, unknown>> = Object.freeze({});\n\n/** Selects which pending interrupt an approval surface answers. */\nexport type StatewireInterruptPredicate = (\n request: StatewireThread.InputRequestState,\n) => request is StatewireThread.CustomInputRequest;\n\nexport interface UseStatewireApprovalRequestOptions {\n /**\n * Which pending interrupt to answer. Defaults to `isApprovalCardInterrupt`\n * (client-tool parks excluded — the client-tool bridge owns those, and a\n * generic resume would corrupt the parked tool call).\n */\n interruptPredicate?: StatewireInterruptPredicate;\n /**\n * How long a sent decision keeps the actions locked when no resume run ever\n * starts. Defaults to {@link HITL_PENDING_RESUME_GRACE_MS}.\n */\n pendingResumeGraceMs?: number;\n /**\n * The host refused the `run/input` (or the transport lost it): the\n * optimistic fold rolls back, the request is on screen again with its\n * actions re-enabled, and this is the surface's one chance to say why.\n */\n onResumeRejected?: (error: unknown, requestId: string) => void;\n}\n\nexport interface StatewireApprovalRequest<TValue> {\n /** The pending interrupt this surface answers, if any. */\n request: StatewireThread.CustomInputRequest | undefined;\n requestId: string | undefined;\n /** The payload narrowed to the Athena HITL approval shape, or `null`. */\n hitl: HitlApprovalPayload | null;\n /** Every gated call the approval covers, in decision order (empty for non-HITL parks). */\n actionRequests: PendingActionRequest[];\n /** An interrupt a non-resume dispatch dropped — the \"approval was cancelled\" notice. */\n abandonedInterrupt: AbandonedInputRequest | undefined;\n /**\n * The mirrored interrupt's HITL payload while the run is still live and no\n * input request is answerable: per-dispatch `task` lanes parked on their\n * gates while sibling lanes stream. `null` once the run parks (the same\n * lanes are then `hitl` / `actionRequests`) and when nothing is parked.\n */\n liveHitl: HitlApprovalPayload | null;\n /** Every gated call `liveHitl` covers (empty when it is `null`). */\n liveActionRequests: PendingActionRequest[];\n /**\n * The decisions the host already holds from a `run/lane-input`, keyed by\n * langgraph interrupt id, to be answered when the run parks — a queued lane\n * reads as decided on every attached client, not just the one that sent it.\n */\n queuedLaneDecisions: Readonly<Record<string, unknown>>;\n /** True after a resume until the request identity changes (or recovery fires). */\n pending: boolean;\n /**\n * Answer the request with `{ type: 'run/input', requestId, response: { type: 'resume', value } }`.\n * Returns the dispatch promise (settles when the host accepts, rejects when\n * it refuses), or `undefined` when the lock dropped the send.\n */\n resume: (value: TValue) => Promise<unknown> | undefined;\n /**\n * Decide per-dispatch lanes by langgraph interrupt id. With a pending\n * request this is the native `lane_decisions` resume through the lock;\n * without one (the run is still streaming siblings) it is `run/lane-input`,\n * which the host queues and answers on the park. Returns the dispatch\n * promise, or `undefined` when the lock dropped a resume.\n */\n sendLaneDecisions: (\n lanes: Readonly<Record<string, StatewireThread.InputResponse>>,\n ) => Promise<unknown> | undefined;\n}\n\n/**\n * The headless half of every statewire approval surface: selects the pending\n * interrupt from the runs projection, narrows its payload, reads the\n * host-published abandoned interrupts, and wires the shared optimistic resume\n * lock to the thread's `run/input` command. `StatewireApprovalCard` renders\n * over it; hosts with their own decision UI (per-lane subagent cards) build\n * on it directly so the selection and lock semantics cannot drift.\n *\n * Requires a statewire aui scope (throws outside one, like every\n * `useStatewire*` hook) — gate the mount with `hasStatewireThreadExtras`.\n */\nexport function useStatewireApprovalRequest<\n TValue extends StatewireThread.InputResponse = Record<string, string>,\n>(options: UseStatewireApprovalRequestOptions = {}): StatewireApprovalRequest<TValue> {\n const {\n interruptPredicate = isApprovalCardInterrupt,\n pendingResumeGraceMs = HITL_PENDING_RESUME_GRACE_MS,\n onResumeRejected,\n } = options;\n const sendCommand = useStatewireSendCommand();\n // The runs projection is memoized by the lib, so this derivation is\n // reference-stable between state updates.\n const request = useStatewireRuns().inputRequests?.find(interruptPredicate);\n const requestId = request?.id;\n\n // The host publishes the interrupts a non-resume dispatch dropped. Select\n // the raw array reference (pre-stream-safe) and derive inline; a `.find`\n // selector would return a fresh object every update.\n const abandoned = useStatewireState(readAbandonedInputRequests);\n const abandonedInterrupt = findAbandonedInterrupt(abandoned);\n\n const hitl = asHitlApproval(request?.payload);\n const actionRequests = useMemo(() => pendingActionRequests(hitl), [hitl]);\n\n const isRunning = useAthenaRunLive();\n // The host mirrors the interrupt into state as soon as a lane parks — while\n // the run manager still reports `running` (siblings streaming) and has no\n // request to answer. Raw reference selectors; derive inline.\n const mirror = useStatewireState(readInterruptMirror);\n const liveHitl =\n request === undefined && isRunning ? asHitlApproval(interruptMirrorPayload(mirror)) : null;\n const liveActionRequests = useMemo(() => pendingActionRequests(liveHitl), [liveHitl]);\n const queuedLaneDecisions =\n useStatewireState(readQueuedLaneDecisions) ?? NO_QUEUED_LANE_DECISIONS;\n\n const { pending, resume } = useApprovalResumeLock<TValue>({\n requestId,\n isRunning,\n pendingResumeGraceMs,\n onResumeRejected,\n // The dispatch promise is handed to the lock: a refusal releases it and\n // reaches `onResumeRejected`.\n sendResume: (id, value) =>\n sendCommand({\n type: 'run/input',\n requestId: id,\n response: { type: 'resume', value },\n }),\n });\n\n const sendLaneDecisions = useCallback(\n (lanes: Readonly<Record<string, StatewireThread.InputResponse>>) =>\n requestId !== undefined\n ? resume({ lane_decisions: lanes } as unknown as TValue)\n : sendCommand({ type: 'run/lane-input', laneDecisions: lanes }),\n [requestId, resume, sendCommand],\n );\n\n return {\n request,\n requestId,\n hitl,\n actionRequests,\n liveHitl,\n liveActionRequests,\n queuedLaneDecisions,\n abandonedInterrupt,\n pending,\n resume,\n sendLaneDecisions,\n };\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AA2CA,MAAM,2BAA8D,OAAO,OAAO,EAAE;AAmF7E,SAAS,4BAEd,UAA8C,IAAsC;;AACpF,QAAM;AAAA,IACJ,qBAAqB;AAAA,IACrB,uBAAuB;AAAA,IACvB;AAAA,EAAA,IACE;AACJ,QAAM,cAAc,wBAAA;AAGpB,QAAM,WAAU,sBAAA,EAAmB,kBAAnB,mBAAkC,KAAK;AACvD,QAAM,YAAY,mCAAS;AAK3B,QAAM,YAAY,kBAAkB,0BAA0B;AAC9D,QAAM,qBAAqB,uBAAuB,SAAS;AAE3D,QAAM,OAAO,eAAe,mCAAS,OAAO;AAC5C,QAAM,iBAAiB,QAAQ,MAAM,sBAAsB,IAAI,GAAG,CAAC,IAAI,CAAC;AAExE,QAAM,YAAY,iBAAA;AAIlB,QAAM,SAAS,kBAAkB,mBAAmB;AACpD,QAAM,WACJ,YAAY,UAAa,YAAY,eAAe,uBAAuB,MAAM,CAAC,IAAI;AACxF,QAAM,qBAAqB,QAAQ,MAAM,sBAAsB,QAAQ,GAAG,CAAC,QAAQ,CAAC;AACpF,QAAM,sBACJ,kBAAkB,uBAAuB,KAAK;AAEhD,QAAM,EAAE,SAAS,OAAA,IAAW,sBAA8B;AAAA,IACxD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA,YAAY,CAAC,IAAI,UACf,YAAY;AAAA,MACV,MAAM;AAAA,MACN,WAAW;AAAA,MACX,UAAU,EAAE,MAAM,UAAU,MAAA;AAAA,IAAM,CACnC;AAAA,EAAA,CACJ;AAED,QAAM,oBAAoB;AAAA,IACxB,CAAC,UACC,cAAc,SACV,OAAO,EAAE,gBAAgB,MAAA,CAA4B,IACrD,YAAY,EAAE,MAAM,kBAAkB,eAAe,OAAO;AAAA,IAClE,CAAC,WAAW,QAAQ,WAAW;AAAA,EAAA;AAGjC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
@@ -59,7 +59,6 @@ const ASSET_TYPE_ICON_FILE = {
59
59
  catalog: "ic-db",
60
60
  drive: "ic-db",
61
61
  query_snippet: "ic-snippet",
62
- semantic_query: "ic-snippet",
63
62
  semantic_model: "ic-db",
64
63
  dataset: "ic-db",
65
64
  // Meetings & Calls
@@ -1 +1 @@
1
- {"version":3,"file":"asset-type-icons.cjs","sources":["../../../../../../src/chat-ui/mentions/suggestions/data/sources/asset-type-icons.ts"],"sourcesContent":["/**\n * Maps Athena asset types to SVG icon file names.\n *\n * The SVG files are bundled by the host app, which declares where they are\n * served from via `configureAssetIconBasePath`. Hosts that don't bundle them\n * fall back to the Lucide icons carried on every menu item.\n */\n\nlet iconBasePath: string | null = null;\n\n/**\n * Point the mention UI at the host's copy of the asset-type SVGs.\n * `basePath` is resolved against the host document; a trailing \"/\" is added\n * when the caller omits it.\n */\nexport function configureAssetIconBasePath({\n basePath,\n}: {\n basePath: string | null;\n}): void {\n iconBasePath = basePath ? basePath.replace(/\\/*$/, \"/\") : basePath;\n}\n\nconst ASSET_TYPE_ICON_FILE: Record<string, string> = {\n // Documents\n folder: \"ic-folder\",\n txt: \"ic-txt\",\n document: \"ic-word\",\n word_document: \"ic-word\",\n markdown: \"ic-md\",\n pdf: \"ic-pdf\",\n html: \"ic-html\",\n web_page: \"ic-html\",\n yaml: \"ic-yaml\",\n json: \"ic-json\",\n\n // Spreadsheets\n spreadsheet: \"ic-excel\",\n table: \"ic-table\",\n\n // Media\n image: \"ic-img\",\n audio: \"ic-audio\",\n video: \"ic-video\",\n voice_memo: \"ic-voice_memo\",\n\n // Presentations\n powerpoint_deck: \"ic-power_point\",\n presentation_pptx_studio: \"ic-power_point\",\n pptx: \"ic-power_point\",\n\n // Email & Calendar\n email: \"ic-mail\",\n email_message: \"ic-mail\",\n live_email_thread: \"ic-mail\",\n live_calendar_event: \"ic-mail\",\n\n // Code\n python: \"ic-py\",\n javascript: \"ic-js\",\n typescript: \"ic-code\",\n jsx: \"ic-code\",\n tsx: \"ic-code\",\n css: \"ic-code\",\n java: \"ic-code\",\n c: \"ic-code\",\n cpp: \"ic-code\",\n csharp: \"ic-code\",\n ruby: \"ic-code\",\n go: \"ic-code\",\n swift: \"ic-code\",\n php: \"ic-code\",\n rust: \"ic-code\",\n vue: \"ic-code\",\n wm_script: \"ic-code\",\n\n // Structured data\n database: \"ic-snippet\",\n schema: \"ic-schema\",\n catalog: \"ic-db\",\n drive: \"ic-db\",\n query_snippet: \"ic-snippet\",\n semantic_query: \"ic-snippet\",\n semantic_model: \"ic-db\",\n dataset: \"ic-db\",\n\n // Meetings & Calls\n meeting: \"ic-meeting\",\n video_session: \"ic-meeting\",\n phone_call: \"ic-phonecall\",\n\n // Sessions & Devices\n session: \"ic-session\",\n browse_session: \"ic-html\",\n virtual_machine: \"ic-html\",\n computer: \"ic-computer\",\n desktop_session: \"ic-desktop_session\",\n mobile_session: \"ic-mobile_session\",\n\n // Projects & Profiles\n project: \"ic-project\",\n browse_profile: \"ic-profile\",\n\n // Workflows & Automation\n aop: \"ic-flow\",\n playbook: \"ic-playbook\",\n policy: \"ic-policy\",\n flow: \"ic-flow\",\n transform_project: \"ic-transform_project\",\n\n // Collections & Notebooks\n collection: \"ic-collection\",\n notebook: \"ic-notebook\",\n\n // UI & Apps\n user_interface: \"ic-app\",\n application: \"ic-application\",\n canvas: \"ic-canvas\",\n dashboard: \"ic-dashboard\",\n figure: \"ic_figure\",\n\n // People & Companies\n person: \"ic-person\",\n company: \"ic-company\",\n\n // Misc\n prompt: \"ic-ai\",\n skill: \"ic-skill\",\n task_item: \"ic-task\",\n spaces: \"ic-spaces\",\n secret: \"ic-secret\",\n photoshop: \"ic-pts\",\n illustrator: \"ic-ai\",\n zip: \"ic-zip\",\n sevenzip: \"ic-zip\",\n\n // Google Drive / SharePoint / Salesforce\n gdrive_folder: \"ic-folder\",\n gdrive_file: \"ic-file\",\n salesforce_record: \"ic-db\",\n salesforce_file: \"ic-db\",\n};\n\nconst ASSET_TYPE_LUCIDE_FALLBACK: Record<string, string> = {\n folder: \"folder\",\n spreadsheet: \"sheet\",\n image: \"image\",\n video: \"video\",\n audio: \"headphones\",\n email: \"mail\",\n database: \"database\",\n project: \"folder-kanban\",\n meeting: \"video\",\n user_interface: \"layout\",\n};\n\n/**\n * Get the SVG thumbnail URL for an Athena asset type, or undefined when the\n * host app does not bundle the icon files.\n */\nexport function assetTypeToThumb(\n assetType: string | null | undefined,\n): string | undefined {\n if (!iconBasePath) return undefined;\n const name: string =\n (assetType ? ASSET_TYPE_ICON_FILE[assetType.toLowerCase()] : undefined) ??\n \"ic-file\";\n return `${iconBasePath}${name}.svg`;\n}\n\n/**\n * Get Lucide icon name fallback for an asset type.\n * Used when thumbnailUrl can't load.\n */\nexport function assetTypeToIcon(assetType: string | null | undefined): string {\n if (!assetType) return \"file\";\n return ASSET_TYPE_LUCIDE_FALLBACK[assetType.toLowerCase()] ?? \"file\";\n}\n"],"names":[],"mappings":";;AAQA,IAAI,eAA8B;AAO3B,SAAS,2BAA2B;AAAA,EACzC;AACF,GAES;AACP,iBAAe,WAAW,SAAS,QAAQ,QAAQ,GAAG,IAAI;AAC5D;AAEA,MAAM,uBAA+C;AAAA;AAAA,EAEnD,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,UAAU;AAAA,EACV,eAAe;AAAA,EACf,UAAU;AAAA,EACV,KAAK;AAAA,EACL,MAAM;AAAA,EACN,UAAU;AAAA,EACV,MAAM;AAAA,EACN,MAAM;AAAA;AAAA,EAGN,aAAa;AAAA,EACb,OAAO;AAAA;AAAA,EAGP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,YAAY;AAAA;AAAA,EAGZ,iBAAiB;AAAA,EACjB,0BAA0B;AAAA,EAC1B,MAAM;AAAA;AAAA,EAGN,OAAO;AAAA,EACP,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,qBAAqB;AAAA;AAAA,EAGrB,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,GAAG;AAAA,EACH,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,WAAW;AAAA;AAAA,EAGX,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,OAAO;AAAA,EACP,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,SAAS;AAAA;AAAA,EAGT,SAAS;AAAA,EACT,eAAe;AAAA,EACf,YAAY;AAAA;AAAA,EAGZ,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB,gBAAgB;AAAA;AAAA,EAGhB,SAAS;AAAA,EACT,gBAAgB;AAAA;AAAA,EAGhB,KAAK;AAAA,EACL,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,mBAAmB;AAAA;AAAA,EAGnB,YAAY;AAAA,EACZ,UAAU;AAAA;AAAA,EAGV,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EAGR,QAAQ;AAAA,EACR,SAAS;AAAA;AAAA,EAGT,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,aAAa;AAAA,EACb,KAAK;AAAA,EACL,UAAU;AAAA;AAAA,EAGV,eAAe;AAAA,EACf,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,iBAAiB;AACnB;AAEA,MAAM,6BAAqD;AAAA,EACzD,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS;AAAA,EACT,gBAAgB;AAClB;AAMO,SAAS,iBACd,WACoB;AACpB,MAAI,CAAC,aAAc,QAAO;AAC1B,QAAM,QACH,YAAY,qBAAqB,UAAU,YAAA,CAAa,IAAI,WAC7D;AACF,SAAO,GAAG,YAAY,GAAG,IAAI;AAC/B;AAMO,SAAS,gBAAgB,WAA8C;AAC5E,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO,2BAA2B,UAAU,YAAA,CAAa,KAAK;AAChE;;;;"}
1
+ {"version":3,"file":"asset-type-icons.cjs","sources":["../../../../../../src/chat-ui/mentions/suggestions/data/sources/asset-type-icons.ts"],"sourcesContent":["/**\n * Maps Athena asset types to SVG icon file names.\n *\n * The SVG files are bundled by the host app, which declares where they are\n * served from via `configureAssetIconBasePath`. Hosts that don't bundle them\n * fall back to the Lucide icons carried on every menu item.\n */\n\nlet iconBasePath: string | null = null;\n\n/**\n * Point the mention UI at the host's copy of the asset-type SVGs.\n * `basePath` is resolved against the host document; a trailing \"/\" is added\n * when the caller omits it.\n */\nexport function configureAssetIconBasePath({\n basePath,\n}: {\n basePath: string | null;\n}): void {\n iconBasePath = basePath ? basePath.replace(/\\/*$/, \"/\") : basePath;\n}\n\nconst ASSET_TYPE_ICON_FILE: Record<string, string> = {\n // Documents\n folder: \"ic-folder\",\n txt: \"ic-txt\",\n document: \"ic-word\",\n word_document: \"ic-word\",\n markdown: \"ic-md\",\n pdf: \"ic-pdf\",\n html: \"ic-html\",\n web_page: \"ic-html\",\n yaml: \"ic-yaml\",\n json: \"ic-json\",\n\n // Spreadsheets\n spreadsheet: \"ic-excel\",\n table: \"ic-table\",\n\n // Media\n image: \"ic-img\",\n audio: \"ic-audio\",\n video: \"ic-video\",\n voice_memo: \"ic-voice_memo\",\n\n // Presentations\n powerpoint_deck: \"ic-power_point\",\n presentation_pptx_studio: \"ic-power_point\",\n pptx: \"ic-power_point\",\n\n // Email & Calendar\n email: \"ic-mail\",\n email_message: \"ic-mail\",\n live_email_thread: \"ic-mail\",\n live_calendar_event: \"ic-mail\",\n\n // Code\n python: \"ic-py\",\n javascript: \"ic-js\",\n typescript: \"ic-code\",\n jsx: \"ic-code\",\n tsx: \"ic-code\",\n css: \"ic-code\",\n java: \"ic-code\",\n c: \"ic-code\",\n cpp: \"ic-code\",\n csharp: \"ic-code\",\n ruby: \"ic-code\",\n go: \"ic-code\",\n swift: \"ic-code\",\n php: \"ic-code\",\n rust: \"ic-code\",\n vue: \"ic-code\",\n wm_script: \"ic-code\",\n\n // Structured data\n database: \"ic-snippet\",\n schema: \"ic-schema\",\n catalog: \"ic-db\",\n drive: \"ic-db\",\n query_snippet: \"ic-snippet\",\n semantic_model: \"ic-db\",\n dataset: \"ic-db\",\n\n // Meetings & Calls\n meeting: \"ic-meeting\",\n video_session: \"ic-meeting\",\n phone_call: \"ic-phonecall\",\n\n // Sessions & Devices\n session: \"ic-session\",\n browse_session: \"ic-html\",\n virtual_machine: \"ic-html\",\n computer: \"ic-computer\",\n desktop_session: \"ic-desktop_session\",\n mobile_session: \"ic-mobile_session\",\n\n // Projects & Profiles\n project: \"ic-project\",\n browse_profile: \"ic-profile\",\n\n // Workflows & Automation\n aop: \"ic-flow\",\n playbook: \"ic-playbook\",\n policy: \"ic-policy\",\n flow: \"ic-flow\",\n transform_project: \"ic-transform_project\",\n\n // Collections & Notebooks\n collection: \"ic-collection\",\n notebook: \"ic-notebook\",\n\n // UI & Apps\n user_interface: \"ic-app\",\n application: \"ic-application\",\n canvas: \"ic-canvas\",\n dashboard: \"ic-dashboard\",\n figure: \"ic_figure\",\n\n // People & Companies\n person: \"ic-person\",\n company: \"ic-company\",\n\n // Misc\n prompt: \"ic-ai\",\n skill: \"ic-skill\",\n task_item: \"ic-task\",\n spaces: \"ic-spaces\",\n secret: \"ic-secret\",\n photoshop: \"ic-pts\",\n illustrator: \"ic-ai\",\n zip: \"ic-zip\",\n sevenzip: \"ic-zip\",\n\n // Google Drive / SharePoint / Salesforce\n gdrive_folder: \"ic-folder\",\n gdrive_file: \"ic-file\",\n salesforce_record: \"ic-db\",\n salesforce_file: \"ic-db\",\n};\n\nconst ASSET_TYPE_LUCIDE_FALLBACK: Record<string, string> = {\n folder: \"folder\",\n spreadsheet: \"sheet\",\n image: \"image\",\n video: \"video\",\n audio: \"headphones\",\n email: \"mail\",\n database: \"database\",\n project: \"folder-kanban\",\n meeting: \"video\",\n user_interface: \"layout\",\n};\n\n/**\n * Get the SVG thumbnail URL for an Athena asset type, or undefined when the\n * host app does not bundle the icon files.\n */\nexport function assetTypeToThumb(\n assetType: string | null | undefined,\n): string | undefined {\n if (!iconBasePath) return undefined;\n const name: string =\n (assetType ? ASSET_TYPE_ICON_FILE[assetType.toLowerCase()] : undefined) ??\n \"ic-file\";\n return `${iconBasePath}${name}.svg`;\n}\n\n/**\n * Get Lucide icon name fallback for an asset type.\n * Used when thumbnailUrl can't load.\n */\nexport function assetTypeToIcon(assetType: string | null | undefined): string {\n if (!assetType) return \"file\";\n return ASSET_TYPE_LUCIDE_FALLBACK[assetType.toLowerCase()] ?? \"file\";\n}\n"],"names":[],"mappings":";;AAQA,IAAI,eAA8B;AAO3B,SAAS,2BAA2B;AAAA,EACzC;AACF,GAES;AACP,iBAAe,WAAW,SAAS,QAAQ,QAAQ,GAAG,IAAI;AAC5D;AAEA,MAAM,uBAA+C;AAAA;AAAA,EAEnD,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,UAAU;AAAA,EACV,eAAe;AAAA,EACf,UAAU;AAAA,EACV,KAAK;AAAA,EACL,MAAM;AAAA,EACN,UAAU;AAAA,EACV,MAAM;AAAA,EACN,MAAM;AAAA;AAAA,EAGN,aAAa;AAAA,EACb,OAAO;AAAA;AAAA,EAGP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,YAAY;AAAA;AAAA,EAGZ,iBAAiB;AAAA,EACjB,0BAA0B;AAAA,EAC1B,MAAM;AAAA;AAAA,EAGN,OAAO;AAAA,EACP,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,qBAAqB;AAAA;AAAA,EAGrB,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,GAAG;AAAA,EACH,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,WAAW;AAAA;AAAA,EAGX,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,OAAO;AAAA,EACP,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,SAAS;AAAA;AAAA,EAGT,SAAS;AAAA,EACT,eAAe;AAAA,EACf,YAAY;AAAA;AAAA,EAGZ,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB,gBAAgB;AAAA;AAAA,EAGhB,SAAS;AAAA,EACT,gBAAgB;AAAA;AAAA,EAGhB,KAAK;AAAA,EACL,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,mBAAmB;AAAA;AAAA,EAGnB,YAAY;AAAA,EACZ,UAAU;AAAA;AAAA,EAGV,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EAGR,QAAQ;AAAA,EACR,SAAS;AAAA;AAAA,EAGT,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,aAAa;AAAA,EACb,KAAK;AAAA,EACL,UAAU;AAAA;AAAA,EAGV,eAAe;AAAA,EACf,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,iBAAiB;AACnB;AAEA,MAAM,6BAAqD;AAAA,EACzD,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS;AAAA,EACT,gBAAgB;AAClB;AAMO,SAAS,iBACd,WACoB;AACpB,MAAI,CAAC,aAAc,QAAO;AAC1B,QAAM,QACH,YAAY,qBAAqB,UAAU,YAAA,CAAa,IAAI,WAC7D;AACF,SAAO,GAAG,YAAY,GAAG,IAAI;AAC/B;AAMO,SAAS,gBAAgB,WAA8C;AAC5E,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO,2BAA2B,UAAU,YAAA,CAAa,KAAK;AAChE;;;;"}
@@ -57,7 +57,6 @@ const ASSET_TYPE_ICON_FILE = {
57
57
  catalog: "ic-db",
58
58
  drive: "ic-db",
59
59
  query_snippet: "ic-snippet",
60
- semantic_query: "ic-snippet",
61
60
  semantic_model: "ic-db",
62
61
  dataset: "ic-db",
63
62
  // Meetings & Calls
@@ -1 +1 @@
1
- {"version":3,"file":"asset-type-icons.js","sources":["../../../../../../src/chat-ui/mentions/suggestions/data/sources/asset-type-icons.ts"],"sourcesContent":["/**\n * Maps Athena asset types to SVG icon file names.\n *\n * The SVG files are bundled by the host app, which declares where they are\n * served from via `configureAssetIconBasePath`. Hosts that don't bundle them\n * fall back to the Lucide icons carried on every menu item.\n */\n\nlet iconBasePath: string | null = null;\n\n/**\n * Point the mention UI at the host's copy of the asset-type SVGs.\n * `basePath` is resolved against the host document; a trailing \"/\" is added\n * when the caller omits it.\n */\nexport function configureAssetIconBasePath({\n basePath,\n}: {\n basePath: string | null;\n}): void {\n iconBasePath = basePath ? basePath.replace(/\\/*$/, \"/\") : basePath;\n}\n\nconst ASSET_TYPE_ICON_FILE: Record<string, string> = {\n // Documents\n folder: \"ic-folder\",\n txt: \"ic-txt\",\n document: \"ic-word\",\n word_document: \"ic-word\",\n markdown: \"ic-md\",\n pdf: \"ic-pdf\",\n html: \"ic-html\",\n web_page: \"ic-html\",\n yaml: \"ic-yaml\",\n json: \"ic-json\",\n\n // Spreadsheets\n spreadsheet: \"ic-excel\",\n table: \"ic-table\",\n\n // Media\n image: \"ic-img\",\n audio: \"ic-audio\",\n video: \"ic-video\",\n voice_memo: \"ic-voice_memo\",\n\n // Presentations\n powerpoint_deck: \"ic-power_point\",\n presentation_pptx_studio: \"ic-power_point\",\n pptx: \"ic-power_point\",\n\n // Email & Calendar\n email: \"ic-mail\",\n email_message: \"ic-mail\",\n live_email_thread: \"ic-mail\",\n live_calendar_event: \"ic-mail\",\n\n // Code\n python: \"ic-py\",\n javascript: \"ic-js\",\n typescript: \"ic-code\",\n jsx: \"ic-code\",\n tsx: \"ic-code\",\n css: \"ic-code\",\n java: \"ic-code\",\n c: \"ic-code\",\n cpp: \"ic-code\",\n csharp: \"ic-code\",\n ruby: \"ic-code\",\n go: \"ic-code\",\n swift: \"ic-code\",\n php: \"ic-code\",\n rust: \"ic-code\",\n vue: \"ic-code\",\n wm_script: \"ic-code\",\n\n // Structured data\n database: \"ic-snippet\",\n schema: \"ic-schema\",\n catalog: \"ic-db\",\n drive: \"ic-db\",\n query_snippet: \"ic-snippet\",\n semantic_query: \"ic-snippet\",\n semantic_model: \"ic-db\",\n dataset: \"ic-db\",\n\n // Meetings & Calls\n meeting: \"ic-meeting\",\n video_session: \"ic-meeting\",\n phone_call: \"ic-phonecall\",\n\n // Sessions & Devices\n session: \"ic-session\",\n browse_session: \"ic-html\",\n virtual_machine: \"ic-html\",\n computer: \"ic-computer\",\n desktop_session: \"ic-desktop_session\",\n mobile_session: \"ic-mobile_session\",\n\n // Projects & Profiles\n project: \"ic-project\",\n browse_profile: \"ic-profile\",\n\n // Workflows & Automation\n aop: \"ic-flow\",\n playbook: \"ic-playbook\",\n policy: \"ic-policy\",\n flow: \"ic-flow\",\n transform_project: \"ic-transform_project\",\n\n // Collections & Notebooks\n collection: \"ic-collection\",\n notebook: \"ic-notebook\",\n\n // UI & Apps\n user_interface: \"ic-app\",\n application: \"ic-application\",\n canvas: \"ic-canvas\",\n dashboard: \"ic-dashboard\",\n figure: \"ic_figure\",\n\n // People & Companies\n person: \"ic-person\",\n company: \"ic-company\",\n\n // Misc\n prompt: \"ic-ai\",\n skill: \"ic-skill\",\n task_item: \"ic-task\",\n spaces: \"ic-spaces\",\n secret: \"ic-secret\",\n photoshop: \"ic-pts\",\n illustrator: \"ic-ai\",\n zip: \"ic-zip\",\n sevenzip: \"ic-zip\",\n\n // Google Drive / SharePoint / Salesforce\n gdrive_folder: \"ic-folder\",\n gdrive_file: \"ic-file\",\n salesforce_record: \"ic-db\",\n salesforce_file: \"ic-db\",\n};\n\nconst ASSET_TYPE_LUCIDE_FALLBACK: Record<string, string> = {\n folder: \"folder\",\n spreadsheet: \"sheet\",\n image: \"image\",\n video: \"video\",\n audio: \"headphones\",\n email: \"mail\",\n database: \"database\",\n project: \"folder-kanban\",\n meeting: \"video\",\n user_interface: \"layout\",\n};\n\n/**\n * Get the SVG thumbnail URL for an Athena asset type, or undefined when the\n * host app does not bundle the icon files.\n */\nexport function assetTypeToThumb(\n assetType: string | null | undefined,\n): string | undefined {\n if (!iconBasePath) return undefined;\n const name: string =\n (assetType ? ASSET_TYPE_ICON_FILE[assetType.toLowerCase()] : undefined) ??\n \"ic-file\";\n return `${iconBasePath}${name}.svg`;\n}\n\n/**\n * Get Lucide icon name fallback for an asset type.\n * Used when thumbnailUrl can't load.\n */\nexport function assetTypeToIcon(assetType: string | null | undefined): string {\n if (!assetType) return \"file\";\n return ASSET_TYPE_LUCIDE_FALLBACK[assetType.toLowerCase()] ?? \"file\";\n}\n"],"names":[],"mappings":"AAQA,IAAI,eAA8B;AAO3B,SAAS,2BAA2B;AAAA,EACzC;AACF,GAES;AACP,iBAAe,WAAW,SAAS,QAAQ,QAAQ,GAAG,IAAI;AAC5D;AAEA,MAAM,uBAA+C;AAAA;AAAA,EAEnD,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,UAAU;AAAA,EACV,eAAe;AAAA,EACf,UAAU;AAAA,EACV,KAAK;AAAA,EACL,MAAM;AAAA,EACN,UAAU;AAAA,EACV,MAAM;AAAA,EACN,MAAM;AAAA;AAAA,EAGN,aAAa;AAAA,EACb,OAAO;AAAA;AAAA,EAGP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,YAAY;AAAA;AAAA,EAGZ,iBAAiB;AAAA,EACjB,0BAA0B;AAAA,EAC1B,MAAM;AAAA;AAAA,EAGN,OAAO;AAAA,EACP,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,qBAAqB;AAAA;AAAA,EAGrB,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,GAAG;AAAA,EACH,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,WAAW;AAAA;AAAA,EAGX,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,OAAO;AAAA,EACP,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,SAAS;AAAA;AAAA,EAGT,SAAS;AAAA,EACT,eAAe;AAAA,EACf,YAAY;AAAA;AAAA,EAGZ,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB,gBAAgB;AAAA;AAAA,EAGhB,SAAS;AAAA,EACT,gBAAgB;AAAA;AAAA,EAGhB,KAAK;AAAA,EACL,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,mBAAmB;AAAA;AAAA,EAGnB,YAAY;AAAA,EACZ,UAAU;AAAA;AAAA,EAGV,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EAGR,QAAQ;AAAA,EACR,SAAS;AAAA;AAAA,EAGT,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,aAAa;AAAA,EACb,KAAK;AAAA,EACL,UAAU;AAAA;AAAA,EAGV,eAAe;AAAA,EACf,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,iBAAiB;AACnB;AAEA,MAAM,6BAAqD;AAAA,EACzD,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS;AAAA,EACT,gBAAgB;AAClB;AAMO,SAAS,iBACd,WACoB;AACpB,MAAI,CAAC,aAAc,QAAO;AAC1B,QAAM,QACH,YAAY,qBAAqB,UAAU,YAAA,CAAa,IAAI,WAC7D;AACF,SAAO,GAAG,YAAY,GAAG,IAAI;AAC/B;AAMO,SAAS,gBAAgB,WAA8C;AAC5E,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO,2BAA2B,UAAU,YAAA,CAAa,KAAK;AAChE;"}
1
+ {"version":3,"file":"asset-type-icons.js","sources":["../../../../../../src/chat-ui/mentions/suggestions/data/sources/asset-type-icons.ts"],"sourcesContent":["/**\n * Maps Athena asset types to SVG icon file names.\n *\n * The SVG files are bundled by the host app, which declares where they are\n * served from via `configureAssetIconBasePath`. Hosts that don't bundle them\n * fall back to the Lucide icons carried on every menu item.\n */\n\nlet iconBasePath: string | null = null;\n\n/**\n * Point the mention UI at the host's copy of the asset-type SVGs.\n * `basePath` is resolved against the host document; a trailing \"/\" is added\n * when the caller omits it.\n */\nexport function configureAssetIconBasePath({\n basePath,\n}: {\n basePath: string | null;\n}): void {\n iconBasePath = basePath ? basePath.replace(/\\/*$/, \"/\") : basePath;\n}\n\nconst ASSET_TYPE_ICON_FILE: Record<string, string> = {\n // Documents\n folder: \"ic-folder\",\n txt: \"ic-txt\",\n document: \"ic-word\",\n word_document: \"ic-word\",\n markdown: \"ic-md\",\n pdf: \"ic-pdf\",\n html: \"ic-html\",\n web_page: \"ic-html\",\n yaml: \"ic-yaml\",\n json: \"ic-json\",\n\n // Spreadsheets\n spreadsheet: \"ic-excel\",\n table: \"ic-table\",\n\n // Media\n image: \"ic-img\",\n audio: \"ic-audio\",\n video: \"ic-video\",\n voice_memo: \"ic-voice_memo\",\n\n // Presentations\n powerpoint_deck: \"ic-power_point\",\n presentation_pptx_studio: \"ic-power_point\",\n pptx: \"ic-power_point\",\n\n // Email & Calendar\n email: \"ic-mail\",\n email_message: \"ic-mail\",\n live_email_thread: \"ic-mail\",\n live_calendar_event: \"ic-mail\",\n\n // Code\n python: \"ic-py\",\n javascript: \"ic-js\",\n typescript: \"ic-code\",\n jsx: \"ic-code\",\n tsx: \"ic-code\",\n css: \"ic-code\",\n java: \"ic-code\",\n c: \"ic-code\",\n cpp: \"ic-code\",\n csharp: \"ic-code\",\n ruby: \"ic-code\",\n go: \"ic-code\",\n swift: \"ic-code\",\n php: \"ic-code\",\n rust: \"ic-code\",\n vue: \"ic-code\",\n wm_script: \"ic-code\",\n\n // Structured data\n database: \"ic-snippet\",\n schema: \"ic-schema\",\n catalog: \"ic-db\",\n drive: \"ic-db\",\n query_snippet: \"ic-snippet\",\n semantic_model: \"ic-db\",\n dataset: \"ic-db\",\n\n // Meetings & Calls\n meeting: \"ic-meeting\",\n video_session: \"ic-meeting\",\n phone_call: \"ic-phonecall\",\n\n // Sessions & Devices\n session: \"ic-session\",\n browse_session: \"ic-html\",\n virtual_machine: \"ic-html\",\n computer: \"ic-computer\",\n desktop_session: \"ic-desktop_session\",\n mobile_session: \"ic-mobile_session\",\n\n // Projects & Profiles\n project: \"ic-project\",\n browse_profile: \"ic-profile\",\n\n // Workflows & Automation\n aop: \"ic-flow\",\n playbook: \"ic-playbook\",\n policy: \"ic-policy\",\n flow: \"ic-flow\",\n transform_project: \"ic-transform_project\",\n\n // Collections & Notebooks\n collection: \"ic-collection\",\n notebook: \"ic-notebook\",\n\n // UI & Apps\n user_interface: \"ic-app\",\n application: \"ic-application\",\n canvas: \"ic-canvas\",\n dashboard: \"ic-dashboard\",\n figure: \"ic_figure\",\n\n // People & Companies\n person: \"ic-person\",\n company: \"ic-company\",\n\n // Misc\n prompt: \"ic-ai\",\n skill: \"ic-skill\",\n task_item: \"ic-task\",\n spaces: \"ic-spaces\",\n secret: \"ic-secret\",\n photoshop: \"ic-pts\",\n illustrator: \"ic-ai\",\n zip: \"ic-zip\",\n sevenzip: \"ic-zip\",\n\n // Google Drive / SharePoint / Salesforce\n gdrive_folder: \"ic-folder\",\n gdrive_file: \"ic-file\",\n salesforce_record: \"ic-db\",\n salesforce_file: \"ic-db\",\n};\n\nconst ASSET_TYPE_LUCIDE_FALLBACK: Record<string, string> = {\n folder: \"folder\",\n spreadsheet: \"sheet\",\n image: \"image\",\n video: \"video\",\n audio: \"headphones\",\n email: \"mail\",\n database: \"database\",\n project: \"folder-kanban\",\n meeting: \"video\",\n user_interface: \"layout\",\n};\n\n/**\n * Get the SVG thumbnail URL for an Athena asset type, or undefined when the\n * host app does not bundle the icon files.\n */\nexport function assetTypeToThumb(\n assetType: string | null | undefined,\n): string | undefined {\n if (!iconBasePath) return undefined;\n const name: string =\n (assetType ? ASSET_TYPE_ICON_FILE[assetType.toLowerCase()] : undefined) ??\n \"ic-file\";\n return `${iconBasePath}${name}.svg`;\n}\n\n/**\n * Get Lucide icon name fallback for an asset type.\n * Used when thumbnailUrl can't load.\n */\nexport function assetTypeToIcon(assetType: string | null | undefined): string {\n if (!assetType) return \"file\";\n return ASSET_TYPE_LUCIDE_FALLBACK[assetType.toLowerCase()] ?? \"file\";\n}\n"],"names":[],"mappings":"AAQA,IAAI,eAA8B;AAO3B,SAAS,2BAA2B;AAAA,EACzC;AACF,GAES;AACP,iBAAe,WAAW,SAAS,QAAQ,QAAQ,GAAG,IAAI;AAC5D;AAEA,MAAM,uBAA+C;AAAA;AAAA,EAEnD,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,UAAU;AAAA,EACV,eAAe;AAAA,EACf,UAAU;AAAA,EACV,KAAK;AAAA,EACL,MAAM;AAAA,EACN,UAAU;AAAA,EACV,MAAM;AAAA,EACN,MAAM;AAAA;AAAA,EAGN,aAAa;AAAA,EACb,OAAO;AAAA;AAAA,EAGP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,YAAY;AAAA;AAAA,EAGZ,iBAAiB;AAAA,EACjB,0BAA0B;AAAA,EAC1B,MAAM;AAAA;AAAA,EAGN,OAAO;AAAA,EACP,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,qBAAqB;AAAA;AAAA,EAGrB,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,GAAG;AAAA,EACH,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,WAAW;AAAA;AAAA,EAGX,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,OAAO;AAAA,EACP,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,SAAS;AAAA;AAAA,EAGT,SAAS;AAAA,EACT,eAAe;AAAA,EACf,YAAY;AAAA;AAAA,EAGZ,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB,gBAAgB;AAAA;AAAA,EAGhB,SAAS;AAAA,EACT,gBAAgB;AAAA;AAAA,EAGhB,KAAK;AAAA,EACL,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,mBAAmB;AAAA;AAAA,EAGnB,YAAY;AAAA,EACZ,UAAU;AAAA;AAAA,EAGV,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EAGR,QAAQ;AAAA,EACR,SAAS;AAAA;AAAA,EAGT,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,aAAa;AAAA,EACb,KAAK;AAAA,EACL,UAAU;AAAA;AAAA,EAGV,eAAe;AAAA,EACf,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,iBAAiB;AACnB;AAEA,MAAM,6BAAqD;AAAA,EACzD,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS;AAAA,EACT,gBAAgB;AAClB;AAMO,SAAS,iBACd,WACoB;AACpB,MAAI,CAAC,aAAc,QAAO;AAC1B,QAAM,QACH,YAAY,qBAAqB,UAAU,YAAA,CAAa,IAAI,WAC7D;AACF,SAAO,GAAG,YAAY,GAAG,IAAI;AAC/B;AAMO,SAAS,gBAAgB,WAA8C;AAC5E,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO,2BAA2B,UAAU,YAAA,CAAa,KAAK;AAChE;"}
@@ -4,6 +4,7 @@ const websocket = require("@y/websocket");
4
4
  const Y = require("@y/y");
5
5
  const closePolicy = require("./close-policy.cjs");
6
6
  const mint = require("./mint.cjs");
7
+ const tokenRefresh = require("./token-refresh.cjs");
7
8
  function _interopNamespaceDefault(e) {
8
9
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
9
10
  if (e) {
@@ -21,9 +22,6 @@ function _interopNamespaceDefault(e) {
21
22
  return Object.freeze(n);
22
23
  }
23
24
  const Y__namespace = /* @__PURE__ */ _interopNamespaceDefault(Y);
24
- const REFRESH_HEADROOM_MS = 5 * 60 * 1e3;
25
- const MIN_REFRESH_DELAY_MS = 30 * 1e3;
26
- const MAX_CONSECUTIVE_REMINTS = 2;
27
25
  function connectGenericDoc(args) {
28
26
  const access = args.access ?? "view";
29
27
  const doc = new Y__namespace.Doc();
@@ -67,10 +65,7 @@ function connectGenericDoc(args) {
67
65
  };
68
66
  const scheduleRefresh = (grant) => {
69
67
  clearRefreshTimer();
70
- const delay = Math.max(
71
- grant.expires_at_ms - Date.now() - REFRESH_HEADROOM_MS,
72
- MIN_REFRESH_DELAY_MS
73
- );
68
+ const delay = tokenRefresh.refreshDelayMs({ expiresAtMs: grant.expires_at_ms, now: Date.now() });
74
69
  refreshTimer = setTimeout(() => {
75
70
  void connect({ scheduledRefresh: true });
76
71
  }, delay);
@@ -90,7 +85,7 @@ function connectGenericDoc(args) {
90
85
  if (scheduledRefresh && !destroyed) {
91
86
  refreshTimer = setTimeout(() => {
92
87
  void connect({ scheduledRefresh: true });
93
- }, MIN_REFRESH_DELAY_MS);
88
+ }, tokenRefresh.MIN_REFRESH_DELAY_MS);
94
89
  return;
95
90
  }
96
91
  setStatus("error", err instanceof Error ? err : new Error(String(err)));
@@ -136,7 +131,7 @@ function connectGenericDoc(args) {
136
131
  if (action === "remint") {
137
132
  consecutiveRemints += 1;
138
133
  teardownProvider();
139
- if (consecutiveRemints > MAX_CONSECUTIVE_REMINTS) {
134
+ if (consecutiveRemints > tokenRefresh.MAX_CONSECUTIVE_REMINTS) {
140
135
  setStatus(
141
136
  "closed",
142
137
  new Error("Access repeatedly revoked mid-session (4401); giving up.")
@@ -1 +1 @@
1
- {"version":3,"file":"client.cjs","sources":["../../src/collab/client.ts"],"sourcesContent":["/**\n * Vanilla (framework-agnostic) live client for Generic Doc assets.\n *\n * `connectGenericDoc` owns the full connection lifecycle: mint a room-bound\n * token via the public API, open the Keryx WebSocket with the v14 provider\n * (`@y/websocket` + `@y/y`), re-mint before expiry, apply the close-code\n * policy (4401 → one fresh mint, other 44xx → permanent stop, everything\n * else → provider backoff), and expose presence from awareness. React apps\n * use `useGenericDoc` from `@athenaintel/react/collab` instead of calling\n * this directly.\n */\n\nimport { WebsocketProvider } from '@y/websocket';\nimport * as Y from '@y/y';\nimport { classifyClose } from './close-policy';\nimport {\n type CollabAuth,\n type CollabTokenResponse,\n CollabTokenError,\n mintCollabToken,\n} from './mint';\n\nexport type GenericDocStatus =\n | 'connecting'\n | 'connected'\n | 'disconnected'\n | 'closed'\n | 'error';\n\nexport interface PresenceEntry {\n clientId: number;\n state: Record<string, unknown>;\n}\n\nexport interface GenericDocHandle {\n /** The live shared document. Owned by the handle; destroyed with it. */\n readonly doc: Y.Doc;\n readonly status: GenericDocStatus;\n /**\n * Whether the initial server sync has completed for the current connection.\n * `status === 'connected'` fires before the server state arrives, so a doc\n * read immediately after connect can still be empty — gate first reads on\n * this (or `waitForSync`).\n */\n readonly synced: boolean;\n /** Granted access from the last successful mint. */\n readonly accessType: 'r' | 'rw' | null;\n /** Terminal error, when status is 'error' or 'closed'. */\n readonly error: Error | null;\n /** Monotonic change counter — bumps on every status/presence notification. */\n readonly version: number;\n /**\n * Resolve once the initial server sync completes; reject on timeout, on a\n * terminal connection state, or if the handle is destroyed first.\n */\n waitForSync: (timeoutMs?: number) => Promise<void>;\n getPresence: () => PresenceEntry[];\n /** Broadcast this client's presence state (requires a live connection). */\n setPresence: (state: Record<string, unknown> | null) => void;\n /**\n * Force an early token refresh. Session-preserving: the live connection\n * stays up until the fresh token is minted; a failed mint retries on a\n * short timer instead of dropping the session.\n */\n refreshNow: () => Promise<void>;\n onChange: (listener: () => void) => () => void;\n destroy: () => void;\n}\n\n/** Re-mint this long before token expiry (clamped to a floor for short TTLs). */\nconst REFRESH_HEADROOM_MS = 5 * 60 * 1000;\nconst MIN_REFRESH_DELAY_MS = 30 * 1000;\n\n/** Consecutive 4401→mint cycles allowed without a successful connect between. */\nconst MAX_CONSECUTIVE_REMINTS = 2;\n\nexport function connectGenericDoc(args: {\n assetId: string;\n auth: CollabAuth;\n access?: 'view' | 'edit';\n}): GenericDocHandle {\n const access = args.access ?? 'view';\n const doc = new Y.Doc();\n const listeners = new Set<() => void>();\n\n let provider: WebsocketProvider | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n let destroyed = false;\n let status: GenericDocStatus = 'connecting';\n let synced = false;\n let accessType: 'r' | 'rw' | null = null;\n let error: Error | null = null;\n let lastPresence: Record<string, unknown> | null = null;\n let version = 0;\n let consecutiveRemints = 0;\n\n const notify = () => {\n version += 1;\n for (const listener of listeners) listener();\n };\n\n const setStatus = (next: GenericDocStatus, err: Error | null = null) => {\n if (destroyed && next !== 'closed') return;\n status = next;\n error = err;\n notify();\n };\n\n const clearRefreshTimer = () => {\n if (refreshTimer !== null) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n const teardownProvider = () => {\n clearRefreshTimer();\n // A future provider must complete its own initial sync; the doc keeps its\n // content either way. Subscribers must observe the flip immediately —\n // waiting for the next provider event would let them act on synced=true\n // across the replacement gap.\n if (synced) {\n synced = false;\n notify();\n }\n if (provider) {\n provider.destroy();\n provider = null;\n }\n };\n\n const scheduleRefresh = (grant: CollabTokenResponse) => {\n clearRefreshTimer();\n const delay = Math.max(\n grant.expires_at_ms - Date.now() - REFRESH_HEADROOM_MS,\n MIN_REFRESH_DELAY_MS\n );\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, delay);\n };\n\n const connect = async (opts?: { scheduledRefresh?: boolean }): Promise<void> => {\n if (destroyed) return;\n const scheduledRefresh = opts?.scheduledRefresh === true;\n // A scheduled refresh mints FIRST and keeps the live session up: the old\n // token has ~5 minutes of headroom, so a transient mint failure retries\n // on a short timer instead of dropping a healthy connection.\n if (!scheduledRefresh) {\n teardownProvider();\n } else {\n clearRefreshTimer();\n }\n let grant: CollabTokenResponse;\n try {\n grant = await mintCollabToken({ auth: args.auth, assetId: args.assetId, access });\n } catch (err) {\n if (scheduledRefresh && !destroyed) {\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, MIN_REFRESH_DELAY_MS);\n return;\n }\n // A denied mint is authoritative (revoked / not shared / not eligible);\n // anything else (network) is worth telling the caller about too — the\n // handle stays usable via a later explicit reconnect-by-recreate.\n setStatus('error', err instanceof Error ? err : new Error(String(err)));\n return;\n }\n if (destroyed) return;\n if (scheduledRefresh) {\n teardownProvider();\n }\n accessType = grant.access_type;\n\n const nextProvider = new WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {\n params: {\n yauth: grant.token,\n branch: grant.branch,\n gc: 'true',\n },\n // BroadcastChannel would sync same-origin clients locally, bypassing the\n // server's permission enforcement (a read-only tab could write into an\n // editor tab's doc). Permissioned docs must round-trip the server.\n disableBc: true,\n });\n provider = nextProvider;\n\n nextProvider.on('status', (event: { status: 'connecting' | 'connected' | 'disconnected' }) => {\n if (provider !== nextProvider) return;\n if (event.status === 'connected') {\n consecutiveRemints = 0;\n }\n setStatus(event.status);\n if (event.status === 'connected' && lastPresence !== null) {\n nextProvider.awareness.setLocalState(lastPresence);\n }\n });\n\n nextProvider.on('sync', (state: boolean) => {\n if (provider !== nextProvider) return;\n if (synced !== state) {\n synced = state;\n notify();\n }\n });\n\n nextProvider.on('connection-close', (event: CloseEvent | null) => {\n if (provider !== nextProvider) return;\n const action = classifyClose(event?.code);\n if (action === 'remint') {\n // Revoked mid-session: a fresh mint re-runs the server-side permission\n // check. If access is truly gone the mint 403s and we land in 'error'.\n // Bounded: repeated 4401s without an intervening successful connect\n // mean the server keeps rejecting freshly minted tokens — stop rather\n // than loop mint→connect→kick.\n consecutiveRemints += 1;\n teardownProvider();\n if (consecutiveRemints > MAX_CONSECUTIVE_REMINTS) {\n setStatus(\n 'closed',\n new Error('Access repeatedly revoked mid-session (4401); giving up.')\n );\n return;\n }\n void connect();\n } else if (action === 'stop') {\n teardownProvider();\n setStatus(\n 'closed',\n new Error(`Connection closed permanently (code ${event?.code ?? 'unknown'})`)\n );\n }\n // 'retry': the provider's exponential backoff handles it.\n });\n\n nextProvider.awareness.on('change', () => {\n if (provider !== nextProvider) return;\n notify();\n });\n\n scheduleRefresh(grant);\n };\n\n void connect();\n\n const waitForSync = (timeoutMs = 15_000): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n const terminal = (): Error | null => {\n if (destroyed) return new Error('Handle destroyed before initial sync');\n if (status === 'closed' || status === 'error') {\n return error ?? new Error(`Connection ${status} before initial sync`);\n }\n return null;\n };\n if (synced) {\n resolve();\n return;\n }\n const immediate = terminal();\n if (immediate) {\n reject(immediate);\n return;\n }\n const cleanup = () => {\n clearTimeout(timer);\n listeners.delete(listener);\n };\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`Timed out after ${timeoutMs}ms waiting for initial sync`));\n }, timeoutMs);\n const listener = () => {\n if (synced) {\n cleanup();\n resolve();\n return;\n }\n const err = terminal();\n if (err) {\n cleanup();\n reject(err);\n }\n };\n listeners.add(listener);\n });\n\n return {\n doc,\n get status() {\n return status;\n },\n get synced() {\n return synced;\n },\n get accessType() {\n return accessType;\n },\n get error() {\n return error;\n },\n get version() {\n return version;\n },\n waitForSync,\n getPresence: () => {\n if (!provider) return [];\n const entries: PresenceEntry[] = [];\n provider.awareness.getStates().forEach((state, clientId) => {\n entries.push({ clientId, state: state as Record<string, unknown> });\n });\n return entries;\n },\n setPresence: (state) => {\n lastPresence = state;\n provider?.awareness.setLocalState(state);\n },\n refreshNow: () => connect({ scheduledRefresh: true }),\n onChange: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n teardownProvider();\n status = 'closed';\n // Final notification so pending waitForSync callers reject instead of\n // hanging on listeners that are about to be dropped.\n for (const listener of [...listeners]) listener();\n listeners.clear();\n doc.destroy();\n },\n };\n}\n\nexport { CollabTokenError };\nexport type { CollabAuth, CollabTokenResponse };\n"],"names":["Y","mintCollabToken","WebsocketProvider","classifyClose"],"mappings":"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"}
1
+ {"version":3,"file":"client.cjs","sources":["../../src/collab/client.ts"],"sourcesContent":["/**\n * Vanilla (framework-agnostic) live client for Generic Doc assets.\n *\n * `connectGenericDoc` owns the full connection lifecycle: mint a room-bound\n * token via the public API, open the Keryx WebSocket with the v14 provider\n * (`@y/websocket` + `@y/y`), re-mint before expiry, apply the close-code\n * policy (4401 → one fresh mint, other 44xx → permanent stop, everything\n * else → provider backoff), and expose presence from awareness. React apps\n * use `useGenericDoc` from `@athenaintel/react/collab` instead of calling\n * this directly.\n */\n\nimport { WebsocketProvider } from '@y/websocket';\nimport * as Y from '@y/y';\nimport { classifyClose } from './close-policy';\nimport {\n type CollabAuth,\n type CollabTokenResponse,\n CollabTokenError,\n mintCollabToken,\n} from './mint';\nimport { MAX_CONSECUTIVE_REMINTS, MIN_REFRESH_DELAY_MS, refreshDelayMs } from './token-refresh';\n\nexport type GenericDocStatus =\n | 'connecting'\n | 'connected'\n | 'disconnected'\n | 'closed'\n | 'error';\n\nexport interface PresenceEntry {\n clientId: number;\n state: Record<string, unknown>;\n}\n\nexport interface GenericDocHandle {\n /** The live shared document. Owned by the handle; destroyed with it. */\n readonly doc: Y.Doc;\n readonly status: GenericDocStatus;\n /**\n * Whether the initial server sync has completed for the current connection.\n * `status === 'connected'` fires before the server state arrives, so a doc\n * read immediately after connect can still be empty — gate first reads on\n * this (or `waitForSync`).\n */\n readonly synced: boolean;\n /** Granted access from the last successful mint. */\n readonly accessType: 'r' | 'rw' | null;\n /** Terminal error, when status is 'error' or 'closed'. */\n readonly error: Error | null;\n /** Monotonic change counter — bumps on every status/presence notification. */\n readonly version: number;\n /**\n * Resolve once the initial server sync completes; reject on timeout, on a\n * terminal connection state, or if the handle is destroyed first.\n */\n waitForSync: (timeoutMs?: number) => Promise<void>;\n getPresence: () => PresenceEntry[];\n /** Broadcast this client's presence state (requires a live connection). */\n setPresence: (state: Record<string, unknown> | null) => void;\n /**\n * Force an early token refresh. Session-preserving: the live connection\n * stays up until the fresh token is minted; a failed mint retries on a\n * short timer instead of dropping the session.\n */\n refreshNow: () => Promise<void>;\n onChange: (listener: () => void) => () => void;\n destroy: () => void;\n}\n\nexport function connectGenericDoc(args: {\n assetId: string;\n auth: CollabAuth;\n access?: 'view' | 'edit';\n}): GenericDocHandle {\n const access = args.access ?? 'view';\n const doc = new Y.Doc();\n const listeners = new Set<() => void>();\n\n let provider: WebsocketProvider | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n let destroyed = false;\n let status: GenericDocStatus = 'connecting';\n let synced = false;\n let accessType: 'r' | 'rw' | null = null;\n let error: Error | null = null;\n let lastPresence: Record<string, unknown> | null = null;\n let version = 0;\n let consecutiveRemints = 0;\n\n const notify = () => {\n version += 1;\n for (const listener of listeners) listener();\n };\n\n const setStatus = (next: GenericDocStatus, err: Error | null = null) => {\n if (destroyed && next !== 'closed') return;\n status = next;\n error = err;\n notify();\n };\n\n const clearRefreshTimer = () => {\n if (refreshTimer !== null) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n const teardownProvider = () => {\n clearRefreshTimer();\n // A future provider must complete its own initial sync; the doc keeps its\n // content either way. Subscribers must observe the flip immediately —\n // waiting for the next provider event would let them act on synced=true\n // across the replacement gap.\n if (synced) {\n synced = false;\n notify();\n }\n if (provider) {\n provider.destroy();\n provider = null;\n }\n };\n\n const scheduleRefresh = (grant: CollabTokenResponse) => {\n clearRefreshTimer();\n const delay = refreshDelayMs({ expiresAtMs: grant.expires_at_ms, now: Date.now() });\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, delay);\n };\n\n const connect = async (opts?: { scheduledRefresh?: boolean }): Promise<void> => {\n if (destroyed) return;\n const scheduledRefresh = opts?.scheduledRefresh === true;\n // A scheduled refresh mints FIRST and keeps the live session up: the old\n // token has ~5 minutes of headroom, so a transient mint failure retries\n // on a short timer instead of dropping a healthy connection.\n if (!scheduledRefresh) {\n teardownProvider();\n } else {\n clearRefreshTimer();\n }\n let grant: CollabTokenResponse;\n try {\n grant = await mintCollabToken({ auth: args.auth, assetId: args.assetId, access });\n } catch (err) {\n if (scheduledRefresh && !destroyed) {\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, MIN_REFRESH_DELAY_MS);\n return;\n }\n // A denied mint is authoritative (revoked / not shared / not eligible);\n // anything else (network) is worth telling the caller about too — the\n // handle stays usable via a later explicit reconnect-by-recreate.\n setStatus('error', err instanceof Error ? err : new Error(String(err)));\n return;\n }\n if (destroyed) return;\n if (scheduledRefresh) {\n teardownProvider();\n }\n accessType = grant.access_type;\n\n const nextProvider = new WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {\n params: {\n yauth: grant.token,\n branch: grant.branch,\n gc: 'true',\n },\n // BroadcastChannel would sync same-origin clients locally, bypassing the\n // server's permission enforcement (a read-only tab could write into an\n // editor tab's doc). Permissioned docs must round-trip the server.\n disableBc: true,\n });\n provider = nextProvider;\n\n nextProvider.on('status', (event: { status: 'connecting' | 'connected' | 'disconnected' }) => {\n if (provider !== nextProvider) return;\n if (event.status === 'connected') {\n consecutiveRemints = 0;\n }\n setStatus(event.status);\n if (event.status === 'connected' && lastPresence !== null) {\n nextProvider.awareness.setLocalState(lastPresence);\n }\n });\n\n nextProvider.on('sync', (state: boolean) => {\n if (provider !== nextProvider) return;\n if (synced !== state) {\n synced = state;\n notify();\n }\n });\n\n nextProvider.on('connection-close', (event: CloseEvent | null) => {\n if (provider !== nextProvider) return;\n const action = classifyClose(event?.code);\n if (action === 'remint') {\n // Revoked mid-session: a fresh mint re-runs the server-side permission\n // check. If access is truly gone the mint 403s and we land in 'error'.\n // Bounded: repeated 4401s without an intervening successful connect\n // mean the server keeps rejecting freshly minted tokens — stop rather\n // than loop mint→connect→kick.\n consecutiveRemints += 1;\n teardownProvider();\n if (consecutiveRemints > MAX_CONSECUTIVE_REMINTS) {\n setStatus(\n 'closed',\n new Error('Access repeatedly revoked mid-session (4401); giving up.')\n );\n return;\n }\n void connect();\n } else if (action === 'stop') {\n teardownProvider();\n setStatus(\n 'closed',\n new Error(`Connection closed permanently (code ${event?.code ?? 'unknown'})`)\n );\n }\n // 'retry': the provider's exponential backoff handles it.\n });\n\n nextProvider.awareness.on('change', () => {\n if (provider !== nextProvider) return;\n notify();\n });\n\n scheduleRefresh(grant);\n };\n\n void connect();\n\n const waitForSync = (timeoutMs = 15_000): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n const terminal = (): Error | null => {\n if (destroyed) return new Error('Handle destroyed before initial sync');\n if (status === 'closed' || status === 'error') {\n return error ?? new Error(`Connection ${status} before initial sync`);\n }\n return null;\n };\n if (synced) {\n resolve();\n return;\n }\n const immediate = terminal();\n if (immediate) {\n reject(immediate);\n return;\n }\n const cleanup = () => {\n clearTimeout(timer);\n listeners.delete(listener);\n };\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`Timed out after ${timeoutMs}ms waiting for initial sync`));\n }, timeoutMs);\n const listener = () => {\n if (synced) {\n cleanup();\n resolve();\n return;\n }\n const err = terminal();\n if (err) {\n cleanup();\n reject(err);\n }\n };\n listeners.add(listener);\n });\n\n return {\n doc,\n get status() {\n return status;\n },\n get synced() {\n return synced;\n },\n get accessType() {\n return accessType;\n },\n get error() {\n return error;\n },\n get version() {\n return version;\n },\n waitForSync,\n getPresence: () => {\n if (!provider) return [];\n const entries: PresenceEntry[] = [];\n provider.awareness.getStates().forEach((state, clientId) => {\n entries.push({ clientId, state: state as Record<string, unknown> });\n });\n return entries;\n },\n setPresence: (state) => {\n lastPresence = state;\n provider?.awareness.setLocalState(state);\n },\n refreshNow: () => connect({ scheduledRefresh: true }),\n onChange: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n teardownProvider();\n status = 'closed';\n // Final notification so pending waitForSync callers reject instead of\n // hanging on listeners that are about to be dropped.\n for (const listener of [...listeners]) listener();\n listeners.clear();\n doc.destroy();\n },\n };\n}\n\nexport { CollabTokenError };\nexport type { CollabAuth, CollabTokenResponse };\n"],"names":["Y","refreshDelayMs","mintCollabToken","MIN_REFRESH_DELAY_MS","WebsocketProvider","classifyClose","MAX_CONSECUTIVE_REMINTS"],"mappings":"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"}
@@ -3,9 +3,7 @@ import * as Y from "@y/y";
3
3
  import { classifyClose } from "./close-policy.js";
4
4
  import { mintCollabToken } from "./mint.js";
5
5
  import { CollabTokenError } from "./mint.js";
6
- const REFRESH_HEADROOM_MS = 5 * 60 * 1e3;
7
- const MIN_REFRESH_DELAY_MS = 30 * 1e3;
8
- const MAX_CONSECUTIVE_REMINTS = 2;
6
+ import { MIN_REFRESH_DELAY_MS, MAX_CONSECUTIVE_REMINTS, refreshDelayMs } from "./token-refresh.js";
9
7
  function connectGenericDoc(args) {
10
8
  const access = args.access ?? "view";
11
9
  const doc = new Y.Doc();
@@ -49,10 +47,7 @@ function connectGenericDoc(args) {
49
47
  };
50
48
  const scheduleRefresh = (grant) => {
51
49
  clearRefreshTimer();
52
- const delay = Math.max(
53
- grant.expires_at_ms - Date.now() - REFRESH_HEADROOM_MS,
54
- MIN_REFRESH_DELAY_MS
55
- );
50
+ const delay = refreshDelayMs({ expiresAtMs: grant.expires_at_ms, now: Date.now() });
56
51
  refreshTimer = setTimeout(() => {
57
52
  void connect({ scheduledRefresh: true });
58
53
  }, delay);
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sources":["../../src/collab/client.ts"],"sourcesContent":["/**\n * Vanilla (framework-agnostic) live client for Generic Doc assets.\n *\n * `connectGenericDoc` owns the full connection lifecycle: mint a room-bound\n * token via the public API, open the Keryx WebSocket with the v14 provider\n * (`@y/websocket` + `@y/y`), re-mint before expiry, apply the close-code\n * policy (4401 → one fresh mint, other 44xx → permanent stop, everything\n * else → provider backoff), and expose presence from awareness. React apps\n * use `useGenericDoc` from `@athenaintel/react/collab` instead of calling\n * this directly.\n */\n\nimport { WebsocketProvider } from '@y/websocket';\nimport * as Y from '@y/y';\nimport { classifyClose } from './close-policy';\nimport {\n type CollabAuth,\n type CollabTokenResponse,\n CollabTokenError,\n mintCollabToken,\n} from './mint';\n\nexport type GenericDocStatus =\n | 'connecting'\n | 'connected'\n | 'disconnected'\n | 'closed'\n | 'error';\n\nexport interface PresenceEntry {\n clientId: number;\n state: Record<string, unknown>;\n}\n\nexport interface GenericDocHandle {\n /** The live shared document. Owned by the handle; destroyed with it. */\n readonly doc: Y.Doc;\n readonly status: GenericDocStatus;\n /**\n * Whether the initial server sync has completed for the current connection.\n * `status === 'connected'` fires before the server state arrives, so a doc\n * read immediately after connect can still be empty — gate first reads on\n * this (or `waitForSync`).\n */\n readonly synced: boolean;\n /** Granted access from the last successful mint. */\n readonly accessType: 'r' | 'rw' | null;\n /** Terminal error, when status is 'error' or 'closed'. */\n readonly error: Error | null;\n /** Monotonic change counter — bumps on every status/presence notification. */\n readonly version: number;\n /**\n * Resolve once the initial server sync completes; reject on timeout, on a\n * terminal connection state, or if the handle is destroyed first.\n */\n waitForSync: (timeoutMs?: number) => Promise<void>;\n getPresence: () => PresenceEntry[];\n /** Broadcast this client's presence state (requires a live connection). */\n setPresence: (state: Record<string, unknown> | null) => void;\n /**\n * Force an early token refresh. Session-preserving: the live connection\n * stays up until the fresh token is minted; a failed mint retries on a\n * short timer instead of dropping the session.\n */\n refreshNow: () => Promise<void>;\n onChange: (listener: () => void) => () => void;\n destroy: () => void;\n}\n\n/** Re-mint this long before token expiry (clamped to a floor for short TTLs). */\nconst REFRESH_HEADROOM_MS = 5 * 60 * 1000;\nconst MIN_REFRESH_DELAY_MS = 30 * 1000;\n\n/** Consecutive 4401→mint cycles allowed without a successful connect between. */\nconst MAX_CONSECUTIVE_REMINTS = 2;\n\nexport function connectGenericDoc(args: {\n assetId: string;\n auth: CollabAuth;\n access?: 'view' | 'edit';\n}): GenericDocHandle {\n const access = args.access ?? 'view';\n const doc = new Y.Doc();\n const listeners = new Set<() => void>();\n\n let provider: WebsocketProvider | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n let destroyed = false;\n let status: GenericDocStatus = 'connecting';\n let synced = false;\n let accessType: 'r' | 'rw' | null = null;\n let error: Error | null = null;\n let lastPresence: Record<string, unknown> | null = null;\n let version = 0;\n let consecutiveRemints = 0;\n\n const notify = () => {\n version += 1;\n for (const listener of listeners) listener();\n };\n\n const setStatus = (next: GenericDocStatus, err: Error | null = null) => {\n if (destroyed && next !== 'closed') return;\n status = next;\n error = err;\n notify();\n };\n\n const clearRefreshTimer = () => {\n if (refreshTimer !== null) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n const teardownProvider = () => {\n clearRefreshTimer();\n // A future provider must complete its own initial sync; the doc keeps its\n // content either way. Subscribers must observe the flip immediately —\n // waiting for the next provider event would let them act on synced=true\n // across the replacement gap.\n if (synced) {\n synced = false;\n notify();\n }\n if (provider) {\n provider.destroy();\n provider = null;\n }\n };\n\n const scheduleRefresh = (grant: CollabTokenResponse) => {\n clearRefreshTimer();\n const delay = Math.max(\n grant.expires_at_ms - Date.now() - REFRESH_HEADROOM_MS,\n MIN_REFRESH_DELAY_MS\n );\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, delay);\n };\n\n const connect = async (opts?: { scheduledRefresh?: boolean }): Promise<void> => {\n if (destroyed) return;\n const scheduledRefresh = opts?.scheduledRefresh === true;\n // A scheduled refresh mints FIRST and keeps the live session up: the old\n // token has ~5 minutes of headroom, so a transient mint failure retries\n // on a short timer instead of dropping a healthy connection.\n if (!scheduledRefresh) {\n teardownProvider();\n } else {\n clearRefreshTimer();\n }\n let grant: CollabTokenResponse;\n try {\n grant = await mintCollabToken({ auth: args.auth, assetId: args.assetId, access });\n } catch (err) {\n if (scheduledRefresh && !destroyed) {\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, MIN_REFRESH_DELAY_MS);\n return;\n }\n // A denied mint is authoritative (revoked / not shared / not eligible);\n // anything else (network) is worth telling the caller about too — the\n // handle stays usable via a later explicit reconnect-by-recreate.\n setStatus('error', err instanceof Error ? err : new Error(String(err)));\n return;\n }\n if (destroyed) return;\n if (scheduledRefresh) {\n teardownProvider();\n }\n accessType = grant.access_type;\n\n const nextProvider = new WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {\n params: {\n yauth: grant.token,\n branch: grant.branch,\n gc: 'true',\n },\n // BroadcastChannel would sync same-origin clients locally, bypassing the\n // server's permission enforcement (a read-only tab could write into an\n // editor tab's doc). Permissioned docs must round-trip the server.\n disableBc: true,\n });\n provider = nextProvider;\n\n nextProvider.on('status', (event: { status: 'connecting' | 'connected' | 'disconnected' }) => {\n if (provider !== nextProvider) return;\n if (event.status === 'connected') {\n consecutiveRemints = 0;\n }\n setStatus(event.status);\n if (event.status === 'connected' && lastPresence !== null) {\n nextProvider.awareness.setLocalState(lastPresence);\n }\n });\n\n nextProvider.on('sync', (state: boolean) => {\n if (provider !== nextProvider) return;\n if (synced !== state) {\n synced = state;\n notify();\n }\n });\n\n nextProvider.on('connection-close', (event: CloseEvent | null) => {\n if (provider !== nextProvider) return;\n const action = classifyClose(event?.code);\n if (action === 'remint') {\n // Revoked mid-session: a fresh mint re-runs the server-side permission\n // check. If access is truly gone the mint 403s and we land in 'error'.\n // Bounded: repeated 4401s without an intervening successful connect\n // mean the server keeps rejecting freshly minted tokens — stop rather\n // than loop mint→connect→kick.\n consecutiveRemints += 1;\n teardownProvider();\n if (consecutiveRemints > MAX_CONSECUTIVE_REMINTS) {\n setStatus(\n 'closed',\n new Error('Access repeatedly revoked mid-session (4401); giving up.')\n );\n return;\n }\n void connect();\n } else if (action === 'stop') {\n teardownProvider();\n setStatus(\n 'closed',\n new Error(`Connection closed permanently (code ${event?.code ?? 'unknown'})`)\n );\n }\n // 'retry': the provider's exponential backoff handles it.\n });\n\n nextProvider.awareness.on('change', () => {\n if (provider !== nextProvider) return;\n notify();\n });\n\n scheduleRefresh(grant);\n };\n\n void connect();\n\n const waitForSync = (timeoutMs = 15_000): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n const terminal = (): Error | null => {\n if (destroyed) return new Error('Handle destroyed before initial sync');\n if (status === 'closed' || status === 'error') {\n return error ?? new Error(`Connection ${status} before initial sync`);\n }\n return null;\n };\n if (synced) {\n resolve();\n return;\n }\n const immediate = terminal();\n if (immediate) {\n reject(immediate);\n return;\n }\n const cleanup = () => {\n clearTimeout(timer);\n listeners.delete(listener);\n };\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`Timed out after ${timeoutMs}ms waiting for initial sync`));\n }, timeoutMs);\n const listener = () => {\n if (synced) {\n cleanup();\n resolve();\n return;\n }\n const err = terminal();\n if (err) {\n cleanup();\n reject(err);\n }\n };\n listeners.add(listener);\n });\n\n return {\n doc,\n get status() {\n return status;\n },\n get synced() {\n return synced;\n },\n get accessType() {\n return accessType;\n },\n get error() {\n return error;\n },\n get version() {\n return version;\n },\n waitForSync,\n getPresence: () => {\n if (!provider) return [];\n const entries: PresenceEntry[] = [];\n provider.awareness.getStates().forEach((state, clientId) => {\n entries.push({ clientId, state: state as Record<string, unknown> });\n });\n return entries;\n },\n setPresence: (state) => {\n lastPresence = state;\n provider?.awareness.setLocalState(state);\n },\n refreshNow: () => connect({ scheduledRefresh: true }),\n onChange: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n teardownProvider();\n status = 'closed';\n // Final notification so pending waitForSync callers reject instead of\n // hanging on listeners that are about to be dropped.\n for (const listener of [...listeners]) listener();\n listeners.clear();\n doc.destroy();\n },\n };\n}\n\nexport { CollabTokenError };\nexport type { CollabAuth, CollabTokenResponse };\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sources":["../../src/collab/client.ts"],"sourcesContent":["/**\n * Vanilla (framework-agnostic) live client for Generic Doc assets.\n *\n * `connectGenericDoc` owns the full connection lifecycle: mint a room-bound\n * token via the public API, open the Keryx WebSocket with the v14 provider\n * (`@y/websocket` + `@y/y`), re-mint before expiry, apply the close-code\n * policy (4401 → one fresh mint, other 44xx → permanent stop, everything\n * else → provider backoff), and expose presence from awareness. React apps\n * use `useGenericDoc` from `@athenaintel/react/collab` instead of calling\n * this directly.\n */\n\nimport { WebsocketProvider } from '@y/websocket';\nimport * as Y from '@y/y';\nimport { classifyClose } from './close-policy';\nimport {\n type CollabAuth,\n type CollabTokenResponse,\n CollabTokenError,\n mintCollabToken,\n} from './mint';\nimport { MAX_CONSECUTIVE_REMINTS, MIN_REFRESH_DELAY_MS, refreshDelayMs } from './token-refresh';\n\nexport type GenericDocStatus =\n | 'connecting'\n | 'connected'\n | 'disconnected'\n | 'closed'\n | 'error';\n\nexport interface PresenceEntry {\n clientId: number;\n state: Record<string, unknown>;\n}\n\nexport interface GenericDocHandle {\n /** The live shared document. Owned by the handle; destroyed with it. */\n readonly doc: Y.Doc;\n readonly status: GenericDocStatus;\n /**\n * Whether the initial server sync has completed for the current connection.\n * `status === 'connected'` fires before the server state arrives, so a doc\n * read immediately after connect can still be empty — gate first reads on\n * this (or `waitForSync`).\n */\n readonly synced: boolean;\n /** Granted access from the last successful mint. */\n readonly accessType: 'r' | 'rw' | null;\n /** Terminal error, when status is 'error' or 'closed'. */\n readonly error: Error | null;\n /** Monotonic change counter — bumps on every status/presence notification. */\n readonly version: number;\n /**\n * Resolve once the initial server sync completes; reject on timeout, on a\n * terminal connection state, or if the handle is destroyed first.\n */\n waitForSync: (timeoutMs?: number) => Promise<void>;\n getPresence: () => PresenceEntry[];\n /** Broadcast this client's presence state (requires a live connection). */\n setPresence: (state: Record<string, unknown> | null) => void;\n /**\n * Force an early token refresh. Session-preserving: the live connection\n * stays up until the fresh token is minted; a failed mint retries on a\n * short timer instead of dropping the session.\n */\n refreshNow: () => Promise<void>;\n onChange: (listener: () => void) => () => void;\n destroy: () => void;\n}\n\nexport function connectGenericDoc(args: {\n assetId: string;\n auth: CollabAuth;\n access?: 'view' | 'edit';\n}): GenericDocHandle {\n const access = args.access ?? 'view';\n const doc = new Y.Doc();\n const listeners = new Set<() => void>();\n\n let provider: WebsocketProvider | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n let destroyed = false;\n let status: GenericDocStatus = 'connecting';\n let synced = false;\n let accessType: 'r' | 'rw' | null = null;\n let error: Error | null = null;\n let lastPresence: Record<string, unknown> | null = null;\n let version = 0;\n let consecutiveRemints = 0;\n\n const notify = () => {\n version += 1;\n for (const listener of listeners) listener();\n };\n\n const setStatus = (next: GenericDocStatus, err: Error | null = null) => {\n if (destroyed && next !== 'closed') return;\n status = next;\n error = err;\n notify();\n };\n\n const clearRefreshTimer = () => {\n if (refreshTimer !== null) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n const teardownProvider = () => {\n clearRefreshTimer();\n // A future provider must complete its own initial sync; the doc keeps its\n // content either way. Subscribers must observe the flip immediately —\n // waiting for the next provider event would let them act on synced=true\n // across the replacement gap.\n if (synced) {\n synced = false;\n notify();\n }\n if (provider) {\n provider.destroy();\n provider = null;\n }\n };\n\n const scheduleRefresh = (grant: CollabTokenResponse) => {\n clearRefreshTimer();\n const delay = refreshDelayMs({ expiresAtMs: grant.expires_at_ms, now: Date.now() });\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, delay);\n };\n\n const connect = async (opts?: { scheduledRefresh?: boolean }): Promise<void> => {\n if (destroyed) return;\n const scheduledRefresh = opts?.scheduledRefresh === true;\n // A scheduled refresh mints FIRST and keeps the live session up: the old\n // token has ~5 minutes of headroom, so a transient mint failure retries\n // on a short timer instead of dropping a healthy connection.\n if (!scheduledRefresh) {\n teardownProvider();\n } else {\n clearRefreshTimer();\n }\n let grant: CollabTokenResponse;\n try {\n grant = await mintCollabToken({ auth: args.auth, assetId: args.assetId, access });\n } catch (err) {\n if (scheduledRefresh && !destroyed) {\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, MIN_REFRESH_DELAY_MS);\n return;\n }\n // A denied mint is authoritative (revoked / not shared / not eligible);\n // anything else (network) is worth telling the caller about too — the\n // handle stays usable via a later explicit reconnect-by-recreate.\n setStatus('error', err instanceof Error ? err : new Error(String(err)));\n return;\n }\n if (destroyed) return;\n if (scheduledRefresh) {\n teardownProvider();\n }\n accessType = grant.access_type;\n\n const nextProvider = new WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {\n params: {\n yauth: grant.token,\n branch: grant.branch,\n gc: 'true',\n },\n // BroadcastChannel would sync same-origin clients locally, bypassing the\n // server's permission enforcement (a read-only tab could write into an\n // editor tab's doc). Permissioned docs must round-trip the server.\n disableBc: true,\n });\n provider = nextProvider;\n\n nextProvider.on('status', (event: { status: 'connecting' | 'connected' | 'disconnected' }) => {\n if (provider !== nextProvider) return;\n if (event.status === 'connected') {\n consecutiveRemints = 0;\n }\n setStatus(event.status);\n if (event.status === 'connected' && lastPresence !== null) {\n nextProvider.awareness.setLocalState(lastPresence);\n }\n });\n\n nextProvider.on('sync', (state: boolean) => {\n if (provider !== nextProvider) return;\n if (synced !== state) {\n synced = state;\n notify();\n }\n });\n\n nextProvider.on('connection-close', (event: CloseEvent | null) => {\n if (provider !== nextProvider) return;\n const action = classifyClose(event?.code);\n if (action === 'remint') {\n // Revoked mid-session: a fresh mint re-runs the server-side permission\n // check. If access is truly gone the mint 403s and we land in 'error'.\n // Bounded: repeated 4401s without an intervening successful connect\n // mean the server keeps rejecting freshly minted tokens — stop rather\n // than loop mint→connect→kick.\n consecutiveRemints += 1;\n teardownProvider();\n if (consecutiveRemints > MAX_CONSECUTIVE_REMINTS) {\n setStatus(\n 'closed',\n new Error('Access repeatedly revoked mid-session (4401); giving up.')\n );\n return;\n }\n void connect();\n } else if (action === 'stop') {\n teardownProvider();\n setStatus(\n 'closed',\n new Error(`Connection closed permanently (code ${event?.code ?? 'unknown'})`)\n );\n }\n // 'retry': the provider's exponential backoff handles it.\n });\n\n nextProvider.awareness.on('change', () => {\n if (provider !== nextProvider) return;\n notify();\n });\n\n scheduleRefresh(grant);\n };\n\n void connect();\n\n const waitForSync = (timeoutMs = 15_000): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n const terminal = (): Error | null => {\n if (destroyed) return new Error('Handle destroyed before initial sync');\n if (status === 'closed' || status === 'error') {\n return error ?? new Error(`Connection ${status} before initial sync`);\n }\n return null;\n };\n if (synced) {\n resolve();\n return;\n }\n const immediate = terminal();\n if (immediate) {\n reject(immediate);\n return;\n }\n const cleanup = () => {\n clearTimeout(timer);\n listeners.delete(listener);\n };\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`Timed out after ${timeoutMs}ms waiting for initial sync`));\n }, timeoutMs);\n const listener = () => {\n if (synced) {\n cleanup();\n resolve();\n return;\n }\n const err = terminal();\n if (err) {\n cleanup();\n reject(err);\n }\n };\n listeners.add(listener);\n });\n\n return {\n doc,\n get status() {\n return status;\n },\n get synced() {\n return synced;\n },\n get accessType() {\n return accessType;\n },\n get error() {\n return error;\n },\n get version() {\n return version;\n },\n waitForSync,\n getPresence: () => {\n if (!provider) return [];\n const entries: PresenceEntry[] = [];\n provider.awareness.getStates().forEach((state, clientId) => {\n entries.push({ clientId, state: state as Record<string, unknown> });\n });\n return entries;\n },\n setPresence: (state) => {\n lastPresence = state;\n provider?.awareness.setLocalState(state);\n },\n refreshNow: () => connect({ scheduledRefresh: true }),\n onChange: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n teardownProvider();\n status = 'closed';\n // Final notification so pending waitForSync callers reject instead of\n // hanging on listeners that are about to be dropped.\n for (const listener of [...listeners]) listener();\n listeners.clear();\n doc.destroy();\n },\n };\n}\n\nexport { CollabTokenError };\nexport type { CollabAuth, CollabTokenResponse };\n"],"names":[],"mappings":"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"}
@@ -3,8 +3,13 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const closePolicy = require("./close-policy.cjs");
4
4
  const client = require("./client.cjs");
5
5
  const mint = require("./mint.cjs");
6
+ const presenceClient = require("./presence-client.cjs");
7
+ const presenceMint = require("./presence-mint.cjs");
8
+ const presenceReact = require("./presence-react.cjs");
9
+ const presenceSlots = require("./presence-slots.cjs");
6
10
  const react = require("./react.cjs");
7
11
  const shape = require("./shape.cjs");
12
+ const tokenRefresh = require("./token-refresh.cjs");
8
13
  exports.WS_CLOSE_AUTH_REVOKED = closePolicy.WS_CLOSE_AUTH_REVOKED;
9
14
  exports.WS_CLOSE_DOC_DELETED = closePolicy.WS_CLOSE_DOC_DELETED;
10
15
  exports.classifyClose = closePolicy.classifyClose;
@@ -12,6 +17,24 @@ exports.connectGenericDoc = client.connectGenericDoc;
12
17
  exports.CollabTokenError = mint.CollabTokenError;
13
18
  exports.getAthenaApiBaseUrl = mint.getAthenaApiBaseUrl;
14
19
  exports.mintCollabToken = mint.mintCollabToken;
20
+ exports.PRESENCE_SWEEP_INTERVAL_MS = presenceClient.PRESENCE_SWEEP_INTERVAL_MS;
21
+ exports.connectWorkspacePresence = presenceClient.connectWorkspacePresence;
22
+ exports.mintWorkspacePresenceToken = presenceMint.mintWorkspacePresenceToken;
23
+ exports.resolveWorkspacePresence = presenceMint.resolveWorkspacePresence;
24
+ exports.PRESENCE_DEFAULT_TICK_MS = presenceReact.PRESENCE_DEFAULT_TICK_MS;
25
+ exports.useWorkspacePresence = presenceReact.useWorkspacePresence;
26
+ exports.useWorkspacePresenceResolver = presenceReact.useWorkspacePresenceResolver;
27
+ exports.MAX_PRESENCE_CONTEXTS_PER_SLOT = presenceSlots.MAX_PRESENCE_CONTEXTS_PER_SLOT;
28
+ exports.MAX_PRESENCE_STRING_CHARS = presenceSlots.MAX_PRESENCE_STRING_CHARS;
29
+ exports.PRESENCE_ACTIVE_WINDOW_MS = presenceSlots.PRESENCE_ACTIVE_WINDOW_MS;
30
+ exports.PRESENCE_CONTEXT_TYPE = presenceSlots.PRESENCE_CONTEXT_TYPE;
31
+ exports.PRESENCE_IDLE_WINDOW_MS = presenceSlots.PRESENCE_IDLE_WINDOW_MS;
32
+ exports.derivePresenceState = presenceSlots.derivePresenceState;
33
+ exports.mergePresenceStates = presenceSlots.mergePresenceStates;
15
34
  exports.useGenericDoc = react.useGenericDoc;
16
35
  exports.defineDocShape = shape.defineDocShape;
36
+ exports.MAX_CONSECUTIVE_REMINTS = tokenRefresh.MAX_CONSECUTIVE_REMINTS;
37
+ exports.MIN_REFRESH_DELAY_MS = tokenRefresh.MIN_REFRESH_DELAY_MS;
38
+ exports.REFRESH_HEADROOM_MS = tokenRefresh.REFRESH_HEADROOM_MS;
39
+ exports.refreshDelayMs = tokenRefresh.refreshDelayMs;
17
40
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,13 +1,28 @@
1
1
  /**
2
- * `@athenaintel/react/collab` — live collaboration for Generic Doc assets.
2
+ * `@athenaintel/react/collab` — live collaboration for Generic Doc assets and
3
+ * a read-only view of a workspace's presence feed.
3
4
  *
4
5
  * Built on yjs v14 (`@y/y`) and the v14-line WebSocket provider
5
- * (`@y/websocket`). Auth flows through the public collab-token mint
6
+ * (`@y/websocket`). Document auth flows through the public collab-token mint
6
7
  * (`POST /api/v0/assets/{id}/collab-token`), which is allowlisted server-side
7
8
  * to `generic_doc` assets and admin-only during the initial rollout.
9
+ *
10
+ * Workspace presence (`useWorkspacePresence` / `connectWorkspacePresence`)
11
+ * observes the Keryx workspace awareness room (`<workspaceId>/awareness`) —
12
+ * the same per-recipient-filtered feed the Olympus People page renders —
13
+ * through a read-only token from
14
+ * `POST /api/v0/workspaces/{id}/presence-token` (admin-only). The observer
15
+ * never publishes a slot and never writes to the room's document.
16
+ * `resolveWorkspacePresence` (`POST …/presence/resolve`) turns the ids carried
17
+ * in awareness contexts into titles the caller may see.
8
18
  */
9
19
  export { classifyClose, WS_CLOSE_AUTH_REVOKED, WS_CLOSE_DOC_DELETED } from './close-policy';
10
20
  export { CollabTokenError, connectGenericDoc, type CollabAuth, type CollabTokenResponse, type GenericDocHandle, type GenericDocStatus, type PresenceEntry, } from './client';
11
21
  export { getAthenaApiBaseUrl, mintCollabToken } from './mint';
22
+ export { PRESENCE_SWEEP_INTERVAL_MS, connectWorkspacePresence, type WorkspacePresenceHandle, type WorkspacePresenceStatus, } from './presence-client';
23
+ export { mintWorkspacePresenceToken, resolveWorkspacePresence, type WorkspacePresenceResolveRequest, type WorkspacePresenceResolveResponse, type WorkspacePresenceResolvedAsset, type WorkspacePresenceResolvedMember, type WorkspacePresenceResolvedProject, type WorkspacePresenceResolvedSession, type WorkspacePresenceTokenResponse, } from './presence-mint';
24
+ export { PRESENCE_DEFAULT_TICK_MS, useWorkspacePresence, useWorkspacePresenceResolver, type PresenceHumanWithState, type UseWorkspacePresenceOptions, type UseWorkspacePresenceResult, type WorkspacePresenceResolver, } from './presence-react';
25
+ export { MAX_PRESENCE_CONTEXTS_PER_SLOT, MAX_PRESENCE_STRING_CHARS, PRESENCE_ACTIVE_WINDOW_MS, PRESENCE_CONTEXT_TYPE, PRESENCE_IDLE_WINDOW_MS, derivePresenceState, mergePresenceStates, type PresenceAgent, type PresenceContext, type PresenceContextType, type PresenceHuman, type PresenceRoster, type PresenceState, } from './presence-slots';
12
26
  export { useGenericDoc, type UseGenericDocResult } from './react';
13
27
  export { defineDocShape, type BoundDocShape, type DocShapeKind } from './shape';
28
+ export { MAX_CONSECUTIVE_REMINTS, MIN_REFRESH_DELAY_MS, REFRESH_HEADROOM_MS, refreshDelayMs, } from './token-refresh';