@agent-native/core 0.211.0 → 0.212.0-nightly-20261021010049
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 +1 -1
- package/dist/app-config/analytics.d.ts +1 -0
- package/dist/app-config/analytics.js +7 -0
- package/dist/app-config/schema.d.ts +1 -0
- package/dist/chat-threads/store.d.ts +28 -2
- package/dist/chat-threads/store.js +75 -13
- package/dist/cli/template-baseline.js +18 -5
- package/dist/cli/template-sync.js +100 -19
- package/dist/client/agent-chat/index.d.ts +2 -1
- package/dist/client/agent-chat/index.js +2 -1
- package/dist/client/chat/agentkit-protocol.js +21 -1
- package/dist/client/chat/run-outcome.d.ts +1 -0
- package/dist/client/chat/runtime.js +6 -0
- package/dist/client/sharing/index.d.ts +2 -1
- package/dist/client/sharing/index.js +2 -1
- package/dist/client/sharing/share-controller-helpers.d.ts +14 -1
- package/dist/client/sharing/share-controller-helpers.js +46 -1
- package/dist/client/sharing/useShareButtonController.d.ts +12 -0
- package/dist/client/sharing/useShareButtonController.js +76 -57
- package/dist/client/sharing/useShareDialogController.d.ts +3 -0
- package/dist/client/sharing/useShareDialogController.js +77 -49
- package/dist/client/sharing/useShareVisibilityTransition.d.ts +68 -0
- package/dist/client/sharing/useShareVisibilityTransition.js +224 -0
- package/dist/client/use-chat-thread-list.d.ts +40 -0
- package/dist/client/use-chat-thread-list.js +210 -0
- package/dist/client/use-chat-threads.d.ts +67 -0
- package/dist/client/use-chat-threads.js +141 -23
- package/dist/client/use-db-sync.d.ts +1 -0
- package/dist/client/use-db-sync.js +13 -1
- package/dist/collab/__fixtures__/live-copy-tasks.d.ts +1 -0
- package/dist/collab/__fixtures__/live-copy-tasks.js +92 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/bounded-task-bundle.d.ts +17 -0
- package/dist/collab/bounded-task-bundle.js +39 -0
- package/dist/collab/bounded-task-source.d.ts +14 -0
- package/dist/collab/bounded-task-source.js +8 -0
- package/dist/collab/bounded-tasks.d.ts +67 -0
- package/dist/collab/bounded-tasks.js +439 -0
- package/dist/collab/client.js +2 -1
- package/dist/collab/index.d.ts +5 -2
- package/dist/collab/index.js +5 -2
- package/dist/collab/live-copy-tasks.d.ts +80 -0
- package/dist/collab/live-copy-tasks.js +330 -0
- package/dist/collab/live-copy.d.ts +188 -0
- package/dist/collab/live-copy.js +415 -0
- package/dist/collab/routes.d.ts +5 -0
- package/dist/collab/routes.js +24 -8
- package/dist/collab/storage.d.ts +53 -1
- package/dist/collab/storage.js +93 -5
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/collab/ydoc-manager.d.ts +92 -3
- package/dist/collab/ydoc-manager.js +447 -38
- package/dist/db/client.d.ts +6 -0
- package/dist/db/client.js +25 -5
- package/dist/deploy/build.js +2 -0
- package/dist/extensions/actions.js +1 -0
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/mcp/agent-connected.js +5 -1
- package/dist/mcp/build-server.js +39 -10
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/org/index.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
- package/dist/provider-api/actions/provider-api.d.ts +10 -10
- package/dist/server/agent-chat-plugin.js +80 -17
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/analytics.d.ts +1 -0
- package/dist/server/analytics.js +18 -1
- package/dist/server/collab-plugin.d.ts +10 -1
- package/dist/server/collab-plugin.js +89 -20
- package/dist/server/index.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/optional-node-builtins.d.ts +1 -0
- package/dist/shared/optional-node-builtins.js +3 -0
- package/dist/sharing/access.d.ts +1 -0
- package/dist/sharing/access.js +113 -95
- package/dist/tracking/providers.d.ts +1 -0
- package/dist/tracking/providers.js +2 -1
- package/dist/triggers/actions/manage-automation.d.ts +2 -2
- package/dist/vite/bounded-task-plugin.d.ts +22 -0
- package/dist/vite/bounded-task-plugin.js +44 -0
- package/dist/vite/client.js +2 -0
- package/dist/vitest-config.js +3 -0
- package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +1 -1
- package/docs/content/deployment-environment-variables.mdx +1 -0
- package/docs/content/environment-variables.mdx +1 -0
- package/docs/content/sharing.mdx +36 -0
- package/docs/content/toolkit-sharing.mdx +98 -12
- package/package.json +5 -3
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
2
|
+
import { useOrg } from "../org/hooks.js";
|
|
3
|
+
import { extractShareErrorMessage } from "./share-controller-helpers.js";
|
|
4
|
+
/**
|
|
5
|
+
* Whether moving to `to` shows more of the resource to more people than `from`
|
|
6
|
+
* does and so needs an explicit confirmation. Public -> organization narrows
|
|
7
|
+
* access and Public stays confirmation-free, so only private -> organization
|
|
8
|
+
* asks.
|
|
9
|
+
*/
|
|
10
|
+
export function visibilityNeedsConfirmation(from, to) {
|
|
11
|
+
return from === "private" && to === "org";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Why a requested visibility change was never saved: `ignored` when the
|
|
15
|
+
* request was refused up front (closed surface, no manage rights, a save
|
|
16
|
+
* already running) and `dropped` when it was held for confirmation and then
|
|
17
|
+
* cancelled or invalidated.
|
|
18
|
+
*/
|
|
19
|
+
export class ShareVisibilityNotSavedError extends Error {
|
|
20
|
+
reason;
|
|
21
|
+
constructor(reason) {
|
|
22
|
+
super(reason === "dropped"
|
|
23
|
+
? "The visibility change was not confirmed."
|
|
24
|
+
: "The visibility change was not applied.");
|
|
25
|
+
this.name = "ShareVisibilityNotSavedError";
|
|
26
|
+
this.reason = reason;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function notSaved(reason) {
|
|
30
|
+
const rejected = Promise.reject(new ShareVisibilityNotSavedError(reason));
|
|
31
|
+
// Callers that fire and forget must not raise an unhandled rejection; one
|
|
32
|
+
// that awaits the returned promise still sees it.
|
|
33
|
+
rejected.catch(() => undefined);
|
|
34
|
+
return rejected;
|
|
35
|
+
}
|
|
36
|
+
// A surface can unmount while its server write keeps running. Keep the lock
|
|
37
|
+
// for that write's lifetime across all sharing surfaces in this browser session.
|
|
38
|
+
const inflightSaves = new Map();
|
|
39
|
+
const saveListeners = new Set();
|
|
40
|
+
function subscribeToSaves(listener) {
|
|
41
|
+
saveListeners.add(listener);
|
|
42
|
+
return () => {
|
|
43
|
+
saveListeners.delete(listener);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function notifySaves() {
|
|
47
|
+
for (const listener of saveListeners)
|
|
48
|
+
listener();
|
|
49
|
+
}
|
|
50
|
+
export function useShareVisibilityTransition(options) {
|
|
51
|
+
const { resourceType, resourceId, current, canManage, active } = options;
|
|
52
|
+
const resourceKey = `${resourceType}:${resourceId}`;
|
|
53
|
+
const [pending, setPendingState] = useState(null);
|
|
54
|
+
const [confirming, setSubmittingState] = useState(false);
|
|
55
|
+
const inflight = useSyncExternalStore(subscribeToSaves, () => inflightSaves.get(resourceKey), () => undefined);
|
|
56
|
+
const submitting = confirming || inflight !== undefined;
|
|
57
|
+
const [error, setError] = useState(null);
|
|
58
|
+
const pendingRef = useRef(null);
|
|
59
|
+
// Closing the surface drops its UI state, but cannot cancel a server write.
|
|
60
|
+
const confirmingRef = useRef(new Map());
|
|
61
|
+
// Bumped whenever the pending request is dropped, so a save that finishes
|
|
62
|
+
// afterwards cannot touch the next request's state.
|
|
63
|
+
const generationRef = useRef(0);
|
|
64
|
+
const latestRef = useRef({ ...options, resourceKey });
|
|
65
|
+
latestRef.current = { ...options, resourceKey };
|
|
66
|
+
const org = useOrg({
|
|
67
|
+
enabled: active && canManage && pending !== null && options.orgId !== null,
|
|
68
|
+
});
|
|
69
|
+
const organizationName = readOrganizationName(org.data, options.orgId);
|
|
70
|
+
const reset = useCallback(() => {
|
|
71
|
+
generationRef.current += 1;
|
|
72
|
+
// A save that is already running settles the request itself.
|
|
73
|
+
const state = pendingRef.current;
|
|
74
|
+
if (state && confirmingRef.current.get(state.resourceKey) !== state) {
|
|
75
|
+
state.settle.reject(new ShareVisibilityNotSavedError("dropped"));
|
|
76
|
+
}
|
|
77
|
+
pendingRef.current = null;
|
|
78
|
+
setPendingState(null);
|
|
79
|
+
setSubmittingState(false);
|
|
80
|
+
setError(null);
|
|
81
|
+
}, []);
|
|
82
|
+
useEffect(() => reset, [reset]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
reset();
|
|
85
|
+
}, [reset, resourceKey]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!active || !canManage)
|
|
88
|
+
reset();
|
|
89
|
+
}, [active, canManage, reset]);
|
|
90
|
+
// The saved value moved under the dialog (another tab, a refetch), so the
|
|
91
|
+
// change it describes is no longer the one it would make. While a save is
|
|
92
|
+
// running `current` is the optimistic value, which is expected to differ.
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (pending && !submitting && pending.from !== current)
|
|
95
|
+
reset();
|
|
96
|
+
}, [current, pending, reset, submitting]);
|
|
97
|
+
const track = useCallback((key, target, promise) => {
|
|
98
|
+
const entry = { target, promise };
|
|
99
|
+
inflightSaves.set(key, entry);
|
|
100
|
+
notifySaves();
|
|
101
|
+
const clear = () => {
|
|
102
|
+
if (inflightSaves.get(key) === entry) {
|
|
103
|
+
inflightSaves.delete(key);
|
|
104
|
+
notifySaves();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
promise.then(clear, clear);
|
|
108
|
+
}, []);
|
|
109
|
+
const request = useCallback((next) => {
|
|
110
|
+
const latest = latestRef.current;
|
|
111
|
+
const inflight = inflightSaves.get(latest.resourceKey);
|
|
112
|
+
if (inflight?.target === next)
|
|
113
|
+
return inflight.promise;
|
|
114
|
+
if (inflight)
|
|
115
|
+
return notSaved("ignored");
|
|
116
|
+
if (next === latest.current) {
|
|
117
|
+
return Promise.resolve();
|
|
118
|
+
}
|
|
119
|
+
if (!latest.canManage || !latest.active) {
|
|
120
|
+
return notSaved("ignored");
|
|
121
|
+
}
|
|
122
|
+
if (!visibilityNeedsConfirmation(latest.current, next)) {
|
|
123
|
+
reset();
|
|
124
|
+
const generation = generationRef.current;
|
|
125
|
+
const saved = latest.apply(next);
|
|
126
|
+
track(latest.resourceKey, next, saved);
|
|
127
|
+
saved.catch((failure) => {
|
|
128
|
+
// Only the newest intent on this resource reports to the shared
|
|
129
|
+
// error state. An older failure, or one from a resource the surface
|
|
130
|
+
// has left, still rejects for its caller but must not show here.
|
|
131
|
+
// The generation moves in an effect, so the resource is also checked
|
|
132
|
+
// against the live value.
|
|
133
|
+
if (generation === generationRef.current &&
|
|
134
|
+
latestRef.current.resourceKey === latest.resourceKey) {
|
|
135
|
+
latest.onDirectError(failure);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return saved;
|
|
139
|
+
}
|
|
140
|
+
// A held request replaces any earlier one, which is dropped.
|
|
141
|
+
reset();
|
|
142
|
+
const held = new Promise((resolve, reject) => {
|
|
143
|
+
const state = {
|
|
144
|
+
from: latest.current,
|
|
145
|
+
to: next,
|
|
146
|
+
resourceKey: latest.resourceKey,
|
|
147
|
+
settle: { resolve, reject },
|
|
148
|
+
};
|
|
149
|
+
pendingRef.current = state;
|
|
150
|
+
setPendingState(state);
|
|
151
|
+
setError(null);
|
|
152
|
+
});
|
|
153
|
+
held.catch(() => undefined);
|
|
154
|
+
return held;
|
|
155
|
+
}, [reset, track]);
|
|
156
|
+
const confirm = useCallback(async () => {
|
|
157
|
+
const state = pendingRef.current;
|
|
158
|
+
if (!state ||
|
|
159
|
+
confirmingRef.current.has(state.resourceKey) ||
|
|
160
|
+
inflightSaves.has(state.resourceKey))
|
|
161
|
+
return;
|
|
162
|
+
const latest = latestRef.current;
|
|
163
|
+
// The effects that drop a stale request run after render; a click can
|
|
164
|
+
// land before them, so the request is checked against the live values.
|
|
165
|
+
if (state.resourceKey !== latest.resourceKey ||
|
|
166
|
+
!latest.canManage ||
|
|
167
|
+
!latest.active ||
|
|
168
|
+
state.from !== latest.current) {
|
|
169
|
+
reset();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const generation = generationRef.current;
|
|
173
|
+
confirmingRef.current.set(state.resourceKey, state);
|
|
174
|
+
setSubmittingState(true);
|
|
175
|
+
setError(null);
|
|
176
|
+
try {
|
|
177
|
+
const applying = latest.apply(state.to);
|
|
178
|
+
track(state.resourceKey, state.to, applying);
|
|
179
|
+
await applying;
|
|
180
|
+
state.settle.resolve();
|
|
181
|
+
if (generation !== generationRef.current)
|
|
182
|
+
return;
|
|
183
|
+
pendingRef.current = null;
|
|
184
|
+
setPendingState(null);
|
|
185
|
+
}
|
|
186
|
+
catch (failure) {
|
|
187
|
+
if (generation !== generationRef.current) {
|
|
188
|
+
state.settle.reject(new ShareVisibilityNotSavedError("dropped"));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
setError(extractShareErrorMessage(failure));
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
confirmingRef.current.delete(state.resourceKey);
|
|
195
|
+
if (generation === generationRef.current) {
|
|
196
|
+
setSubmittingState(false);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}, [reset, track]);
|
|
200
|
+
const cancel = useCallback(() => {
|
|
201
|
+
const state = pendingRef.current;
|
|
202
|
+
if (state && confirmingRef.current.has(state.resourceKey))
|
|
203
|
+
return;
|
|
204
|
+
reset();
|
|
205
|
+
}, [reset]);
|
|
206
|
+
return {
|
|
207
|
+
pending: pending ? { from: pending.from, to: pending.to } : null,
|
|
208
|
+
organizationName,
|
|
209
|
+
submitting,
|
|
210
|
+
error,
|
|
211
|
+
request,
|
|
212
|
+
confirm,
|
|
213
|
+
cancel,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function readOrganizationName(org, resourceOrgId) {
|
|
217
|
+
if (!org || !resourceOrgId)
|
|
218
|
+
return null;
|
|
219
|
+
const name = org.orgId === resourceOrgId
|
|
220
|
+
? org.orgName
|
|
221
|
+
: org.orgs?.find((summary) => summary.orgId === resourceOrgId)?.orgName;
|
|
222
|
+
const trimmed = typeof name === "string" ? name.trim() : "";
|
|
223
|
+
return trimmed || null;
|
|
224
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ChatThreadOwnership, type ChatThreadListRow, type ChatThreadScope } from "./use-chat-threads.js";
|
|
2
|
+
export interface UseChatThreadListOptions {
|
|
3
|
+
/** Which list this is. Narrowed on the server before paging. */
|
|
4
|
+
ownership: ChatThreadOwnership;
|
|
5
|
+
/** Rows per page, an integer from 1 to 200. Defaults to 50. Anything else throws `ChatThreadListConfigError`. */
|
|
6
|
+
pageSize?: number;
|
|
7
|
+
scope?: ChatThreadScope | null;
|
|
8
|
+
includeExternal?: boolean;
|
|
9
|
+
/** Search text. Empty lists the whole set. */
|
|
10
|
+
query?: string;
|
|
11
|
+
/** When false nothing is fetched and the list is empty. Defaults to true. */
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/** Listen for thread update events and refresh loaded rows. Defaults to true. Does not affect initial or manual refreshes. */
|
|
14
|
+
refreshOnThreadUpdates?: boolean;
|
|
15
|
+
apiUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface UseChatThreadListResult {
|
|
18
|
+
threads: ChatThreadListRow[];
|
|
19
|
+
/** True until the first page of the current list settles. */
|
|
20
|
+
loading: boolean;
|
|
21
|
+
loadingMore: boolean;
|
|
22
|
+
/** The last request failed; `threads` keeps whatever was already loaded. */
|
|
23
|
+
error: Error | null;
|
|
24
|
+
/** The server has rows past the ones loaded. */
|
|
25
|
+
hasMore: boolean;
|
|
26
|
+
loadMore: () => Promise<void>;
|
|
27
|
+
refresh: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/** `pageSize` is not an integer between 1 and the server's page cap. */
|
|
30
|
+
export declare class ChatThreadListConfigError extends Error {
|
|
31
|
+
constructor(pageSize: unknown);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One paginated chat history list (`mine`, `shared`, or `all`). Each list pages
|
|
35
|
+
* independently, so a long Shared list never waits on My chats. It refetches
|
|
36
|
+
* by default when a thread mutation in this tab calls `notifyChatThreadsUpdated`,
|
|
37
|
+
* keeping every row already loaded, however many server pages that spans,
|
|
38
|
+
* rather than collapsing to the first page.
|
|
39
|
+
*/
|
|
40
|
+
export declare function useChatThreadList(options: UseChatThreadListOptions): UseChatThreadListResult;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useState } from "react";
|
|
2
|
+
import { agentNativePath } from "./api-path.js";
|
|
3
|
+
import { CHAT_THREADS_MAX_PAGE_SIZE, CHAT_THREADS_UPDATED_EVENT, ChatThreadListError, fetchChatThreadListPage, } from "./use-chat-threads.js";
|
|
4
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
5
|
+
/** `pageSize` is not an integer between 1 and the server's page cap. */
|
|
6
|
+
export class ChatThreadListConfigError extends Error {
|
|
7
|
+
constructor(pageSize) {
|
|
8
|
+
super(`useChatThreadList pageSize must be an integer from 1 to ${CHAT_THREADS_MAX_PAGE_SIZE}, got ${String(pageSize)}.`);
|
|
9
|
+
this.name = "ChatThreadListConfigError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function resolvePageSize(pageSize) {
|
|
13
|
+
if (pageSize === undefined)
|
|
14
|
+
return DEFAULT_PAGE_SIZE;
|
|
15
|
+
if (!Number.isInteger(pageSize) ||
|
|
16
|
+
pageSize < 1 ||
|
|
17
|
+
pageSize > CHAT_THREADS_MAX_PAGE_SIZE) {
|
|
18
|
+
throw new ChatThreadListConfigError(pageSize);
|
|
19
|
+
}
|
|
20
|
+
return pageSize;
|
|
21
|
+
}
|
|
22
|
+
function asError(error) {
|
|
23
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
24
|
+
}
|
|
25
|
+
function createListState(identity, enabled) {
|
|
26
|
+
return {
|
|
27
|
+
identity,
|
|
28
|
+
session: {
|
|
29
|
+
active: false,
|
|
30
|
+
requestId: 0,
|
|
31
|
+
inFlight: null,
|
|
32
|
+
// Deduped rows can be fewer than the server rows consumed.
|
|
33
|
+
nextOffset: 0,
|
|
34
|
+
hasMore: false,
|
|
35
|
+
},
|
|
36
|
+
threads: [],
|
|
37
|
+
loading: enabled,
|
|
38
|
+
loadingMore: false,
|
|
39
|
+
error: null,
|
|
40
|
+
hasMore: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One paginated chat history list (`mine`, `shared`, or `all`). Each list pages
|
|
45
|
+
* independently, so a long Shared list never waits on My chats. It refetches
|
|
46
|
+
* by default when a thread mutation in this tab calls `notifyChatThreadsUpdated`,
|
|
47
|
+
* keeping every row already loaded, however many server pages that spans,
|
|
48
|
+
* rather than collapsing to the first page.
|
|
49
|
+
*/
|
|
50
|
+
export function useChatThreadList(options) {
|
|
51
|
+
const { ownership, includeExternal = false, enabled = true, refreshOnThreadUpdates = true, } = options;
|
|
52
|
+
const apiUrl = options.apiUrl ?? agentNativePath("/_agent-native/agent-chat");
|
|
53
|
+
const pageSize = resolvePageSize(options.pageSize);
|
|
54
|
+
const query = options.query?.trim() ?? "";
|
|
55
|
+
const scopeType = options.scope?.type ?? null;
|
|
56
|
+
const scopeId = options.scope?.id ?? null;
|
|
57
|
+
const identity = JSON.stringify([
|
|
58
|
+
apiUrl,
|
|
59
|
+
ownership,
|
|
60
|
+
query,
|
|
61
|
+
scopeType,
|
|
62
|
+
scopeId,
|
|
63
|
+
pageSize,
|
|
64
|
+
includeExternal,
|
|
65
|
+
enabled,
|
|
66
|
+
]);
|
|
67
|
+
const [stored, setState] = useState(() => createListState(identity, enabled));
|
|
68
|
+
// Reset during render: consumers must never observe the preceding list,
|
|
69
|
+
// including during their layout effects before passive cleanup runs.
|
|
70
|
+
const state = stored.identity === identity ? stored : createListState(identity, enabled);
|
|
71
|
+
if (state !== stored)
|
|
72
|
+
setState(state);
|
|
73
|
+
const { session } = state;
|
|
74
|
+
const update = useCallback((patch) => {
|
|
75
|
+
setState((previous) => previous.session === session ? { ...previous, ...patch } : previous);
|
|
76
|
+
}, [session]);
|
|
77
|
+
useLayoutEffect(() => {
|
|
78
|
+
session.active = true;
|
|
79
|
+
return () => {
|
|
80
|
+
session.active = false;
|
|
81
|
+
session.requestId += 1;
|
|
82
|
+
session.inFlight = null;
|
|
83
|
+
};
|
|
84
|
+
}, [session]);
|
|
85
|
+
const request = useCallback((page) => fetchChatThreadListPage({
|
|
86
|
+
apiUrl,
|
|
87
|
+
offset: page.offset,
|
|
88
|
+
limit: page.limit,
|
|
89
|
+
query,
|
|
90
|
+
ownership,
|
|
91
|
+
includeExternal,
|
|
92
|
+
scope: scopeType && scopeId ? { type: scopeType, id: scopeId } : null,
|
|
93
|
+
}).then((result) => {
|
|
94
|
+
if (result.hasMore === undefined) {
|
|
95
|
+
throw new ChatThreadListError("Chat history response did not say whether more threads exist.");
|
|
96
|
+
}
|
|
97
|
+
// A page that claims more rows exist but returned none would be
|
|
98
|
+
// requested again from the same offset forever.
|
|
99
|
+
if (result.hasMore && result.threads.length === 0) {
|
|
100
|
+
throw new ChatThreadListError("Chat history response reported more threads but returned none.");
|
|
101
|
+
}
|
|
102
|
+
return { threads: result.threads, hasMore: result.hasMore };
|
|
103
|
+
}), [apiUrl, includeExternal, ownership, query, scopeId, scopeType]);
|
|
104
|
+
// Reload the first `window` server rows, one capped page at a time, and swap
|
|
105
|
+
// them in only once every page has arrived. A failure part-way leaves the
|
|
106
|
+
// rows and offset already on screen untouched.
|
|
107
|
+
const replace = useCallback(async (window) => {
|
|
108
|
+
if (!session.active)
|
|
109
|
+
return;
|
|
110
|
+
const requestId = ++session.requestId;
|
|
111
|
+
const isCurrent = () => session.active && requestId === session.requestId;
|
|
112
|
+
session.inFlight = "replace";
|
|
113
|
+
update({ loadingMore: false });
|
|
114
|
+
try {
|
|
115
|
+
const rows = new Map();
|
|
116
|
+
let consumed = 0;
|
|
117
|
+
let more = true;
|
|
118
|
+
while (more && consumed < window) {
|
|
119
|
+
const page = await request({
|
|
120
|
+
offset: consumed,
|
|
121
|
+
limit: Math.min(CHAT_THREADS_MAX_PAGE_SIZE, window - consumed),
|
|
122
|
+
});
|
|
123
|
+
if (!isCurrent())
|
|
124
|
+
return;
|
|
125
|
+
for (const thread of page.threads) {
|
|
126
|
+
if (!rows.has(thread.id))
|
|
127
|
+
rows.set(thread.id, thread);
|
|
128
|
+
}
|
|
129
|
+
consumed += page.threads.length;
|
|
130
|
+
more = page.hasMore;
|
|
131
|
+
}
|
|
132
|
+
session.nextOffset = consumed;
|
|
133
|
+
session.hasMore = more;
|
|
134
|
+
update({ threads: [...rows.values()], hasMore: more, error: null });
|
|
135
|
+
}
|
|
136
|
+
catch (failure) {
|
|
137
|
+
if (!isCurrent())
|
|
138
|
+
return;
|
|
139
|
+
update({ error: asError(failure) });
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
if (isCurrent()) {
|
|
143
|
+
session.inFlight = null;
|
|
144
|
+
update({ loading: false });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}, [request, session, update]);
|
|
148
|
+
const refresh = useCallback(async () => {
|
|
149
|
+
if (!enabled)
|
|
150
|
+
return;
|
|
151
|
+
// Reload every row already on screen so a mutation does not collapse a
|
|
152
|
+
// list the person has paged through.
|
|
153
|
+
await replace(Math.max(pageSize, session.nextOffset));
|
|
154
|
+
}, [enabled, pageSize, replace, session]);
|
|
155
|
+
const loadMore = useCallback(async () => {
|
|
156
|
+
if (!session.active || session.inFlight || !session.hasMore)
|
|
157
|
+
return;
|
|
158
|
+
const requestId = session.requestId;
|
|
159
|
+
const isCurrent = () => session.active && requestId === session.requestId;
|
|
160
|
+
session.inFlight = "append";
|
|
161
|
+
update({ loadingMore: true });
|
|
162
|
+
try {
|
|
163
|
+
const page = await request({
|
|
164
|
+
offset: session.nextOffset,
|
|
165
|
+
limit: pageSize,
|
|
166
|
+
});
|
|
167
|
+
if (!isCurrent())
|
|
168
|
+
return;
|
|
169
|
+
session.nextOffset += page.threads.length;
|
|
170
|
+
session.hasMore = page.hasMore;
|
|
171
|
+
setState((previous) => {
|
|
172
|
+
if (previous.session !== session)
|
|
173
|
+
return previous;
|
|
174
|
+
const known = new Set(previous.threads.map((thread) => thread.id));
|
|
175
|
+
return {
|
|
176
|
+
...previous,
|
|
177
|
+
threads: [
|
|
178
|
+
...previous.threads,
|
|
179
|
+
...page.threads.filter((thread) => !known.has(thread.id)),
|
|
180
|
+
],
|
|
181
|
+
hasMore: page.hasMore,
|
|
182
|
+
error: null,
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch (failure) {
|
|
187
|
+
if (!isCurrent())
|
|
188
|
+
return;
|
|
189
|
+
update({ error: asError(failure) });
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
if (isCurrent()) {
|
|
193
|
+
session.inFlight = null;
|
|
194
|
+
update({ loadingMore: false });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}, [pageSize, request, session, update]);
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
void refresh();
|
|
200
|
+
}, [refresh]);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (!enabled || !refreshOnThreadUpdates || typeof window === "undefined")
|
|
203
|
+
return;
|
|
204
|
+
const onUpdated = () => void refresh();
|
|
205
|
+
window.addEventListener(CHAT_THREADS_UPDATED_EVENT, onUpdated);
|
|
206
|
+
return () => window.removeEventListener(CHAT_THREADS_UPDATED_EVENT, onUpdated);
|
|
207
|
+
}, [enabled, refresh, refreshOnThreadUpdates]);
|
|
208
|
+
const { threads, loading, loadingMore, error, hasMore } = state;
|
|
209
|
+
return { threads, loading, loadingMore, error, hasMore, loadMore, refresh };
|
|
210
|
+
}
|
|
@@ -6,6 +6,14 @@ export interface ChatThreadScope {
|
|
|
6
6
|
context?: string;
|
|
7
7
|
contextVersion?: string;
|
|
8
8
|
}
|
|
9
|
+
/** Which of the caller's lists a thread belongs to; `mine` is ownership alone. */
|
|
10
|
+
export type ChatThreadOwnership = "all" | "mine" | "shared";
|
|
11
|
+
/**
|
|
12
|
+
* Why the caller can see a thread. A direct per-user grant outranks
|
|
13
|
+
* organization access when both apply.
|
|
14
|
+
*/
|
|
15
|
+
export type ChatThreadAccessBasis = "owner" | "direct" | "organization";
|
|
16
|
+
export type ChatThreadVisibility = "private" | "org" | "public";
|
|
9
17
|
export interface ChatThreadSummary {
|
|
10
18
|
id: string;
|
|
11
19
|
title: string;
|
|
@@ -17,6 +25,14 @@ export interface ChatThreadSummary {
|
|
|
17
25
|
source?: ChatThreadSource | null;
|
|
18
26
|
pinnedAt?: number | null;
|
|
19
27
|
archivedAt?: number | null;
|
|
28
|
+
/**
|
|
29
|
+
* Server-supplied on listed threads. Absent on optimistic local drafts,
|
|
30
|
+
* which the caller owns by definition.
|
|
31
|
+
*/
|
|
32
|
+
ownerEmail?: string;
|
|
33
|
+
accessBasis?: ChatThreadAccessBasis;
|
|
34
|
+
visibility?: ChatThreadVisibility;
|
|
35
|
+
orgId?: string | null;
|
|
20
36
|
}
|
|
21
37
|
export interface ChatThreadSource {
|
|
22
38
|
platform?: string;
|
|
@@ -61,8 +77,59 @@ export interface UseChatThreadsOptions {
|
|
|
61
77
|
routeThreadId?: string | null;
|
|
62
78
|
includeExternal?: boolean;
|
|
63
79
|
isolateHistoryByScope?: boolean;
|
|
80
|
+
/** Narrows the history list and search. Defaults to `all`. */
|
|
81
|
+
ownership?: ChatThreadOwnership;
|
|
64
82
|
}
|
|
83
|
+
export declare const CHAT_THREADS_UPDATED_EVENT = "agent-chat:threads-updated";
|
|
84
|
+
/** The server caps one page here; larger requests are silently shortened. */
|
|
85
|
+
export declare const CHAT_THREADS_MAX_PAGE_SIZE = 200;
|
|
86
|
+
/** The thread list endpoint answered with a failure status or a malformed body. */
|
|
87
|
+
export declare class ChatThreadListError extends Error {
|
|
88
|
+
readonly status: number | null;
|
|
89
|
+
constructor(message: string, status?: number | null);
|
|
90
|
+
}
|
|
91
|
+
export interface ChatThreadListPageRequest {
|
|
92
|
+
apiUrl?: string;
|
|
93
|
+
/** Non-negative safe integer. Undefined uses the server default. */
|
|
94
|
+
offset?: number;
|
|
95
|
+
/** Positive safe integer. Undefined uses the server default. The server caps pages at 200 rows. */
|
|
96
|
+
limit?: number;
|
|
97
|
+
/** Switches the endpoint to search; ownership still narrows before paging. */
|
|
98
|
+
query?: string;
|
|
99
|
+
ownership?: ChatThreadOwnership;
|
|
100
|
+
includeExternal?: boolean;
|
|
101
|
+
scope?: ChatThreadScope | null;
|
|
102
|
+
}
|
|
103
|
+
/** A listed thread as the server returns it: ownership metadata always present. */
|
|
104
|
+
export type ChatThreadListRow = ChatThreadSummary & {
|
|
105
|
+
ownerEmail: string;
|
|
106
|
+
accessBasis: ChatThreadAccessBasis;
|
|
107
|
+
visibility: ChatThreadVisibility;
|
|
108
|
+
};
|
|
109
|
+
export interface ChatThreadListPage {
|
|
110
|
+
threads: ChatThreadListRow[];
|
|
111
|
+
/** `undefined` when the server did not report it. */
|
|
112
|
+
hasMore: boolean | undefined;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* One page of the thread list or search, for the ownership-aware lists. Throws
|
|
116
|
+
* `ChatThreadListError` for a non-2xx response, a body that is not a thread
|
|
117
|
+
* list, or a row missing `ownerEmail` / `accessBasis` / `visibility` or sitting
|
|
118
|
+
* on the wrong side of `ownership`, so callers cannot mistake a failed page
|
|
119
|
+
* for an empty one. Invalid explicit pagination arguments reject with
|
|
120
|
+
* `TypeError` before a request is sent.
|
|
121
|
+
*
|
|
122
|
+
* The server caps each page at 200 rows. Advance offsets by the actual number
|
|
123
|
+
* of returned rows and use `hasMore`, rather than the requested limit.
|
|
124
|
+
*/
|
|
125
|
+
export declare function fetchChatThreadListPage(request?: ChatThreadListPageRequest): Promise<ChatThreadListPage>;
|
|
65
126
|
export declare function appendChatThreadScopeParams(params: URLSearchParams, scope?: ChatThreadScope | null): void;
|
|
127
|
+
/**
|
|
128
|
+
* Tell every mounted chat history list the thread set or its sharing changed,
|
|
129
|
+
* so they refetch. Anything that mutates a thread outside `useChatThreads`
|
|
130
|
+
* (a share grant, a visibility change) must call this.
|
|
131
|
+
*/
|
|
132
|
+
export declare function notifyChatThreadsUpdated(): void;
|
|
66
133
|
export declare function useChatThreads(apiUrl?: string, storageKey?: string, scope?: ChatThreadScope | null, options?: UseChatThreadsOptions): {
|
|
67
134
|
threads: ChatThreadSummary[];
|
|
68
135
|
activeThreadId: string | null;
|