@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.
Files changed (84) hide show
  1. package/dist/agent/reaped-run-thread.js +6 -0
  2. package/dist/application-state/handlers.d.ts +3 -3
  3. package/dist/application-state/handlers.js +6 -1
  4. package/dist/application-state/index.d.ts +3 -2
  5. package/dist/application-state/index.js +3 -2
  6. package/dist/application-state/last-used-tab.d.ts +11 -0
  7. package/dist/application-state/last-used-tab.js +43 -0
  8. package/dist/application-state/script-helpers.d.ts +26 -0
  9. package/dist/application-state/script-helpers.js +83 -15
  10. package/dist/application-state/store.d.ts +8 -0
  11. package/dist/application-state/store.js +16 -0
  12. package/dist/client/agent-sidebar-state.d.ts +2 -0
  13. package/dist/client/browser-tab-use.d.ts +9 -0
  14. package/dist/client/browser-tab-use.js +112 -0
  15. package/dist/client/navigation/index.d.ts +1 -0
  16. package/dist/client/navigation/index.js +1 -0
  17. package/dist/client/review/use-review.d.ts +28 -0
  18. package/dist/client/review/use-review.js +41 -12
  19. package/dist/client/route-state.js +2 -0
  20. package/dist/client/use-db-sync.d.ts +8 -1
  21. package/dist/client/use-db-sync.js +6 -5
  22. package/dist/collab/routes.d.ts +1 -1
  23. package/dist/collab/struct-routes.d.ts +1 -1
  24. package/dist/db/schema-snapshot.generated.js +6 -1
  25. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  26. package/dist/file-upload/index.d.ts +1 -1
  27. package/dist/file-upload/index.js +1 -1
  28. package/dist/file-upload/s3.d.ts +9 -0
  29. package/dist/file-upload/s3.js +14 -1
  30. package/dist/framework-package-versions.json +7 -7
  31. package/dist/jobs/scheduler.js +4 -35
  32. package/dist/mcp/browser-tab-target.d.ts +16 -0
  33. package/dist/mcp/browser-tab-target.js +58 -0
  34. package/dist/mcp/build-server.js +9 -2
  35. package/dist/notifications/routes.d.ts +3 -3
  36. package/dist/org/handlers.d.ts +1 -1
  37. package/dist/progress/routes.d.ts +1 -1
  38. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  39. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  40. package/dist/resources/handlers.d.ts +1 -1
  41. package/dist/review/actions/create-review-comment.d.ts +7 -0
  42. package/dist/review/actions/create-review-comment.js +29 -3
  43. package/dist/review/actions/delete-review-comment.js +2 -2
  44. package/dist/review/actions/list-review-comments.d.ts +3 -0
  45. package/dist/review/actions/list-review-comments.js +41 -17
  46. package/dist/review/actions/react-to-review-comment.js +2 -2
  47. package/dist/review/actions/reply-review-comment.d.ts +4 -0
  48. package/dist/review/actions/reply-review-comment.js +15 -6
  49. package/dist/review/actions/update-review-comment-anchor.d.ts +6 -0
  50. package/dist/review/actions/update-review-comment-anchor.js +39 -10
  51. package/dist/review/actions/update-review-comment.d.ts +3 -0
  52. package/dist/review/actions/update-review-comment.js +28 -3
  53. package/dist/review/errors.d.ts +33 -0
  54. package/dist/review/errors.js +51 -0
  55. package/dist/review/index.d.ts +5 -4
  56. package/dist/review/index.js +3 -2
  57. package/dist/review/registry.d.ts +51 -1
  58. package/dist/review/registry.js +117 -0
  59. package/dist/review/store.d.ts +59 -14
  60. package/dist/review/store.js +622 -101
  61. package/dist/review/suggestions/actions.js +10 -8
  62. package/dist/review/types.d.ts +89 -0
  63. package/dist/search/query.d.ts +11 -5
  64. package/dist/search/query.js +52 -15
  65. package/dist/search/tokenize.d.ts +1 -0
  66. package/dist/search/tokenize.js +3 -0
  67. package/dist/secrets/routes.d.ts +2 -2
  68. package/dist/server/request-context.d.ts +26 -0
  69. package/dist/shared/app-state-routes.d.ts +7 -0
  70. package/dist/shared/app-state-routes.js +7 -0
  71. package/dist/shared/runtime-config.js +5 -1
  72. package/dist/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
  73. package/dist/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
  74. package/dist/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +13 -0
  75. package/dist/triggers/condition-evaluator.d.ts +2 -0
  76. package/dist/triggers/condition-evaluator.js +93 -3
  77. package/dist/triggers/dispatcher.js +24 -6
  78. package/docs/content/create-with-an-agent.mdx +10 -4
  79. package/docs/content/durable-resume.mdx +11 -2
  80. package/docs/content/toolkit-comments-review.mdx +54 -1
  81. package/package.json +3 -3
  82. package/src/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
  83. package/src/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
  84. 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
- return readUnscopedAppState(requestScopedAppStateKey(key));
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
- return appStatePut(sessionId, requestScopedAppStateKey(key), value, {
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
- return appStateDelete(sessionId, requestScopedAppStateKey(key), {
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
- return appStateCompareAndSet(sessionId, requestScopedAppStateKey(key), expectedValue, nextValue, { requestSource: "agent" });
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
- return appStateCompareAndSetMany(sessionId, operations.map((operation) => ({
52
+ const scopedOperations = await Promise.all(operations.map(async (operation) => ({
43
53
  ...operation,
44
- key: requestScopedAppStateKey(operation.key),
45
- })), { requestSource: "agent" });
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
- return appStateKeyForBrowserTab(key, getCurrentRequestBrowserTabId());
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 browserTabId = getCurrentRequestBrowserTabId();
95
- const tabKey = appStateKeyForBrowserTab(key, browserTabId);
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 (options?.fallbackToGlobal !== true)
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 tabKey = appStateKeyForBrowserTab(key, getCurrentRequestBrowserTabId());
107
- return writeAppState(tabKey, value);
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[];