@athenaintel/react 0.21.1 → 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.
- package/README.md +63 -0
- package/dist/chat/StatewireApprovalCard.cjs +70 -10
- package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
- package/dist/chat/StatewireApprovalCard.d.ts +12 -1
- package/dist/chat/StatewireApprovalCard.js +73 -13
- package/dist/chat/StatewireApprovalCard.js.map +1 -1
- package/dist/chat/tiptap-text.cjs +28 -23
- package/dist/chat/tiptap-text.cjs.map +1 -1
- package/dist/chat/tiptap-text.js +28 -23
- package/dist/chat/tiptap-text.js.map +1 -1
- package/dist/chat/use-statewire-approval-request.cjs +22 -1
- package/dist/chat/use-statewire-approval-request.cjs.map +1 -1
- package/dist/chat/use-statewire-approval-request.d.ts +41 -0
- package/dist/chat/use-statewire-approval-request.js +24 -3
- package/dist/chat/use-statewire-approval-request.js.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs +0 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js +0 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js.map +1 -1
- package/dist/collab/client.cjs +4 -9
- package/dist/collab/client.cjs.map +1 -1
- package/dist/collab/client.js +2 -7
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/index.cjs +23 -0
- package/dist/collab/index.cjs.map +1 -1
- package/dist/collab/index.d.ts +17 -2
- package/dist/collab/index.js +24 -1
- package/dist/collab/index.js.map +1 -1
- package/dist/collab/mint.cjs +18 -13
- package/dist/collab/mint.cjs.map +1 -1
- package/dist/collab/mint.d.ts +10 -0
- package/dist/collab/mint.js +18 -13
- package/dist/collab/mint.js.map +1 -1
- package/dist/collab/presence-client.cjs +211 -0
- package/dist/collab/presence-client.cjs.map +1 -0
- package/dist/collab/presence-client.d.ts +60 -0
- package/dist/collab/presence-client.js +194 -0
- package/dist/collab/presence-client.js.map +1 -0
- package/dist/collab/presence-mint.cjs +68 -0
- package/dist/collab/presence-mint.cjs.map +1 -0
- package/dist/collab/presence-mint.d.ts +87 -0
- package/dist/collab/presence-mint.js +68 -0
- package/dist/collab/presence-mint.js.map +1 -0
- package/dist/collab/presence-react.cjs +114 -0
- package/dist/collab/presence-react.cjs.map +1 -0
- package/dist/collab/presence-react.d.ts +47 -0
- package/dist/collab/presence-react.js +114 -0
- package/dist/collab/presence-react.js.map +1 -0
- package/dist/collab/presence-slots.cjs +220 -0
- package/dist/collab/presence-slots.cjs.map +1 -0
- package/dist/collab/presence-slots.d.ts +119 -0
- package/dist/collab/presence-slots.js +220 -0
- package/dist/collab/presence-slots.js.map +1 -0
- package/dist/collab/token-refresh.cjs +13 -0
- package/dist/collab/token-refresh.cjs.map +1 -0
- package/dist/collab/token-refresh.d.ts +20 -0
- package/dist/collab/token-refresh.js +13 -0
- package/dist/collab/token-refresh.js.map +1 -0
- package/dist/packages/agent-runtime/src/interrupts.cjs +40 -1
- package/dist/packages/agent-runtime/src/interrupts.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/interrupts.js +40 -1
- package/dist/packages/agent-runtime/src/interrupts.js.map +1 -1
- package/dist/packages/react/package.json.cjs +1 -1
- package/dist/packages/react/package.json.js +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public-API clients for the workspace presence feed.
|
|
3
|
+
*
|
|
4
|
+
* Two endpoints, both admin-only and both under the workspace:
|
|
5
|
+
*
|
|
6
|
+
* - `POST /api/v0/workspaces/{workspaceId}/presence-token` mints a
|
|
7
|
+
* **read-only** Keryx token for the workspace awareness room
|
|
8
|
+
* (`<workspaceId>/awareness`) — the same per-recipient-filtered presence
|
|
9
|
+
* feed the Olympus People page renders. The holder can receive presence and
|
|
10
|
+
* broadcast nothing; `connectWorkspacePresence` re-mints before expiry.
|
|
11
|
+
* - `POST /api/v0/workspaces/{workspaceId}/presence/resolve` turns the opaque
|
|
12
|
+
* ids carried in awareness contexts (asset ids, session thread ids, project
|
|
13
|
+
* ids) into titles the caller may see. Only things the caller may open come
|
|
14
|
+
* back; missing ids are simply absent.
|
|
15
|
+
*
|
|
16
|
+
* Wire shapes are snake_case, matching `CollabTokenResponse`.
|
|
17
|
+
*/
|
|
18
|
+
import { type CollabAuth } from './mint';
|
|
19
|
+
/** Grant for the workspace awareness room. Always read-only (`access_type: 'r'`). */
|
|
20
|
+
export interface WorkspacePresenceTokenResponse {
|
|
21
|
+
token: string;
|
|
22
|
+
access_type: 'r' | 'rw';
|
|
23
|
+
expires_at_ms: number;
|
|
24
|
+
ws_url: string;
|
|
25
|
+
org: string;
|
|
26
|
+
doc_id: string;
|
|
27
|
+
branch: string;
|
|
28
|
+
}
|
|
29
|
+
export interface WorkspacePresenceResolveRequest {
|
|
30
|
+
asset_ids?: string[];
|
|
31
|
+
/** Thread ids, as published in `session` awareness contexts. */
|
|
32
|
+
session_ids?: string[];
|
|
33
|
+
project_ids?: string[];
|
|
34
|
+
include_projects?: boolean;
|
|
35
|
+
include_members?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface WorkspacePresenceResolvedAsset {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
asset_type: string;
|
|
41
|
+
}
|
|
42
|
+
export interface WorkspacePresenceResolvedSession {
|
|
43
|
+
thread_id: string;
|
|
44
|
+
asset_id: string;
|
|
45
|
+
title: string;
|
|
46
|
+
}
|
|
47
|
+
export interface WorkspacePresenceResolvedProject {
|
|
48
|
+
id: string;
|
|
49
|
+
title: string;
|
|
50
|
+
assets: WorkspacePresenceResolvedAsset[];
|
|
51
|
+
}
|
|
52
|
+
export interface WorkspacePresenceResolvedMember {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
email: string;
|
|
56
|
+
avatar_url: string | null;
|
|
57
|
+
}
|
|
58
|
+
export interface WorkspacePresenceResolveResponse {
|
|
59
|
+
assets: WorkspacePresenceResolvedAsset[];
|
|
60
|
+
sessions: WorkspacePresenceResolvedSession[];
|
|
61
|
+
projects: WorkspacePresenceResolvedProject[];
|
|
62
|
+
members: WorkspacePresenceResolvedMember[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Mint a read-only token for the workspace awareness room.
|
|
66
|
+
*
|
|
67
|
+
* Rejects with `CollabTokenError` on 401 (bad credential) and 403 (admin
|
|
68
|
+
* required, roster not enabled for this workspace, or a sandbox credential
|
|
69
|
+
* that may not read presence), carrying the server's `detail`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function mintWorkspacePresenceToken(args: {
|
|
72
|
+
auth: CollabAuth;
|
|
73
|
+
workspaceId: string;
|
|
74
|
+
signal?: AbortSignal;
|
|
75
|
+
}): Promise<WorkspacePresenceTokenResponse>;
|
|
76
|
+
/**
|
|
77
|
+
* Resolve awareness ids to titles. Every section of the response is always
|
|
78
|
+
* present on the returned object (an omitted section reads as empty), so
|
|
79
|
+
* callers can index without guarding. Failures reject with `CollabTokenError`
|
|
80
|
+
* — the same status + detail shape as the mint.
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveWorkspacePresence(args: {
|
|
83
|
+
auth: CollabAuth;
|
|
84
|
+
workspaceId: string;
|
|
85
|
+
request: WorkspacePresenceResolveRequest;
|
|
86
|
+
signal?: AbortSignal;
|
|
87
|
+
}): Promise<WorkspacePresenceResolveResponse>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getCollabAuthHeaders, collabErrorFromResponse, CollabTokenError, getAthenaApiBaseUrl } from "./mint.js";
|
|
2
|
+
function workspaceUrl({
|
|
3
|
+
auth,
|
|
4
|
+
workspaceId,
|
|
5
|
+
path
|
|
6
|
+
}) {
|
|
7
|
+
const base = getAthenaApiBaseUrl(auth.backendUrl);
|
|
8
|
+
return `${base}/api/v0/workspaces/${encodeURIComponent(workspaceId)}/${path}`;
|
|
9
|
+
}
|
|
10
|
+
async function mintWorkspacePresenceToken(args) {
|
|
11
|
+
const response = await fetch(
|
|
12
|
+
workspaceUrl({ auth: args.auth, workspaceId: args.workspaceId, path: "presence-token" }),
|
|
13
|
+
{
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: {
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
...getCollabAuthHeaders(args.auth)
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({}),
|
|
20
|
+
signal: args.signal
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw await collabErrorFromResponse(
|
|
25
|
+
response,
|
|
26
|
+
`Workspace presence token mint failed with status ${response.status}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return await response.json();
|
|
30
|
+
}
|
|
31
|
+
function arrayOrEmpty(value) {
|
|
32
|
+
return Array.isArray(value) ? value : [];
|
|
33
|
+
}
|
|
34
|
+
async function resolveWorkspacePresence(args) {
|
|
35
|
+
const response = await fetch(
|
|
36
|
+
workspaceUrl({ auth: args.auth, workspaceId: args.workspaceId, path: "presence/resolve" }),
|
|
37
|
+
{
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
...getCollabAuthHeaders(args.auth)
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify(args.request),
|
|
44
|
+
signal: args.signal
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw await collabErrorFromResponse(
|
|
49
|
+
response,
|
|
50
|
+
`Workspace presence resolve failed with status ${response.status}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const body = await response.json();
|
|
54
|
+
if (!body || typeof body !== "object") {
|
|
55
|
+
throw new CollabTokenError(response.status, "Workspace presence resolve returned no body");
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
assets: arrayOrEmpty(body.assets),
|
|
59
|
+
sessions: arrayOrEmpty(body.sessions),
|
|
60
|
+
projects: arrayOrEmpty(body.projects),
|
|
61
|
+
members: arrayOrEmpty(body.members)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
mintWorkspacePresenceToken,
|
|
66
|
+
resolveWorkspacePresence
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=presence-mint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence-mint.js","sources":["../../src/collab/presence-mint.ts"],"sourcesContent":["/**\n * Public-API clients for the workspace presence feed.\n *\n * Two endpoints, both admin-only and both under the workspace:\n *\n * - `POST /api/v0/workspaces/{workspaceId}/presence-token` mints a\n * **read-only** Keryx token for the workspace awareness room\n * (`<workspaceId>/awareness`) — the same per-recipient-filtered presence\n * feed the Olympus People page renders. The holder can receive presence and\n * broadcast nothing; `connectWorkspacePresence` re-mints before expiry.\n * - `POST /api/v0/workspaces/{workspaceId}/presence/resolve` turns the opaque\n * ids carried in awareness contexts (asset ids, session thread ids, project\n * ids) into titles the caller may see. Only things the caller may open come\n * back; missing ids are simply absent.\n *\n * Wire shapes are snake_case, matching `CollabTokenResponse`.\n */\n\nimport {\n type CollabAuth,\n CollabTokenError,\n collabErrorFromResponse,\n getAthenaApiBaseUrl,\n getCollabAuthHeaders,\n} from './mint';\n\n/** Grant for the workspace awareness room. Always read-only (`access_type: 'r'`). */\nexport interface WorkspacePresenceTokenResponse {\n token: string;\n access_type: 'r' | 'rw';\n expires_at_ms: number;\n ws_url: string;\n org: string;\n doc_id: string;\n branch: string;\n}\n\nexport interface WorkspacePresenceResolveRequest {\n asset_ids?: string[];\n /** Thread ids, as published in `session` awareness contexts. */\n session_ids?: string[];\n project_ids?: string[];\n include_projects?: boolean;\n include_members?: boolean;\n}\n\nexport interface WorkspacePresenceResolvedAsset {\n id: string;\n title: string;\n asset_type: string;\n}\n\nexport interface WorkspacePresenceResolvedSession {\n thread_id: string;\n asset_id: string;\n title: string;\n}\n\nexport interface WorkspacePresenceResolvedProject {\n id: string;\n title: string;\n assets: WorkspacePresenceResolvedAsset[];\n}\n\nexport interface WorkspacePresenceResolvedMember {\n id: string;\n name: string;\n email: string;\n avatar_url: string | null;\n}\n\nexport interface WorkspacePresenceResolveResponse {\n assets: WorkspacePresenceResolvedAsset[];\n sessions: WorkspacePresenceResolvedSession[];\n projects: WorkspacePresenceResolvedProject[];\n members: WorkspacePresenceResolvedMember[];\n}\n\nfunction workspaceUrl({\n auth,\n workspaceId,\n path,\n}: {\n auth: CollabAuth;\n workspaceId: string;\n path: string;\n}): string {\n const base = getAthenaApiBaseUrl(auth.backendUrl);\n return `${base}/api/v0/workspaces/${encodeURIComponent(workspaceId)}/${path}`;\n}\n\n/**\n * Mint a read-only token for the workspace awareness room.\n *\n * Rejects with `CollabTokenError` on 401 (bad credential) and 403 (admin\n * required, roster not enabled for this workspace, or a sandbox credential\n * that may not read presence), carrying the server's `detail`.\n */\nexport async function mintWorkspacePresenceToken(args: {\n auth: CollabAuth;\n workspaceId: string;\n signal?: AbortSignal;\n}): Promise<WorkspacePresenceTokenResponse> {\n const response = await fetch(\n workspaceUrl({ auth: args.auth, workspaceId: args.workspaceId, path: 'presence-token' }),\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...getCollabAuthHeaders(args.auth),\n },\n body: JSON.stringify({}),\n signal: args.signal,\n }\n );\n if (!response.ok) {\n throw await collabErrorFromResponse(\n response,\n `Workspace presence token mint failed with status ${response.status}`\n );\n }\n return (await response.json()) as WorkspacePresenceTokenResponse;\n}\n\nfunction arrayOrEmpty<T>(value: T[] | undefined | null): T[] {\n return Array.isArray(value) ? value : [];\n}\n\n/**\n * Resolve awareness ids to titles. Every section of the response is always\n * present on the returned object (an omitted section reads as empty), so\n * callers can index without guarding. Failures reject with `CollabTokenError`\n * — the same status + detail shape as the mint.\n */\nexport async function resolveWorkspacePresence(args: {\n auth: CollabAuth;\n workspaceId: string;\n request: WorkspacePresenceResolveRequest;\n signal?: AbortSignal;\n}): Promise<WorkspacePresenceResolveResponse> {\n const response = await fetch(\n workspaceUrl({ auth: args.auth, workspaceId: args.workspaceId, path: 'presence/resolve' }),\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...getCollabAuthHeaders(args.auth),\n },\n body: JSON.stringify(args.request),\n signal: args.signal,\n }\n );\n if (!response.ok) {\n throw await collabErrorFromResponse(\n response,\n `Workspace presence resolve failed with status ${response.status}`\n );\n }\n const body = (await response.json()) as Partial<WorkspacePresenceResolveResponse> | null;\n if (!body || typeof body !== 'object') {\n throw new CollabTokenError(response.status, 'Workspace presence resolve returned no body');\n }\n return {\n assets: arrayOrEmpty(body.assets),\n sessions: arrayOrEmpty(body.sessions),\n projects: arrayOrEmpty(body.projects),\n members: arrayOrEmpty(body.members),\n };\n}\n"],"names":[],"mappings":";AA8EA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AACF,GAIW;AACT,QAAM,OAAO,oBAAoB,KAAK,UAAU;AAChD,SAAO,GAAG,IAAI,sBAAsB,mBAAmB,WAAW,CAAC,IAAI,IAAI;AAC7E;AASA,eAAsB,2BAA2B,MAIL;AAC1C,QAAM,WAAW,MAAM;AAAA,IACrB,aAAa,EAAE,MAAM,KAAK,MAAM,aAAa,KAAK,aAAa,MAAM,kBAAkB;AAAA,IACvF;AAAA,MACE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAG,qBAAqB,KAAK,IAAI;AAAA,MAAA;AAAA,MAEnC,MAAM,KAAK,UAAU,EAAE;AAAA,MACvB,QAAQ,KAAK;AAAA,IAAA;AAAA,EACf;AAEF,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,MAAM;AAAA,MACV;AAAA,MACA,oDAAoD,SAAS,MAAM;AAAA,IAAA;AAAA,EAEvE;AACA,SAAQ,MAAM,SAAS,KAAA;AACzB;AAEA,SAAS,aAAgB,OAAoC;AAC3D,SAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAA;AACxC;AAQA,eAAsB,yBAAyB,MAKD;AAC5C,QAAM,WAAW,MAAM;AAAA,IACrB,aAAa,EAAE,MAAM,KAAK,MAAM,aAAa,KAAK,aAAa,MAAM,oBAAoB;AAAA,IACzF;AAAA,MACE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAG,qBAAqB,KAAK,IAAI;AAAA,MAAA;AAAA,MAEnC,MAAM,KAAK,UAAU,KAAK,OAAO;AAAA,MACjC,QAAQ,KAAK;AAAA,IAAA;AAAA,EACf;AAEF,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,MAAM;AAAA,MACV;AAAA,MACA,iDAAiD,SAAS,MAAM;AAAA,IAAA;AAAA,EAEpE;AACA,QAAM,OAAQ,MAAM,SAAS,KAAA;AAC7B,MAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACrC,UAAM,IAAI,iBAAiB,SAAS,QAAQ,6CAA6C;AAAA,EAC3F;AACA,SAAO;AAAA,IACL,QAAQ,aAAa,KAAK,MAAM;AAAA,IAChC,UAAU,aAAa,KAAK,QAAQ;AAAA,IACpC,UAAU,aAAa,KAAK,QAAQ;AAAA,IACpC,SAAS,aAAa,KAAK,OAAO;AAAA,EAAA;AAEtC;"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const react = require("react");
|
|
4
|
+
const AthenaContext = require("../provider/AthenaContext.cjs");
|
|
5
|
+
const presenceClient = require("./presence-client.cjs");
|
|
6
|
+
const presenceMint = require("./presence-mint.cjs");
|
|
7
|
+
const presenceSlots = require("./presence-slots.cjs");
|
|
8
|
+
const PRESENCE_DEFAULT_TICK_MS = 1e4;
|
|
9
|
+
const EMPTY_SNAPSHOT = "init||0";
|
|
10
|
+
const NO_HUMANS = [];
|
|
11
|
+
const NO_AGENTS = [];
|
|
12
|
+
function useWorkspacePresence(workspaceId, options) {
|
|
13
|
+
const config = AthenaContext.useAthenaConfig();
|
|
14
|
+
const enabled = (options == null ? void 0 : options.enabled) !== false && workspaceId !== null;
|
|
15
|
+
const tickMs = (options == null ? void 0 : options.tickMs) ?? PRESENCE_DEFAULT_TICK_MS;
|
|
16
|
+
const [entry, setEntry] = react.useState(null);
|
|
17
|
+
const handleRef = react.useRef(null);
|
|
18
|
+
react.useEffect(() => {
|
|
19
|
+
if (!enabled || workspaceId === null) {
|
|
20
|
+
setEntry(null);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const next = presenceClient.connectWorkspacePresence({
|
|
24
|
+
workspaceId,
|
|
25
|
+
auth: {
|
|
26
|
+
backendUrl: config.backendUrl,
|
|
27
|
+
apiKey: config.apiKey,
|
|
28
|
+
token: config.token
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
handleRef.current = next;
|
|
32
|
+
setEntry({ workspaceId, handle: next });
|
|
33
|
+
return () => {
|
|
34
|
+
handleRef.current = null;
|
|
35
|
+
next.destroy();
|
|
36
|
+
};
|
|
37
|
+
}, [enabled, workspaceId, config.backendUrl, config.apiKey, config.token]);
|
|
38
|
+
const handle = enabled && entry !== null && entry.workspaceId === workspaceId ? entry.handle : null;
|
|
39
|
+
const subscribe = react.useMemo(() => {
|
|
40
|
+
return (onStoreChange) => {
|
|
41
|
+
if (!handle) return () => {
|
|
42
|
+
};
|
|
43
|
+
return handle.onChange(onStoreChange);
|
|
44
|
+
};
|
|
45
|
+
}, [handle]);
|
|
46
|
+
react.useSyncExternalStore(
|
|
47
|
+
subscribe,
|
|
48
|
+
() => {
|
|
49
|
+
if (!handle) return EMPTY_SNAPSHOT;
|
|
50
|
+
return `${handle.status}|${handle.accessType ?? ""}|${handle.version}`;
|
|
51
|
+
},
|
|
52
|
+
() => EMPTY_SNAPSHOT
|
|
53
|
+
);
|
|
54
|
+
const [now, setNow] = react.useState(() => Date.now());
|
|
55
|
+
react.useEffect(() => {
|
|
56
|
+
if (!enabled) return;
|
|
57
|
+
setNow(Date.now());
|
|
58
|
+
const timer = setInterval(() => {
|
|
59
|
+
setNow(Date.now());
|
|
60
|
+
}, tickMs);
|
|
61
|
+
return () => {
|
|
62
|
+
clearInterval(timer);
|
|
63
|
+
};
|
|
64
|
+
}, [enabled, tickMs]);
|
|
65
|
+
const rosterHumans = (handle == null ? void 0 : handle.humans) ?? NO_HUMANS;
|
|
66
|
+
const humans = react.useMemo(
|
|
67
|
+
() => rosterHumans.map((human) => ({
|
|
68
|
+
...human,
|
|
69
|
+
state: presenceSlots.derivePresenceState({
|
|
70
|
+
lastIntentAt: human.lastIntentAt,
|
|
71
|
+
hidden: human.hidden,
|
|
72
|
+
manualAway: human.manualAway,
|
|
73
|
+
now
|
|
74
|
+
})
|
|
75
|
+
})),
|
|
76
|
+
[rosterHumans, now]
|
|
77
|
+
);
|
|
78
|
+
const refreshNow = react.useCallback(() => {
|
|
79
|
+
var _a;
|
|
80
|
+
return ((_a = handleRef.current) == null ? void 0 : _a.refreshNow()) ?? Promise.resolve();
|
|
81
|
+
}, []);
|
|
82
|
+
return {
|
|
83
|
+
status: handle ? handle.status : enabled ? "connecting" : "closed",
|
|
84
|
+
error: (handle == null ? void 0 : handle.error) ?? null,
|
|
85
|
+
accessType: (handle == null ? void 0 : handle.accessType) ?? null,
|
|
86
|
+
humans,
|
|
87
|
+
agents: (handle == null ? void 0 : handle.agents) ?? NO_AGENTS,
|
|
88
|
+
refreshNow
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function useWorkspacePresenceResolver(workspaceId) {
|
|
92
|
+
const config = AthenaContext.useAthenaConfig();
|
|
93
|
+
const { backendUrl, apiKey, token } = config;
|
|
94
|
+
return react.useCallback(
|
|
95
|
+
(request, signal) => {
|
|
96
|
+
if (workspaceId === null) {
|
|
97
|
+
return Promise.reject(
|
|
98
|
+
new Error("[AthenaSDK] useWorkspacePresenceResolver: no workspace selected")
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return presenceMint.resolveWorkspacePresence({
|
|
102
|
+
auth: { backendUrl, apiKey, token },
|
|
103
|
+
workspaceId,
|
|
104
|
+
request,
|
|
105
|
+
signal
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
[workspaceId, backendUrl, apiKey, token]
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
exports.PRESENCE_DEFAULT_TICK_MS = PRESENCE_DEFAULT_TICK_MS;
|
|
112
|
+
exports.useWorkspacePresence = useWorkspacePresence;
|
|
113
|
+
exports.useWorkspacePresenceResolver = useWorkspacePresenceResolver;
|
|
114
|
+
//# sourceMappingURL=presence-react.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence-react.cjs","sources":["../../src/collab/presence-react.ts"],"sourcesContent":["/**\n * React bindings for the workspace presence feed.\n *\n * `useWorkspacePresence(workspaceId)` opens (and owns) a\n * `connectWorkspacePresence` handle, resolving auth from the surrounding\n * `AthenaProvider` (per-viewer bearer when present, API key otherwise), and\n * re-renders on status/roster changes via `useSyncExternalStore`. Each person\n * carries a `state` on the intent ladder, recomputed against a clock that\n * ticks every `tickMs` so `active` decays to `idle` and `away` between\n * awareness events.\n *\n * `useWorkspacePresenceResolver(workspaceId)` returns the batch id→title\n * resolver bound to the same auth. It is deliberately thin: callers own\n * caching (TanStack Query is the expected home for it).\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n type WorkspacePresenceHandle,\n type WorkspacePresenceStatus,\n connectWorkspacePresence,\n} from './presence-client';\nimport {\n type WorkspacePresenceResolveRequest,\n type WorkspacePresenceResolveResponse,\n resolveWorkspacePresence,\n} from './presence-mint';\nimport {\n type PresenceAgent,\n type PresenceHuman,\n type PresenceState,\n derivePresenceState,\n} from './presence-slots';\n\n/** Default cadence for re-deriving each person's intent state. */\nexport const PRESENCE_DEFAULT_TICK_MS = 10_000;\n\nexport interface PresenceHumanWithState extends PresenceHuman {\n /** Where the person sits on the intent ladder as of the last tick. */\n state: PresenceState;\n}\n\nexport interface UseWorkspacePresenceOptions {\n /** `false` closes the feed without unmounting the caller. Default `true`. */\n enabled?: boolean;\n /** How often `state` is re-derived from `lastIntentAt`. Default 10 s. */\n tickMs?: number;\n}\n\nexport interface UseWorkspacePresenceResult {\n /** `'closed'` while disabled or without a workspace; `'connecting'` until the first mint resolves. */\n status: WorkspacePresenceStatus;\n error: Error | null;\n accessType: 'r' | 'rw' | null;\n humans: PresenceHumanWithState[];\n agents: PresenceAgent[];\n /** Force an early token refresh; resolves immediately when no feed is open. */\n refreshNow: () => Promise<void>;\n}\n\nconst EMPTY_SNAPSHOT = 'init||0';\nconst NO_HUMANS: PresenceHuman[] = [];\nconst NO_AGENTS: PresenceAgent[] = [];\n\nexport function useWorkspacePresence(\n workspaceId: string | null,\n options?: UseWorkspacePresenceOptions\n): UseWorkspacePresenceResult {\n const config = useAthenaConfig();\n const enabled = options?.enabled !== false && workspaceId !== null;\n const tickMs = options?.tickMs ?? PRESENCE_DEFAULT_TICK_MS;\n const [entry, setEntry] = useState<{\n workspaceId: string;\n handle: WorkspacePresenceHandle;\n } | null>(null);\n const handleRef = useRef<WorkspacePresenceHandle | null>(null);\n\n useEffect(() => {\n if (!enabled || workspaceId === null) {\n setEntry(null);\n return;\n }\n const next = connectWorkspacePresence({\n workspaceId,\n auth: {\n backendUrl: config.backendUrl,\n apiKey: config.apiKey,\n token: config.token,\n },\n });\n handleRef.current = next;\n setEntry({ workspaceId, handle: next });\n return () => {\n handleRef.current = null;\n next.destroy();\n };\n // Recreate when the target or credentials change; the handle re-mints on\n // its own schedule otherwise.\n }, [enabled, workspaceId, config.backendUrl, config.apiKey, config.token]);\n\n // React renders with the new arguments before the effect above runs, so a\n // handle opened for a previous workspace, or before the feed was disabled,\n // still sits in state for that one render. Expose it only while it is the\n // handle this render asked for; otherwise the closed / connecting empty\n // result wins, and a disabled feed never leaks a roster.\n const handle =\n enabled && entry !== null && entry.workspaceId === workspaceId ? entry.handle : null;\n\n const subscribe = useMemo(() => {\n return (onStoreChange: () => void) => {\n if (!handle) return () => {};\n return handle.onChange(onStoreChange);\n };\n }, [handle]);\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => {\n if (!handle) return EMPTY_SNAPSHOT;\n return `${handle.status}|${handle.accessType ?? ''}|${handle.version}`;\n },\n () => EMPTY_SNAPSHOT\n );\n void snapshot;\n\n // The ladder decays with wall-clock time, not with awareness events; a\n // person who stops typing must drift to idle even when nothing arrives.\n const [now, setNow] = useState(() => Date.now());\n useEffect(() => {\n if (!enabled) return;\n setNow(Date.now());\n const timer = setInterval(() => {\n setNow(Date.now());\n }, tickMs);\n return () => {\n clearInterval(timer);\n };\n }, [enabled, tickMs]);\n\n const rosterHumans = handle?.humans ?? NO_HUMANS;\n const humans = useMemo(\n () =>\n rosterHumans.map((human) => ({\n ...human,\n state: derivePresenceState({\n lastIntentAt: human.lastIntentAt,\n hidden: human.hidden,\n manualAway: human.manualAway,\n now,\n }),\n })),\n [rosterHumans, now]\n );\n\n const refreshNow = useCallback(() => handleRef.current?.refreshNow() ?? Promise.resolve(), []);\n\n return {\n status: handle ? handle.status : enabled ? 'connecting' : 'closed',\n error: handle?.error ?? null,\n accessType: handle?.accessType ?? null,\n humans,\n agents: handle?.agents ?? NO_AGENTS,\n refreshNow,\n };\n}\n\nexport type WorkspacePresenceResolver = (\n request: WorkspacePresenceResolveRequest,\n signal?: AbortSignal\n) => Promise<WorkspacePresenceResolveResponse>;\n\n/**\n * A stable resolver bound to the provider's auth and the given workspace.\n * Rejects when called without a workspace; no caching — callers own that.\n */\nexport function useWorkspacePresenceResolver(\n workspaceId: string | null\n): WorkspacePresenceResolver {\n const config = useAthenaConfig();\n const { backendUrl, apiKey, token } = config;\n return useCallback<WorkspacePresenceResolver>(\n (request, signal) => {\n if (workspaceId === null) {\n return Promise.reject(\n new Error('[AthenaSDK] useWorkspacePresenceResolver: no workspace selected')\n );\n }\n return resolveWorkspacePresence({\n auth: { backendUrl, apiKey, token },\n workspaceId,\n request,\n signal,\n });\n },\n [workspaceId, backendUrl, apiKey, token]\n );\n}\n"],"names":["useAthenaConfig","useState","useRef","useEffect","connectWorkspacePresence","useMemo","useSyncExternalStore","derivePresenceState","useCallback","resolveWorkspacePresence"],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React bindings for the workspace presence feed.
|
|
3
|
+
*
|
|
4
|
+
* `useWorkspacePresence(workspaceId)` opens (and owns) a
|
|
5
|
+
* `connectWorkspacePresence` handle, resolving auth from the surrounding
|
|
6
|
+
* `AthenaProvider` (per-viewer bearer when present, API key otherwise), and
|
|
7
|
+
* re-renders on status/roster changes via `useSyncExternalStore`. Each person
|
|
8
|
+
* carries a `state` on the intent ladder, recomputed against a clock that
|
|
9
|
+
* ticks every `tickMs` so `active` decays to `idle` and `away` between
|
|
10
|
+
* awareness events.
|
|
11
|
+
*
|
|
12
|
+
* `useWorkspacePresenceResolver(workspaceId)` returns the batch id→title
|
|
13
|
+
* resolver bound to the same auth. It is deliberately thin: callers own
|
|
14
|
+
* caching (TanStack Query is the expected home for it).
|
|
15
|
+
*/
|
|
16
|
+
import { type WorkspacePresenceStatus } from './presence-client';
|
|
17
|
+
import { type WorkspacePresenceResolveRequest, type WorkspacePresenceResolveResponse } from './presence-mint';
|
|
18
|
+
import { type PresenceAgent, type PresenceHuman, type PresenceState } from './presence-slots';
|
|
19
|
+
/** Default cadence for re-deriving each person's intent state. */
|
|
20
|
+
export declare const PRESENCE_DEFAULT_TICK_MS = 10000;
|
|
21
|
+
export interface PresenceHumanWithState extends PresenceHuman {
|
|
22
|
+
/** Where the person sits on the intent ladder as of the last tick. */
|
|
23
|
+
state: PresenceState;
|
|
24
|
+
}
|
|
25
|
+
export interface UseWorkspacePresenceOptions {
|
|
26
|
+
/** `false` closes the feed without unmounting the caller. Default `true`. */
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
/** How often `state` is re-derived from `lastIntentAt`. Default 10 s. */
|
|
29
|
+
tickMs?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface UseWorkspacePresenceResult {
|
|
32
|
+
/** `'closed'` while disabled or without a workspace; `'connecting'` until the first mint resolves. */
|
|
33
|
+
status: WorkspacePresenceStatus;
|
|
34
|
+
error: Error | null;
|
|
35
|
+
accessType: 'r' | 'rw' | null;
|
|
36
|
+
humans: PresenceHumanWithState[];
|
|
37
|
+
agents: PresenceAgent[];
|
|
38
|
+
/** Force an early token refresh; resolves immediately when no feed is open. */
|
|
39
|
+
refreshNow: () => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export declare function useWorkspacePresence(workspaceId: string | null, options?: UseWorkspacePresenceOptions): UseWorkspacePresenceResult;
|
|
42
|
+
export type WorkspacePresenceResolver = (request: WorkspacePresenceResolveRequest, signal?: AbortSignal) => Promise<WorkspacePresenceResolveResponse>;
|
|
43
|
+
/**
|
|
44
|
+
* A stable resolver bound to the provider's auth and the given workspace.
|
|
45
|
+
* Rejects when called without a workspace; no caching — callers own that.
|
|
46
|
+
*/
|
|
47
|
+
export declare function useWorkspacePresenceResolver(workspaceId: string | null): WorkspacePresenceResolver;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useMemo, useSyncExternalStore, useCallback } from "react";
|
|
2
|
+
import { useAthenaConfig } from "../provider/AthenaContext.js";
|
|
3
|
+
import { connectWorkspacePresence } from "./presence-client.js";
|
|
4
|
+
import { resolveWorkspacePresence } from "./presence-mint.js";
|
|
5
|
+
import { derivePresenceState } from "./presence-slots.js";
|
|
6
|
+
const PRESENCE_DEFAULT_TICK_MS = 1e4;
|
|
7
|
+
const EMPTY_SNAPSHOT = "init||0";
|
|
8
|
+
const NO_HUMANS = [];
|
|
9
|
+
const NO_AGENTS = [];
|
|
10
|
+
function useWorkspacePresence(workspaceId, options) {
|
|
11
|
+
const config = useAthenaConfig();
|
|
12
|
+
const enabled = (options == null ? void 0 : options.enabled) !== false && workspaceId !== null;
|
|
13
|
+
const tickMs = (options == null ? void 0 : options.tickMs) ?? PRESENCE_DEFAULT_TICK_MS;
|
|
14
|
+
const [entry, setEntry] = useState(null);
|
|
15
|
+
const handleRef = useRef(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!enabled || workspaceId === null) {
|
|
18
|
+
setEntry(null);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const next = connectWorkspacePresence({
|
|
22
|
+
workspaceId,
|
|
23
|
+
auth: {
|
|
24
|
+
backendUrl: config.backendUrl,
|
|
25
|
+
apiKey: config.apiKey,
|
|
26
|
+
token: config.token
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
handleRef.current = next;
|
|
30
|
+
setEntry({ workspaceId, handle: next });
|
|
31
|
+
return () => {
|
|
32
|
+
handleRef.current = null;
|
|
33
|
+
next.destroy();
|
|
34
|
+
};
|
|
35
|
+
}, [enabled, workspaceId, config.backendUrl, config.apiKey, config.token]);
|
|
36
|
+
const handle = enabled && entry !== null && entry.workspaceId === workspaceId ? entry.handle : null;
|
|
37
|
+
const subscribe = useMemo(() => {
|
|
38
|
+
return (onStoreChange) => {
|
|
39
|
+
if (!handle) return () => {
|
|
40
|
+
};
|
|
41
|
+
return handle.onChange(onStoreChange);
|
|
42
|
+
};
|
|
43
|
+
}, [handle]);
|
|
44
|
+
useSyncExternalStore(
|
|
45
|
+
subscribe,
|
|
46
|
+
() => {
|
|
47
|
+
if (!handle) return EMPTY_SNAPSHOT;
|
|
48
|
+
return `${handle.status}|${handle.accessType ?? ""}|${handle.version}`;
|
|
49
|
+
},
|
|
50
|
+
() => EMPTY_SNAPSHOT
|
|
51
|
+
);
|
|
52
|
+
const [now, setNow] = useState(() => Date.now());
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!enabled) return;
|
|
55
|
+
setNow(Date.now());
|
|
56
|
+
const timer = setInterval(() => {
|
|
57
|
+
setNow(Date.now());
|
|
58
|
+
}, tickMs);
|
|
59
|
+
return () => {
|
|
60
|
+
clearInterval(timer);
|
|
61
|
+
};
|
|
62
|
+
}, [enabled, tickMs]);
|
|
63
|
+
const rosterHumans = (handle == null ? void 0 : handle.humans) ?? NO_HUMANS;
|
|
64
|
+
const humans = useMemo(
|
|
65
|
+
() => rosterHumans.map((human) => ({
|
|
66
|
+
...human,
|
|
67
|
+
state: derivePresenceState({
|
|
68
|
+
lastIntentAt: human.lastIntentAt,
|
|
69
|
+
hidden: human.hidden,
|
|
70
|
+
manualAway: human.manualAway,
|
|
71
|
+
now
|
|
72
|
+
})
|
|
73
|
+
})),
|
|
74
|
+
[rosterHumans, now]
|
|
75
|
+
);
|
|
76
|
+
const refreshNow = useCallback(() => {
|
|
77
|
+
var _a;
|
|
78
|
+
return ((_a = handleRef.current) == null ? void 0 : _a.refreshNow()) ?? Promise.resolve();
|
|
79
|
+
}, []);
|
|
80
|
+
return {
|
|
81
|
+
status: handle ? handle.status : enabled ? "connecting" : "closed",
|
|
82
|
+
error: (handle == null ? void 0 : handle.error) ?? null,
|
|
83
|
+
accessType: (handle == null ? void 0 : handle.accessType) ?? null,
|
|
84
|
+
humans,
|
|
85
|
+
agents: (handle == null ? void 0 : handle.agents) ?? NO_AGENTS,
|
|
86
|
+
refreshNow
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function useWorkspacePresenceResolver(workspaceId) {
|
|
90
|
+
const config = useAthenaConfig();
|
|
91
|
+
const { backendUrl, apiKey, token } = config;
|
|
92
|
+
return useCallback(
|
|
93
|
+
(request, signal) => {
|
|
94
|
+
if (workspaceId === null) {
|
|
95
|
+
return Promise.reject(
|
|
96
|
+
new Error("[AthenaSDK] useWorkspacePresenceResolver: no workspace selected")
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return resolveWorkspacePresence({
|
|
100
|
+
auth: { backendUrl, apiKey, token },
|
|
101
|
+
workspaceId,
|
|
102
|
+
request,
|
|
103
|
+
signal
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
[workspaceId, backendUrl, apiKey, token]
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
PRESENCE_DEFAULT_TICK_MS,
|
|
111
|
+
useWorkspacePresence,
|
|
112
|
+
useWorkspacePresenceResolver
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=presence-react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence-react.js","sources":["../../src/collab/presence-react.ts"],"sourcesContent":["/**\n * React bindings for the workspace presence feed.\n *\n * `useWorkspacePresence(workspaceId)` opens (and owns) a\n * `connectWorkspacePresence` handle, resolving auth from the surrounding\n * `AthenaProvider` (per-viewer bearer when present, API key otherwise), and\n * re-renders on status/roster changes via `useSyncExternalStore`. Each person\n * carries a `state` on the intent ladder, recomputed against a clock that\n * ticks every `tickMs` so `active` decays to `idle` and `away` between\n * awareness events.\n *\n * `useWorkspacePresenceResolver(workspaceId)` returns the batch id→title\n * resolver bound to the same auth. It is deliberately thin: callers own\n * caching (TanStack Query is the expected home for it).\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n type WorkspacePresenceHandle,\n type WorkspacePresenceStatus,\n connectWorkspacePresence,\n} from './presence-client';\nimport {\n type WorkspacePresenceResolveRequest,\n type WorkspacePresenceResolveResponse,\n resolveWorkspacePresence,\n} from './presence-mint';\nimport {\n type PresenceAgent,\n type PresenceHuman,\n type PresenceState,\n derivePresenceState,\n} from './presence-slots';\n\n/** Default cadence for re-deriving each person's intent state. */\nexport const PRESENCE_DEFAULT_TICK_MS = 10_000;\n\nexport interface PresenceHumanWithState extends PresenceHuman {\n /** Where the person sits on the intent ladder as of the last tick. */\n state: PresenceState;\n}\n\nexport interface UseWorkspacePresenceOptions {\n /** `false` closes the feed without unmounting the caller. Default `true`. */\n enabled?: boolean;\n /** How often `state` is re-derived from `lastIntentAt`. Default 10 s. */\n tickMs?: number;\n}\n\nexport interface UseWorkspacePresenceResult {\n /** `'closed'` while disabled or without a workspace; `'connecting'` until the first mint resolves. */\n status: WorkspacePresenceStatus;\n error: Error | null;\n accessType: 'r' | 'rw' | null;\n humans: PresenceHumanWithState[];\n agents: PresenceAgent[];\n /** Force an early token refresh; resolves immediately when no feed is open. */\n refreshNow: () => Promise<void>;\n}\n\nconst EMPTY_SNAPSHOT = 'init||0';\nconst NO_HUMANS: PresenceHuman[] = [];\nconst NO_AGENTS: PresenceAgent[] = [];\n\nexport function useWorkspacePresence(\n workspaceId: string | null,\n options?: UseWorkspacePresenceOptions\n): UseWorkspacePresenceResult {\n const config = useAthenaConfig();\n const enabled = options?.enabled !== false && workspaceId !== null;\n const tickMs = options?.tickMs ?? PRESENCE_DEFAULT_TICK_MS;\n const [entry, setEntry] = useState<{\n workspaceId: string;\n handle: WorkspacePresenceHandle;\n } | null>(null);\n const handleRef = useRef<WorkspacePresenceHandle | null>(null);\n\n useEffect(() => {\n if (!enabled || workspaceId === null) {\n setEntry(null);\n return;\n }\n const next = connectWorkspacePresence({\n workspaceId,\n auth: {\n backendUrl: config.backendUrl,\n apiKey: config.apiKey,\n token: config.token,\n },\n });\n handleRef.current = next;\n setEntry({ workspaceId, handle: next });\n return () => {\n handleRef.current = null;\n next.destroy();\n };\n // Recreate when the target or credentials change; the handle re-mints on\n // its own schedule otherwise.\n }, [enabled, workspaceId, config.backendUrl, config.apiKey, config.token]);\n\n // React renders with the new arguments before the effect above runs, so a\n // handle opened for a previous workspace, or before the feed was disabled,\n // still sits in state for that one render. Expose it only while it is the\n // handle this render asked for; otherwise the closed / connecting empty\n // result wins, and a disabled feed never leaks a roster.\n const handle =\n enabled && entry !== null && entry.workspaceId === workspaceId ? entry.handle : null;\n\n const subscribe = useMemo(() => {\n return (onStoreChange: () => void) => {\n if (!handle) return () => {};\n return handle.onChange(onStoreChange);\n };\n }, [handle]);\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => {\n if (!handle) return EMPTY_SNAPSHOT;\n return `${handle.status}|${handle.accessType ?? ''}|${handle.version}`;\n },\n () => EMPTY_SNAPSHOT\n );\n void snapshot;\n\n // The ladder decays with wall-clock time, not with awareness events; a\n // person who stops typing must drift to idle even when nothing arrives.\n const [now, setNow] = useState(() => Date.now());\n useEffect(() => {\n if (!enabled) return;\n setNow(Date.now());\n const timer = setInterval(() => {\n setNow(Date.now());\n }, tickMs);\n return () => {\n clearInterval(timer);\n };\n }, [enabled, tickMs]);\n\n const rosterHumans = handle?.humans ?? NO_HUMANS;\n const humans = useMemo(\n () =>\n rosterHumans.map((human) => ({\n ...human,\n state: derivePresenceState({\n lastIntentAt: human.lastIntentAt,\n hidden: human.hidden,\n manualAway: human.manualAway,\n now,\n }),\n })),\n [rosterHumans, now]\n );\n\n const refreshNow = useCallback(() => handleRef.current?.refreshNow() ?? Promise.resolve(), []);\n\n return {\n status: handle ? handle.status : enabled ? 'connecting' : 'closed',\n error: handle?.error ?? null,\n accessType: handle?.accessType ?? null,\n humans,\n agents: handle?.agents ?? NO_AGENTS,\n refreshNow,\n };\n}\n\nexport type WorkspacePresenceResolver = (\n request: WorkspacePresenceResolveRequest,\n signal?: AbortSignal\n) => Promise<WorkspacePresenceResolveResponse>;\n\n/**\n * A stable resolver bound to the provider's auth and the given workspace.\n * Rejects when called without a workspace; no caching — callers own that.\n */\nexport function useWorkspacePresenceResolver(\n workspaceId: string | null\n): WorkspacePresenceResolver {\n const config = useAthenaConfig();\n const { backendUrl, apiKey, token } = config;\n return useCallback<WorkspacePresenceResolver>(\n (request, signal) => {\n if (workspaceId === null) {\n return Promise.reject(\n new Error('[AthenaSDK] useWorkspacePresenceResolver: no workspace selected')\n );\n }\n return resolveWorkspacePresence({\n auth: { backendUrl, apiKey, token },\n workspaceId,\n request,\n signal,\n });\n },\n [workspaceId, backendUrl, apiKey, token]\n );\n}\n"],"names":[],"mappings":"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"}
|