@agent-native/core 0.221.0 → 0.222.0-nightly-20261028092719
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/dist/client/agent-chat.js +6 -1
- package/dist/client/api-path.d.ts +2 -0
- package/dist/client/api-path.js +76 -55
- package/dist/client/application-state.js +12 -6
- package/dist/client/browser-tab-id.d.ts +2 -0
- package/dist/client/browser-tab-id.js +113 -13
- package/dist/client/collab/index.d.ts +1 -1
- package/dist/client/embed-auth.js +145 -26
- package/dist/client/route-state.js +15 -4
- package/dist/client/sse-event-processor.js +3 -0
- package/dist/client/use-change-version.js +6 -1
- package/dist/client/use-db-sync.js +70 -11
- package/dist/collab/client.d.ts +34 -0
- package/dist/collab/client.js +506 -53
- package/dist/deploy/build.js +10 -0
- package/dist/mcp/embed-app.js +165 -16
- package/dist/provider-api/actions/custom-provider-registration.d.ts +3 -3
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/server/app-origin-config.d.ts +2 -0
- package/dist/server/app-origin-config.js +10 -0
- package/dist/server/auth.d.ts +28 -2
- package/dist/server/auth.js +79 -0
- package/dist/server/credential-provider.d.ts +19 -1
- package/dist/server/credential-provider.js +71 -7
- package/dist/shared/mcp-app-widget-embed.d.ts +23 -1
- package/dist/shared/mcp-app-widget-embed.js +178 -5
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +41 -18
- package/package.json +4 -4
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +41 -18
|
@@ -2,20 +2,20 @@ import { EMBED_MODE_QUERY_PARAM, EMBED_START_PATH, EMBED_TARGET_HEADER, EMBED_TA
|
|
|
2
2
|
import { isMcpDirectoryWidgetReadCapabilityScope, isMcpDirectoryWidgetWriteCapabilityScope, } from "../shared/embed-auth.js";
|
|
3
3
|
import { FRAMEWORK_INTERNAL_ROUTE_PREFIX } from "../shared/framework-route-prefix.js";
|
|
4
4
|
import { MCP_APP_HOST_FILL_ATTRIBUTE } from "../shared/mcp-app-display.js";
|
|
5
|
-
import {
|
|
5
|
+
import { embedTokenStorageKey, mcpChatBridgeStorageKey, } from "../shared/mcp-app-widget-embed.js";
|
|
6
6
|
import { SIGN_IN_ENTRY_PATH, SIGN_IN_LEGACY_ENTRY_PATH, } from "../shared/sign-in-journey.js";
|
|
7
|
-
import { frameworkRoutePrefix } from "./api-path.js";
|
|
7
|
+
import { frameworkRoutePrefix, isWorkspaceRuntime, resolveAppBasePath, resolveExternalEmbedTargetAppBasePath, WorkspaceAppMountResolutionError, } from "./api-path.js";
|
|
8
8
|
let installed = false;
|
|
9
|
-
|
|
9
|
+
const memoryTokens = new Map();
|
|
10
10
|
let renewedFromUrlToken = null;
|
|
11
11
|
let pendingWidgetSessionRenewal = null;
|
|
12
12
|
let widgetSessionRenewalRequestId = 0;
|
|
13
13
|
let mcpChatBridgeActive = false;
|
|
14
14
|
let mcpChatBridgeScope = null;
|
|
15
|
+
let mcpChatBridgeStorageKeyAtState = null;
|
|
15
16
|
const AUTH_FAILURE_COOLDOWN_MS = 60_000;
|
|
16
17
|
const GUARDED_METHODS = new Set(["GET", "HEAD"]);
|
|
17
18
|
const AUTH_FAILURE_HEADER = "x-agent-native-auth-circuit-breaker";
|
|
18
|
-
const EMBED_TOKEN_RENEWED_FROM_STORAGE_KEY = `${EMBED_TOKEN_STORAGE_KEY}:renewed-from`;
|
|
19
19
|
const EMBED_SESSION_RENEWAL_TIMEOUT_MS = 15_000;
|
|
20
20
|
const MCP_CHAT_BRIDGE_VIEWPORT_STYLE_ID = "agent-native-mcp-chat-bridge-viewport";
|
|
21
21
|
const MCP_CHAT_BRIDGE_VIEWPORT_HEIGHT = 560;
|
|
@@ -24,6 +24,48 @@ const authFailureCache = new Map();
|
|
|
24
24
|
function browserWindow() {
|
|
25
25
|
return typeof window === "undefined" ? null : window;
|
|
26
26
|
}
|
|
27
|
+
function currentEmbedBasePath() {
|
|
28
|
+
try {
|
|
29
|
+
return resolveAppBasePath();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof WorkspaceAppMountResolutionError)
|
|
33
|
+
return null;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function currentEmbedTokenStorageKey() {
|
|
38
|
+
const basePath = currentEmbedBasePath();
|
|
39
|
+
return basePath === null
|
|
40
|
+
? null
|
|
41
|
+
: embedTokenStorageKey(basePath, isWorkspaceRuntime());
|
|
42
|
+
}
|
|
43
|
+
function currentMcpChatBridgeStorageKey() {
|
|
44
|
+
const basePath = currentEmbedBasePath();
|
|
45
|
+
return basePath === null
|
|
46
|
+
? null
|
|
47
|
+
: mcpChatBridgeStorageKey(basePath, isWorkspaceRuntime());
|
|
48
|
+
}
|
|
49
|
+
function memoryTokenFor(storageKey) {
|
|
50
|
+
return memoryTokens.get(storageKey) ?? null;
|
|
51
|
+
}
|
|
52
|
+
function externalEmbedToken(win) {
|
|
53
|
+
const config = win.__AGENT_NATIVE_EXTERNAL_EMBED;
|
|
54
|
+
return typeof config?.token === "string" && config.token
|
|
55
|
+
? config.token
|
|
56
|
+
: null;
|
|
57
|
+
}
|
|
58
|
+
function externalEmbedTokenStorageKey(win) {
|
|
59
|
+
const config = win.__AGENT_NATIVE_EXTERNAL_EMBED;
|
|
60
|
+
const target = config?.target;
|
|
61
|
+
if (typeof target !== "string" || !target.startsWith("/"))
|
|
62
|
+
return null;
|
|
63
|
+
const workspaceRuntime = isWorkspaceRuntime();
|
|
64
|
+
const basePath = resolveExternalEmbedTargetAppBasePath(target);
|
|
65
|
+
if (workspaceRuntime && basePath === undefined)
|
|
66
|
+
return null;
|
|
67
|
+
return embedTokenStorageKey(basePath, workspaceRuntime);
|
|
68
|
+
}
|
|
27
69
|
function currentUrl(win) {
|
|
28
70
|
try {
|
|
29
71
|
return new URL(win.location.href);
|
|
@@ -48,13 +90,20 @@ export function readEmbedMcpChatBridgeFlagFromUrl() {
|
|
|
48
90
|
return value === "1" || value === "true";
|
|
49
91
|
}
|
|
50
92
|
function currentMcpChatBridgeScope(win) {
|
|
51
|
-
|
|
93
|
+
const storageKey = currentEmbedTokenStorageKey();
|
|
94
|
+
if (storageKey === null)
|
|
95
|
+
return null;
|
|
96
|
+
return getEmbedAuthToken() ?? storedToken(win, storageKey);
|
|
52
97
|
}
|
|
53
98
|
function clearMcpChatBridge(win) {
|
|
54
99
|
mcpChatBridgeActive = false;
|
|
55
100
|
mcpChatBridgeScope = null;
|
|
101
|
+
mcpChatBridgeStorageKeyAtState = null;
|
|
102
|
+
const storageKey = currentMcpChatBridgeStorageKey();
|
|
103
|
+
if (storageKey === null)
|
|
104
|
+
return;
|
|
56
105
|
try {
|
|
57
|
-
win.sessionStorage
|
|
106
|
+
win.sessionStorage.removeItem(storageKey);
|
|
58
107
|
}
|
|
59
108
|
catch {
|
|
60
109
|
// ignore unavailable session storage
|
|
@@ -62,15 +111,19 @@ function clearMcpChatBridge(win) {
|
|
|
62
111
|
}
|
|
63
112
|
export function markEmbedMcpChatBridgeActive() {
|
|
64
113
|
const win = browserWindow();
|
|
65
|
-
const
|
|
114
|
+
const storageKey = currentMcpChatBridgeStorageKey();
|
|
115
|
+
const scope = win && storageKey !== null ? currentMcpChatBridgeScope(win) : null;
|
|
66
116
|
mcpChatBridgeActive = true;
|
|
67
117
|
mcpChatBridgeScope = scope;
|
|
118
|
+
mcpChatBridgeStorageKeyAtState = storageKey;
|
|
119
|
+
if (storageKey === null)
|
|
120
|
+
return;
|
|
68
121
|
try {
|
|
69
122
|
if (scope) {
|
|
70
|
-
win?.sessionStorage
|
|
123
|
+
win?.sessionStorage.setItem(storageKey, scope);
|
|
71
124
|
}
|
|
72
125
|
else {
|
|
73
|
-
win?.sessionStorage
|
|
126
|
+
win?.sessionStorage.removeItem(storageKey);
|
|
74
127
|
}
|
|
75
128
|
}
|
|
76
129
|
catch {
|
|
@@ -82,6 +135,23 @@ export function isEmbedMcpChatBridgeActive() {
|
|
|
82
135
|
const win = browserWindow();
|
|
83
136
|
if (!win)
|
|
84
137
|
return false;
|
|
138
|
+
const storageKey = currentMcpChatBridgeStorageKey();
|
|
139
|
+
if (storageKey === null) {
|
|
140
|
+
if (isEmbedAuthActive() && readEmbedMcpChatBridgeFlagFromUrl()) {
|
|
141
|
+
markEmbedMcpChatBridgeActive();
|
|
142
|
+
}
|
|
143
|
+
// A temporary route without a declared mount does not prove the host
|
|
144
|
+
// unenrolled this document from the bridge.
|
|
145
|
+
return mcpChatBridgeActive;
|
|
146
|
+
}
|
|
147
|
+
if (mcpChatBridgeActive && mcpChatBridgeStorageKeyAtState === null) {
|
|
148
|
+
markEmbedMcpChatBridgeActive();
|
|
149
|
+
}
|
|
150
|
+
if (mcpChatBridgeActive && mcpChatBridgeStorageKeyAtState !== storageKey) {
|
|
151
|
+
mcpChatBridgeActive = false;
|
|
152
|
+
mcpChatBridgeScope = null;
|
|
153
|
+
mcpChatBridgeStorageKeyAtState = null;
|
|
154
|
+
}
|
|
85
155
|
if (!isEmbedAuthActive()) {
|
|
86
156
|
clearMcpChatBridge(win);
|
|
87
157
|
return false;
|
|
@@ -108,14 +178,15 @@ export function isEmbedMcpChatBridgeActive() {
|
|
|
108
178
|
return false;
|
|
109
179
|
}
|
|
110
180
|
try {
|
|
111
|
-
const storedScope = win.sessionStorage
|
|
181
|
+
const storedScope = win.sessionStorage.getItem(storageKey);
|
|
112
182
|
if (storedScope && (scope == null || storedScope === scope)) {
|
|
113
183
|
mcpChatBridgeActive = true;
|
|
114
184
|
mcpChatBridgeScope = storedScope;
|
|
185
|
+
mcpChatBridgeStorageKeyAtState = storageKey;
|
|
115
186
|
return true;
|
|
116
187
|
}
|
|
117
188
|
if (storedScope && scope != null && storedScope !== scope) {
|
|
118
|
-
win.sessionStorage
|
|
189
|
+
win.sessionStorage.removeItem(storageKey);
|
|
119
190
|
}
|
|
120
191
|
return false;
|
|
121
192
|
}
|
|
@@ -123,18 +194,46 @@ export function isEmbedMcpChatBridgeActive() {
|
|
|
123
194
|
return false;
|
|
124
195
|
}
|
|
125
196
|
}
|
|
126
|
-
function storedToken(win) {
|
|
197
|
+
function storedToken(win, storageKey) {
|
|
127
198
|
try {
|
|
128
|
-
return win.sessionStorage
|
|
199
|
+
return win.sessionStorage.getItem(storageKey) ?? null;
|
|
129
200
|
}
|
|
130
201
|
catch {
|
|
131
202
|
return null;
|
|
132
203
|
}
|
|
133
204
|
}
|
|
134
|
-
function
|
|
135
|
-
|
|
205
|
+
function embedTokenTargetStorageKey(token) {
|
|
206
|
+
const payload = token.split(".")[0] ?? "";
|
|
207
|
+
let decodedClaims;
|
|
136
208
|
try {
|
|
137
|
-
|
|
209
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
210
|
+
const bytes = Uint8Array.from(atob(base64 + "=".repeat((4 - (base64.length % 4)) % 4)), (char) => char.charCodeAt(0));
|
|
211
|
+
decodedClaims = JSON.parse(new TextDecoder().decode(bytes));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// coercion-ok: an unreadable token has no mount binding; null makes the caller reject it.
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (!decodedClaims ||
|
|
218
|
+
typeof decodedClaims !== "object" ||
|
|
219
|
+
Array.isArray(decodedClaims)) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const claims = decodedClaims;
|
|
223
|
+
if (typeof claims.audienceBasePath === "string") {
|
|
224
|
+
return embedTokenStorageKey(claims.audienceBasePath, true);
|
|
225
|
+
}
|
|
226
|
+
if (claims.audienceBasePath !== undefined)
|
|
227
|
+
return null;
|
|
228
|
+
if (typeof claims.targetPath !== "string")
|
|
229
|
+
return null;
|
|
230
|
+
const basePath = resolveExternalEmbedTargetAppBasePath(claims.targetPath);
|
|
231
|
+
return basePath === undefined ? null : embedTokenStorageKey(basePath, true);
|
|
232
|
+
}
|
|
233
|
+
function storeToken(token, win, storageKey) {
|
|
234
|
+
memoryTokens.set(storageKey, token);
|
|
235
|
+
try {
|
|
236
|
+
win.sessionStorage.setItem(storageKey, token);
|
|
138
237
|
}
|
|
139
238
|
catch {
|
|
140
239
|
// Session storage may be unavailable in some sandboxed hosts. The
|
|
@@ -145,37 +244,56 @@ export function getEmbedAuthToken() {
|
|
|
145
244
|
const win = browserWindow();
|
|
146
245
|
if (!win)
|
|
147
246
|
return null;
|
|
247
|
+
const storageKey = currentEmbedTokenStorageKey();
|
|
248
|
+
if (storageKey === null)
|
|
249
|
+
return null;
|
|
250
|
+
const renewedFromStorageKey = `${storageKey}:renewed-from`;
|
|
148
251
|
const fromUrl = readTokenFromUrl(win);
|
|
149
252
|
if (fromUrl) {
|
|
253
|
+
if (isWorkspaceRuntime() &&
|
|
254
|
+
embedTokenTargetStorageKey(fromUrl) !== storageKey) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
150
257
|
let renewedFrom = renewedFromUrlToken;
|
|
151
258
|
if (!renewedFrom) {
|
|
152
259
|
try {
|
|
153
|
-
renewedFrom =
|
|
154
|
-
win.sessionStorage?.getItem(EMBED_TOKEN_RENEWED_FROM_STORAGE_KEY) ??
|
|
155
|
-
null;
|
|
260
|
+
renewedFrom = win.sessionStorage.getItem(renewedFromStorageKey) ?? null;
|
|
156
261
|
}
|
|
157
262
|
catch {
|
|
158
263
|
renewedFrom = null;
|
|
159
264
|
}
|
|
160
265
|
}
|
|
161
266
|
if (renewedFrom === fromUrl) {
|
|
162
|
-
const refreshed =
|
|
267
|
+
const refreshed = memoryTokenFor(storageKey) ?? storedToken(win, storageKey);
|
|
163
268
|
if (refreshed && refreshed !== fromUrl)
|
|
164
269
|
return refreshed;
|
|
165
270
|
}
|
|
166
271
|
if (renewedFrom && renewedFrom !== fromUrl) {
|
|
167
272
|
renewedFromUrlToken = null;
|
|
168
273
|
try {
|
|
169
|
-
win.sessionStorage
|
|
274
|
+
win.sessionStorage.removeItem(renewedFromStorageKey);
|
|
170
275
|
}
|
|
171
276
|
catch {
|
|
172
277
|
// coercion-ok: cleanup is best-effort; the refreshed URL token still supersedes this marker.
|
|
173
278
|
}
|
|
174
279
|
}
|
|
175
|
-
storeToken(fromUrl, win);
|
|
280
|
+
storeToken(fromUrl, win, storageKey);
|
|
176
281
|
return fromUrl;
|
|
177
282
|
}
|
|
178
|
-
|
|
283
|
+
const inMemoryToken = memoryTokenFor(storageKey);
|
|
284
|
+
if (inMemoryToken)
|
|
285
|
+
return inMemoryToken;
|
|
286
|
+
const stored = storedToken(win, storageKey);
|
|
287
|
+
if (stored)
|
|
288
|
+
return stored;
|
|
289
|
+
const external = externalEmbedToken(win);
|
|
290
|
+
if (!external)
|
|
291
|
+
return null;
|
|
292
|
+
// The frame config outlives navigation, but its token belongs to one mount.
|
|
293
|
+
if (externalEmbedTokenStorageKey(win) !== storageKey)
|
|
294
|
+
return null;
|
|
295
|
+
storeToken(external, win, storageKey);
|
|
296
|
+
return external;
|
|
179
297
|
}
|
|
180
298
|
function readEmbedTokenScope(token) {
|
|
181
299
|
const payload = token.split(".")[0] ?? "";
|
|
@@ -362,13 +480,14 @@ export function _resetEmbedAuthForTests() {
|
|
|
362
480
|
pending.win.clearTimeout(id);
|
|
363
481
|
}
|
|
364
482
|
installed = false;
|
|
365
|
-
|
|
483
|
+
memoryTokens.clear();
|
|
366
484
|
renewedFromUrlToken = null;
|
|
367
485
|
pendingWidgetSessionRenewal = null;
|
|
368
486
|
widgetSessionRenewalRequestId = 0;
|
|
369
487
|
widgetScopeCache = null;
|
|
370
488
|
mcpChatBridgeActive = false;
|
|
371
489
|
mcpChatBridgeScope = null;
|
|
490
|
+
mcpChatBridgeStorageKeyAtState = null;
|
|
372
491
|
authFailureCache.clear();
|
|
373
492
|
}
|
|
374
493
|
/**
|
|
@@ -654,8 +773,8 @@ export function ensureEmbedAuthFetchInterceptor() {
|
|
|
654
773
|
markEmbedMcpChatBridgeActive();
|
|
655
774
|
const urlToken = readTokenFromUrl(win);
|
|
656
775
|
if (urlToken) {
|
|
657
|
-
getEmbedAuthToken()
|
|
658
|
-
|
|
776
|
+
if (getEmbedAuthToken())
|
|
777
|
+
stripTokenFromUrl(win);
|
|
659
778
|
}
|
|
660
779
|
ensureMcpChatBridgeViewportClamp(win);
|
|
661
780
|
if (installed)
|
|
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
|
3
3
|
import { useLocation, useNavigate, } from "react-router";
|
|
4
4
|
import { isWorkspaceAppPath } from "./api-path.js";
|
|
5
5
|
import { deleteClientAppState, readClientAppState, setClientAppState, } from "./application-state.js";
|
|
6
|
-
import { getBrowserTabId } from "./browser-tab-id.js";
|
|
6
|
+
import { getBrowserTabId, isBrowserTabIdForCurrentDocument, } from "./browser-tab-id.js";
|
|
7
7
|
import { navigateWithAgentChatViewTransition, } from "./chat-view-transition.js";
|
|
8
8
|
import { postAgentNativeWorkspaceAppRoute } from "./workspace-app-navigation.js";
|
|
9
9
|
const SAFE_BROWSER_TAB_ID_RE = /^[A-Za-z0-9_-]{1,96}$/;
|
|
@@ -195,8 +195,19 @@ export function useAgentRouteState(options) {
|
|
|
195
195
|
const { navigationKey = "navigation", commandKey = "navigate" } = options;
|
|
196
196
|
const location = useLocation();
|
|
197
197
|
const navigate = useNavigate();
|
|
198
|
-
const
|
|
199
|
-
|
|
198
|
+
const defaultTabId = defaultBrowserTabId();
|
|
199
|
+
const explicitTabId = normalizeBrowserTabId(options.browserTabId);
|
|
200
|
+
// Templates expose TAB_ID as a module-level constant. If it matches the
|
|
201
|
+
// current document's generated IDs, keep following the active mount.
|
|
202
|
+
const browserTabId = explicitTabId === undefined ||
|
|
203
|
+
isBrowserTabIdForCurrentDocument(explicitTabId)
|
|
204
|
+
? defaultTabId
|
|
205
|
+
: explicitTabId;
|
|
206
|
+
const requestSourceFollowsDefault = options.requestSource === undefined ||
|
|
207
|
+
isBrowserTabIdForCurrentDocument(options.requestSource);
|
|
208
|
+
const requestSource = requestSourceFollowsDefault
|
|
209
|
+
? browserTabId
|
|
210
|
+
: options.requestSource;
|
|
200
211
|
const writeGlobalNavigation = options.writeGlobalNavigation ?? false;
|
|
201
212
|
const readGlobalCommandFallback = options.readGlobalCommandFallback ?? false;
|
|
202
213
|
useEffect(() => {
|
|
@@ -237,7 +248,7 @@ export function useAgentRouteState(options) {
|
|
|
237
248
|
commandKeys,
|
|
238
249
|
commandQueryKey,
|
|
239
250
|
browserTabId,
|
|
240
|
-
requestSource
|
|
251
|
+
requestSource,
|
|
241
252
|
commandRefetchInterval: options.refetchInterval,
|
|
242
253
|
enabled: options.enabled,
|
|
243
254
|
keepalive: options.keepalive,
|
|
@@ -3,6 +3,7 @@ import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE, PROVIDER_TRANSIENT_REJECTION_ERROR
|
|
|
3
3
|
import { normalizeConnectRequiredResult } from "../shared/connect-required.js";
|
|
4
4
|
import { isNonProgressEventType } from "../shared/progress-events.js";
|
|
5
5
|
import { toolInputsMatch } from "../shared/tool-inputs.js";
|
|
6
|
+
import { getBrowserTabId } from "./browser-tab-id.js";
|
|
6
7
|
import { emitChatFirstOpenApp } from "./chat-first-state.js";
|
|
7
8
|
import { formatChatErrorText, normalizeChatError } from "./error-format.js";
|
|
8
9
|
import { humanizeToolLabelText, humanizeToolName, isToolCallActive, runningToolLabel, } from "./tool-display.js";
|
|
@@ -1443,6 +1444,8 @@ export function processEvent(ev, content, toolCallCounter, tabId, state, context
|
|
|
1443
1444
|
isError: ev.isError === true,
|
|
1444
1445
|
completedSideEffect: ev.completedSideEffect === true,
|
|
1445
1446
|
tabId,
|
|
1447
|
+
browserTabId: getBrowserTabId(),
|
|
1448
|
+
runId: context?.runId,
|
|
1446
1449
|
eventId: ev.eventId ?? ev.id,
|
|
1447
1450
|
},
|
|
1448
1451
|
}));
|
|
@@ -87,7 +87,12 @@ class ChangeVersionStore {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
const
|
|
90
|
+
const CHANGE_VERSION_STORE_SYMBOL = Symbol.for("@agent-native/core/client/change-version-store");
|
|
91
|
+
const globalChangeVersionStore = globalThis;
|
|
92
|
+
// Vite can load an optimized package root beside a directly served deep import.
|
|
93
|
+
const store = globalChangeVersionStore[CHANGE_VERSION_STORE_SYMBOL] ??
|
|
94
|
+
(globalChangeVersionStore[CHANGE_VERSION_STORE_SYMBOL] =
|
|
95
|
+
new ChangeVersionStore());
|
|
91
96
|
export function bumpChangeVersion(source, version) {
|
|
92
97
|
return store.bump(source, version);
|
|
93
98
|
}
|
|
@@ -5,7 +5,7 @@ export { REALTIME_CAP_POLL_LIVE } from "../realtime-protocol.js";
|
|
|
5
5
|
import { addSurfaceVisibilityListener, isHostSurfaceHidden, isSurfaceHidden, } from "../shared/surface-visibility.js";
|
|
6
6
|
import { actionQueryAffectedByResources } from "./action-query-scope.js";
|
|
7
7
|
import { agentNativePath } from "./api-path.js";
|
|
8
|
-
import { getBrowserTabId } from "./browser-tab-id.js";
|
|
8
|
+
import { getBrowserTabId, resolveBrowserTabIdAlias } from "./browser-tab-id.js";
|
|
9
9
|
import { isTerminalAuthFailure } from "./create-query-client.js";
|
|
10
10
|
import { ensureEmbedAuthFetchInterceptor, isEmbedAuthActive, } from "./embed-auth.js";
|
|
11
11
|
import { bumpChangeVersion, bumpActiveLocalChangeVersions, bumpLocalChangeVersion, } from "./use-change-version.js";
|
|
@@ -1372,10 +1372,7 @@ export function useDbSync(options = {}) {
|
|
|
1372
1372
|
.map((key) => `${key.length}:${key}`)
|
|
1373
1373
|
.join("|");
|
|
1374
1374
|
const needsBroadAppStateRefresh = appStateKeys.some((key) => key === "*" ||
|
|
1375
|
-
|
|
1376
|
-
source: "app-state",
|
|
1377
|
-
key,
|
|
1378
|
-
}));
|
|
1375
|
+
INTERACTION_CRITICAL_APP_STATE_KEYS.includes(key));
|
|
1379
1376
|
invalidateWithoutCancel(needsBroadAppStateRefresh
|
|
1380
1377
|
? { queryKey: ["app-state"] }
|
|
1381
1378
|
: {
|
|
@@ -1432,11 +1429,19 @@ export function useDbSync(options = {}) {
|
|
|
1432
1429
|
subscriberCursor = maxSyncCursor(subscriberCursor, cursor);
|
|
1433
1430
|
}
|
|
1434
1431
|
const sideEffectToolsByTab = new Map();
|
|
1435
|
-
const
|
|
1432
|
+
const browserTabIdsByTab = new Map();
|
|
1433
|
+
const eventsForTool = (tool, completedSideEffect, failed, browserTabId) => {
|
|
1436
1434
|
const events = [];
|
|
1437
1435
|
if (completedSideEffect) {
|
|
1438
1436
|
events.push({ source: "action", key: tool, version: 0 });
|
|
1439
1437
|
}
|
|
1438
|
+
if (tool === "navigate" && completedSideEffect) {
|
|
1439
|
+
events.push({
|
|
1440
|
+
source: "app-state",
|
|
1441
|
+
key: browserTabId ? `navigate:${browserTabId}` : "navigate",
|
|
1442
|
+
version: 0,
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1440
1445
|
if (["__set_url__", "set-url", "set-search-params"].includes(tool)) {
|
|
1441
1446
|
events.push({ source: "app-state", key: "__set_url__", version: 0 });
|
|
1442
1447
|
}
|
|
@@ -1478,13 +1483,36 @@ export function useDbSync(options = {}) {
|
|
|
1478
1483
|
return;
|
|
1479
1484
|
const tool = detail.tool;
|
|
1480
1485
|
const completedSideEffect = detail.completedSideEffect === true;
|
|
1481
|
-
const
|
|
1486
|
+
const tabId = typeof detail.tabId === "string" && detail.tabId
|
|
1487
|
+
? detail.tabId
|
|
1488
|
+
: "__default__";
|
|
1489
|
+
const runId = typeof detail.runId === "string" && detail.runId
|
|
1490
|
+
? detail.runId
|
|
1491
|
+
: undefined;
|
|
1492
|
+
const runStart = browserTabIdsByTab.get(tabId);
|
|
1493
|
+
if (runId && runStart?.runId && runId !== runStart.runId)
|
|
1494
|
+
return;
|
|
1495
|
+
const aliasedRunStartId = runStart
|
|
1496
|
+
? resolveBrowserTabIdAlias(runStart.browserTabId)
|
|
1497
|
+
: undefined;
|
|
1498
|
+
const browserTabId = aliasedRunStartId ??
|
|
1499
|
+
runStart?.browserTabId ??
|
|
1500
|
+
(typeof detail.browserTabId === "string"
|
|
1501
|
+
? detail.browserTabId
|
|
1502
|
+
: getBrowserTabId());
|
|
1503
|
+
if (runStart && browserTabId !== runStart.browserTabId) {
|
|
1504
|
+
browserTabIdsByTab.set(tabId, { ...runStart, browserTabId });
|
|
1505
|
+
}
|
|
1506
|
+
if (completedSideEffect && !runStart) {
|
|
1507
|
+
browserTabIdsByTab.set(tabId, {
|
|
1508
|
+
browserTabId,
|
|
1509
|
+
...(runId ? { runId } : {}),
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
const events = eventsForTool(tool, completedSideEffect, detail.isError === true, browserTabId);
|
|
1482
1513
|
if (events.length === 0)
|
|
1483
1514
|
return;
|
|
1484
1515
|
if (completedSideEffect) {
|
|
1485
|
-
const tabId = typeof detail.tabId === "string" && detail.tabId
|
|
1486
|
-
? detail.tabId
|
|
1487
|
-
: "__default__";
|
|
1488
1516
|
const tools = sideEffectToolsByTab.get(tabId) ?? new Map();
|
|
1489
1517
|
const previouslyFailed = tools.get(tool);
|
|
1490
1518
|
tools.set(tool, previouslyFailed === false ? false : detail.isError === true);
|
|
@@ -1498,6 +1526,22 @@ export function useDbSync(options = {}) {
|
|
|
1498
1526
|
const tabId = typeof detail.tabId === "string" && detail.tabId
|
|
1499
1527
|
? detail.tabId
|
|
1500
1528
|
: "__default__";
|
|
1529
|
+
const browserTabId = typeof detail.browserTabId === "string"
|
|
1530
|
+
? detail.browserTabId
|
|
1531
|
+
: getBrowserTabId();
|
|
1532
|
+
const runId = typeof detail.runId === "string" && detail.runId
|
|
1533
|
+
? detail.runId
|
|
1534
|
+
: undefined;
|
|
1535
|
+
const runStart = browserTabIdsByTab.get(tabId);
|
|
1536
|
+
if (!runId) {
|
|
1537
|
+
browserTabIdsByTab.set(tabId, { browserTabId });
|
|
1538
|
+
}
|
|
1539
|
+
else if (!runStart || (runStart.runId && runStart.runId !== runId)) {
|
|
1540
|
+
browserTabIdsByTab.set(tabId, { browserTabId, runId });
|
|
1541
|
+
}
|
|
1542
|
+
else if (!runStart.runId) {
|
|
1543
|
+
browserTabIdsByTab.set(tabId, { ...runStart, runId });
|
|
1544
|
+
}
|
|
1501
1545
|
sideEffectToolsByTab.delete(tabId);
|
|
1502
1546
|
return;
|
|
1503
1547
|
}
|
|
@@ -1506,11 +1550,26 @@ export function useDbSync(options = {}) {
|
|
|
1506
1550
|
const tabId = typeof detail.tabId === "string" && detail.tabId
|
|
1507
1551
|
? detail.tabId
|
|
1508
1552
|
: "__default__";
|
|
1553
|
+
const runStart = browserTabIdsByTab.get(tabId);
|
|
1554
|
+
const runId = typeof detail.runId === "string" && detail.runId
|
|
1555
|
+
? detail.runId
|
|
1556
|
+
: undefined;
|
|
1557
|
+
if (runId && runStart?.runId && runId !== runStart.runId)
|
|
1558
|
+
return;
|
|
1559
|
+
const aliasedRunStartId = runStart
|
|
1560
|
+
? resolveBrowserTabIdAlias(runStart.browserTabId)
|
|
1561
|
+
: undefined;
|
|
1562
|
+
const browserTabId = aliasedRunStartId ??
|
|
1563
|
+
runStart?.browserTabId ??
|
|
1564
|
+
(typeof detail.browserTabId === "string"
|
|
1565
|
+
? detail.browserTabId
|
|
1566
|
+
: getBrowserTabId());
|
|
1567
|
+
browserTabIdsByTab.delete(tabId);
|
|
1509
1568
|
const tools = sideEffectToolsByTab.get(tabId);
|
|
1510
1569
|
if (!tools?.size)
|
|
1511
1570
|
return;
|
|
1512
1571
|
sideEffectToolsByTab.delete(tabId);
|
|
1513
|
-
applyRunEvents(event, [...tools].flatMap(([tool, failed]) => eventsForTool(tool, true, failed)), processedRunEndEvents);
|
|
1572
|
+
applyRunEvents(event, [...tools].flatMap(([tool, failed]) => eventsForTool(tool, true, failed, browserTabId)), processedRunEndEvents);
|
|
1514
1573
|
};
|
|
1515
1574
|
window.addEventListener("agent-native:tool-done", handleRunToolDone);
|
|
1516
1575
|
window.addEventListener("agentNative.chatRunning", handleRunStatus);
|
package/dist/collab/client.d.ts
CHANGED
|
@@ -18,6 +18,29 @@ export interface UseCollaborativeDocOptions {
|
|
|
18
18
|
};
|
|
19
19
|
/** Application events received alongside document updates on the existing transport. */
|
|
20
20
|
onEvent?: (event: SyncEvent) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Keeps this tab's unacknowledged updates in localStorage until the server
|
|
23
|
+
* acknowledges them, and sends the ones a closed or reloaded tab left
|
|
24
|
+
* behind when the doc next opens for the same user. Needs `user`, whose
|
|
25
|
+
* email scopes what is kept.
|
|
26
|
+
*/
|
|
27
|
+
durableUpdates?: boolean;
|
|
28
|
+
/** New updates need a known live-copy outcome handled by a current strict subscriber. */
|
|
29
|
+
requireLiveCopyOutcome?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Called for each update the server acknowledged, with the live-copy
|
|
32
|
+
* outcome it reported. With it, an acknowledgment whose body cannot be read
|
|
33
|
+
* counts as undelivered and the update is sent again.
|
|
34
|
+
*/
|
|
35
|
+
onUpdateAcknowledged?: (ack: CollabUpdateAck) => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What the server reported for an acknowledged update: whether the app's rows
|
|
39
|
+
* were derived from its live copy, or null when the server defines none.
|
|
40
|
+
*/
|
|
41
|
+
export interface CollabUpdateAck {
|
|
42
|
+
liveCopy: "derived" | "not_derived" | "unchanged" | null;
|
|
43
|
+
liveCopyReason?: string;
|
|
21
44
|
}
|
|
22
45
|
export type CollabInitializationErrorCategory = "forbidden-or-not-found" | "server" | "network" | "invalid-payload";
|
|
23
46
|
export type CollabInitializationState = {
|
|
@@ -51,6 +74,17 @@ export interface UseCollaborativeDocResult {
|
|
|
51
74
|
* can already receive it through the document instead of inserting it twice.
|
|
52
75
|
*/
|
|
53
76
|
flushUpdates: () => Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* True once keeping this tab's unacknowledged updates, or reading the ones
|
|
79
|
+
* another tab left, failed. Updates still send while the tab stays open.
|
|
80
|
+
*/
|
|
81
|
+
durableUpdatesFailed: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* True once the server refused one of this tab's updates as too large.
|
|
84
|
+
* Resending it cannot succeed, so this tab stops sending updates; they stay
|
|
85
|
+
* unacknowledged, and kept with `durableUpdates`.
|
|
86
|
+
*/
|
|
87
|
+
updatesRefused: boolean;
|
|
54
88
|
activeUsers: CollabUser[];
|
|
55
89
|
agentActive: boolean;
|
|
56
90
|
agentPresent: boolean;
|