@agent-native/core 0.227.0 → 0.228.0-nightly-20261110212400
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/agent/reaped-run-thread.js +6 -0
- package/dist/application-state/handlers.d.ts +3 -3
- package/dist/application-state/handlers.js +6 -1
- package/dist/application-state/index.d.ts +3 -2
- package/dist/application-state/index.js +3 -2
- package/dist/application-state/last-used-tab.d.ts +11 -0
- package/dist/application-state/last-used-tab.js +43 -0
- package/dist/application-state/script-helpers.d.ts +26 -0
- package/dist/application-state/script-helpers.js +83 -15
- package/dist/application-state/store.d.ts +8 -0
- package/dist/application-state/store.js +16 -0
- package/dist/client/agent-sidebar-state.d.ts +2 -0
- package/dist/client/browser-tab-use.d.ts +9 -0
- package/dist/client/browser-tab-use.js +112 -0
- package/dist/client/navigation/index.d.ts +1 -0
- package/dist/client/navigation/index.js +1 -0
- package/dist/client/review/use-review.d.ts +28 -0
- package/dist/client/review/use-review.js +41 -12
- package/dist/client/route-state.js +2 -0
- package/dist/client/use-db-sync.d.ts +8 -1
- package/dist/client/use-db-sync.js +6 -5
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/schema-snapshot.generated.js +6 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/file-upload/index.d.ts +1 -1
- package/dist/file-upload/index.js +1 -1
- package/dist/file-upload/s3.d.ts +9 -0
- package/dist/file-upload/s3.js +14 -1
- package/dist/framework-package-versions.json +7 -7
- package/dist/jobs/scheduler.js +4 -35
- package/dist/mcp/browser-tab-target.d.ts +16 -0
- package/dist/mcp/browser-tab-target.js +58 -0
- package/dist/mcp/build-server.js +9 -2
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/org/handlers.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/review/actions/create-review-comment.d.ts +7 -0
- package/dist/review/actions/create-review-comment.js +29 -3
- package/dist/review/actions/delete-review-comment.js +2 -2
- package/dist/review/actions/list-review-comments.d.ts +3 -0
- package/dist/review/actions/list-review-comments.js +41 -17
- package/dist/review/actions/react-to-review-comment.js +2 -2
- package/dist/review/actions/reply-review-comment.d.ts +4 -0
- package/dist/review/actions/reply-review-comment.js +15 -6
- package/dist/review/actions/update-review-comment-anchor.d.ts +6 -0
- package/dist/review/actions/update-review-comment-anchor.js +39 -10
- package/dist/review/actions/update-review-comment.d.ts +3 -0
- package/dist/review/actions/update-review-comment.js +28 -3
- package/dist/review/errors.d.ts +33 -0
- package/dist/review/errors.js +51 -0
- package/dist/review/index.d.ts +5 -4
- package/dist/review/index.js +3 -2
- package/dist/review/registry.d.ts +51 -1
- package/dist/review/registry.js +117 -0
- package/dist/review/store.d.ts +59 -14
- package/dist/review/store.js +622 -101
- package/dist/review/suggestions/actions.js +10 -8
- package/dist/review/types.d.ts +89 -0
- package/dist/search/query.d.ts +11 -5
- package/dist/search/query.js +52 -15
- package/dist/search/tokenize.d.ts +1 -0
- package/dist/search/tokenize.js +3 -0
- package/dist/secrets/routes.d.ts +2 -2
- package/dist/server/request-context.d.ts +26 -0
- package/dist/shared/app-state-routes.d.ts +7 -0
- package/dist/shared/app-state-routes.js +7 -0
- package/dist/shared/runtime-config.js +5 -1
- package/dist/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
- package/dist/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
- package/dist/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +13 -0
- package/dist/triggers/condition-evaluator.d.ts +2 -0
- package/dist/triggers/condition-evaluator.js +93 -3
- package/dist/triggers/dispatcher.js +24 -6
- package/docs/content/create-with-an-agent.mdx +10 -4
- package/docs/content/durable-resume.mdx +11 -2
- package/docs/content/toolkit-comments-review.mdx +54 -1
- package/package.json +3 -3
- package/src/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
- package/src/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
- package/src/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +13 -0
|
@@ -87,6 +87,12 @@ export async function foldReapedRunIntoThread(runId, terminalError) {
|
|
|
87
87
|
if (!thread)
|
|
88
88
|
return "superseded";
|
|
89
89
|
const repo = readThreadRepository(run.threadId, run.turnId, thread.threadData);
|
|
90
|
+
// A thread with no messages has no tail for the retry check below to wait
|
|
91
|
+
// on, so retrying would defer forever and fail every sweep.
|
|
92
|
+
if (repo.messages.length === 0) {
|
|
93
|
+
console.warn(`[agent-chat] dropping stopped run ${runId}: thread ${run.threadId} has no messages (turn ${run.turnId})`);
|
|
94
|
+
return "superseded";
|
|
95
|
+
}
|
|
90
96
|
const folded = foldAssistantTurnWithSteers(repo, assistant, steered, {
|
|
91
97
|
runId,
|
|
92
98
|
turnId: run.turnId,
|
|
@@ -36,18 +36,18 @@ export declare const listComposeDrafts: import("h3").EventHandlerWithFetch<impor
|
|
|
36
36
|
export declare const getComposeDraft: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<Record<string, unknown> | null>>;
|
|
37
37
|
export declare const putComposeDraft: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<any>>;
|
|
38
38
|
export declare const saveComposeDraft: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
39
|
-
errorCode?: undefined;
|
|
40
39
|
error: string;
|
|
41
40
|
value?: undefined;
|
|
42
41
|
revision?: undefined;
|
|
42
|
+
errorCode?: undefined;
|
|
43
43
|
details?: undefined;
|
|
44
44
|
} | {
|
|
45
|
-
errorCode?: undefined;
|
|
46
|
-
error?: undefined;
|
|
47
45
|
value: {
|
|
48
46
|
id: string;
|
|
49
47
|
};
|
|
50
48
|
revision: string;
|
|
49
|
+
error?: undefined;
|
|
50
|
+
errorCode?: undefined;
|
|
51
51
|
details?: undefined;
|
|
52
52
|
} | {
|
|
53
53
|
value?: undefined;
|
|
@@ -2,7 +2,8 @@ import { createError, defineEventHandler, getRouterParam, getHeader, getQuery, s
|
|
|
2
2
|
import { isSaveConflict, saveRevisionOf } from "../save-conflict/index.js";
|
|
3
3
|
import { readBrowserTabIdHeader } from "../server/agent-run-context.js";
|
|
4
4
|
import { readBody } from "../server/h3-helpers.js";
|
|
5
|
-
import { APP_STATE_BATCH_ROUTE_HEADER, APP_STATE_SEQ_HEADER, APP_STATE_WRITER_HEADER, TAB_SCOPED_APP_STATE_KEYS, } from "../shared/app-state-routes.js";
|
|
5
|
+
import { APP_STATE_BATCH_ROUTE_HEADER, APP_STATE_SEQ_HEADER, APP_STATE_WRITER_HEADER, BROWSER_TAB_USED_APP_STATE_KEY, TAB_SCOPED_APP_STATE_KEYS, } from "../shared/app-state-routes.js";
|
|
6
|
+
import { browserTabUsedKey } from "./last-used-tab.js";
|
|
6
7
|
import { appStateSaveIfCurrent } from "./save-if-current.js";
|
|
7
8
|
import { appStateKeyForBrowserTab } from "./script-helpers.js";
|
|
8
9
|
import { appStateGet, appStateGetManyEntries, appStatePut, appStateCompareAndSet, appStateDelete, appStateList, appStateDeleteByPrefix, } from "./store.js";
|
|
@@ -54,6 +55,10 @@ function headerSeq(event) {
|
|
|
54
55
|
return seq !== undefined && /^[0-9]{1,16}$/.test(seq) ? Number(seq) : seq;
|
|
55
56
|
}
|
|
56
57
|
function requestScopedKey(key, event) {
|
|
58
|
+
if (key === BROWSER_TAB_USED_APP_STATE_KEY) {
|
|
59
|
+
const browserTabId = readBrowserTabIdHeader(event);
|
|
60
|
+
return browserTabId ? browserTabUsedKey(browserTabId) : key;
|
|
61
|
+
}
|
|
57
62
|
return TAB_SCOPED_APP_STATE_KEYS.has(key)
|
|
58
63
|
? appStateKeyForBrowserTab(key, readBrowserTabIdHeader(event))
|
|
59
64
|
: key;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { appStateGet, appStateGetMany, appStateGetManyEntries, appStatePut, appStateDelete, appStateCompareAndSet, appStateCompareAndSetMany, appStateList, appStateDeleteByPrefix, type AppStateCompareAndSetOperation, } from "./store.js";
|
|
1
|
+
export { appStateGet, appStateGetMany, appStateGetManyEntries, appStatePut, appStateDelete, appStateCompareAndSet, appStateCompareAndSetMany, appStateList, appStateListUpdatedAt, appStateDeleteByPrefix, type AppStateCompareAndSetOperation, } from "./store.js";
|
|
2
|
+
export { browserTabUsedKey, findLastUsedBrowserTab, LAST_USED_BROWSER_TAB_MAX_AGE_MS, } from "./last-used-tab.js";
|
|
2
3
|
export { getAppStateEmitter, emitAppStateChange, emitAppStateDelete, type AppStateEvent, } from "./emitter.js";
|
|
3
4
|
export { getState, getStateMany, MAX_APP_STATE_BATCH_KEYS, MAX_APP_STATE_BATCH_WRITES, putState, putStateMany, deleteState, listComposeDrafts, getComposeDraft, putComposeDraft, deleteComposeDraft, deleteAllComposeDrafts, } from "./handlers.js";
|
|
4
|
-
export { readAppState, writeAppState, deleteAppState, compareAndSetAppState, compareAndSetManyAppState, listAppState, deleteAppStateByPrefix, readAppStateForCurrentTab, writeAppStateForCurrentTab, appStateKeyForBrowserTab, getCurrentRequestBrowserTabId, } from "./script-helpers.js";
|
|
5
|
+
export { readAppState, writeAppState, deleteAppState, compareAndSetAppState, compareAndSetManyAppState, listAppState, deleteAppStateByPrefix, readAppStateForCurrentTab, writeAppStateForCurrentTab, appStateKeyForBrowserTab, getCurrentRequestBrowserTabId, resolveCurrentRequestStateTab, } from "./script-helpers.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { appStateGet, appStateGetMany, appStateGetManyEntries, appStatePut, appStateDelete, appStateCompareAndSet, appStateCompareAndSetMany, appStateList, appStateDeleteByPrefix, } from "./store.js";
|
|
1
|
+
export { appStateGet, appStateGetMany, appStateGetManyEntries, appStatePut, appStateDelete, appStateCompareAndSet, appStateCompareAndSetMany, appStateList, appStateListUpdatedAt, appStateDeleteByPrefix, } from "./store.js";
|
|
2
|
+
export { browserTabUsedKey, findLastUsedBrowserTab, LAST_USED_BROWSER_TAB_MAX_AGE_MS, } from "./last-used-tab.js";
|
|
2
3
|
export { getAppStateEmitter, emitAppStateChange, emitAppStateDelete, } from "./emitter.js";
|
|
3
4
|
export { getState, getStateMany, MAX_APP_STATE_BATCH_KEYS, MAX_APP_STATE_BATCH_WRITES, putState, putStateMany, deleteState, listComposeDrafts, getComposeDraft, putComposeDraft, deleteComposeDraft, deleteAllComposeDrafts, } from "./handlers.js";
|
|
4
|
-
export { readAppState, writeAppState, deleteAppState, compareAndSetAppState, compareAndSetManyAppState, listAppState, deleteAppStateByPrefix, readAppStateForCurrentTab, writeAppStateForCurrentTab, appStateKeyForBrowserTab, getCurrentRequestBrowserTabId, } from "./script-helpers.js";
|
|
5
|
+
export { readAppState, writeAppState, deleteAppState, compareAndSetAppState, compareAndSetManyAppState, listAppState, deleteAppStateByPrefix, readAppStateForCurrentTab, writeAppStateForCurrentTab, appStateKeyForBrowserTab, getCurrentRequestBrowserTabId, resolveCurrentRequestStateTab, } from "./script-helpers.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BrowserTabTarget } from "../server/request-context.js";
|
|
2
|
+
/** How long after its last use a tab still counts as the one to act on. */
|
|
3
|
+
export declare const LAST_USED_BROWSER_TAB_MAX_AGE_MS: number;
|
|
4
|
+
/** The row that records a tab's last use: one per app and tab. */
|
|
5
|
+
export declare function browserTabUsedKey(browserTabId: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* The tab of this app the user used last, if one was used within
|
|
8
|
+
* LAST_USED_BROWSER_TAB_MAX_AGE_MS. Tabs are ordered by when the server
|
|
9
|
+
* stored their use, so browser clocks don't matter.
|
|
10
|
+
*/
|
|
11
|
+
export declare function findLastUsedBrowserTab(sessionId: string, now?: number): Promise<BrowserTabTarget>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getAppConfig } from "../app-config/index.js";
|
|
2
|
+
import { BROWSER_TAB_USED_APP_STATE_KEY } from "../shared/app-state-routes.js";
|
|
3
|
+
import { appStateListUpdatedAt } from "./store.js";
|
|
4
|
+
/** How long after its last use a tab still counts as the one to act on. */
|
|
5
|
+
export const LAST_USED_BROWSER_TAB_MAX_AGE_MS = 12 * 60 * 60 * 1000;
|
|
6
|
+
const SAFE_TAB_ID_RE = /^[A-Za-z0-9_-]{1,96}$/;
|
|
7
|
+
// Several apps can share one database (Design and Slides do), so each app
|
|
8
|
+
// keeps its tabs' records under its own name. The browser's write and the
|
|
9
|
+
// MCP lookup both run in this app's server, so they name it the same way.
|
|
10
|
+
// Encoding keeps distinct names distinct and keeps ":" out of the name.
|
|
11
|
+
function appScope() {
|
|
12
|
+
const app = getAppConfig().app;
|
|
13
|
+
const name = app.workspaceId ?? app.id ?? app.slug ?? app.packageName ?? app.name;
|
|
14
|
+
return name ? encodeURIComponent(name) : "app";
|
|
15
|
+
}
|
|
16
|
+
function browserTabUsedKeyPrefix() {
|
|
17
|
+
return `${BROWSER_TAB_USED_APP_STATE_KEY}:${appScope()}:`;
|
|
18
|
+
}
|
|
19
|
+
/** The row that records a tab's last use: one per app and tab. */
|
|
20
|
+
export function browserTabUsedKey(browserTabId) {
|
|
21
|
+
return `${browserTabUsedKeyPrefix()}${browserTabId}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The tab of this app the user used last, if one was used within
|
|
25
|
+
* LAST_USED_BROWSER_TAB_MAX_AGE_MS. Tabs are ordered by when the server
|
|
26
|
+
* stored their use, so browser clocks don't matter.
|
|
27
|
+
*/
|
|
28
|
+
export async function findLastUsedBrowserTab(sessionId, now = Date.now()) {
|
|
29
|
+
const prefix = browserTabUsedKeyPrefix();
|
|
30
|
+
let best;
|
|
31
|
+
for (const { key, updatedAt } of await appStateListUpdatedAt(sessionId, prefix, now - LAST_USED_BROWSER_TAB_MAX_AGE_MS)) {
|
|
32
|
+
const tabId = key.slice(prefix.length);
|
|
33
|
+
if (!SAFE_TAB_ID_RE.test(tabId))
|
|
34
|
+
continue;
|
|
35
|
+
if (!Number.isFinite(updatedAt))
|
|
36
|
+
continue;
|
|
37
|
+
if (now - updatedAt > LAST_USED_BROWSER_TAB_MAX_AGE_MS)
|
|
38
|
+
continue;
|
|
39
|
+
if (!best || updatedAt > best.usedAt)
|
|
40
|
+
best = { tabId, usedAt: updatedAt };
|
|
41
|
+
}
|
|
42
|
+
return best ? { kind: "last-used", ...best } : { kind: "unavailable" };
|
|
43
|
+
}
|
|
@@ -10,9 +10,35 @@ export declare function listAppState(prefix: string): Promise<Array<{
|
|
|
10
10
|
}>>;
|
|
11
11
|
export declare function deleteAppStateByPrefix(prefix: string): Promise<number>;
|
|
12
12
|
export declare function normalizeBrowserTabId(value: unknown): string | null;
|
|
13
|
+
/** The browser tab the current request came from, if any. */
|
|
13
14
|
export declare function getCurrentRequestBrowserTabId(): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* The tab whose tab-scoped app state the current request reads and writes:
|
|
17
|
+
* the tab it came from or, for an MCP call from no tab, the tab of this app
|
|
18
|
+
* the user used last. `unavailable` when the call looked for a tab and none
|
|
19
|
+
* was used recently. Requests that have neither (scripts, jobs, the CLI) get
|
|
20
|
+
* no tab and keep the account-wide rows.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveCurrentRequestStateTab(): Promise<{
|
|
23
|
+
tabId: string | null;
|
|
24
|
+
unavailable: boolean;
|
|
25
|
+
}>;
|
|
14
26
|
export declare function appStateKeyForBrowserTab(key: string, browserTabId: unknown): string;
|
|
27
|
+
/**
|
|
28
|
+
* Reads `key` for the current request's tab (see
|
|
29
|
+
* resolveCurrentRequestStateTab), falling back to the account-wide row only
|
|
30
|
+
* when `fallbackToGlobal` is true. A request with no tab at all reads the
|
|
31
|
+
* account-wide row. An MCP call that found no recently used tab reads it
|
|
32
|
+
* only when it opted in, and never for the ambient keys: no tab keeps those
|
|
33
|
+
* rows current.
|
|
34
|
+
*/
|
|
15
35
|
export declare function readAppStateForCurrentTab(key: string, options?: {
|
|
16
36
|
fallbackToGlobal?: boolean;
|
|
17
37
|
}): Promise<Record<string, unknown> | null>;
|
|
38
|
+
/**
|
|
39
|
+
* Writes `key` for the current request's tab (see
|
|
40
|
+
* resolveCurrentRequestStateTab). With no tab, it writes the account-wide
|
|
41
|
+
* row, as it always has: an MCP caller learns from the call's result that no
|
|
42
|
+
* tab was found.
|
|
43
|
+
*/
|
|
18
44
|
export declare function writeAppStateForCurrentTab(key: string, value: Record<string, unknown>): Promise<void>;
|
|
@@ -19,30 +19,43 @@ async function resolveSessionId() {
|
|
|
19
19
|
throw new Error("Application state access requires an authenticated request context or AGENT_USER_EMAIL env var");
|
|
20
20
|
}
|
|
21
21
|
export async function readAppState(key) {
|
|
22
|
-
|
|
22
|
+
const scoped = await requestScopedAppStateKey(key);
|
|
23
|
+
// No tab to read from: a call that asked for one gets nothing rather than
|
|
24
|
+
// the account-wide row, which no tab keeps current.
|
|
25
|
+
if (scoped.unavailable)
|
|
26
|
+
return null;
|
|
27
|
+
return readUnscopedAppState(scoped.key);
|
|
23
28
|
}
|
|
24
29
|
export async function writeAppState(key, value) {
|
|
25
30
|
const sessionId = await resolveSessionId();
|
|
26
|
-
|
|
31
|
+
const { key: scopedKey } = await requestScopedAppStateKey(key);
|
|
32
|
+
return appStatePut(sessionId, scopedKey, value, {
|
|
27
33
|
requestSource: "agent",
|
|
28
34
|
});
|
|
29
35
|
}
|
|
30
36
|
export async function deleteAppState(key) {
|
|
31
37
|
const sessionId = await resolveSessionId();
|
|
32
|
-
|
|
38
|
+
const { key: scopedKey } = await requestScopedAppStateKey(key);
|
|
39
|
+
return appStateDelete(sessionId, scopedKey, {
|
|
33
40
|
requestSource: "agent",
|
|
34
41
|
});
|
|
35
42
|
}
|
|
36
43
|
export async function compareAndSetAppState(key, expectedValue, nextValue) {
|
|
37
44
|
const sessionId = await resolveSessionId();
|
|
38
|
-
|
|
45
|
+
const { key: scopedKey } = await requestScopedAppStateKey(key);
|
|
46
|
+
return appStateCompareAndSet(sessionId, scopedKey, expectedValue, nextValue, {
|
|
47
|
+
requestSource: "agent",
|
|
48
|
+
});
|
|
39
49
|
}
|
|
40
50
|
export async function compareAndSetManyAppState(operations) {
|
|
41
51
|
const sessionId = await resolveSessionId();
|
|
42
|
-
|
|
52
|
+
const scopedOperations = await Promise.all(operations.map(async (operation) => ({
|
|
43
53
|
...operation,
|
|
44
|
-
key: requestScopedAppStateKey(operation.key),
|
|
45
|
-
}))
|
|
54
|
+
key: (await requestScopedAppStateKey(operation.key)).key,
|
|
55
|
+
})));
|
|
56
|
+
return appStateCompareAndSetMany(sessionId, scopedOperations, {
|
|
57
|
+
requestSource: "agent",
|
|
58
|
+
});
|
|
46
59
|
}
|
|
47
60
|
export async function listAppState(prefix) {
|
|
48
61
|
const sessionId = await resolveSessionId();
|
|
@@ -69,6 +82,7 @@ export function normalizeBrowserTabId(value) {
|
|
|
69
82
|
const trimmed = value.trim();
|
|
70
83
|
return SAFE_TAB_ID_RE.test(trimmed) ? trimmed : null;
|
|
71
84
|
}
|
|
85
|
+
/** The browser tab the current request came from, if any. */
|
|
72
86
|
export function getCurrentRequestBrowserTabId() {
|
|
73
87
|
try {
|
|
74
88
|
return normalizeBrowserTabId(getRequestRunContext()?.browserTabId);
|
|
@@ -77,32 +91,86 @@ export function getCurrentRequestBrowserTabId() {
|
|
|
77
91
|
return null;
|
|
78
92
|
}
|
|
79
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* The tab whose tab-scoped app state the current request reads and writes:
|
|
96
|
+
* the tab it came from or, for an MCP call from no tab, the tab of this app
|
|
97
|
+
* the user used last. `unavailable` when the call looked for a tab and none
|
|
98
|
+
* was used recently. Requests that have neither (scripts, jobs, the CLI) get
|
|
99
|
+
* no tab and keep the account-wide rows.
|
|
100
|
+
*/
|
|
101
|
+
export async function resolveCurrentRequestStateTab() {
|
|
102
|
+
const browserTabId = getCurrentRequestBrowserTabId();
|
|
103
|
+
if (browserTabId)
|
|
104
|
+
return { tabId: browserTabId, unavailable: false };
|
|
105
|
+
let slot;
|
|
106
|
+
try {
|
|
107
|
+
slot = getRequestRunContext()?.browserTabTarget;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
slot = undefined;
|
|
111
|
+
}
|
|
112
|
+
if (!slot)
|
|
113
|
+
return { tabId: null, unavailable: false };
|
|
114
|
+
const target = await slot.resolve();
|
|
115
|
+
if (target.kind === "last-used") {
|
|
116
|
+
const tabId = normalizeBrowserTabId(target.tabId);
|
|
117
|
+
if (tabId)
|
|
118
|
+
return { tabId, unavailable: false };
|
|
119
|
+
}
|
|
120
|
+
return { tabId: null, unavailable: true };
|
|
121
|
+
}
|
|
80
122
|
export function appStateKeyForBrowserTab(key, browserTabId) {
|
|
81
123
|
const normalized = normalizeBrowserTabId(browserTabId);
|
|
82
124
|
return normalized ? `${key}:${normalized}` : key;
|
|
83
125
|
}
|
|
84
|
-
function requestScopedAppStateKey(key) {
|
|
126
|
+
async function requestScopedAppStateKey(key) {
|
|
85
127
|
if (!TAB_SCOPED_AMBIENT_KEYS.has(key))
|
|
86
|
-
return key;
|
|
87
|
-
|
|
128
|
+
return { key, unavailable: false };
|
|
129
|
+
const tab = await resolveCurrentRequestStateTab();
|
|
130
|
+
return {
|
|
131
|
+
key: appStateKeyForBrowserTab(key, tab.tabId),
|
|
132
|
+
unavailable: tab.unavailable,
|
|
133
|
+
};
|
|
88
134
|
}
|
|
89
135
|
async function readUnscopedAppState(key) {
|
|
90
136
|
const sessionId = await resolveSessionId();
|
|
91
137
|
return appStateGet(sessionId, key);
|
|
92
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Reads `key` for the current request's tab (see
|
|
141
|
+
* resolveCurrentRequestStateTab), falling back to the account-wide row only
|
|
142
|
+
* when `fallbackToGlobal` is true. A request with no tab at all reads the
|
|
143
|
+
* account-wide row. An MCP call that found no recently used tab reads it
|
|
144
|
+
* only when it opted in, and never for the ambient keys: no tab keeps those
|
|
145
|
+
* rows current.
|
|
146
|
+
*/
|
|
93
147
|
export async function readAppStateForCurrentTab(key, options) {
|
|
94
|
-
const
|
|
95
|
-
const tabKey = appStateKeyForBrowserTab(key,
|
|
148
|
+
const tab = await resolveCurrentRequestStateTab();
|
|
149
|
+
const tabKey = appStateKeyForBrowserTab(key, tab.tabId);
|
|
150
|
+
const fallbackToGlobal = options?.fallbackToGlobal === true;
|
|
96
151
|
if (tabKey !== key) {
|
|
97
152
|
const scoped = await readAppState(tabKey);
|
|
98
153
|
if (scoped)
|
|
99
154
|
return scoped;
|
|
100
|
-
if (
|
|
155
|
+
if (!fallbackToGlobal)
|
|
101
156
|
return null;
|
|
102
157
|
}
|
|
158
|
+
if (tab.unavailable &&
|
|
159
|
+
(!fallbackToGlobal || TAB_SCOPED_AMBIENT_KEYS.has(key)))
|
|
160
|
+
return null;
|
|
103
161
|
return readUnscopedAppState(key);
|
|
104
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Writes `key` for the current request's tab (see
|
|
165
|
+
* resolveCurrentRequestStateTab). With no tab, it writes the account-wide
|
|
166
|
+
* row, as it always has: an MCP caller learns from the call's result that no
|
|
167
|
+
* tab was found.
|
|
168
|
+
*/
|
|
105
169
|
export async function writeAppStateForCurrentTab(key, value) {
|
|
106
|
-
const
|
|
107
|
-
|
|
170
|
+
const tab = await resolveCurrentRequestStateTab();
|
|
171
|
+
const tabKey = appStateKeyForBrowserTab(key, tab.tabId);
|
|
172
|
+
if (tabKey !== key)
|
|
173
|
+
return writeAppState(tabKey, value);
|
|
174
|
+
const sessionId = await resolveSessionId();
|
|
175
|
+
return appStatePut(sessionId, key, value, { requestSource: "agent" });
|
|
108
176
|
}
|
|
@@ -46,6 +46,14 @@ export declare function appStateList(sessionId: string, keyPrefix: string): Prom
|
|
|
46
46
|
key: string;
|
|
47
47
|
value: Record<string, unknown>;
|
|
48
48
|
}>>;
|
|
49
|
+
/**
|
|
50
|
+
* Lists the keys under `keyPrefix` with when the server last wrote each,
|
|
51
|
+
* leaving out any written before `writtenSince`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function appStateListUpdatedAt(sessionId: string, keyPrefix: string, writtenSince?: number): Promise<Array<{
|
|
54
|
+
key: string;
|
|
55
|
+
updatedAt: number;
|
|
56
|
+
}>>;
|
|
49
57
|
export declare function appStateListByKeyPrefix(keyPrefix: string, limit?: number): Promise<Array<{
|
|
50
58
|
sessionId: string;
|
|
51
59
|
key: string;
|
|
@@ -253,6 +253,22 @@ export async function appStateList(sessionId, keyPrefix) {
|
|
|
253
253
|
value: JSON.parse(row.value),
|
|
254
254
|
}));
|
|
255
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Lists the keys under `keyPrefix` with when the server last wrote each,
|
|
258
|
+
* leaving out any written before `writtenSince`.
|
|
259
|
+
*/
|
|
260
|
+
export async function appStateListUpdatedAt(sessionId, keyPrefix, writtenSince = 0) {
|
|
261
|
+
await ensureTable();
|
|
262
|
+
const client = getDbExec();
|
|
263
|
+
const { rows } = await client.execute({
|
|
264
|
+
sql: `SELECT key, updated_at FROM application_state WHERE session_id = ? AND key LIKE ? ESCAPE '!' AND updated_at >= ?`,
|
|
265
|
+
args: [sessionId, escapeLike(keyPrefix) + "%", writtenSince],
|
|
266
|
+
});
|
|
267
|
+
return rows.map((row) => ({
|
|
268
|
+
key: row.key,
|
|
269
|
+
updatedAt: Number(row.updated_at),
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
256
272
|
export async function appStateListByKeyPrefix(keyPrefix, limit = 100) {
|
|
257
273
|
await ensureTable();
|
|
258
274
|
const client = getDbExec();
|
|
@@ -13,6 +13,8 @@ export interface AgentSidebarStateChangeDetail {
|
|
|
13
13
|
open: boolean;
|
|
14
14
|
source: AgentSidebarStateSource;
|
|
15
15
|
mode: AgentSidebarStateMode;
|
|
16
|
+
/** The panel draws inside the app's `panelHost`, not beside the page. */
|
|
17
|
+
hosted?: boolean;
|
|
16
18
|
}
|
|
17
19
|
export declare function snapAgentSidebarWidth(width: number): number;
|
|
18
20
|
export declare function getAgentSidebarWideWidth(viewportWidth?: number): number;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records when someone uses this browser tab (it gains focus, or its route
|
|
3
|
+
* changes while focused) and forgets the tab when the page goes away. An MCP
|
|
4
|
+
* tool call comes from no tab, so the server points its tab-scoped app
|
|
5
|
+
* state (navigation, navigate commands, selection) at the tab of this app
|
|
6
|
+
* used last. `useAgentRouteState` calls it; an app with its own navigation
|
|
7
|
+
* sync calls it once with its current route.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useBrowserTabUse(route: string, enabled?: boolean): void;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { BROWSER_TAB_USED_APP_STATE_KEY } from "../shared/app-state-routes.js";
|
|
3
|
+
import { deleteClientAppState, setClientAppState, } from "./application-state.js";
|
|
4
|
+
import { getBrowserTabId } from "./browser-tab-id.js";
|
|
5
|
+
// While the tab keeps focus, moving around in it refreshes its record at most
|
|
6
|
+
// this often. Coming back to the tab from elsewhere always records it.
|
|
7
|
+
const ROUTE_CHANGE_RECORD_INTERVAL_MS = 60_000;
|
|
8
|
+
let mounts = 0;
|
|
9
|
+
let removeListeners;
|
|
10
|
+
let lastRecordedAt = 0;
|
|
11
|
+
// Whether focus has left the page (to another tab, window or app) since the
|
|
12
|
+
// last record. Focus moving into one of the page's own frames doesn't count.
|
|
13
|
+
let leftSinceRecorded = true;
|
|
14
|
+
function hasFocus() {
|
|
15
|
+
return (typeof document !== "undefined" &&
|
|
16
|
+
typeof document.hasFocus === "function" &&
|
|
17
|
+
document.hasFocus());
|
|
18
|
+
}
|
|
19
|
+
// A record that fails to save leaves the tab unfindable, and the MCP call
|
|
20
|
+
// then says no tab was used, so a failed write needs no handling here.
|
|
21
|
+
function record() {
|
|
22
|
+
lastRecordedAt = Date.now();
|
|
23
|
+
leftSinceRecorded = false;
|
|
24
|
+
void setClientAppState(BROWSER_TAB_USED_APP_STATE_KEY, { usedAt: lastRecordedAt }, { requestSource: getBrowserTabId() }).catch(() => { });
|
|
25
|
+
}
|
|
26
|
+
function recordIfReturned() {
|
|
27
|
+
if (leftSinceRecorded ||
|
|
28
|
+
Date.now() - lastRecordedAt >= ROUTE_CHANGE_RECORD_INTERVAL_MS) {
|
|
29
|
+
record();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// A lost delete leaves the record to expire after
|
|
33
|
+
// LAST_USED_BROWSER_TAB_MAX_AGE_MS.
|
|
34
|
+
function forget() {
|
|
35
|
+
leftSinceRecorded = true;
|
|
36
|
+
void deleteClientAppState(BROWSER_TAB_USED_APP_STATE_KEY, {
|
|
37
|
+
keepalive: true,
|
|
38
|
+
requestSource: getBrowserTabId(),
|
|
39
|
+
}).catch(() => { });
|
|
40
|
+
}
|
|
41
|
+
function listen() {
|
|
42
|
+
if (typeof window.addEventListener !== "function" ||
|
|
43
|
+
typeof document === "undefined" ||
|
|
44
|
+
typeof document.addEventListener !== "function") {
|
|
45
|
+
return () => { };
|
|
46
|
+
}
|
|
47
|
+
const onFocus = () => recordIfReturned();
|
|
48
|
+
const onBlur = () => {
|
|
49
|
+
// Focus moving into one of the page's frames blurs the window, but the
|
|
50
|
+
// page still has focus once it settles.
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
if (!hasFocus())
|
|
53
|
+
leftSinceRecorded = true;
|
|
54
|
+
}, 0);
|
|
55
|
+
};
|
|
56
|
+
const onVisibilityChange = () => {
|
|
57
|
+
if (document.visibilityState === "hidden")
|
|
58
|
+
leftSinceRecorded = true;
|
|
59
|
+
else if (hasFocus())
|
|
60
|
+
recordIfReturned();
|
|
61
|
+
};
|
|
62
|
+
const onPageShow = (event) => {
|
|
63
|
+
if (event.persisted && hasFocus())
|
|
64
|
+
record();
|
|
65
|
+
};
|
|
66
|
+
const onPageHide = () => forget();
|
|
67
|
+
window.addEventListener("focus", onFocus);
|
|
68
|
+
window.addEventListener("blur", onBlur);
|
|
69
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
70
|
+
window.addEventListener("pageshow", onPageShow);
|
|
71
|
+
window.addEventListener("pagehide", onPageHide);
|
|
72
|
+
return () => {
|
|
73
|
+
window.removeEventListener("focus", onFocus);
|
|
74
|
+
window.removeEventListener("blur", onBlur);
|
|
75
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
76
|
+
window.removeEventListener("pageshow", onPageShow);
|
|
77
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Records when someone uses this browser tab (it gains focus, or its route
|
|
82
|
+
* changes while focused) and forgets the tab when the page goes away. An MCP
|
|
83
|
+
* tool call comes from no tab, so the server points its tab-scoped app
|
|
84
|
+
* state (navigation, navigate commands, selection) at the tab of this app
|
|
85
|
+
* used last. `useAgentRouteState` calls it; an app with its own navigation
|
|
86
|
+
* sync calls it once with its current route.
|
|
87
|
+
*/
|
|
88
|
+
export function useBrowserTabUse(route, enabled = true) {
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!enabled || typeof window === "undefined")
|
|
91
|
+
return;
|
|
92
|
+
mounts += 1;
|
|
93
|
+
if (mounts === 1) {
|
|
94
|
+
removeListeners = listen();
|
|
95
|
+
if (hasFocus())
|
|
96
|
+
record();
|
|
97
|
+
}
|
|
98
|
+
return () => {
|
|
99
|
+
mounts -= 1;
|
|
100
|
+
if (mounts === 0) {
|
|
101
|
+
removeListeners?.();
|
|
102
|
+
removeListeners = undefined;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}, [enabled]);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!enabled || typeof window === "undefined")
|
|
108
|
+
return;
|
|
109
|
+
if (hasFocus())
|
|
110
|
+
recordIfReturned();
|
|
111
|
+
}, [enabled, route]);
|
|
112
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { agentNativePath, appApiPath, appBasePath, appPath, } from "../api-path.js";
|
|
2
2
|
export { useAgentRouteState, useSemanticNavigationState, type AgentRouteTarget, type AgentRouteLocation, type SemanticNavigationCommandEnvelope, type UseAgentRouteStateOptions, type UseAgentRouteStateResult, type UseSemanticNavigationStateOptions, type UseSemanticNavigationStateResult, type WorkspaceAppRouteTarget, } from "../route-state.js";
|
|
3
|
+
export { useBrowserTabUse } from "../browser-tab-use.js";
|
|
3
4
|
export { AGENT_NATIVE_WORKSPACE_APP_ROUTE_MESSAGE_TYPE, postAgentNativeWorkspaceAppRoute, type AgentNativeWorkspaceAppRouteMessage, } from "../workspace-app-navigation.js";
|
|
4
5
|
export { buildOpenRouteLink, buildOpenRoutePath, buildLegacyAgentSettingsRoute, buildResourceRoute, buildSettingsEntryRoute, buildSettingsRedirectRoute, buildSettingsRoute, buildStandardAppRoute, buildTeamRoute, createStandardOpenPathResolver, resolveLegacySettingsId, resolveSettingsSectionRedirect, SETTINGS_PAGE_IDS, STANDARD_APP_ROUTES, STANDARD_SETTINGS_TABS, type BuildResourceRouteOptions, type BuildSettingsRouteOptions, type CoreSettingsPageId, type SettingsRedirect, type BuildStandardAppRouteOptions, type NavigationLink, type NavigationTarget, type StandardAppRouteId, type StandardOpenPathResolverOptions, type StandardOpenPathRoute, type StandardSettingsTabId, } from "../../navigation/index.js";
|
|
5
6
|
export { getSettingsShortcutHint, isSettingsRoutePath, isSettingsShortcutEvent, OPEN_SETTINGS_PAGE_EVENT, openSettingsPage, SETTINGS_SHORTCUT_PAGE, settingsPagePath, useSettingsShortcut, type OpenSettingsPageDetail, } from "../use-settings-shortcut.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { agentNativePath, appApiPath, appBasePath, appPath, } from "../api-path.js";
|
|
2
2
|
export { useAgentRouteState, useSemanticNavigationState, } from "../route-state.js";
|
|
3
|
+
export { useBrowserTabUse } from "../browser-tab-use.js";
|
|
3
4
|
export { AGENT_NATIVE_WORKSPACE_APP_ROUTE_MESSAGE_TYPE, postAgentNativeWorkspaceAppRoute, } from "../workspace-app-navigation.js";
|
|
4
5
|
export { buildOpenRouteLink, buildOpenRoutePath, buildLegacyAgentSettingsRoute, buildResourceRoute, buildSettingsEntryRoute, buildSettingsRedirectRoute, buildSettingsRoute, buildStandardAppRoute, buildTeamRoute, createStandardOpenPathResolver, resolveLegacySettingsId, resolveSettingsSectionRedirect, SETTINGS_PAGE_IDS, STANDARD_APP_ROUTES, STANDARD_SETTINGS_TABS, } from "../../navigation/index.js";
|
|
5
6
|
export { getSettingsShortcutHint, isSettingsRoutePath, isSettingsShortcutEvent, OPEN_SETTINGS_PAGE_EVENT, openSettingsPage, SETTINGS_SHORTCUT_PAGE, settingsPagePath, useSettingsShortcut, } from "../use-settings-shortcut.js";
|
|
@@ -7,11 +7,21 @@ export interface ListReviewCommentsParams {
|
|
|
7
7
|
includeResolved?: boolean;
|
|
8
8
|
includeDeleted?: boolean;
|
|
9
9
|
targetId?: string | null;
|
|
10
|
+
/** One surface's comments, or `null` for those on no surface. */
|
|
11
|
+
surfaceId?: string | null;
|
|
10
12
|
newestFirst?: boolean;
|
|
11
13
|
limit?: number;
|
|
14
|
+
/** The `nextCursor` from the previous page. */
|
|
15
|
+
cursor?: string;
|
|
12
16
|
}
|
|
13
17
|
export interface ListReviewCommentsResult {
|
|
14
18
|
comments: ReviewComment[];
|
|
19
|
+
/**
|
|
20
|
+
* Oldest first: the next page's cursor, to pass back as `cursor`, or null on
|
|
21
|
+
* the last page. A newest-first list isn't paged and leaves it out; its
|
|
22
|
+
* `summary` still counts every open thread.
|
|
23
|
+
*/
|
|
24
|
+
nextCursor?: string | null;
|
|
15
25
|
discussion: ReviewDiscussionState;
|
|
16
26
|
reviewStatus: ReviewStatusEntry | null;
|
|
17
27
|
summary: {
|
|
@@ -32,8 +42,11 @@ export interface CreateReviewCommentInput {
|
|
|
32
42
|
resourceType: string;
|
|
33
43
|
resourceId: string;
|
|
34
44
|
targetId?: string | null;
|
|
45
|
+
surfaceId?: string | null;
|
|
35
46
|
kind?: ReviewCommentKind;
|
|
36
47
|
anchor?: unknown;
|
|
48
|
+
anchorKind?: string | null;
|
|
49
|
+
anchorVersion?: number | null;
|
|
37
50
|
body: string;
|
|
38
51
|
authorName?: string | null;
|
|
39
52
|
resolutionTarget?: ReviewResolutionTarget | null;
|
|
@@ -120,7 +133,11 @@ export interface UpdateReviewCommentInput {
|
|
|
120
133
|
commentId: string;
|
|
121
134
|
body?: string;
|
|
122
135
|
anchor?: unknown;
|
|
136
|
+
anchorKind?: string | null;
|
|
137
|
+
anchorVersion?: number | null;
|
|
123
138
|
mentions?: ReviewMention[];
|
|
139
|
+
/** Refuse the edit if the comment changed since this revision. */
|
|
140
|
+
expectedRevision?: number;
|
|
124
141
|
}
|
|
125
142
|
export interface ConsumeReviewFeedbackInput {
|
|
126
143
|
resourceType: string;
|
|
@@ -236,6 +253,17 @@ export declare class ReviewOptimisticCache {
|
|
|
236
253
|
* its first comment, so the cache does the same.
|
|
237
254
|
*/
|
|
238
255
|
export declare function markCommentDeleted(comments: ReviewComment[], commentId: string): ReviewComment[];
|
|
256
|
+
/**
|
|
257
|
+
* Adds a new comment to a cached list. Oldest first, it lists last, so a page
|
|
258
|
+
* with more after it is left alone; the comment shows on the last page.
|
|
259
|
+
*/
|
|
260
|
+
export declare function addOptimisticComment(data: unknown, params: unknown, comment: ReviewComment): unknown;
|
|
261
|
+
/**
|
|
262
|
+
* Adds a reply to a cached list that holds its parent. Oldest first, a new
|
|
263
|
+
* reply lists after every comment there is, so like a new comment it goes
|
|
264
|
+
* only on the last page.
|
|
265
|
+
*/
|
|
266
|
+
export declare function addOptimisticReply(data: unknown, params: unknown, parentId: string, reply: (parent: ReviewComment) => ReviewComment): unknown;
|
|
239
267
|
export declare function insertOptimisticComment(comments: ReviewComment[], comment: ReviewComment, params: unknown): ReviewComment[];
|
|
240
268
|
export declare function replaceOptimisticSuggestion(suggestions: ResourceSuggestion[], optimisticId: string, result: ResourceSuggestion): ResourceSuggestion[];
|
|
241
269
|
export declare function reconcileSuggestionAmendment(suggestions: ResourceSuggestion[], saved: ResourceSuggestion): ResourceSuggestion[];
|