@agent-native/core 0.149.0 → 0.149.2

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 (134) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +2 -0
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +150 -46
  4. package/corpus/templates/analytics/app/global.css +1 -1
  5. package/corpus/templates/analytics/app/i18n-data.ts +37 -0
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
  7. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -0
  8. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
  9. package/corpus/templates/analytics/changelog/2026-08-10-dashboard-tables-can-be-copied-and-sorted-by-multiple-column.md +6 -0
  10. package/corpus/templates/analytics/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  11. package/corpus/templates/assets/app/global.css +1 -1
  12. package/corpus/templates/assets/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  13. package/corpus/templates/brain/app/global.css +1 -1
  14. package/corpus/templates/brain/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  15. package/corpus/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  16. package/corpus/templates/clips/changelog/2026-08-10-clips-desktop-sign-in-now-starts-with-magic-links-and-google.md +6 -0
  17. package/corpus/templates/clips/chrome-extension/src/overlay.css +1 -0
  18. package/corpus/templates/clips/desktop/src/app.tsx +274 -137
  19. package/corpus/templates/clips/desktop/src/styles.css +35 -0
  20. package/corpus/templates/content/app/global.css +1 -0
  21. package/corpus/templates/crm/app/global.css +1 -1
  22. package/corpus/templates/crm/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  23. package/corpus/templates/dispatch/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  24. package/corpus/templates/factory/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  25. package/corpus/templates/forms/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  26. package/corpus/templates/plan/app/global.css +1 -0
  27. package/corpus/templates/plan/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  28. package/corpus/templates/plan/server/ui-plan-html.ts +4 -1
  29. package/corpus/templates/slides/AGENTS.md +11 -10
  30. package/corpus/templates/slides/actions/add-deck.ts +2 -0
  31. package/corpus/templates/slides/actions/add-slide.ts +15 -1
  32. package/corpus/templates/slides/actions/create-deck.ts +18 -6
  33. package/corpus/templates/slides/actions/get-deck.ts +29 -1
  34. package/corpus/templates/slides/actions/patch-deck.ts +139 -10
  35. package/corpus/templates/slides/actions/save-deck.ts +24 -1
  36. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +46 -0
  37. package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +194 -3
  38. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +10 -1
  39. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
  40. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +74 -63
  41. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
  42. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
  43. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
  44. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
  45. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -7
  46. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
  47. package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
  48. package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
  49. package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
  50. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
  51. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
  52. package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
  53. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  54. package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
  55. package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
  56. package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
  57. package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
  58. package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
  59. package/corpus/templates/slides/app/pages/DeckEditor.tsx +16 -27
  60. package/corpus/templates/slides/app/pages/Index.tsx +256 -98
  61. package/corpus/templates/slides/changelog/2026-08-08-google-slides-export-now-tells-you-why-a-deck-fell-back-to-a.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-09-delete-key-now-removes-a-selected-image.md +6 -0
  63. package/corpus/templates/slides/changelog/2026-08-09-editing-a-slide-in-an-imported-deck-no-longer-fails-to-save.md +6 -0
  64. package/corpus/templates/slides/changelog/2026-08-09-fixed-pdf-and-custom-size-powerpoint-imports-rendering-with-.md +6 -0
  65. package/corpus/templates/slides/changelog/2026-08-09-images-from-sites-without-cors-no-longer-export-blank.md +6 -0
  66. package/corpus/templates/slides/changelog/2026-08-09-imported-pdf-headings-no-longer-lose-the-space-between-color.md +6 -0
  67. package/corpus/templates/slides/changelog/2026-08-09-imported-slide-text-no-longer-renders-too-large-for-its-box.md +6 -0
  68. package/corpus/templates/slides/changelog/2026-08-09-large-decks-no-longer-render-slowly-or-glitch-in-the-editor.md +6 -0
  69. package/corpus/templates/slides/changelog/2026-08-09-revise-highlighted-slide-text-with-ai.md +6 -0
  70. package/corpus/templates/slides/changelog/2026-08-09-the-deck-editor-no-longer-stays-dimmed-after-the-window-narro.md +6 -0
  71. package/corpus/templates/slides/changelog/2026-08-10-ai-editing-avatar-badge.md +6 -0
  72. package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
  73. package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
  74. package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
  75. package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
  76. package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
  77. package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -0
  79. package/corpus/templates/slides/server/handlers/import/html-converter.ts +115 -22
  80. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +11 -2
  81. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  82. package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
  83. package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
  84. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
  85. package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
  86. package/corpus/templates/slides/shared/api.ts +2 -1
  87. package/corpus/templates/slides/shared/deck-title.ts +135 -0
  88. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
  89. package/dist/agent/engine/ai-sdk-engine.js +1 -1
  90. package/dist/agent/production-agent.d.ts +21 -1
  91. package/dist/agent/production-agent.js +129 -10
  92. package/dist/agent/tool-approval-store.d.ts +18 -0
  93. package/dist/agent/tool-approval-store.js +122 -0
  94. package/dist/agent/types.d.ts +1 -1
  95. package/dist/cli/code-agent-executor.js +11 -2
  96. package/dist/client/AgentPanel.js +31 -6
  97. package/dist/client/AssistantChat.d.ts +3 -2
  98. package/dist/client/AssistantChat.js +7 -20
  99. package/dist/client/agent-chat/index.d.ts +1 -1
  100. package/dist/client/agent-chat-adapter.js +11 -0
  101. package/dist/client/agent-chat.d.ts +1 -1
  102. package/dist/client/chat/markdown-renderer.js +14 -2
  103. package/dist/client/chat/message-components.d.ts +6 -2
  104. package/dist/client/chat/message-components.js +25 -47
  105. package/dist/client/chat/tool-call-display.d.ts +3 -5
  106. package/dist/client/chat/tool-call-display.js +7 -212
  107. package/dist/client/chat-first/apps-rail.js +3 -3
  108. package/dist/client/chat-first/browser-pane.js +4 -1
  109. package/dist/client/chat-first/chat-history.d.ts +3 -1
  110. package/dist/client/chat-first/chat-history.js +2 -2
  111. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  112. package/dist/client/chat-first/surface-tabs.js +52 -41
  113. package/dist/client/chat-first/types.d.ts +3 -0
  114. package/dist/client/chat-first.d.ts +10 -0
  115. package/dist/client/chat-first.js +37 -2
  116. package/dist/client/components/ui/message-scroller.js +4 -1
  117. package/dist/client/i18n.js +3 -1
  118. package/dist/client/progress/RunsTray.js +14 -1
  119. package/dist/client/resources/McpConnectionSuggestion.js +9 -1
  120. package/dist/client/sse-event-processor.js +10 -0
  121. package/dist/client/use-chat-models.js +156 -80
  122. package/dist/integrations/webhook-handler.js +1 -1
  123. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  124. package/dist/secrets/routes.d.ts +6 -6
  125. package/dist/server/auth.js +262 -31
  126. package/dist/server/onboarding-html.js +56 -9
  127. package/dist/shared/reasoning-effort.d.ts +8 -8
  128. package/dist/shared/reasoning-effort.js +10 -10
  129. package/dist/shared/streaming-text-smoothing.js +41 -11
  130. package/dist/styles/agent-native.css +19 -32
  131. package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  132. package/docs/content/template-chat.mdx +1 -1
  133. package/package.json +2 -2
  134. package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
@@ -39,6 +39,7 @@ import { ProcessorChain, TripWire, toolCallsFromContent, } from "./processors.js
39
39
  import { startRun, subscribeToRun, getActiveRunForThread, getActiveRunForThreadAsync, getRun, abortRun, abortRunDurably, abortTurnDurably, tryClaimRunSlot, isHostedRuntime, resolveRunSoftTimeoutMs, resolveRunToolTimeoutCeilingMs, } from "./run-manager.js";
40
40
  import { writeLedgerEntry, readLedgerEntry, clearLedgerForThread, insertRun, insertRunEvent, isTurnAborted, markRunAborted, updateRunHeartbeat, updateRunStatusIfRunning, setRunError, setRunTerminalReason, claimBackgroundRun, readBackgroundRunClaim, recordRunDiagnostic, countRunsForTurn, RUN_DIAG_STAGE, UNCLAIMED_BACKGROUND_RUN_GRACE_MS, } from "./run-store.js";
41
41
  import { buildCurrentTimeUserContext } from "./runtime-context.js";
42
+ import { consumeAgentToolApproval, createAgentToolApproval, } from "./tool-approval-store.js";
42
43
  import { findCompletedJournalEntry, } from "./tool-call-journal.js";
43
44
  import { redactSensitiveFields, sanitizeToolErrorText, sanitizeToolErrorValue, } from "./tool-error-redaction.js";
44
45
  import { describeToolResultImages, extractAgentImagesFromActionResult, } from "./tool-result-images.js";
@@ -688,7 +689,7 @@ const VISIBLE_RETRY_THRESHOLD_MS = 10_000;
688
689
  */
689
690
  const FOREGROUND_FIRST_MODEL_EVENT_TIMEOUT_MS = 25_000;
690
691
  // Raised from 1 -> 2 now that each retry actually adapts (raises the token
691
- // ceiling and steps reasoning effort down a tier) instead of re-issuing the
692
+ // ceiling and steps effort down a tier) instead of re-issuing the
692
693
  // exact same doomed request twice.
693
694
  const EMPTY_FINAL_RESPONSE_RETRY_LIMIT = 2;
694
695
  const MAIN_CHAT_INTERNAL_CONTINUATION_LIMIT = 6;
@@ -2120,6 +2121,8 @@ export async function executeAgentToolCall(options) {
2120
2121
  threadId: options.threadId,
2121
2122
  turnId: options.turnId,
2122
2123
  approvedToolCalls: options.approvedToolCalls,
2124
+ onApprovalRequired: options.onApprovalRequired,
2125
+ consumeApproval: options.consumeApproval,
2123
2126
  });
2124
2127
  }
2125
2128
  catch (error) {
@@ -2307,6 +2310,51 @@ function stableStringify(value) {
2307
2310
  export function toolCallCacheKey(toolName, input) {
2308
2311
  return `${toolName}:${stableStringify(normalizeToolCallInputForHistory(input))}`;
2309
2312
  }
2313
+ export function findApprovedStructuredToolCall(history, approvedToolCalls) {
2314
+ if (!Array.isArray(history) || !approvedToolCalls?.length)
2315
+ return undefined;
2316
+ const approved = new Set(approvedToolCalls);
2317
+ const calls = new Map();
2318
+ let match;
2319
+ for (const message of history) {
2320
+ if (message.role === "assistant") {
2321
+ for (const part of message.content) {
2322
+ if (part.type !== "tool-call")
2323
+ continue;
2324
+ const callId = typeof part.id === "string"
2325
+ ? part.id
2326
+ : typeof part.toolCallId === "string"
2327
+ ? part.toolCallId
2328
+ : "";
2329
+ const name = typeof part.name === "string"
2330
+ ? part.name
2331
+ : typeof part.toolName === "string"
2332
+ ? part.toolName
2333
+ : "";
2334
+ if (!callId || !name)
2335
+ continue;
2336
+ calls.set(callId, { name, input: part.input ?? part.args ?? {} });
2337
+ }
2338
+ continue;
2339
+ }
2340
+ for (const part of message.content) {
2341
+ if (part.type !== "tool-result")
2342
+ continue;
2343
+ const call = calls.get(part.toolCallId);
2344
+ if (!call || !approved.has(toolCallCacheKey(call.name, call.input))) {
2345
+ continue;
2346
+ }
2347
+ const result = part.content.toLowerCase();
2348
+ if (!result.includes("did not execute") &&
2349
+ !result.includes("awaiting human approval") &&
2350
+ !result.includes("waiting for your approval")) {
2351
+ continue;
2352
+ }
2353
+ match = { ...call, callId: part.toolCallId };
2354
+ }
2355
+ }
2356
+ return match;
2357
+ }
2310
2358
  const INTERRUPTED_TOOL_LEDGER_POLL_MS = process.env.NODE_ENV === "test" ? 0 : 2_000;
2311
2359
  async function waitForInterruptedToolLedgerEntry(opts) {
2312
2360
  const pollMs = INTERRUPTED_TOOL_LEDGER_POLL_MS;
@@ -3965,7 +4013,16 @@ export async function runAgentLoop(opts) {
3965
4013
  const approvalKey = toolCallCacheKey(toolCall.name, toolCall.input);
3966
4014
  // Consume a grant on its first exact match. A second identical call in
3967
4015
  // the same continuation must request its own human approval.
3968
- const wasApproved = approvedToolCallKeys.delete(approvalKey);
4016
+ const approvalBinding = {
4017
+ toolName: toolCall.name,
4018
+ input: toolCall.input,
4019
+ callId: toolCall.id,
4020
+ approvalKey,
4021
+ };
4022
+ const requestedApproval = approvedToolCallKeys.delete(approvalKey);
4023
+ const wasApproved = requestedApproval && opts.consumeApproval
4024
+ ? await opts.consumeApproval(approvalBinding)
4025
+ : requestedApproval;
3969
4026
  if (actionEntry.needsApproval && !wasApproved) {
3970
4027
  let mustApprove = false;
3971
4028
  try {
@@ -3989,6 +4046,7 @@ export async function runAgentLoop(opts) {
3989
4046
  mustApprove = true;
3990
4047
  }
3991
4048
  if (mustApprove) {
4049
+ await opts.onApprovalRequired?.(approvalBinding);
3992
4050
  send({
3993
4051
  type: "tool_start",
3994
4052
  id: toolCall.id,
@@ -6433,6 +6491,12 @@ export function createProductionAgentHandler(options) {
6433
6491
  ...historyMessages,
6434
6492
  { role: "user", content: userContent },
6435
6493
  ];
6494
+ const requestedApprovedToolCalls = Array.isArray(body.approvedToolCalls) && body.approvedToolCalls.length > 0
6495
+ ? body.approvedToolCalls
6496
+ .filter((key) => typeof key === "string")
6497
+ .slice(0, 200)
6498
+ : undefined;
6499
+ const exactApprovedToolCall = findApprovedStructuredToolCall(structuredHistory, requestedApprovedToolCalls);
6436
6500
  const firstRequestPayloadDetail = buildFirstRequestPayloadDetail({
6437
6501
  isFirstRequest: history.length === 0,
6438
6502
  systemPrompt: requestSystemPrompt,
@@ -6486,6 +6550,38 @@ export function createProductionAgentHandler(options) {
6486
6550
  : typeof requestTurnId === "string" && requestTurnId.trim()
6487
6551
  ? requestTurnId.trim()
6488
6552
  : runId;
6553
+ const approvalStoreBinding = (binding) => {
6554
+ if (!ownerEmail) {
6555
+ throw new Error("Cannot create or consume an approval without an authenticated owner");
6556
+ }
6557
+ return {
6558
+ ownerEmail,
6559
+ orgId: getRequestOrgId() ?? null,
6560
+ threadId: effectiveThreadId,
6561
+ turnId: effectiveTurnId,
6562
+ toolName: binding.toolName,
6563
+ callId: binding.callId,
6564
+ approvalKey: binding.approvalKey,
6565
+ };
6566
+ };
6567
+ const approvalHooks = {
6568
+ onApprovalRequired: async (binding) => {
6569
+ await createAgentToolApproval(approvalStoreBinding(binding));
6570
+ },
6571
+ consumeApproval: async (binding) => {
6572
+ if (!ownerEmail)
6573
+ return false;
6574
+ return consumeAgentToolApproval(approvalStoreBinding(binding));
6575
+ },
6576
+ };
6577
+ const exactApprovedToolEntry = exactApprovedToolCall
6578
+ ? requestActions[exactApprovedToolCall.name]
6579
+ : undefined;
6580
+ const approvedToolCallsForExecution = exactApprovedToolCall && exactApprovedToolEntry?.needsApproval
6581
+ ? [
6582
+ toolCallCacheKey(exactApprovedToolCall.name, exactApprovedToolCall.input),
6583
+ ]
6584
+ : undefined;
6489
6585
  if (isBackgroundWorker &&
6490
6586
  (await isTurnAborted(effectiveThreadId, effectiveTurnId))) {
6491
6587
  await markRunAborted(runId, "user").catch(() => { });
@@ -7236,15 +7332,13 @@ export function createProductionAgentHandler(options) {
7236
7332
  ? { threadId: effectiveThreadId, turnId: effectiveTurnId }
7237
7333
  : {}),
7238
7334
  // Human-in-the-loop approval grants for this turn (sanitized — the
7239
- // request is untrusted; accept only a bounded list of string keys).
7240
- ...(Array.isArray(body.approvedToolCalls) &&
7241
- body.approvedToolCalls.length
7242
- ? {
7243
- approvedToolCalls: body.approvedToolCalls
7244
- .filter((k) => typeof k === "string")
7245
- .slice(0, 200),
7246
- }
7335
+ // request is untrusted; only the exact structured call is passed to
7336
+ // the loop, where the durable grant is consumed atomically.
7337
+ ...(approvedToolCallsForExecution
7338
+ ? { approvedToolCalls: approvedToolCallsForExecution }
7247
7339
  : {}),
7340
+ onApprovalRequired: approvalHooks.onApprovalRequired,
7341
+ consumeApproval: approvalHooks.consumeApproval,
7248
7342
  // A worker PROVEN to be running inside the real 15-min Netlify
7249
7343
  // `-background` function (`runsInBackgroundFunction`) has minutes of
7250
7344
  // budget left on THIS invocation. Let it catch a recoverable
@@ -7269,6 +7363,31 @@ export function createProductionAgentHandler(options) {
7269
7363
  send({ type: "activity", label: "Contacting model" });
7270
7364
  let instrumented = false;
7271
7365
  try {
7366
+ if (exactApprovedToolCall &&
7367
+ approvedToolCallsForExecution &&
7368
+ requestActions[exactApprovedToolCall.name]) {
7369
+ send({
7370
+ type: "activity",
7371
+ label: `Running approved ${exactApprovedToolCall.name} action`,
7372
+ tool: exactApprovedToolCall.name,
7373
+ });
7374
+ await executeAgentToolCall({
7375
+ actions: requestActions,
7376
+ name: exactApprovedToolCall.name,
7377
+ input: exactApprovedToolCall.input,
7378
+ callId: exactApprovedToolCall.callId,
7379
+ signal: agentLoopOpts.signal,
7380
+ ownerEmail,
7381
+ orgId: getRequestOrgId() ?? null,
7382
+ threadId: effectiveThreadId,
7383
+ turnId: effectiveTurnId,
7384
+ approvedToolCalls: approvedToolCallsForExecution,
7385
+ onApprovalRequired: approvalHooks.onApprovalRequired,
7386
+ consumeApproval: approvalHooks.consumeApproval,
7387
+ send,
7388
+ });
7389
+ return;
7390
+ }
7272
7391
  try {
7273
7392
  const { getObservabilityConfig, instrumentAgentLoop } = await import("../observability/traces.js");
7274
7393
  const obsConfig = await getObservabilityConfig();
@@ -0,0 +1,18 @@
1
+ export interface AgentToolApprovalBinding {
2
+ ownerEmail: string;
3
+ orgId?: string | null;
4
+ threadId?: string | null;
5
+ turnId?: string | null;
6
+ toolName: string;
7
+ callId: string;
8
+ approvalKey: string;
9
+ expiresAt?: number;
10
+ }
11
+ export declare function hashAgentToolApprovalKey(approvalKey: string): string;
12
+ export declare function createAgentToolApproval(binding: AgentToolApprovalBinding): Promise<void>;
13
+ /**
14
+ * Atomically consume the server-created approval for one exact tool call.
15
+ * Client history and approval keys are only lookup input; the pending row is
16
+ * the authorization boundary and cannot be manufactured by the client.
17
+ */
18
+ export declare function consumeAgentToolApproval(binding: AgentToolApprovalBinding): Promise<boolean>;
@@ -0,0 +1,122 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { getDbExec, intType, isPostgres, retryOnDdlRace, } from "../db/client.js";
3
+ import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
4
+ const APPROVAL_TTL_MS = 15 * 60_000;
5
+ const APPROVAL_CLEANUP_AGE_MS = 24 * 60 * 60_000;
6
+ let initPromise;
7
+ async function ensureAgentToolApprovalTable() {
8
+ if (!initPromise) {
9
+ initPromise = (async () => {
10
+ const createSql = `CREATE TABLE IF NOT EXISTS agent_tool_approvals (
11
+ id TEXT PRIMARY KEY,
12
+ owner_email TEXT NOT NULL,
13
+ org_id TEXT,
14
+ thread_id TEXT,
15
+ turn_id TEXT,
16
+ tool_name TEXT NOT NULL,
17
+ call_id TEXT NOT NULL,
18
+ approval_key_hash TEXT NOT NULL,
19
+ status TEXT NOT NULL,
20
+ expires_at ${intType()} NOT NULL,
21
+ consumed_at ${intType()},
22
+ created_at ${intType()} NOT NULL,
23
+ updated_at ${intType()} NOT NULL
24
+ )`;
25
+ const indexSql = `CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_binding
26
+ ON agent_tool_approvals(owner_email, org_id, thread_id, tool_name, call_id, status)`;
27
+ if (isPostgres()) {
28
+ await ensureTableExists("agent_tool_approvals", createSql);
29
+ await ensureIndexExists("idx_agent_tool_approvals_binding", indexSql);
30
+ return;
31
+ }
32
+ const client = getDbExec();
33
+ await retryOnDdlRace(() => client.execute(createSql));
34
+ await retryOnDdlRace(() => client.execute(indexSql));
35
+ })().catch((error) => {
36
+ initPromise = undefined;
37
+ throw error;
38
+ });
39
+ }
40
+ return initPromise;
41
+ }
42
+ export function hashAgentToolApprovalKey(approvalKey) {
43
+ return createHash("sha256").update(approvalKey).digest("hex");
44
+ }
45
+ export async function createAgentToolApproval(binding) {
46
+ await ensureAgentToolApprovalTable();
47
+ const now = Date.now();
48
+ const expiresAt = binding.expiresAt ?? now + APPROVAL_TTL_MS;
49
+ await getDbExec().execute({
50
+ sql: `INSERT INTO agent_tool_approvals
51
+ (id, owner_email, org_id, thread_id, turn_id, tool_name, call_id,
52
+ approval_key_hash, status, expires_at, consumed_at, created_at, updated_at)
53
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, NULL, ?, ?)`,
54
+ args: [
55
+ `agent-tool-approval-${randomUUID()}`,
56
+ binding.ownerEmail,
57
+ binding.orgId ?? null,
58
+ binding.threadId ?? null,
59
+ binding.turnId ?? null,
60
+ binding.toolName,
61
+ binding.callId,
62
+ hashAgentToolApprovalKey(binding.approvalKey),
63
+ expiresAt,
64
+ now,
65
+ now,
66
+ ],
67
+ });
68
+ // Cleanup is hygiene only. Expired rows remain unusable because consume uses
69
+ // an atomic status and expiry predicate, so cleanup failures must not affect
70
+ // the approval that was just recorded.
71
+ try {
72
+ await getDbExec().execute({
73
+ sql: `DELETE FROM agent_tool_approvals
74
+ WHERE expires_at < ? AND status <> 'pending'`,
75
+ args: [now - APPROVAL_CLEANUP_AGE_MS],
76
+ });
77
+ }
78
+ catch (error) {
79
+ console.warn("[agent] Could not clean expired tool approval records", error);
80
+ }
81
+ }
82
+ /**
83
+ * Atomically consume the server-created approval for one exact tool call.
84
+ * Client history and approval keys are only lookup input; the pending row is
85
+ * the authorization boundary and cannot be manufactured by the client.
86
+ */
87
+ export async function consumeAgentToolApproval(binding) {
88
+ await ensureAgentToolApprovalTable();
89
+ const now = Date.now();
90
+ const result = await getDbExec().execute({
91
+ sql: `UPDATE agent_tool_approvals
92
+ SET status = 'consumed', consumed_at = ?, updated_at = ?
93
+ WHERE id = (
94
+ SELECT id FROM agent_tool_approvals
95
+ WHERE owner_email = ?
96
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
97
+ AND ((thread_id IS NULL AND CAST(? AS TEXT) IS NULL) OR thread_id = ?)
98
+ AND tool_name = ?
99
+ AND call_id = ?
100
+ AND approval_key_hash = ?
101
+ AND status = 'pending'
102
+ AND expires_at > ?
103
+ ORDER BY created_at DESC
104
+ LIMIT 1
105
+ )
106
+ AND status = 'pending'`,
107
+ args: [
108
+ now,
109
+ now,
110
+ binding.ownerEmail,
111
+ binding.orgId ?? null,
112
+ binding.orgId ?? null,
113
+ binding.threadId ?? null,
114
+ binding.threadId ?? null,
115
+ binding.toolName,
116
+ binding.callId,
117
+ hashAgentToolApprovalKey(binding.approvalKey),
118
+ now,
119
+ ],
120
+ });
121
+ return result.rowsAffected === 1;
122
+ }
@@ -189,7 +189,7 @@ export interface AgentChatRequest {
189
189
  model?: string;
190
190
  /** Per-request engine override (sent alongside model for cross-provider switches). */
191
191
  engine?: string;
192
- /** Per-request reasoning effort override (ephemeral, from the composer picker). */
192
+ /** Per-request effort override (ephemeral, from the composer picker). */
193
193
  effort?: ReasoningEffort;
194
194
  /** Usage-tracking label for this call (e.g. "chat", "summarize"). Default: "chat". */
195
195
  usageLabel?: string;
@@ -471,7 +471,14 @@ async function executeClaudeCliRun(options) {
471
471
  });
472
472
  try {
473
473
  const result = await runClaudeCodeParticipant({
474
- role: options.permissionMode === "full-auto" ? "driver" : "watchdog",
474
+ // Claude's driver uses acceptEdits, which is the closest available
475
+ // mapping for auto-edit and full-auto. Keep ask-before-edit on the
476
+ // read-only watchdog path because this non-interactive runner has no
477
+ // approval channel to honor an edit prompt safely.
478
+ role: options.permissionMode === "auto-edit" ||
479
+ options.permissionMode === "full-auto"
480
+ ? "driver"
481
+ : "watchdog",
475
482
  prompt: buildClaudeCliPrompt(options.run, options.prompt),
476
483
  cwd,
477
484
  model,
@@ -604,7 +611,9 @@ async function executeClaudeCliRun(options) {
604
611
  }
605
612
  function buildClaudeCliPrompt(run, prompt) {
606
613
  const permissionMode = run.permissionMode ?? "full-auto";
607
- const mode = permissionMode === "full-auto" ? "Auto" : "Plan";
614
+ const mode = permissionMode === "auto-edit" || permissionMode === "full-auto"
615
+ ? "Auto"
616
+ : "Plan";
608
617
  const modeInstruction = mode === "Plan"
609
618
  ? "Inspect and explain only. Do not edit files or run mutating commands."
610
619
  : "Edit and verify as needed. Do not create, switch, reset, rebase, or stash git branches.";
@@ -24,6 +24,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
24
24
  import { Tooltip as DesignSystemTooltip } from "@agent-native/toolkit/design-system";
25
25
  import { IconMessageCircle, IconMessageDots, IconTerminal2, IconSettings, IconLayoutSidebarRightCollapse, IconLayoutGrid, IconCheck, IconPlus, IconX, IconDotsVertical, IconHistory, IconArrowsHorizontal, IconArrowsMaximize, IconExternalLink, } from "@tabler/icons-react";
26
26
  import React, { useState, useEffect, useRef, useCallback, useMemo, lazy, Suspense, startTransition, } from "react";
27
+ import { flushSync } from "react-dom";
27
28
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, } from "./components/ui/dropdown-menu.js";
28
29
  import { normalizeTooltipText } from "./components/ui/tooltip.js";
29
30
  import { ErrorReportActions } from "./ErrorReportActions.js";
@@ -1700,6 +1701,26 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1700
1701
  return next;
1701
1702
  });
1702
1703
  }, []);
1704
+ // `view-transition-name` is only legal to carry while a transition is
1705
+ // actually capturing. Left on permanently it makes the panel its own
1706
+ // stacking context and the containing block for every fixed/absolute
1707
+ // descendant, and enlists it as a captured group in unrelated transitions
1708
+ // (any React Router `viewTransition` navigation), which is how overlays end
1709
+ // up painted at stale offsets. Apply it only around the drawer morph, and
1710
+ // flush it into the DOM first: `startViewTransition` captures the old state
1711
+ // before it invokes the callback, so a name applied in a normal React commit
1712
+ // would land too late to be captured.
1713
+ const [drawerMorphing, setDrawerMorphing] = useState(false);
1714
+ const runDrawerMorph = useCallback((apply) => {
1715
+ flushSync(() => setDrawerMorphing(true));
1716
+ const settle = () => setDrawerMorphing(false);
1717
+ const transition = startAgentChatViewTransition(apply);
1718
+ if (!transition) {
1719
+ settle();
1720
+ return;
1721
+ }
1722
+ transition.finished.then(settle, settle);
1723
+ }, []);
1703
1724
  const snapTo75Percent = useCallback(() => {
1704
1725
  if (drawerExitTimerRef.current !== null) {
1705
1726
  clearTimeout(drawerExitTimerRef.current);
@@ -1719,8 +1740,8 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1719
1740
  }
1720
1741
  catch { }
1721
1742
  };
1722
- startAgentChatViewTransition(apply);
1723
- }, [drawerPlaceholderWidth, isWideDrawer, width]);
1743
+ runDrawerMorph(apply);
1744
+ }, [runDrawerMorph, drawerPlaceholderWidth, isWideDrawer, width]);
1724
1745
  const exitWideDrawer = useCallback(() => {
1725
1746
  if (!isWideDrawer || drawerExitTimerRef.current !== null)
1726
1747
  return;
@@ -1738,8 +1759,8 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1738
1759
  setIsWideDrawer(false);
1739
1760
  }, SIDEBAR_ANIMATION_MS + 32);
1740
1761
  };
1741
- startAgentChatViewTransition(apply);
1742
- }, [drawerPlaceholderWidth, isWideDrawer]);
1762
+ runDrawerMorph(apply);
1763
+ }, [runDrawerMorph, drawerPlaceholderWidth, isWideDrawer]);
1743
1764
  const handleResizeStart = useCallback(() => setIsResizing(true), []);
1744
1765
  const handleResizeEnd = useCallback(() => setIsResizing(false), []);
1745
1766
  const isLeft = position === "left";
@@ -1820,7 +1841,9 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1820
1841
  borderLeft: isLeft ? "none" : "1px solid hsl(var(--border))",
1821
1842
  borderRight: isLeft ? "1px solid hsl(var(--border))" : "none",
1822
1843
  display: "flex",
1823
- viewTransitionName: SIDEBAR_DRAWER_VIEW_TRANSITION_NAME,
1844
+ ...(drawerMorphing
1845
+ ? { viewTransitionName: SIDEBAR_DRAWER_VIEW_TRANSITION_NAME }
1846
+ : null),
1824
1847
  };
1825
1848
  }
1826
1849
  else {
@@ -1841,7 +1864,9 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1841
1864
  display: desktopAnimationEnabled || panelOpen ? "flex" : "none",
1842
1865
  minWidth: desktopAnimationEnabled ? 0 : undefined,
1843
1866
  pointerEvents: desktopAnimationEnabled && !panelOpen ? "none" : undefined,
1844
- viewTransitionName: SIDEBAR_DRAWER_VIEW_TRANSITION_NAME,
1867
+ ...(drawerMorphing
1868
+ ? { viewTransitionName: SIDEBAR_DRAWER_VIEW_TRANSITION_NAME }
1869
+ : null),
1845
1870
  };
1846
1871
  }
1847
1872
  // Mount the live chat surface only while visible or actively needed. Keeping
@@ -136,6 +136,7 @@ type QueuedMessage = {
136
136
  recoveryAction?: AgentRecoveryAction;
137
137
  trackInRunsTray?: boolean;
138
138
  hideUserMessage?: boolean;
139
+ approvedToolCalls?: string[];
139
140
  /**
140
141
  * Model/engine/effort snapshotted at enqueue time, for the same reason
141
142
  * `requestMode` is: the picker is global and live, so a queue that flushes
@@ -306,7 +307,7 @@ export interface AssistantChatProps {
306
307
  defaultModel?: string;
307
308
  /** Selected engine override for this conversation */
308
309
  selectedEngine?: string;
309
- /** Selected reasoning effort override for this conversation */
310
+ /** Selected effort override for this conversation */
310
311
  selectedEffort?: ReasoningEffort;
311
312
  /** Available engine/model list for the model picker */
312
313
  availableModels?: Array<{
@@ -319,7 +320,7 @@ export interface AssistantChatProps {
319
320
  modelListLoading?: boolean;
320
321
  /** Callback when user picks a model from the picker */
321
322
  onModelChange?: (model: string, engine: string) => void;
322
- /** Callback when user picks a reasoning effort from the picker */
323
+ /** Callback when user picks an effort from the picker */
323
324
  onEffortChange?: (effort: ReasoningEffort) => void;
324
325
  /**
325
326
  * Optional secondary model menu (e.g. an image-generation model) shown inside
@@ -3102,7 +3102,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3102
3102
  ...(messageAttachments.length > 0
3103
3103
  ? { attachments: messageAttachments }
3104
3104
  : {}),
3105
- ...createUserMessageRunConfig(next.references, next.requestMode, next.recoveryAction, next.trackInRunsTray, undefined, next.id, next.hideUserMessage, {
3105
+ ...createUserMessageRunConfig(next.references, next.requestMode, next.recoveryAction, next.trackInRunsTray, next.approvedToolCalls, next.id, next.hideUserMessage, {
3106
3106
  model: next.model,
3107
3107
  engine: next.engine,
3108
3108
  effort: next.effort,
@@ -3433,7 +3433,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3433
3433
  stopActiveRunRef.current({ preserveQueuedMessages: true });
3434
3434
  }, [applyLocalQueuedMessages]);
3435
3435
  const visibleQueuedMessages = useMemo(() => queuedMessages.filter((message) => !message.hideUserMessage), [queuedMessages]);
3436
- const addToQueue = useCallback(async (text, images, references, attachments, requestMode, intent = "queued", recoveryAction, includeComposerContext = false, trackInRunsTray = false, preserveReconnectAutoRecoveryBudget = false, hideUserMessage = false, submitMessageId) => {
3436
+ const addToQueue = useCallback(async (text, images, references, attachments, requestMode, intent = "queued", recoveryAction, includeComposerContext = false, trackInRunsTray = false, preserveReconnectAutoRecoveryBudget = false, hideUserMessage = false, submitMessageId, approvedToolCalls) => {
3437
3437
  if (isAgentChatSubmitCancelled(submitMessageId))
3438
3438
  return;
3439
3439
  if (engineSetupRequired) {
@@ -3582,6 +3582,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3582
3582
  recoveryAction,
3583
3583
  trackInRunsTray,
3584
3584
  hideUserMessage,
3585
+ approvedToolCalls,
3585
3586
  ...modelSnapshot,
3586
3587
  },
3587
3588
  ]);
@@ -3602,6 +3603,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3602
3603
  recoveryAction,
3603
3604
  trackInRunsTray,
3604
3605
  hideUserMessage,
3606
+ approvedToolCalls,
3605
3607
  ...modelSnapshot,
3606
3608
  },
3607
3609
  ]);
@@ -3615,7 +3617,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3615
3617
  ...(messageAttachments.length > 0
3616
3618
  ? { attachments: messageAttachments }
3617
3619
  : {}),
3618
- ...createUserMessageRunConfig(references, effectiveRequestMode, recoveryAction, trackInRunsTray, undefined, undefined, hideUserMessage),
3620
+ ...createUserMessageRunConfig(references, effectiveRequestMode, recoveryAction, trackInRunsTray, approvedToolCalls, undefined, hideUserMessage),
3619
3621
  });
3620
3622
  }
3621
3623
  catch (error) {
@@ -4006,31 +4008,16 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4006
4008
  getApprovalResolution,
4007
4009
  onApprovalResolved: recordApprovalResolution,
4008
4010
  onApprove: (approvalKey) => {
4009
- markOptimisticRunning();
4010
- appendThreadMessage({
4011
- role: "user",
4012
- content: [
4013
- {
4014
- type: "text",
4015
- text: "Approved. Go ahead and run the requested action.",
4016
- },
4017
- ],
4018
- ...createUserMessageRunConfig(undefined, execMode === "plan"
4019
- ? "plan"
4020
- : execMode === "build"
4021
- ? "act"
4022
- : undefined, undefined, undefined, [approvalKey]),
4023
- });
4011
+ void addToQueue("Approved. Go ahead and run the requested action.", undefined, undefined, undefined, undefined, "queued", undefined, false, false, false, false, undefined, [approvalKey]);
4024
4012
  },
4025
4013
  ...(approvalActions?.onDeny ? { onDeny: approvalActions.onDeny } : {}),
4026
4014
  ...(approvalActions?.onAlwaysAllow
4027
4015
  ? { onAlwaysAllow: approvalActions.onAlwaysAllow }
4028
4016
  : {}),
4029
4017
  }), [
4030
- appendThreadMessage,
4018
+ addToQueue,
4031
4019
  execMode,
4032
4020
  getApprovalResolution,
4033
- markOptimisticRunning,
4034
4021
  approvalActions,
4035
4022
  recordApprovalResolution,
4036
4023
  ]);
@@ -39,4 +39,4 @@ export { AGENT_SIDEBAR_DEFAULT_MAX_WIDTH, AGENT_SIDEBAR_MIN_WIDTH, AGENT_SIDEBAR
39
39
  export { clearReservedToolRenderersForTests, clearToolRenderersForTests, registerActionChatRenderer, registerFallbackToolRenderer, registerReservedActionChatRenderer, registerReservedFallbackToolRenderer, registerReservedToolRenderer, registerToolRenderer, resolveToolRenderer, type ActionChatRendererRegistration, type ToolRendererComponent, type ToolRendererContext, type ToolRendererMatch, type ToolRendererProps, type ToolRendererRegistration, } from "../chat/tool-render-registry.js";
40
40
  export * from "../chat/connectors.js";
41
41
  export * from "../chat/runtime.js";
42
- export { CHAT_FIRST_APP_LAYOUT_STORAGE_KEY, CHAT_FIRST_DEFAULT_APP_IDS, CHAT_FIRST_SURFACE_TABS_STORAGE_KEY, CHAT_FIRST_SURFACE_PANEL_STORAGE_KEY, CHAT_FIRST_SURFACE_WIDTH_DEFAULT, CHAT_FIRST_SURFACE_WIDTH_MIN, clampChatFirstSurfaceWidth, CHAT_FIRST_MODE_STORAGE_KEY, CHAT_FIRST_MODE_CHANGED_EVENT, CHAT_FIRST_OPEN_APP_EVENT, CHAT_FIRST_OPEN_BROWSER_EVENT, CHAT_FIRST_WATCH_SESSION_EVENT, CHAT_FIRST_SURFACE_CATALOG, chatFirstSurfaceTabId, closeChatFirstSessionWatch, emitChatFirstOpenApp, emitChatFirstOpenBrowser, emitChatFirstSessionWatch, getChatFirstSessionWatchStore, getChatFirstSurfaceTabsStore, getChatFirstSurfacePanelStore, orderChatFirstAppIds, readChatFirstAppLayout, resolveChatFirstAppTarget, resolveChatFirstBrowserTarget, resolveChatFirstSessionId, normalizeChatFirstSessionReference, readChatFirstMode, readChatFirstModeState, readChatFirstSurfaceWidth, subscribeChatFirstOpenApp, subscribeChatFirstOpenBrowser, subscribeChatFirstSessionWatch, useChatFirstSessionWatch, useChatFirstSurfaceResize, useChatFirstSurfacePanel, useChatFirstSurfaceTabs, writeChatFirstAppLayout, writeChatFirstMode, writeChatFirstSurfaceWidth, type ChatFirstAppLayoutPreference, type ChatFirstModeReadResult, type ChatFirstAppRegistration, type ChatFirstAppResolution, type ChatFirstAppTarget, type ChatFirstAgentActivity, type ChatFirstAgentActivityStatus, type ChatFirstOpenAppDelivery, type ChatFirstOpenAppDetail, type ChatFirstOpenBrowserDetail, type ChatFirstBrowserResolution, type ChatFirstSessionKind, type ChatFirstSessionReference, type ChatFirstSessionWatchDelivery, type ChatFirstSessionWatchState, type ChatFirstSessionWatchStore, type ChatFirstSurfaceKind, type ChatFirstSurfaceTab, type ChatFirstSurfacePanelState, type ChatFirstSurfacePanelStore, type ChatFirstSurfaceTabsState, type ChatFirstSurfaceTabsStore, } from "../chat-first.js";
42
+ export { CHAT_FIRST_APP_LAYOUT_STORAGE_KEY, CHAT_FIRST_DEFAULT_APP_IDS, CHAT_FIRST_SURFACE_TABS_STORAGE_KEY, CHAT_FIRST_SURFACE_PANEL_STORAGE_KEY, CHAT_FIRST_SURFACE_WIDTH_DEFAULT, CHAT_FIRST_SURFACE_WIDTH_MIN, clampChatFirstSurfaceWidth, CHAT_FIRST_MODE_STORAGE_KEY, CHAT_FIRST_MODE_CHANGED_EVENT, CHAT_FIRST_OPEN_APP_EVENT, CHAT_FIRST_OPEN_BROWSER_EVENT, CHAT_FIRST_WATCH_SESSION_EVENT, CHAT_FIRST_SURFACE_CATALOG, chatFirstSurfaceTabId, closeChatFirstSessionWatch, emitChatFirstOpenApp, emitChatFirstOpenBrowser, emitChatFirstSessionWatch, getChatFirstSessionWatchStore, getChatFirstSurfaceTabsStore, getChatFirstSurfacePanelStore, orderChatFirstAppIds, readChatFirstAppLayout, resolveChatFirstAppTarget, resolveChatFirstBrowserTarget, resolveChatFirstSessionId, normalizeChatFirstSessionReference, readChatFirstMode, readChatFirstModeState, readChatFirstSurfaceWidth, subscribeChatFirstOpenApp, subscribeChatFirstOpenBrowser, subscribeChatFirstSessionWatch, useChatFirstSessionWatch, useChatFirstSurfaceResize, useChatFirstSurfacePanel, useChatFirstSurfaceTabs, writeChatFirstAppLayout, writeChatFirstMode, writeChatFirstSurfaceWidth, type ChatFirstAppLayoutPreference, type ChatFirstModeReadResult, type ChatFirstAppRegistration, type ChatFirstAppResolution, type ChatFirstAppTarget, type ChatFirstAppSurfacePlacement, type ChatFirstAgentActivity, type ChatFirstAgentActivityStatus, type ChatFirstOpenAppDelivery, type ChatFirstOpenAppDetail, type ChatFirstOpenBrowserDetail, type ChatFirstBrowserResolution, type ChatFirstSessionKind, type ChatFirstSessionReference, type ChatFirstSessionWatchDelivery, type ChatFirstSessionWatchState, type ChatFirstSessionWatchStore, type ChatFirstSurfaceKind, type ChatFirstSurfaceTab, type ChatFirstSurfacePanelState, type ChatFirstSurfacePanelStore, type ChatFirstSurfaceTabsState, type ChatFirstSurfaceTabsStore, } from "../chat-first.js";
@@ -85,6 +85,16 @@ const LARGE_INPUT_TOOL_NAMES = new Set([
85
85
  ]);
86
86
  const MAX_HISTORY_LARGE_TOOL_ARGS_CHARS = 200_000;
87
87
  const STARTUP_RESPONSE_TIMEOUT_MS = 45_000;
88
+ function dispatchTerminalChatUiCleanup(tabId) {
89
+ if (typeof window === "undefined")
90
+ return;
91
+ // Some terminal outcomes are synthesized by the adapter after recovery
92
+ // (for example, a completed tool whose final response timed out) and never
93
+ // pass through a terminal SSE frame. Clear both halves of the status UI so
94
+ // the wrap-up result cannot leave a preparation label or "Resuming" state.
95
+ window.dispatchEvent(new CustomEvent("agent-chat:activity-clear", { detail: { tabId } }));
96
+ window.dispatchEvent(new CustomEvent("agent-chat:stream-progress", { detail: { tabId } }));
97
+ }
88
98
  // ── Background follow mode ──────────────────────────────────────────────────
89
99
  // For server-continued runs (dispatchMode starts with "background" or equals
90
100
  // "foreground-self-chain") the SERVER normally chains continuation chunks
@@ -3526,6 +3536,7 @@ export function createAgentChatAdapter(options) {
3526
3536
  }
3527
3537
  finally {
3528
3538
  if (typeof window !== "undefined") {
3539
+ dispatchTerminalChatUiCleanup(tabId);
3529
3540
  window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
3530
3541
  detail: { isRunning: false, tabId },
3531
3542
  }));
@@ -42,7 +42,7 @@ export interface AgentChatMessage {
42
42
  model?: string;
43
43
  /** Engine preference paired with model for cross-provider switches. */
44
44
  engine?: string;
45
- /** Reasoning effort preference paired with model. */
45
+ /** Effort preference paired with model. */
46
46
  effort?: ReasoningEffort;
47
47
  /**
48
48
  * Execution mode for this submitted turn. When omitted, sendToAgentChat
@@ -264,6 +264,8 @@ function rememberStreamingText(resetKey, targetText, visibleText) {
264
264
  if (oldestKey !== undefined)
265
265
  smoothStreamingTextCache.delete(oldestKey);
266
266
  }
267
+ /** Stable placeholder so the grapheme ref stays `string[]` before it is seeded. */
268
+ const EMPTY_GRAPHEMES = [];
267
269
  export function useSmoothStreamingText(targetText, streaming, resetKey) {
268
270
  const prefersReducedMotion = usePrefersReducedMotion();
269
271
  const [visibleText, setVisibleText] = useState(() => {
@@ -276,9 +278,19 @@ export function useSmoothStreamingText(targetText, streaming, resetKey) {
276
278
  return sliceGraphemes(targetText, graphemes, initialSmoothStreamingGraphemeCount(graphemes));
277
279
  });
278
280
  const visibleTextRef = useRef(visibleText);
279
- const visibleCountRef = useRef(splitStreamingTextGraphemes(visibleText).length);
280
281
  const targetTextRef = useRef(targetText);
281
- const targetGraphemesRef = useRef(splitStreamingTextGraphemes(targetText));
282
+ // `useRef(expr)` evaluates `expr` on EVERY render and throws it away after
283
+ // mount. Segmenting here therefore ran a full Intl.Segmenter pass per commit
284
+ // — up to 60Hz per streaming message — and, because `visibleText` is a strict
285
+ // prefix of `targetText`, that pass could never hit the incremental path and
286
+ // evicted the shared segmenter cache for every other live stream too. Seed
287
+ // once; the streaming effect below keeps both refs authoritative after that.
288
+ const visibleCountRef = useRef(-1);
289
+ const targetGraphemesRef = useRef(EMPTY_GRAPHEMES);
290
+ if (visibleCountRef.current < 0) {
291
+ targetGraphemesRef.current = splitStreamingTextGraphemes(targetText);
292
+ visibleCountRef.current = splitStreamingTextGraphemes(visibleText).length;
293
+ }
282
294
  const frameRef = useRef(null);
283
295
  const lastCommitAtRef = useRef(0);
284
296
  const pauseUntilRef = useRef(0);