@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.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/dist/app-config/analytics.d.ts +1 -0
  3. package/dist/app-config/analytics.js +7 -0
  4. package/dist/app-config/schema.d.ts +1 -0
  5. package/dist/chat-threads/store.d.ts +28 -2
  6. package/dist/chat-threads/store.js +75 -13
  7. package/dist/cli/template-baseline.js +18 -5
  8. package/dist/cli/template-sync.js +100 -19
  9. package/dist/client/agent-chat/index.d.ts +2 -1
  10. package/dist/client/agent-chat/index.js +2 -1
  11. package/dist/client/chat/agentkit-protocol.js +21 -1
  12. package/dist/client/chat/run-outcome.d.ts +1 -0
  13. package/dist/client/chat/runtime.js +6 -0
  14. package/dist/client/sharing/index.d.ts +2 -1
  15. package/dist/client/sharing/index.js +2 -1
  16. package/dist/client/sharing/share-controller-helpers.d.ts +14 -1
  17. package/dist/client/sharing/share-controller-helpers.js +46 -1
  18. package/dist/client/sharing/useShareButtonController.d.ts +12 -0
  19. package/dist/client/sharing/useShareButtonController.js +76 -57
  20. package/dist/client/sharing/useShareDialogController.d.ts +3 -0
  21. package/dist/client/sharing/useShareDialogController.js +77 -49
  22. package/dist/client/sharing/useShareVisibilityTransition.d.ts +68 -0
  23. package/dist/client/sharing/useShareVisibilityTransition.js +224 -0
  24. package/dist/client/use-chat-thread-list.d.ts +40 -0
  25. package/dist/client/use-chat-thread-list.js +210 -0
  26. package/dist/client/use-chat-threads.d.ts +67 -0
  27. package/dist/client/use-chat-threads.js +141 -23
  28. package/dist/client/use-db-sync.d.ts +1 -0
  29. package/dist/client/use-db-sync.js +13 -1
  30. package/dist/collab/__fixtures__/live-copy-tasks.d.ts +1 -0
  31. package/dist/collab/__fixtures__/live-copy-tasks.js +92 -0
  32. package/dist/collab/awareness.d.ts +2 -2
  33. package/dist/collab/bounded-task-bundle.d.ts +17 -0
  34. package/dist/collab/bounded-task-bundle.js +39 -0
  35. package/dist/collab/bounded-task-source.d.ts +14 -0
  36. package/dist/collab/bounded-task-source.js +8 -0
  37. package/dist/collab/bounded-tasks.d.ts +67 -0
  38. package/dist/collab/bounded-tasks.js +439 -0
  39. package/dist/collab/client.js +2 -1
  40. package/dist/collab/index.d.ts +5 -2
  41. package/dist/collab/index.js +5 -2
  42. package/dist/collab/live-copy-tasks.d.ts +80 -0
  43. package/dist/collab/live-copy-tasks.js +330 -0
  44. package/dist/collab/live-copy.d.ts +188 -0
  45. package/dist/collab/live-copy.js +415 -0
  46. package/dist/collab/routes.d.ts +5 -0
  47. package/dist/collab/routes.js +24 -8
  48. package/dist/collab/storage.d.ts +53 -1
  49. package/dist/collab/storage.js +93 -5
  50. package/dist/collab/struct-routes.d.ts +1 -1
  51. package/dist/collab/ydoc-manager.d.ts +92 -3
  52. package/dist/collab/ydoc-manager.js +447 -38
  53. package/dist/db/client.d.ts +6 -0
  54. package/dist/db/client.js +25 -5
  55. package/dist/deploy/build.js +2 -0
  56. package/dist/extensions/actions.js +1 -0
  57. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  58. package/dist/mcp/agent-connected.js +5 -1
  59. package/dist/mcp/build-server.js +39 -10
  60. package/dist/notifications/routes.d.ts +1 -1
  61. package/dist/observability/routes.d.ts +1 -1
  62. package/dist/org/index.d.ts +2 -2
  63. package/dist/progress/routes.d.ts +1 -1
  64. package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
  65. package/dist/provider-api/actions/provider-api.d.ts +10 -10
  66. package/dist/server/agent-chat-plugin.js +80 -17
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  69. package/dist/server/analytics.d.ts +1 -0
  70. package/dist/server/analytics.js +18 -1
  71. package/dist/server/collab-plugin.d.ts +10 -1
  72. package/dist/server/collab-plugin.js +89 -20
  73. package/dist/server/index.d.ts +1 -1
  74. package/dist/server/realtime-token.d.ts +1 -1
  75. package/dist/shared/optional-node-builtins.d.ts +1 -0
  76. package/dist/shared/optional-node-builtins.js +3 -0
  77. package/dist/sharing/access.d.ts +1 -0
  78. package/dist/sharing/access.js +113 -95
  79. package/dist/tracking/providers.d.ts +1 -0
  80. package/dist/tracking/providers.js +2 -1
  81. package/dist/triggers/actions/manage-automation.d.ts +2 -2
  82. package/dist/vite/bounded-task-plugin.d.ts +22 -0
  83. package/dist/vite/bounded-task-plugin.js +44 -0
  84. package/dist/vite/client.js +2 -0
  85. package/dist/vitest-config.js +3 -0
  86. package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +1 -1
  87. package/docs/content/deployment-environment-variables.mdx +1 -0
  88. package/docs/content/environment-variables.mdx +1 -0
  89. package/docs/content/sharing.mdx +36 -0
  90. package/docs/content/toolkit-sharing.mdx +98 -12
  91. 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;