@agent-native/core 0.84.54 → 0.84.56

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 (48) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/recap.ts +146 -5
  5. package/corpus/core/src/client/AssistantChat.tsx +58 -32
  6. package/corpus/core/src/client/agent-chat-adapter.ts +14 -4
  7. package/corpus/core/src/client/chat/message-components.tsx +5 -1
  8. package/corpus/core/src/client/sse-event-processor.ts +11 -4
  9. package/corpus/core/src/client/use-agent-engine-configured.ts +54 -12
  10. package/corpus/core/src/server/agent-chat-plugin.ts +9 -5
  11. package/corpus/templates/plan/actions/create-visual-recap.ts +39 -0
  12. package/corpus/templates/plan/actions/list-visual-plans.ts +2 -0
  13. package/corpus/templates/plan/actions/view-screen.ts +2 -0
  14. package/corpus/templates/plan/app/pages/PlansPage.tsx +279 -114
  15. package/corpus/templates/plan/changelog/2026-07-02-plan-comment-shortcuts-now-open-comment-mode-from-the-keyboa.md +6 -0
  16. package/corpus/templates/plan/server/db/schema.ts +3 -0
  17. package/corpus/templates/plan/server/lib/comment-notifications.ts +21 -2
  18. package/corpus/templates/plan/server/plans.ts +6 -0
  19. package/corpus/templates/plan/server/plugins/db.ts +11 -0
  20. package/corpus/templates/plan/shared/comment-context.ts +1 -0
  21. package/corpus/templates/plan/shared/types.ts +4 -0
  22. package/dist/cli/recap.d.ts +14 -0
  23. package/dist/cli/recap.d.ts.map +1 -1
  24. package/dist/cli/recap.js +88 -5
  25. package/dist/cli/recap.js.map +1 -1
  26. package/dist/client/AssistantChat.d.ts.map +1 -1
  27. package/dist/client/AssistantChat.js +33 -20
  28. package/dist/client/AssistantChat.js.map +1 -1
  29. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  30. package/dist/client/agent-chat-adapter.js +12 -2
  31. package/dist/client/agent-chat-adapter.js.map +1 -1
  32. package/dist/client/chat/message-components.d.ts.map +1 -1
  33. package/dist/client/chat/message-components.js +5 -1
  34. package/dist/client/chat/message-components.js.map +1 -1
  35. package/dist/client/sse-event-processor.d.ts.map +1 -1
  36. package/dist/client/sse-event-processor.js +9 -4
  37. package/dist/client/sse-event-processor.js.map +1 -1
  38. package/dist/client/use-agent-engine-configured.d.ts +10 -1
  39. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  40. package/dist/client/use-agent-engine-configured.js +35 -10
  41. package/dist/client/use-agent-engine-configured.js.map +1 -1
  42. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  43. package/dist/notifications/routes.d.ts +3 -3
  44. package/dist/observability/routes.d.ts +4 -4
  45. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  46. package/dist/server/agent-chat-plugin.js +9 -2
  47. package/dist/server/agent-chat-plugin.js.map +1 -1
  48. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2044
31
- - template files: 5003
31
+ - template files: 5004
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.56
4
+
5
+ ### Patch Changes
6
+
7
+ - a0615f8: Make PR visual recap screenshots link directly to the interactive recap and publish source-author metadata for recap comments.
8
+
9
+ ## 0.84.55
10
+
11
+ ### Patch Changes
12
+
13
+ - ea97dc1: Make agent chat setup, auth, and title state resilient to transient status checks and hidden context payloads.
14
+
3
15
  ## 0.84.54
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.54",
3
+ "version": "0.84.56",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -1634,6 +1634,20 @@ type GitHubComment = {
1634
1634
 
1635
1635
  type GitHubPullRequest = {
1636
1636
  head?: { sha?: string | null } | null;
1637
+ user?: { login?: string | null; type?: string | null } | null;
1638
+ };
1639
+
1640
+ type GitHubUserProfile = {
1641
+ login?: string | null;
1642
+ name?: string | null;
1643
+ email?: string | null;
1644
+ };
1645
+
1646
+ type GitHubPullRequestCommit = {
1647
+ author?: { login?: string | null } | null;
1648
+ commit?: {
1649
+ author?: { name?: string | null; email?: string | null } | null;
1650
+ } | null;
1637
1651
  };
1638
1652
 
1639
1653
  function repoParts(repoFullName: string): { owner: string; repo: string } {
@@ -1642,6 +1656,20 @@ function repoParts(repoFullName: string): { owner: string; repo: string } {
1642
1656
  return { owner, repo };
1643
1657
  }
1644
1658
 
1659
+ function nonEmptyTrimmed(value: string | null | undefined): string | undefined {
1660
+ const trimmed = value?.trim();
1661
+ return trimmed || undefined;
1662
+ }
1663
+
1664
+ function normalizeSourceAuthorEmail(
1665
+ email: string | null | undefined,
1666
+ ): string | undefined {
1667
+ const trimmed = email?.trim().toLowerCase();
1668
+ if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) return undefined;
1669
+ if (trimmed.endsWith("@users.noreply.github.com")) return undefined;
1670
+ return trimmed;
1671
+ }
1672
+
1645
1673
  async function githubRequest<T>(
1646
1674
  token: string,
1647
1675
  apiPath: string,
@@ -1667,6 +1695,71 @@ async function githubRequest<T>(
1667
1695
  return (await res.json()) as T;
1668
1696
  }
1669
1697
 
1698
+ export async function resolveGitHubPullRequestAuthor(input: {
1699
+ token: string;
1700
+ repo: string;
1701
+ pr: string;
1702
+ fetchFn?: typeof fetch;
1703
+ }): Promise<{
1704
+ email?: string;
1705
+ name?: string;
1706
+ login?: string;
1707
+ }> {
1708
+ const fn = input.fetchFn ?? fetch;
1709
+ const { owner, repo } = repoParts(input.repo);
1710
+ const pr = await githubRequest<GitHubPullRequest>(
1711
+ input.token,
1712
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(
1713
+ repo,
1714
+ )}/pulls/${encodeURIComponent(input.pr)}`,
1715
+ {},
1716
+ fn,
1717
+ );
1718
+ const login = nonEmptyTrimmed(pr.user?.login);
1719
+ const profile = login
1720
+ ? await githubRequest<GitHubUserProfile>(
1721
+ input.token,
1722
+ `/users/${encodeURIComponent(login)}`,
1723
+ {},
1724
+ fn,
1725
+ ).catch(() => null)
1726
+ : null;
1727
+ const profileEmail = normalizeSourceAuthorEmail(profile?.email);
1728
+ const profileName = nonEmptyTrimmed(profile?.name);
1729
+ let commitEmail: string | undefined;
1730
+ let commitName: string | undefined;
1731
+ if (!profileEmail) {
1732
+ const commits = await githubRequest<GitHubPullRequestCommit[]>(
1733
+ input.token,
1734
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(
1735
+ repo,
1736
+ )}/pulls/${encodeURIComponent(input.pr)}/commits?per_page=100`,
1737
+ {},
1738
+ fn,
1739
+ ).catch(() => []);
1740
+ for (const commit of commits) {
1741
+ const commitLogin = nonEmptyTrimmed(commit.author?.login);
1742
+ if (
1743
+ login &&
1744
+ commitLogin &&
1745
+ commitLogin.toLowerCase() !== login.toLowerCase()
1746
+ ) {
1747
+ continue;
1748
+ }
1749
+ const email = normalizeSourceAuthorEmail(commit.commit?.author?.email);
1750
+ if (!email) continue;
1751
+ commitEmail = email;
1752
+ commitName = nonEmptyTrimmed(commit.commit?.author?.name);
1753
+ break;
1754
+ }
1755
+ }
1756
+ return {
1757
+ email: profileEmail ?? commitEmail,
1758
+ name: profileName ?? commitName ?? login,
1759
+ login,
1760
+ };
1761
+ }
1762
+
1670
1763
  export async function isPullRequestHeadCurrent(input: {
1671
1764
  token: string;
1672
1765
  owner: string;
@@ -1995,14 +2088,15 @@ export function buildCommentBody(env: NodeJS.ProcessEnv = process.env): string {
1995
2088
 
1996
2089
  lines.push(`Here's a [visual recap](${safeUrl}) of what changed:`);
1997
2090
  lines.push("");
1998
- lines.push(`<picture>`);
2091
+ const pictureParts = [`<picture>`];
1999
2092
  if (lightImageUrl && darkImageUrl) {
2000
- lines.push(
2093
+ pictureParts.push(
2001
2094
  ` <source media="(prefers-color-scheme: dark)" srcset="${darkImageUrl}">`,
2002
2095
  );
2003
2096
  }
2004
- lines.push(` <img alt="Visual recap" src="${fallbackImageUrl}">`);
2005
- lines.push(`</picture>`);
2097
+ pictureParts.push(` <img alt="Visual recap" src="${fallbackImageUrl}">`);
2098
+ pictureParts.push(`</picture>`);
2099
+ lines.push(`<a href="${safeUrl}">${pictureParts.join("")}</a>`);
2006
2100
  lines.push("");
2007
2101
  lines.push(`**Open the [full interactive recap](${safeUrl})**`);
2008
2102
  if (env.DIFF_HUGE === "true") {
@@ -2297,6 +2391,7 @@ function recapPublishIdempotencyKey(input: {
2297
2391
  export async function publishRecapSource(input: {
2298
2392
  appUrl: string;
2299
2393
  token: string;
2394
+ githubToken?: string;
2300
2395
  sourcePath?: string;
2301
2396
  out?: string;
2302
2397
  prevPlanId?: string;
@@ -2308,6 +2403,9 @@ export async function publishRecapSource(input: {
2308
2403
  sourcePrNumber?: string;
2309
2404
  sourcePrState?: string;
2310
2405
  sourcePrMergedAt?: string;
2406
+ sourceAuthorEmail?: string;
2407
+ sourceAuthorName?: string;
2408
+ sourceAuthorLogin?: string;
2311
2409
  fetchFn?: typeof fetch;
2312
2410
  cwd?: string;
2313
2411
  }): Promise<{ ok: true; url: string; out: string }> {
@@ -2330,6 +2428,34 @@ export async function publishRecapSource(input: {
2330
2428
  (sourceRepo && sourcePrNumber ? "pull-request" : undefined);
2331
2429
  const sourcePrState =
2332
2430
  input.sourcePrState ?? (input.sourcePrMergedAt ? "merged" : undefined);
2431
+ const explicitSourceAuthor = {
2432
+ email: normalizeSourceAuthorEmail(input.sourceAuthorEmail),
2433
+ name: nonEmptyTrimmed(input.sourceAuthorName),
2434
+ login: nonEmptyTrimmed(input.sourceAuthorLogin),
2435
+ };
2436
+ let resolvedSourceAuthor:
2437
+ | Awaited<ReturnType<typeof resolveGitHubPullRequestAuthor>>
2438
+ | undefined;
2439
+ if (
2440
+ input.githubToken &&
2441
+ sourceRepo &&
2442
+ sourcePrNumber &&
2443
+ (!explicitSourceAuthor.email ||
2444
+ !explicitSourceAuthor.name ||
2445
+ !explicitSourceAuthor.login)
2446
+ ) {
2447
+ resolvedSourceAuthor = await resolveGitHubPullRequestAuthor({
2448
+ token: input.githubToken,
2449
+ repo: sourceRepo,
2450
+ pr: sourcePrNumber,
2451
+ fetchFn: input.fetchFn,
2452
+ }).catch(() => undefined);
2453
+ }
2454
+ const sourceAuthor = {
2455
+ email: explicitSourceAuthor.email ?? resolvedSourceAuthor?.email,
2456
+ name: explicitSourceAuthor.name ?? resolvedSourceAuthor?.name,
2457
+ login: explicitSourceAuthor.login ?? resolvedSourceAuthor?.login,
2458
+ };
2333
2459
  const idempotencyKey = recapPublishIdempotencyKey({
2334
2460
  prevPlanId: input.prevPlanId,
2335
2461
  repo: input.repo,
@@ -2353,6 +2479,9 @@ export async function publishRecapSource(input: {
2353
2479
  ...(input.sourcePrMergedAt
2354
2480
  ? { sourcePrMergedAt: input.sourcePrMergedAt }
2355
2481
  : {}),
2482
+ ...(sourceAuthor.email ? { sourceAuthorEmail: sourceAuthor.email } : {}),
2483
+ ...(sourceAuthor.name ? { sourceAuthorName: sourceAuthor.name } : {}),
2484
+ ...(sourceAuthor.login ? { sourceAuthorLogin: sourceAuthor.login } : {}),
2356
2485
  currentFocus: "visual recap review",
2357
2486
  status: "review",
2358
2487
  mdx: source.mdx,
@@ -2470,6 +2599,10 @@ async function runPublish(
2470
2599
  const result = await publishRecapSource({
2471
2600
  appUrl,
2472
2601
  token,
2602
+ githubToken:
2603
+ optionalArg(args, "github-token") ??
2604
+ process.env.GH_TOKEN ??
2605
+ process.env.GITHUB_TOKEN,
2473
2606
  sourcePath: optionalArg(args, "source") ?? RECAP_SOURCE_FILENAME,
2474
2607
  out,
2475
2608
  prevPlanId: optionalArg(args, "prev-plan-id"),
@@ -2481,6 +2614,14 @@ async function runPublish(
2481
2614
  sourcePrNumber: optionalArg(args, "source-pr-number"),
2482
2615
  sourcePrState: optionalArg(args, "source-pr-state"),
2483
2616
  sourcePrMergedAt: optionalArg(args, "source-pr-merged-at"),
2617
+ sourceAuthorEmail:
2618
+ optionalArg(args, "source-author-email") ?? process.env.PR_AUTHOR_EMAIL,
2619
+ sourceAuthorName:
2620
+ optionalArg(args, "source-author-name") ?? process.env.PR_AUTHOR_NAME,
2621
+ sourceAuthorLogin:
2622
+ optionalArg(args, "source-author-login") ??
2623
+ process.env.PR_AUTHOR_LOGIN ??
2624
+ process.env.GITHUB_ACTOR,
2484
2625
  });
2485
2626
  writeGitHubOutput("ok", "true");
2486
2627
  writeGitHubOutput("plan_url", result.url);
@@ -4081,7 +4222,7 @@ Usage:
4081
4222
  npx @agent-native/core@latest recap block-reference [--app-url <url>] [--out recap-blocks.md]
4082
4223
  npx @agent-native/core@latest recap scan --diff <path> [--mode off|high-confidence|strict]
4083
4224
  npx @agent-native/core@latest recap build-prompt --pr <n> [--repo owner/name] [--head <sha>] [--app-url <url>] [--diff <path>] [--stat <path>] [--block-reference recap-blocks.md] [--prev-plan-id <id>] [--huge] [--local-files] [--local-dir <folder>] [--skill-source auto|latest|repo] [--out <path>]
4084
- npx @agent-native/core@latest recap publish [--source recap-source.json] [--out recap-url.txt] [--repo owner/name] [--pr <n>] [--prev-plan-id <id>] [--source-pr-state open|closed|merged] [--source-pr-merged-at <iso>] [--app-url <url>] [--token <planToken>]
4225
+ npx @agent-native/core@latest recap publish [--source recap-source.json] [--out recap-url.txt] [--repo owner/name] [--pr <n>] [--prev-plan-id <id>] [--source-pr-state open|closed|merged] [--source-pr-merged-at <iso>] [--source-author-email <email>] [--source-author-name <name>] [--source-author-login <login>] [--app-url <url>] [--token <planToken>] [--github-token <ghToken>]
4085
4226
  npx @agent-native/core@latest recap shot --url <planUrl> [--token <planToken>] [--app-url <url>] [--out recap.png] [--theme light|dark] [--image-cache-key <key>]
4086
4227
  npx @agent-native/core@latest recap usage --plan-url <planUrl> --result-file <path> --app-url <url> --token <planToken> [--agent claude|codex] [--model <id>]
4087
4228
  npx @agent-native/core@latest recap agent-summary --result-file <path> [--stderr-file <path>] [--exit-code-file <path>] [--agent claude|codex]
@@ -89,6 +89,7 @@ import {
89
89
  AssistantMessage,
90
90
  SelectionAttachedPill,
91
91
  RunningActivityStatus,
92
+ displayableUserMessageText,
92
93
  } from "./chat/message-components.js";
93
94
  import {
94
95
  repoHasAssistantMessage,
@@ -178,6 +179,8 @@ export {
178
179
 
179
180
  export { displayableUserMessageText } from "./chat/message-components.js";
180
181
 
182
+ type AuthSessionCheckResult = "available" | "missing" | "unknown";
183
+
181
184
  const useBrowserLayoutEffect =
182
185
  typeof window === "undefined" ? useEffect : useLayoutEffect;
183
186
 
@@ -492,13 +495,14 @@ function getMessageText(message: unknown): string {
492
495
  const msg = (message as { message?: unknown })?.message ?? message;
493
496
  const content = (msg as { content?: unknown })?.content;
494
497
  if (Array.isArray(content)) {
495
- return content
496
- .filter((p: any) => p?.type === "text" && typeof p.text === "string")
497
- .map((p: any) => p.text)
498
- .join("\n")
499
- .trim();
498
+ return displayableUserMessageText(
499
+ content
500
+ .filter((p: any) => p?.type === "text" && typeof p.text === "string")
501
+ .map((p: any) => p.text)
502
+ .join("\n"),
503
+ );
500
504
  }
501
- return typeof content === "string" ? content.trim() : "";
505
+ return typeof content === "string" ? displayableUserMessageText(content) : "";
502
506
  }
503
507
 
504
508
  function contentPartFollowKey(part: any): string {
@@ -1263,6 +1267,7 @@ const AssistantChatInner = forwardRef<
1263
1267
  }, [threadRuntime]);
1264
1268
  const agentEngineConfigured = useAgentEngineConfigured(
1265
1269
  providerStatusChecksEnabled,
1270
+ { tabId, threadId },
1266
1271
  );
1267
1272
  const missingApiKey = agentEngineConfigured.missing;
1268
1273
  const isComposerDisabled = missingApiKey || composerDisabled;
@@ -1294,10 +1299,19 @@ const AssistantChatInner = forwardRef<
1294
1299
  setComposerError(LLM_MISSING_CREDENTIALS_MESSAGE);
1295
1300
  setMissingKeyBouncePulse((p) => p + 1);
1296
1301
  if (typeof window !== "undefined") {
1297
- window.dispatchEvent(new Event("agent-chat:missing-api-key"));
1302
+ window.dispatchEvent(
1303
+ new CustomEvent("agent-chat:missing-api-key", {
1304
+ detail: { tabId, threadId },
1305
+ }),
1306
+ );
1298
1307
  }
1299
1308
  return false;
1300
- }, [agentEngineConfigured.state, providerStatusChecksEnabled]);
1309
+ }, [
1310
+ agentEngineConfigured.state,
1311
+ providerStatusChecksEnabled,
1312
+ tabId,
1313
+ threadId,
1314
+ ]);
1301
1315
  const [authError, setAuthError] = useState<{
1302
1316
  sessionExpired?: boolean;
1303
1317
  } | null>(null);
@@ -2443,23 +2457,31 @@ const AssistantChatInner = forwardRef<
2443
2457
  }, []);
2444
2458
 
2445
2459
  // Listen for auth error events from the adapter
2446
- const checkAuthSession = useCallback(async () => {
2447
- try {
2448
- const res = await fetch(agentNativePath("/_agent-native/auth/session"), {
2449
- cache: "no-store",
2450
- });
2451
- if (!res.ok) return false;
2452
- const data = await res.json().catch(() => null);
2453
- const hasSession = !!data && !data.error;
2454
- setAuthSessionAvailable(hasSession);
2455
- if (hasSession) {
2456
- setAuthError(null);
2460
+ const checkAuthSession =
2461
+ useCallback(async (): Promise<AuthSessionCheckResult> => {
2462
+ try {
2463
+ const res = await fetch(
2464
+ agentNativePath("/_agent-native/auth/session"),
2465
+ {
2466
+ cache: "no-store",
2467
+ },
2468
+ );
2469
+ if (!res.ok) {
2470
+ return res.status === 401 || res.status === 403
2471
+ ? "missing"
2472
+ : "unknown";
2473
+ }
2474
+ const data = await res.json().catch(() => null);
2475
+ const hasSession = !!data && !data.error;
2476
+ setAuthSessionAvailable(hasSession);
2477
+ if (hasSession) {
2478
+ setAuthError(null);
2479
+ }
2480
+ return hasSession ? "available" : "missing";
2481
+ } catch {
2482
+ return "unknown";
2457
2483
  }
2458
- return hasSession;
2459
- } catch {
2460
- return false;
2461
- }
2462
- }, []);
2484
+ }, []);
2463
2485
 
2464
2486
  useEffect(() => {
2465
2487
  const handler = (e: Event) => {
@@ -2481,9 +2503,12 @@ const AssistantChatInner = forwardRef<
2481
2503
  ) {
2482
2504
  return;
2483
2505
  }
2484
- setAuthSessionAvailable(false);
2485
- setAuthError({ sessionExpired: detail?.reason === "session-expired" });
2486
- void checkAuthSession();
2506
+ void (async () => {
2507
+ const sessionState = await checkAuthSession();
2508
+ if (sessionState !== "missing") return;
2509
+ setAuthSessionAvailable(false);
2510
+ setAuthError({ sessionExpired: detail?.reason === "session-expired" });
2511
+ })();
2487
2512
  };
2488
2513
  window.addEventListener("agent-chat:auth-error", handler);
2489
2514
  return () => window.removeEventListener("agent-chat:auth-error", handler);
@@ -2499,8 +2524,9 @@ const AssistantChatInner = forwardRef<
2499
2524
  // symptom we want signal on.
2500
2525
  const stuckCapture = window.setTimeout(() => {
2501
2526
  void (async () => {
2502
- const hasSession = await checkAuthSession();
2503
- if (hasSession) return;
2527
+ const sessionState = await checkAuthSession();
2528
+ if (sessionState === "available") return;
2529
+ if (sessionState !== "missing") return;
2504
2530
  if (!shouldCaptureStuckAuthCard) return;
2505
2531
  captureError(new Error("agent-chat:auth_error_card_stuck"), {
2506
2532
  tags: {
@@ -3790,9 +3816,9 @@ const AssistantChatInner = forwardRef<
3790
3816
  <RunningActivityStatus label={runningStatusLabel} />
3791
3817
  )}
3792
3818
  {queuedMessages.map((msg) => {
3793
- const displayText = msg.text
3794
- .replace(/<context>[\s\S]*?<\/context>\n?/g, "")
3795
- .trim();
3819
+ const displayText = displayableUserMessageText(
3820
+ msg.text,
3821
+ );
3796
3822
  return (
3797
3823
  <div
3798
3824
  key={msg.id}
@@ -1526,6 +1526,18 @@ export function createAgentChatAdapter(
1526
1526
  );
1527
1527
  };
1528
1528
 
1529
+ const dispatchMissingApiKey = () => {
1530
+ if (typeof window === "undefined") return;
1531
+ window.dispatchEvent(
1532
+ new CustomEvent("agent-chat:missing-api-key", {
1533
+ detail: {
1534
+ ...(tabId ? { tabId } : {}),
1535
+ ...(threadId ? { threadId } : {}),
1536
+ },
1537
+ }),
1538
+ );
1539
+ };
1540
+
1529
1541
  const tryRecoverAuthOnce = async (): Promise<boolean> => {
1530
1542
  if (authRecoveryAttempted || abortSignal.aborted) return false;
1531
1543
  authRecoveryAttempted = true;
@@ -2418,9 +2430,7 @@ export function createAgentChatAdapter(
2418
2430
  if (isMissingCredentialMessage(body)) {
2419
2431
  const failure = missingCredentialFailure(body);
2420
2432
  if (typeof window !== "undefined") {
2421
- window.dispatchEvent(
2422
- new Event("agent-chat:missing-api-key"),
2423
- );
2433
+ dispatchMissingApiKey();
2424
2434
  window.dispatchEvent(
2425
2435
  new CustomEvent("agent-chat:run-error", {
2426
2436
  detail: { ...failure.runError, tabId },
@@ -2633,7 +2643,7 @@ export function createAgentChatAdapter(
2633
2643
  if (isMissingCredentialMessage(errMsg)) {
2634
2644
  const failure = missingCredentialFailure(errMsg);
2635
2645
  if (typeof window !== "undefined") {
2636
- window.dispatchEvent(new Event("agent-chat:missing-api-key"));
2646
+ dispatchMissingApiKey();
2637
2647
  window.dispatchEvent(
2638
2648
  new CustomEvent("agent-chat:run-error", {
2639
2649
  detail: { ...failure.runError, tabId },
@@ -75,7 +75,11 @@ const PENDING_SELECTION_KEY = "pending-selection-context";
75
75
  // ─── displayableUserMessageText ───────────────────────────────────────────────
76
76
 
77
77
  export function displayableUserMessageText(text: string): string {
78
- return text.replace(/<context>[\s\S]*?<\/context>\n?/g, "").trim();
78
+ return text
79
+ .replace(/<context\b[^>]*>[\s\S]*?<\/context>\n?/gi, "")
80
+ .replace(/<context\b[^>]*>[\s\S]*$/gi, "")
81
+ .replace(/<\/context>/gi, "")
82
+ .trim();
79
83
  }
80
84
 
81
85
  // ─── Message timestamp helpers ────────────────────────────────────────────────
@@ -601,6 +601,15 @@ function dispatchActivityClear(tabId: string | undefined) {
601
601
  );
602
602
  }
603
603
 
604
+ function dispatchMissingApiKey(tabId: string | undefined) {
605
+ if (typeof window === "undefined") return;
606
+ window.dispatchEvent(
607
+ new CustomEvent("agent-chat:missing-api-key", {
608
+ detail: { tabId },
609
+ }),
610
+ );
611
+ }
612
+
604
613
  function pendingToolNames(content: ContentPart[]): {
605
614
  activity: string[];
606
615
  running: string[];
@@ -1056,7 +1065,7 @@ export function processEvent(
1056
1065
  errorCode,
1057
1066
  };
1058
1067
  if (typeof window !== "undefined") {
1059
- window.dispatchEvent(new Event("agent-chat:missing-api-key"));
1068
+ dispatchMissingApiKey(tabId);
1060
1069
  window.dispatchEvent(
1061
1070
  new CustomEvent("agent-chat:run-error", {
1062
1071
  detail: { ...runError, tabId },
@@ -1156,9 +1165,7 @@ export function processEvent(
1156
1165
  }
1157
1166
  const normalized = normalizeChatError(errMsg, ev.errorCode);
1158
1167
  if (isMissingCredentialText(errMsg, ev.errorCode)) {
1159
- if (typeof window !== "undefined") {
1160
- window.dispatchEvent(new Event("agent-chat:missing-api-key"));
1161
- }
1168
+ dispatchMissingApiKey(tabId);
1162
1169
  }
1163
1170
  const runError = {
1164
1171
  message: normalized.message,
@@ -15,10 +15,20 @@ export interface UseAgentEngineConfiguredResult {
15
15
  }
16
16
 
17
17
  export interface FetchAgentEngineConfiguredStateOptions {
18
+ /**
19
+ * Legacy hint from explicit missing-key stream events. Kept for API
20
+ * compatibility, but missing state still requires authoritative status
21
+ * responses so transient endpoint failures do not clobber connected state.
22
+ */
18
23
  missingFallback?: boolean;
19
24
  timeoutMs?: number;
20
25
  }
21
26
 
27
+ export interface UseAgentEngineConfiguredOptions {
28
+ tabId?: string | null;
29
+ threadId?: string | null;
30
+ }
31
+
22
32
  const DEFAULT_STATUS_CHECK_TIMEOUT_MS = 2500;
23
33
 
24
34
  async function fetchStatusJson(
@@ -60,6 +70,27 @@ function hasConfiguredFlag(value: unknown): value is { configured: boolean } {
60
70
  );
61
71
  }
62
72
 
73
+ function missingKeyEventMatchesScope(
74
+ event: Event,
75
+ options: UseAgentEngineConfiguredOptions | undefined,
76
+ ): boolean {
77
+ const detail = (event as CustomEvent).detail as
78
+ | { tabId?: unknown; threadId?: unknown }
79
+ | undefined;
80
+ const eventTabId = typeof detail?.tabId === "string" ? detail.tabId : null;
81
+ const eventThreadId =
82
+ typeof detail?.threadId === "string" ? detail.threadId : null;
83
+ if (!eventTabId && !eventThreadId) return true;
84
+
85
+ const tabId = options?.tabId ?? null;
86
+ const threadId = options?.threadId ?? null;
87
+ if (!tabId && !threadId) return true;
88
+ return (
89
+ (eventTabId != null && eventTabId === tabId) ||
90
+ (eventThreadId != null && eventThreadId === threadId)
91
+ );
92
+ }
93
+
63
94
  export async function fetchAgentEngineConfiguredState(
64
95
  enabled = true,
65
96
  options?: FetchAgentEngineConfiguredStateOptions,
@@ -76,22 +107,31 @@ export async function fetchAgentEngineConfiguredState(
76
107
  fetchStatusJson("/_agent-native/agent-engine/status", timeoutMs),
77
108
  ]);
78
109
 
79
- // All three failed — likely a flaky network; keep the caller in unknown
80
- // unless this check is reacting to an explicit missing-key stream event.
110
+ // All three failed — likely a flaky network; keep the caller in unknown.
111
+ // Even an explicit missing-key stream event should not pin the composer into
112
+ // setup without a fresh authoritative status response.
81
113
  if (envKeys == null && builderStatus == null && engineStatus == null) {
82
- return options?.missingFallback ? "missing" : "unknown";
114
+ return "unknown";
83
115
  }
84
116
 
85
- const keys = (envKeys ?? []) as Array<{
86
- key: string;
87
- configured: boolean;
88
- }>;
117
+ const envKeysKnown = Array.isArray(envKeys);
118
+ const builderStatusKnown = hasConfiguredFlag(builderStatus);
119
+ const engineStatusKnown = hasConfiguredFlag(engineStatus);
120
+ const keys = envKeysKnown
121
+ ? (envKeys as Array<{
122
+ key: string;
123
+ configured: boolean;
124
+ }>)
125
+ : [];
89
126
  const llmKeys = keys.filter((k) => PROVIDER_ENV_VAR_SET.has(k.key));
90
127
  const anyConfigured =
91
128
  llmKeys.some((k) => k.configured) ||
92
- (hasConfiguredFlag(builderStatus) && builderStatus.configured) ||
93
- (hasConfiguredFlag(engineStatus) && engineStatus.configured);
94
- return anyConfigured ? "configured" : "missing";
129
+ (builderStatusKnown && builderStatus.configured) ||
130
+ (engineStatusKnown && engineStatus.configured);
131
+ if (anyConfigured) return "configured";
132
+ return envKeysKnown && builderStatusKnown && engineStatusKnown
133
+ ? "missing"
134
+ : "unknown";
95
135
  }
96
136
 
97
137
  /**
@@ -103,6 +143,7 @@ export async function fetchAgentEngineConfiguredState(
103
143
  */
104
144
  export function useAgentEngineConfigured(
105
145
  enabled = true,
146
+ options?: UseAgentEngineConfiguredOptions,
106
147
  ): UseAgentEngineConfiguredResult {
107
148
  const [state, setState] = useState<AgentEngineConfiguredState>("unknown");
108
149
 
@@ -119,7 +160,8 @@ export function useAgentEngineConfigured(
119
160
  const onConfiguredChanged = () => {
120
161
  void check();
121
162
  };
122
- const onMissing = () => {
163
+ const onMissing = (event: Event) => {
164
+ if (!missingKeyEventMatchesScope(event, options)) return;
123
165
  if (!enabled) {
124
166
  setState("configured");
125
167
  return;
@@ -143,7 +185,7 @@ export function useAgentEngineConfigured(
143
185
  );
144
186
  window.removeEventListener("agent-chat:missing-api-key", onMissing);
145
187
  };
146
- }, [enabled]);
188
+ }, [enabled, options?.tabId, options?.threadId]);
147
189
 
148
190
  return { missing: state === "missing", state };
149
191
  }
@@ -7124,11 +7124,15 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
7124
7124
  setResponseStatus(event, 400);
7125
7125
  return { error: "message is required" };
7126
7126
  }
7127
- // Strip mention markup: @[Name|type] @Name
7128
- const cleanMessage = message.replace(
7129
- /@\[([^\]|]+)\|[^\]]*\]/g,
7130
- "@$1",
7131
- );
7127
+ // Strip hidden context and mention markup before title generation.
7128
+ // Fallback titles are often direct truncations, so never let injected
7129
+ // prompt context become a visible tab label.
7130
+ const cleanMessage = message
7131
+ .replace(/<context\b[^>]*>[\s\S]*?<\/context>\n?/gi, "")
7132
+ .replace(/<context\b[^>]*>[\s\S]*$/gi, "")
7133
+ .replace(/<\/context>/gi, "")
7134
+ .replace(/@\[([^\]|]+)\|[^\]]*\]/g, "@$1")
7135
+ .trim();
7132
7136
  // Mirror the chat-run resolution so BYO-key users have title
7133
7137
  // generation billed to their own key instead of the platform key.
7134
7138
  const { getOwnerActiveApiKey } =