@agent-native/core 0.157.5 → 0.157.7

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 (109) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/agent-native.config.ts +5 -0
  3. package/corpus/templates/analytics/netlify.toml +1 -0
  4. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +23 -5
  5. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
  6. package/corpus/templates/assets/netlify.toml +3 -0
  7. package/corpus/templates/brain/netlify.toml +3 -0
  8. package/corpus/templates/calendar/agent-native.config.ts +5 -0
  9. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -1
  10. package/corpus/templates/calendar/netlify.toml +3 -0
  11. package/corpus/templates/chat/agent-native.config.ts +5 -0
  12. package/corpus/templates/chat/netlify.toml +3 -0
  13. package/corpus/templates/clips/actions/create-meeting.ts +19 -1
  14. package/corpus/templates/clips/actions/lib/meeting-content.ts +28 -0
  15. package/corpus/templates/clips/actions/list-meetings.ts +33 -11
  16. package/corpus/templates/clips/app/components/meetings/bullet-link.tsx +48 -25
  17. package/corpus/templates/clips/app/components/meetings/canvas-editor.tsx +7 -6
  18. package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +388 -178
  19. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +7 -13
  20. package/corpus/templates/clips/app/components/transcript/transcript-segment-row.tsx +98 -0
  21. package/corpus/templates/clips/app/i18n/en-US.ts +5 -0
  22. package/corpus/templates/clips/app/lib/meeting-history-query.ts +11 -0
  23. package/corpus/templates/clips/app/lib/public-meeting.ts +4 -0
  24. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +49 -31
  25. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +47 -21
  26. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +15 -1
  27. package/corpus/templates/clips/netlify.toml +8 -4
  28. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +10 -0
  29. package/corpus/templates/clips/shared/transcript-segments.ts +20 -2
  30. package/corpus/templates/content/netlify.toml +3 -0
  31. package/corpus/templates/design/app/i18n-data.ts +2 -0
  32. package/corpus/templates/design/netlify.toml +3 -0
  33. package/corpus/templates/dispatch/netlify.toml +3 -0
  34. package/corpus/templates/factory/netlify.toml +3 -0
  35. package/corpus/templates/forms/netlify.toml +3 -0
  36. package/corpus/templates/macros/netlify.toml +3 -0
  37. package/corpus/templates/mail/agent-native.config.ts +5 -0
  38. package/corpus/templates/mail/netlify.toml +3 -0
  39. package/corpus/templates/plan/netlify.toml +3 -0
  40. package/corpus/templates/slides/netlify.toml +3 -0
  41. package/dist/agent/harness/acp-adapter.js +17 -2
  42. package/dist/agent/harness/ai-sdk-adapter.js +14 -3
  43. package/dist/agent/harness/background.js +10 -5
  44. package/dist/agent/harness/hosted.d.ts +20 -0
  45. package/dist/agent/harness/hosted.js +100 -0
  46. package/dist/agent/harness/runner.js +100 -77
  47. package/dist/agent/observational-memory/observer.js +12 -0
  48. package/dist/agent/production-agent.d.ts +3 -0
  49. package/dist/agent/production-agent.js +74 -4
  50. package/dist/agent/thread-data-builder.d.ts +12 -0
  51. package/dist/agent/thread-data-builder.js +23 -0
  52. package/dist/agent/types.d.ts +6 -0
  53. package/dist/client/AgentPanel.d.ts +5 -1
  54. package/dist/client/AgentPanel.js +113 -17
  55. package/dist/client/AgentTaskCard.d.ts +2 -2
  56. package/dist/client/AgentTaskCard.js +8 -22
  57. package/dist/client/AssistantChat.d.ts +8 -0
  58. package/dist/client/AssistantChat.js +106 -59
  59. package/dist/client/MultiTabAssistantChat.js +50 -6
  60. package/dist/client/NewWorkspaceAppFlow.js +7 -1
  61. package/dist/client/agent-chat-adapter.d.ts +6 -0
  62. package/dist/client/agent-chat-adapter.js +36 -8
  63. package/dist/client/app-chat-sidebar.d.ts +5 -0
  64. package/dist/client/app-chat-sidebar.js +53 -9
  65. package/dist/client/chat/composer-draft.d.ts +4 -0
  66. package/dist/client/chat/composer-draft.js +63 -0
  67. package/dist/client/chat-first/apps-rail.js +10 -1
  68. package/dist/client/chat-first/primary-nav.js +10 -5
  69. package/dist/client/frame-protocol.d.ts +1 -0
  70. package/dist/client/hooks/index.d.ts +1 -1
  71. package/dist/client/hooks/index.js +1 -1
  72. package/dist/client/i18n.js +5 -2
  73. package/dist/client/progress/RunsTray.js +129 -16
  74. package/dist/client/settings/SettingsRow.js +1 -1
  75. package/dist/client/use-chat-threads.js +11 -6
  76. package/dist/collab/routes.d.ts +1 -1
  77. package/dist/collab/struct-routes.d.ts +1 -1
  78. package/dist/config.d.ts +11 -0
  79. package/dist/config.js +52 -0
  80. package/dist/deploy/build.js +28 -11
  81. package/dist/extensions/actions.js +22 -1
  82. package/dist/hosted-harness/actions/get-hosted-harness-config.d.ts +2 -0
  83. package/dist/hosted-harness/actions/get-hosted-harness-config.js +13 -0
  84. package/dist/hosted-harness/actions/set-hosted-harness-enabled.d.ts +4 -0
  85. package/dist/hosted-harness/actions/set-hosted-harness-enabled.js +32 -0
  86. package/dist/localization/default-messages.d.ts +4 -1
  87. package/dist/localization/default-messages.js +5 -2
  88. package/dist/observability/routes.d.ts +5 -5
  89. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  90. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  91. package/dist/resources/handlers.d.ts +1 -1
  92. package/dist/secrets/routes.d.ts +9 -9
  93. package/dist/server/action-discovery.js +12 -0
  94. package/dist/server/agent-chat-plugin.js +24 -4
  95. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  96. package/dist/server/agent-teams.d.ts +20 -12
  97. package/dist/server/agent-teams.js +69 -35
  98. package/dist/server/hosted-harness-policy.d.ts +17 -0
  99. package/dist/server/hosted-harness-policy.js +38 -0
  100. package/dist/server/realtime-token.d.ts +1 -1
  101. package/dist/server/request-context.d.ts +4 -0
  102. package/dist/server/transcribe-voice.d.ts +1 -1
  103. package/dist/styles/agent-native.css +39 -0
  104. package/dist/templates/chat/agent-native.config.ts +5 -0
  105. package/dist/templates/chat/netlify.toml +3 -0
  106. package/dist/vite/action-types-plugin.js +8 -0
  107. package/package.json +4 -2
  108. package/src/templates/chat/agent-native.config.ts +5 -0
  109. package/src/templates/chat/netlify.toml +3 -0
@@ -18,6 +18,7 @@ import { modelCatalogConfirmsMissing } from "./chat-model-groups.js";
18
18
  import { AGENT_CHAT_VIEW_TRANSITION_PREPARE_EVENT } from "./chat-view-transition.js";
19
19
  // ─── chat/ module imports ─────────────────────────────────────────────────────
20
20
  import { DownscalingImageAttachmentAdapter, BinaryDocumentAttachmentAdapter, MAX_ESTIMATED_BODY_BYTES, AGGRESSIVE_MAX_IMAGE_DIMENSION, AGGRESSIVE_JPEG_QUALITY, transcodeImageToDataURL, createAgentImageAttachments, serializeQueuedAttachments, estimateAttachmentBodyBytes, } from "./chat/attachment-adapters.js";
21
+ import { readAssistantChatComposerDraft, writeAssistantChatComposerDraft, } from "./chat/composer-draft.js";
21
22
  import { TextStreamingContext } from "./chat/markdown-renderer.js";
22
23
  import { CheckpointContext, MessageActionsContext, assistantMessageRunId, UserMessage, AssistantMessage, SelectionAttachedPill, RunningActivityStatus, displayableUserMessageText, isHiddenUserMessage, ServerRunActiveContext, UserStoppedRunContext, } from "./chat/message-components.js";
23
24
  import { repoHasAssistantMessage, getRepoMessages, getRepoMessage, shouldImportServerThreadData, dedupeRepoMessagesById, dropEmptyAssistantMessages, withLastAssistantRunDuration, } from "./chat/repo-helpers.js";
@@ -288,12 +289,6 @@ function clearPendingSelection() {
288
289
  window.dispatchEvent(new CustomEvent("agent-panel:selection-cleared"));
289
290
  }
290
291
  }
291
- // Thread ids the server has already told us don't exist (a prior mount's
292
- // /threads/:id probe returned 404). Module-scoped so it survives remounts:
293
- // re-probing a known-absent thread on every navigation just re-spams DevTools
294
- // with 404s for a thread that has no server row yet (e.g. a freshly created,
295
- // not-yet-sent chat). Reset on a full page reload.
296
- const knownAbsentThreadIds = new Set();
297
292
  export async function waitForThreadRunToClear(apiUrl, threadId) {
298
293
  if (!threadId)
299
294
  return true;
@@ -1375,7 +1370,7 @@ export function useAutoResumeStatus(tabId, forceStopped) {
1375
1370
  function approvalResolutionIdentity(approvalKey, toolCallId) {
1376
1371
  return `${toolCallId ?? ""}\u0000${approvalKey}`;
1377
1372
  }
1378
- const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateText, suggestions, dynamicSuggestions, threadFooterSlot, emptyStateAddon, showHeader = true, onSwitchToCli, className, apiUrl, tabId, browserTabId, threadId, contextScope, contextNamespace, isActiveComposer = true, onMessageCountChange, onSaveThread, onGenerateTitle, composerSlot, onComposerTextChange, composerAreaClassName, composerPlaceholder, missingApiKeySetupLayout = "default", composerLayoutVariant = "default", centerComposerWhenEmpty = false, emptyStateDisplay = "default", composerToolbarSlot, composerExtraActionButton, showModelSelector = true, composerDisabled = false, composerDisabledPlaceholder, isNewThread, isThreadStateLoading, onSlashCommand, execMode, onExecModeChange, approvalActions, planModeDisabled, planModeDisabledReason, selectedModel, defaultModel, selectedEngine, selectedEffort, availableModels, modelListLoading, onModelChange, onEffortChange, availableAgents, selectedAgent, onAgentChange, imageModelMenu, onForkChat, onConnectProvider, onConnectLocalRuntime, plusMenuMode = "full", providerStatusChecksEnabled = true, loadHistoryRepository, historyReloadKey, externalStreaming = false, agentChatSurface = "app", suppressInlineOpenApp = false, }, ref) {
1373
+ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateText, suggestions, dynamicSuggestions, threadFooterSlot, emptyStateAddon, showHeader = true, onSwitchToCli, className, apiUrl, tabId, browserTabId, threadId, contextScope, contextNamespace, isActiveComposer = true, onMessageCountChange, onSaveThread, onGenerateTitle, composerSlot, onComposerTextChange, composerAreaClassName, composerPlaceholder, missingApiKeySetupLayout = "default", composerLayoutVariant = "default", centerComposerWhenEmpty = false, emptyStateDisplay = "default", composerToolbarSlot, composerExtraActionButton, showModelSelector = true, composerDisabled = false, composerDisabledPlaceholder, isNewThread, isThreadStateLoading, onSlashCommand, execMode, onExecModeChange, approvalActions, planModeDisabled, planModeDisabledReason, selectedModel, defaultModel, selectedEngine, selectedEffort, availableModels, modelListLoading, onModelChange, onEffortChange, availableAgents, selectedAgent, hostedHarness, onAgentChange, imageModelMenu, onForkChat, onConnectProvider, onConnectLocalRuntime, plusMenuMode = "full", providerStatusChecksEnabled = true, loadHistoryRepository, historyReloadKey, externalStreaming = false, agentChatSurface = "app", suppressInlineOpenApp = false, }, ref) {
1379
1374
  const t = useT();
1380
1375
  const thread = useThread();
1381
1376
  const threadRuntime = useThreadRuntime();
@@ -1419,10 +1414,13 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1419
1414
  // Cleared on the next message send.
1420
1415
  const [composerError, setComposerError] = useState(null);
1421
1416
  const [composerText, setComposerText] = useState("");
1417
+ const composerDraftScope = threadId || tabId;
1418
+ const initialComposerText = useMemo(() => readAssistantChatComposerDraft(composerDraftScope), [composerDraftScope]);
1422
1419
  const handleComposerTextChange = useCallback((text) => {
1423
1420
  setComposerText(text);
1421
+ writeAssistantChatComposerDraft(composerDraftScope, text);
1424
1422
  onComposerTextChange?.(text);
1425
- }, [onComposerTextChange]);
1423
+ }, [composerDraftScope, onComposerTextChange]);
1426
1424
  const dropDepthRef = useRef(0);
1427
1425
  const handleChatDragEnter = useCallback((e) => {
1428
1426
  if (!Array.from(e.dataTransfer?.types ?? []).includes("Files"))
@@ -1598,17 +1596,19 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1598
1596
  return next;
1599
1597
  });
1600
1598
  }, []);
1601
- useEffect(() => {
1599
+ useBrowserLayoutEffect(() => {
1602
1600
  if (!isActiveComposer)
1603
1601
  return;
1604
1602
  let cancelled = false;
1605
- void refreshAgentChatContext().then((state) => {
1603
+ const applyVisibleItems = (state) => {
1606
1604
  if (cancelled || !isActiveComposerRef.current)
1607
1605
  return;
1608
1606
  const visibleItems = filterAgentChatContextItems(state.items, normalizedContextNamespace);
1609
1607
  composerContextItemsRef.current = visibleItems;
1610
1608
  setComposerContextItems(visibleItems);
1611
- });
1609
+ };
1610
+ applyVisibleItems(getAgentChatContextState());
1611
+ void refreshAgentChatContext().then(applyVisibleItems);
1612
1612
  const unsubscribe = subscribeAgentChatContext(() => {
1613
1613
  if (cancelled || !isActiveComposerRef.current)
1614
1614
  return;
@@ -1622,6 +1622,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1622
1622
  unsubscribe();
1623
1623
  };
1624
1624
  }, [isActiveComposer, normalizedContextNamespace]);
1625
+ const visibleComposerContextItems = useMemo(() => filterAgentChatContextItems(composerContextItems, normalizedContextNamespace), [composerContextItems, normalizedContextNamespace]);
1625
1626
  // Tracks the JSON of the last queue we successfully persisted so the
1626
1627
  // debounced save effect can skip no-op writes (e.g. restore-from-server
1627
1628
  // on mount, or queue state that hasn't actually changed).
@@ -1824,11 +1825,21 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1824
1825
  }, [optimisticRunning]);
1825
1826
  // ─── Chat persistence ──────────────────────────────────────────────
1826
1827
  const hasRestoredRef = useRef(false);
1828
+ const [threadRestoreError, setThreadRestoreError] = useState(null);
1829
+ const [restoreAttempt, setRestoreAttempt] = useState(0);
1827
1830
  const [initialCachedThreadSnapshot] = useState(() => readCachedThreadSnapshot(apiUrl, threadId));
1828
1831
  const hasImportedInitialCachedSnapshotRef = useRef(false);
1829
1832
  const [isRestoring, setIsRestoring] = useState(!!(threadId || loadHistoryRepository) &&
1830
1833
  !isNewThread &&
1831
1834
  !initialCachedThreadSnapshot);
1835
+ const retryThreadRestore = useCallback(() => {
1836
+ if (!threadId || isNewThread)
1837
+ return;
1838
+ hasRestoredRef.current = false;
1839
+ setThreadRestoreError(null);
1840
+ setIsRestoring(true);
1841
+ setRestoreAttempt((attempt) => attempt + 1);
1842
+ }, [isNewThread, threadId]);
1832
1843
  const onSaveThreadRef = useRef(onSaveThread);
1833
1844
  onSaveThreadRef.current = onSaveThread;
1834
1845
  const onGenerateTitleRef = useRef(onGenerateTitle);
@@ -2610,87 +2621,111 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2610
2621
  return;
2611
2622
  hasRestoredRef.current = true;
2612
2623
  if (loadHistoryRepository) {
2624
+ let cancelled = false;
2613
2625
  (async () => {
2614
2626
  try {
2615
2627
  const repo = await loadHistoryRepository();
2628
+ if (cancelled)
2629
+ return;
2616
2630
  if (repo) {
2617
2631
  importThreadData(repo, { markTitleGenerated: true });
2618
2632
  }
2619
2633
  titleGeneratedRef.current = true;
2634
+ setThreadRestoreError(null);
2620
2635
  }
2621
2636
  catch {
2622
- // Start fresh
2637
+ if (!cancelled)
2638
+ setThreadRestoreError("unavailable");
2623
2639
  }
2624
2640
  finally {
2625
- setIsRestoring(false);
2641
+ if (!cancelled)
2642
+ setIsRestoring(false);
2626
2643
  }
2627
2644
  })();
2645
+ return () => {
2646
+ cancelled = true;
2647
+ };
2628
2648
  }
2629
2649
  else if (threadId && isNewThread) {
2630
2650
  // Client-created empty tabs do not have a server row until the first
2631
2651
  // message is sent. Avoid probing /threads/:id on mount; that request
2632
2652
  // can only 404 and makes normal app startup look broken in DevTools.
2633
- setIsRestoring(false);
2634
- }
2635
- else if (threadId && knownAbsentThreadIds.has(threadId)) {
2636
- // A prior mount already learned this thread has no server row (404).
2637
- // Skip the re-probe so remounts don't re-spam 404s for the same id.
2653
+ setThreadRestoreError(null);
2638
2654
  setIsRestoring(false);
2639
2655
  }
2640
2656
  else if (threadId) {
2657
+ let cancelled = false;
2641
2658
  (async () => {
2659
+ let canReconnect = false;
2642
2660
  try {
2643
2661
  const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}`);
2644
- if (res.ok) {
2645
- const data = await res.json();
2646
- if (data.threadData) {
2647
- const repo = importThreadData(data.threadData, {
2648
- markTitleGenerated: true,
2649
- });
2650
- if (repo) {
2651
- let shouldCacheServerSnapshot = true;
2652
- try {
2653
- shouldCacheServerSnapshot = shouldImportServerThreadData(normalizeThreadRepository(threadRuntime.export()), repo);
2654
- }
2655
- catch {
2656
- shouldCacheServerSnapshot = false;
2657
- }
2658
- if (shouldCacheServerSnapshot) {
2659
- const { title, preview } = extractThreadMeta(repo);
2660
- writeCachedThreadSnapshot(apiUrl, threadId, {
2661
- threadData: typeof data.threadData === "string"
2662
- ? data.threadData
2663
- : JSON.stringify(data.threadData),
2664
- title: data.title || title,
2665
- preview,
2666
- messageCount: Array.isArray(repo.messages)
2667
- ? repo.messages.length
2668
- : 0,
2669
- });
2670
- }
2662
+ if (!res.ok) {
2663
+ if (!cancelled) {
2664
+ setThreadRestoreError(res.status === 404 ? "not-found" : "unavailable");
2665
+ }
2666
+ return;
2667
+ }
2668
+ const data = await res.json();
2669
+ if (cancelled || !data || typeof data !== "object") {
2670
+ if (!cancelled)
2671
+ setThreadRestoreError("unavailable");
2672
+ return;
2673
+ }
2674
+ if (!data.threadData) {
2675
+ if (!cancelled)
2676
+ setThreadRestoreError("unavailable");
2677
+ return;
2678
+ }
2679
+ if (data.threadData) {
2680
+ const repo = importThreadData(data.threadData, {
2681
+ markTitleGenerated: true,
2682
+ });
2683
+ if (repo) {
2684
+ let shouldCacheServerSnapshot = true;
2685
+ try {
2686
+ shouldCacheServerSnapshot = shouldImportServerThreadData(normalizeThreadRepository(threadRuntime.export()), repo);
2687
+ }
2688
+ catch {
2689
+ shouldCacheServerSnapshot = false;
2690
+ }
2691
+ if (shouldCacheServerSnapshot) {
2692
+ const { title, preview } = extractThreadMeta(repo);
2693
+ writeCachedThreadSnapshot(apiUrl, threadId, {
2694
+ threadData: typeof data.threadData === "string"
2695
+ ? data.threadData
2696
+ : JSON.stringify(data.threadData),
2697
+ title: data.title || title,
2698
+ preview,
2699
+ messageCount: Array.isArray(repo.messages)
2700
+ ? repo.messages.length
2701
+ : 0,
2702
+ });
2671
2703
  }
2672
2704
  }
2673
2705
  // Also skip title generation if thread already has a title
2674
2706
  if (data.title) {
2675
2707
  titleGeneratedRef.current = true;
2676
2708
  }
2677
- }
2678
- else if (res.status === 404) {
2679
- // No server row for this thread yet — remember it so later remounts
2680
- // skip the probe instead of re-fetching a known 404.
2681
- knownAbsentThreadIds.add(threadId);
2709
+ if (!cancelled) {
2710
+ setThreadRestoreError(null);
2711
+ canReconnect = true;
2712
+ }
2682
2713
  }
2683
2714
  }
2684
2715
  catch {
2685
- // Start fresh
2716
+ if (!cancelled)
2717
+ setThreadRestoreError("unavailable");
2686
2718
  }
2687
2719
  finally {
2688
2720
  // Clear the skeleton as soon as the persisted messages are imported.
2689
2721
  // The active-run reconnect probe below must NOT gate first paint — it
2690
2722
  // only matters when a run is mid-flight (e.g. after a hot reload), and
2691
2723
  // it streams on top of the already-rendered messages.
2692
- setIsRestoring(false);
2724
+ if (!cancelled)
2725
+ setIsRestoring(false);
2693
2726
  }
2727
+ if (cancelled || !canReconnect)
2728
+ return;
2694
2729
  // Reconnect to an in-progress run after the skeleton has cleared, so a
2695
2730
  // background `/runs/active` probe never delays showing the conversation.
2696
2731
  try {
@@ -2700,6 +2735,9 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2700
2735
  // No active run to reconnect to.
2701
2736
  }
2702
2737
  })();
2738
+ return () => {
2739
+ cancelled = true;
2740
+ };
2703
2741
  }
2704
2742
  else {
2705
2743
  // Legacy: restore from sessionStorage
@@ -2726,6 +2764,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2726
2764
  loadHistoryRepository,
2727
2765
  isNewThread,
2728
2766
  isThreadStateLoading,
2767
+ restoreAttempt,
2729
2768
  ]);
2730
2769
  useEffect(() => {
2731
2770
  if (!loadHistoryRepository ||
@@ -4063,6 +4102,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4063
4102
  !authError &&
4064
4103
  showEmptyState &&
4065
4104
  !isRestoring;
4105
+ const threadRestoreErrorSurface = threadRestoreError ? (_jsxs("div", { role: "alert", className: "flex max-w-[320px] flex-col items-center gap-3 rounded-lg border border-border bg-muted/30 px-4 py-3 text-center", children: [_jsx(IconRefresh, { className: "h-5 w-5 text-muted-foreground" }), _jsx("p", { className: "text-sm text-muted-foreground", children: t("agentChat.message.restoreRequestFailed") }), _jsx("button", { type: "button", onClick: retryThreadRestore, className: "rounded-md border border-border px-3 py-1.5 text-xs font-medium text-foreground hover:bg-accent", children: t("agentChat.common.retry") })] })) : null;
4066
4106
  // Clarifying-question surface: the `ask-question` action writes a
4067
4107
  // GuidedQuestionPayload to application_state under "guided-questions". The
4068
4108
  // hook polls that key, and on submit/skip composes the answer as a normal
@@ -4079,7 +4119,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4079
4119
  showComposerSlot ||
4080
4120
  showCenteredEmptyThreadFooterSlot ||
4081
4121
  (guidedQuestions && guidedQuestions.length > 0) ||
4082
- composerContextItems.length > 0 ||
4122
+ visibleComposerContextItems.length > 0 ||
4083
4123
  showPlanModeCallout ||
4084
4124
  showInlineMissingKeySetup);
4085
4125
  const approvalResolutionScope = threadId ?? tabId ?? "default";
@@ -4173,7 +4213,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4173
4213
  : "text-xs text-muted-foreground hover:text-foreground px-3 py-1.5 rounded-md border border-border hover:bg-accent", children: t("agentChat.auth.refreshChat") })] })] })) : isRestoring && centeredRestoringState ? (_jsxs("div", { className: cn("agent-empty-state", emptyStateDisplay === "hidden"
4174
4214
  ? "sr-only"
4175
4215
  : "flex h-full flex-col items-center justify-center gap-3 px-4 py-16"), "aria-busy": "true", children: [_jsx(IconMessage, { className: "h-5 w-5 text-muted-foreground/60" }), _jsx("p", { className: "sr-only", children: emptyStateText ??
4176
- t("agentChat.empty.loadingChat") })] })) : isRestoring ? (_jsxs("div", { className: "flex flex-col gap-3 p-4", children: [_jsx("div", { className: "flex justify-end", children: _jsx("div", { className: "h-8 w-32 rounded-lg bg-muted animate-pulse" }) }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("div", { className: "h-4 w-48 rounded bg-muted animate-pulse" }), _jsx("div", { className: "h-4 w-64 rounded bg-muted animate-pulse" }), _jsx("div", { className: "h-4 w-40 rounded bg-muted animate-pulse" })] })] })) : showEmptyState ? (_jsxs("div", { className: cn("agent-empty-state", emptyStateDisplay === "hidden"
4216
+ t("agentChat.empty.loadingChat") })] })) : isRestoring ? (_jsxs("div", { className: "flex flex-col gap-3 p-4", children: [_jsx("div", { className: "flex justify-end", children: _jsx("div", { className: "h-8 w-32 rounded-lg bg-muted animate-pulse" }) }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("div", { className: "h-4 w-48 rounded bg-muted animate-pulse" }), _jsx("div", { className: "h-4 w-64 rounded bg-muted animate-pulse" }), _jsx("div", { className: "h-4 w-40 rounded bg-muted animate-pulse" })] })] })) : threadRestoreError &&
4217
+ messages.length === 0 ? (_jsx("div", { className: "flex h-full flex-col items-center justify-center gap-3 px-4 py-16", children: threadRestoreErrorSurface })) : showEmptyState ? (_jsxs("div", { className: cn("agent-empty-state", emptyStateDisplay === "hidden"
4177
4218
  ? "sr-only"
4178
4219
  : "flex h-full flex-col items-center justify-center gap-3 px-4 py-16"), children: [_jsx(IconMessage, { className: "h-5 w-5 text-muted-foreground/60" }), _jsx("p", { className: "sr-only", children: emptyStateText ??
4179
4220
  t("agentChat.empty.prompt") }), emptyStateAddon, resolvedSuggestions &&
@@ -4183,7 +4224,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4183
4224
  return;
4184
4225
  }
4185
4226
  void addToQueue(suggestion);
4186
- }, className: "w-full px-2 py-1 text-center text-[13px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: suggestion }, suggestion))) })) : null, showInlineEmptyThreadFooterSlot ? (_jsx("div", { className: "agent-thread-footer-slot agent-thread-footer-slot--empty", children: resolvedThreadFooterSlot })) : null] })) : (_jsxs(MessageScrollerContent, { className: "agent-thread-content gap-4 px-4 py-4", children: [_jsx(AssistantMessageListErrorBoundary, { resetKey: messageListResetKey, children: _jsx(UserStoppedRunContext.Provider, { value: wasRecentlyStoppedRun, children: _jsx(ThreadPrimitive.Messages
4227
+ }, className: "w-full px-2 py-1 text-center text-[13px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: suggestion }, suggestion))) })) : null, showInlineEmptyThreadFooterSlot ? (_jsx("div", { className: "agent-thread-footer-slot agent-thread-footer-slot--empty", children: resolvedThreadFooterSlot })) : null] })) : (_jsxs(MessageScrollerContent, { className: "agent-thread-content gap-4 px-4 py-4", children: [threadRestoreErrorSurface ? (_jsx(MessageScrollerItem, { children: threadRestoreErrorSurface })) : null, _jsx(AssistantMessageListErrorBoundary, { resetKey: messageListResetKey, children: _jsx(UserStoppedRunContext.Provider, { value: wasRecentlyStoppedRun, children: _jsx(ThreadPrimitive.Messages
4187
4228
  // Deliberately NOT keyed on part structure. Doing that
4188
4229
  // remounted the whole transcript every time a tool call
4189
4230
  // started or a placeholder id was rewritten — a flash and a
@@ -4252,7 +4293,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4252
4293
  "sidebar"
4253
4294
  ? t("agentChat.setup.connectToChat")
4254
4295
  : t("agentChat.setup.connectToStart") }), missingApiKeySetupLayout !==
4255
- "sidebar" ? (_jsx("span", { className: "agent-composer-missing-key-description", children: t("agentChat.setup.builderCredits") })) : null] }), _jsx("span", { className: "agent-composer-missing-key-cta", children: t("agentChat.setup.connectAi") })] }) }) })) : (_jsxs(_Fragment, { children: [_jsx(ComposerAttachmentPreviewStrip, {}), _jsx(TiptapComposer, { focusRef: tiptapRef, onTextChange: isActiveComposer
4296
+ "sidebar" ? (_jsx("span", { className: "agent-composer-missing-key-description", children: t("agentChat.setup.builderCredits") })) : null] }), _jsx("span", { className: "agent-composer-missing-key-cta", children: t("agentChat.setup.connectAi") })] }) }) })) : (_jsxs(_Fragment, { children: [_jsx(ComposerAttachmentPreviewStrip, {}), _jsx(TiptapComposer, { focusRef: tiptapRef, initialText: initialComposerText ?? undefined, initialTextKey: composerDraftScope, onTextChange: isActiveComposer
4256
4297
  ? handleComposerTextChange
4257
4298
  : undefined, disabled: isComposerDisabled ||
4258
4299
  showInlineMissingKeySetup, placeholder: showInlineMissingKeySetup
@@ -4269,16 +4310,16 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4269
4310
  })
4270
4311
  : t("agentChat.queue.followUp")
4271
4312
  : resolveAssistantChatComposerPlaceholder(composerPlaceholder), onSubmit: isRunning ||
4272
- composerContextItems.length > 0
4313
+ visibleComposerContextItems.length > 0
4273
4314
  ? (text, references, attachments, options) => void addToQueue(text, undefined, references.length > 0
4274
4315
  ? references
4275
4316
  : undefined, attachments, undefined, resolveAssistantChatSubmitIntent({
4276
4317
  isRunning,
4277
4318
  requestedIntent: options?.intent,
4278
4319
  }), undefined, true)
4279
- : undefined, willQueue: engineSetupRequired || isRunning, onSlashCommand: onSlashCommand, execMode: execMode, onExecModeChange: onExecModeChange, planModeDisabled: planModeDisabled, planModeDisabledReason: planModeDisabledReason, selectedModel: selectedModel ?? defaultModel, selectedEffort: selectedEffort, availableModels: availableModels, availableAgents: availableAgents, selectedAgent: selectedAgent, modelListLoading: modelListLoading, onModelChange: shouldShowAssistantChatModelSelector(showModelSelector)
4320
+ : undefined, willQueue: engineSetupRequired || isRunning, onSlashCommand: onSlashCommand, execMode: execMode, onExecModeChange: onExecModeChange, planModeDisabled: planModeDisabled, planModeDisabledReason: planModeDisabledReason, selectedModel: selectedModel ?? defaultModel, selectedEffort: selectedEffort, availableModels: availableModels, availableAgents: availableAgents, selectedAgent: selectedAgent, hostedHarness: hostedHarness, modelListLoading: modelListLoading, onModelChange: shouldShowAssistantChatModelSelector(showModelSelector)
4280
4321
  ? onModelChange
4281
- : undefined, onEffortChange: onEffortChange, onAgentChange: onAgentChange, imageModelMenu: imageModelMenu, onConnectProvider: onConnectProvider, onConnectLocalRuntime: onConnectLocalRuntime, toolbarSlot: composerToolbarSlot, contextItems: composerContextItems, onRemoveContextItem: removeComposerContextItem, plusMenuMode: plusMenuMode, layoutVariant: composerLayoutVariant, providerConnectStatusEnabled: providerStatusChecksEnabled, voiceEnabled: true, draftScope: threadId || tabId, interceptBuildRequestsForBuilder: true, onAttachmentError: setComposerError, extraActionButton: composerExtraActionButton, stopButton: showRunningInUI ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", onClick: () => stopActiveRun({
4322
+ : undefined, onEffortChange: onEffortChange, onAgentChange: onAgentChange, imageModelMenu: imageModelMenu, onConnectProvider: onConnectProvider, onConnectLocalRuntime: onConnectLocalRuntime, toolbarSlot: composerToolbarSlot, contextItems: visibleComposerContextItems, onRemoveContextItem: removeComposerContextItem, plusMenuMode: plusMenuMode, layoutVariant: composerLayoutVariant, providerConnectStatusEnabled: providerStatusChecksEnabled, voiceEnabled: true, draftScope: composerDraftScope, interceptBuildRequestsForBuilder: true, onAttachmentError: setComposerError, extraActionButton: composerExtraActionButton, stopButton: showRunningInUI ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", onClick: () => stopActiveRun({
4282
4323
  preserveQueuedMessages: true,
4283
4324
  }), "aria-label": t("agentChat.composer.stopResponse"), "data-agent-composer-slot": "stop-button", className: "shrink-0 flex h-7 w-7 cursor-pointer items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: _jsx(IconPlayerStopFilled, { className: "h-3 w-3" }) }) }), _jsx(TooltipContent, { children: t("agentChat.composer.stopResponse") })] })) : undefined })] })) }), showMissingKeySetup &&
4284
4325
  !showInlineMissingKeySetup ? (_jsx(PopoverContent, { side: missingApiKeySetupLayout === "sidebar"
@@ -4292,6 +4333,10 @@ export const AssistantChat = forwardRef(function AssistantChat({ apiUrl = agentN
4292
4333
  engineRef.current = props.selectedEngine;
4293
4334
  const effortRef = useRef(props.selectedEffort);
4294
4335
  effortRef.current = props.selectedEffort;
4336
+ const harnessRef = useRef(props.hostedHarness ? props.selectedAgent : undefined);
4337
+ harnessRef.current = props.hostedHarness ? props.selectedAgent : undefined;
4338
+ const hostedHarnessRef = useRef(props.hostedHarness === true);
4339
+ hostedHarnessRef.current = props.hostedHarness === true;
4295
4340
  const execModeRef = useRef(props.execMode);
4296
4341
  execModeRef.current = props.execMode;
4297
4342
  const scopeRef = useRef(contextScope);
@@ -4309,6 +4354,8 @@ export const AssistantChat = forwardRef(function AssistantChat({ apiUrl = agentN
4309
4354
  modelRef,
4310
4355
  engineRef,
4311
4356
  effortRef,
4357
+ harnessRef,
4358
+ hostedHarnessRef,
4312
4359
  execModeRef,
4313
4360
  browserTabId,
4314
4361
  scopeRef,
@@ -1,6 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { isClaudeCodeAgentId, isLunaModel, resolvePreferredAgentModel, } from "@agent-native/toolkit/composer/model-selection";
2
3
  import { IconPlus, IconHistory, IconX } from "@tabler/icons-react";
3
- import { useState, useRef, useEffect, useCallback } from "react";
4
+ import { useState, useRef, useEffect, useLayoutEffect, useCallback, } from "react";
4
5
  import { DEFAULT_MODEL } from "../agent/default-model.js";
5
6
  import { DEFAULT_REASONING_EFFORT, isReasoningEffort, resolveReasoningEffortSelection, } from "../shared/reasoning-effort.js";
6
7
  import { AGENT_CHAT_CLEAR_CONTEXT_MESSAGE_TYPE, AGENT_CHAT_REMOVE_CONTEXT_MESSAGE_TYPE, AGENT_CHAT_SET_CONTEXT_MESSAGE_TYPE, appendAgentChatContextToMessage, claimAgentChatOpenRequest, claimAgentChatSubmit, drainBufferedAgentChatOpenRequests, drainBufferedAgentChatSubmits, filterAgentChatContextItems, getAgentChatContextState, isAgentChatSubmitCancelled, normalizeAgentChatContextItem, parseSubmitChatMessage, removeAgentChatContextItem, reportAgentChatSubmitResult, reportAgentChatSubmitTarget, setAgentChatContextItem, } from "./agent-chat.js";
@@ -20,6 +21,7 @@ import { CHAT_MODEL_SELECTION_CHANGED_EVENT } from "./use-chat-models.js";
20
21
  import { useChatThreads, } from "./use-chat-threads.js";
21
22
  import { usePollLoop } from "./use-poll-loop.js";
22
23
  import { cn } from "./utils.js";
24
+ const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
23
25
  /** The single path that hands a queued send to a mounted chat ref. */
24
26
  function deliverPendingSend(ref, send) {
25
27
  if (isAgentChatSubmitCancelled(send.submitMessageId))
@@ -630,13 +632,23 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
630
632
  const threadId = activeThreadIdRef.current;
631
633
  if (!threadId)
632
634
  return;
635
+ const preferredAgentModel = isClaudeCodeAgentId(props.selectedAgent) && isLunaModel(model)
636
+ ? resolvePreferredAgentModel(props.selectedAgent, availableModels)
637
+ : undefined;
638
+ const nextModel = preferredAgentModel?.model ?? model;
639
+ const nextEngine = preferredAgentModel?.engine ?? engine;
633
640
  const existing = threadModelRef.current.get(threadId);
634
- const effort = resolveReasoningEffortSelection(model, existing?.effort);
635
- const selection = { model, engine, effort };
641
+ const effort = resolveReasoningEffortSelection(nextModel, existing?.effort);
642
+ const selection = { model: nextModel, engine: nextEngine, effort };
636
643
  threadModelRef.current.set(threadId, selection);
637
644
  persistModelSelection(selection);
638
645
  bumpModelSelectionVersion();
639
- }, [bumpModelSelectionVersion, persistModelSelection]);
646
+ }, [
647
+ availableModels,
648
+ bumpModelSelectionVersion,
649
+ persistModelSelection,
650
+ props.selectedAgent,
651
+ ]);
640
652
  const handleEffortChange = useCallback((effort) => {
641
653
  const threadId = activeThreadIdRef.current;
642
654
  if (!threadId)
@@ -659,6 +671,38 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
659
671
  persistModelSelection,
660
672
  resolveThreadModelSelection,
661
673
  ]);
674
+ useEffect(() => {
675
+ const preferredAgentModel = props.selectedAgent
676
+ ? resolvePreferredAgentModel(props.selectedAgent, availableModels)
677
+ : undefined;
678
+ if (!preferredAgentModel)
679
+ return;
680
+ const threadId = activeThreadIdRef.current;
681
+ const current = threadId
682
+ ? resolveThreadModelSelection(threadId)
683
+ : persistedModelSelection;
684
+ if (current?.model === preferredAgentModel.model &&
685
+ current.engine === preferredAgentModel.engine) {
686
+ return;
687
+ }
688
+ const selection = {
689
+ model: preferredAgentModel.model,
690
+ engine: preferredAgentModel.engine,
691
+ effort: resolveReasoningEffortSelection(preferredAgentModel.model, current?.effort),
692
+ };
693
+ if (threadId) {
694
+ threadModelRef.current.set(threadId, selection);
695
+ bumpModelSelectionVersion();
696
+ }
697
+ persistModelSelection(selection);
698
+ }, [
699
+ availableModels,
700
+ bumpModelSelectionVersion,
701
+ persistedModelSelection,
702
+ props.selectedAgent,
703
+ persistModelSelection,
704
+ resolveThreadModelSelection,
705
+ ]);
662
706
  const refreshEngines = useCallback(() => {
663
707
  setModelListLoading(true);
664
708
  Promise.all([
@@ -771,7 +815,7 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
771
815
  openTabIdsRef.current = openTabIds;
772
816
  const initializedRef = useRef(false);
773
817
  const openTabsKeyRef = useRef(OPEN_TABS_KEY);
774
- useEffect(() => {
818
+ useBrowserLayoutEffect(() => {
775
819
  const nextScope = scope;
776
820
  if (!nextScope)
777
821
  return;
@@ -782,7 +826,7 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
782
826
  const marker = `Resource context: ${nextScope.type}:${nextScope.id}`;
783
827
  const existing = getAgentChatContextState().items.find((item) => item.key === key);
784
828
  let ownsContextItem = false;
785
- if (!existing || existing.context.startsWith(marker)) {
829
+ if (!existing || existing.context.startsWith("Resource context:")) {
786
830
  setAgentChatContextItem({
787
831
  key,
788
832
  title,
@@ -226,7 +226,13 @@ export function NewWorkspaceAppFlow({ sourceApp = "chat", className = "", dispat
226
226
  setStatusMessage("Sent to Builder chat.");
227
227
  }
228
228
  else if (isDevMode) {
229
- sendToAgentChat({ message, submit: true, type: "code", newTab: true });
229
+ sendToAgentChat({
230
+ message,
231
+ submit: true,
232
+ type: "code",
233
+ newTab: true,
234
+ reuseEmptyTab: true,
235
+ });
230
236
  setStatusMessage("Sent to the local agent.");
231
237
  }
232
238
  else {
@@ -61,6 +61,12 @@ export interface CreateAgentChatAdapterOptions {
61
61
  effortRef?: {
62
62
  current: ReasoningEffort | undefined;
63
63
  };
64
+ harnessRef?: {
65
+ current: string | undefined;
66
+ };
67
+ hostedHarnessRef?: {
68
+ current: boolean;
69
+ };
64
70
  execModeRef?: {
65
71
  current: "build" | "plan" | undefined;
66
72
  };
@@ -65,6 +65,10 @@ const MAX_HISTORY_ATTACHMENT_CHARS = 60_000;
65
65
  const MAX_OUTBOUND_ATTACHMENT_CHARS = 200_000;
66
66
  const MAX_HISTORY_MESSAGES = 24;
67
67
  const MAX_HISTORY_TOTAL_CHARS = 64_000;
68
+ // Budget for everything actually said in the thread — every user ask and every
69
+ // assistant conclusion. Separate from MAX_HISTORY_TOTAL_CHARS, which bounds the
70
+ // tool args/results those turns produced. See limitPriorMessagesForRequest.
71
+ const MAX_HISTORY_WORD_CHARS = 32_000;
68
72
  const MAX_HISTORY_MESSAGE_CHARS = 12_000;
69
73
  const MAX_HISTORY_TOOL_ARGS_CHARS = 8_000;
70
74
  const MAX_HISTORY_TOOL_RESULT_CHARS = 12_000;
@@ -765,20 +769,36 @@ function estimateHistoryMessageCost(message) {
765
769
  function limitPriorMessagesForRequest(messages) {
766
770
  const recent = messages.slice(-MAX_HISTORY_MESSAGES);
767
771
  const kept = [];
768
- let totalChars = 0;
772
+ let words = 0;
773
+ let payload = 0;
769
774
  for (let i = recent.length - 1; i >= 0; i--) {
770
775
  const message = recent[i];
771
776
  if (message.role !== "user" && message.role !== "assistant")
772
777
  continue;
773
- const cost = estimateHistoryMessageCost(message);
774
- if (kept.length > 0 && totalChars + cost > MAX_HISTORY_TOTAL_CHARS) {
775
- break;
776
- }
777
- kept.push(message);
778
- totalChars += cost;
778
+ // What was said is cheap and cannot be re-derived; what a tool returned is
779
+ // ~97% of a tool-heavy turn and can always be re-read. Pricing both against
780
+ // one budget let a single read-heavy turn evict the whole conversation, so
781
+ // the agent re-derived the same answer and re-asked the same question.
782
+ const wordCost = messageTextForHistory(message).length;
783
+ if (kept.length > 0 && words + wordCost > MAX_HISTORY_WORD_CHARS)
784
+ continue;
785
+ const payloadCost = estimateHistoryMessageCost(message) - wordCost;
786
+ const affordsPayload = payload + payloadCost <= MAX_HISTORY_TOTAL_CHARS;
787
+ const content = affordsPayload
788
+ ? message.content
789
+ : message.content.filter((part) => part.type === "text");
790
+ if (!content.length)
791
+ continue;
792
+ kept.push(affordsPayload ? message : { ...message, content });
793
+ words += wordCost;
794
+ if (affordsPayload)
795
+ payload += payloadCost;
779
796
  }
780
797
  kept.reverse();
781
- while (kept.length > 0 && kept[0].role !== "user") {
798
+ // Stop before emptying: this rule exists to start history on a user turn for
799
+ // providers that require it, not to license sending no history at all. It was
800
+ // the last step that turned an over-budget turn into a blank conversation.
801
+ while (kept.length > 1 && kept[0].role !== "user") {
782
802
  kept.shift();
783
803
  }
784
804
  return kept;
@@ -1471,6 +1491,8 @@ export function createAgentChatAdapter(options) {
1471
1491
  const modelRef = options?.modelRef;
1472
1492
  const engineRef = options?.engineRef;
1473
1493
  const effortRef = options?.effortRef;
1494
+ const harnessRef = options?.harnessRef;
1495
+ const hostedHarnessRef = options?.hostedHarnessRef;
1474
1496
  const execModeRef = options?.execModeRef;
1475
1497
  const browserTabId = options?.browserTabId;
1476
1498
  const scopeRef = options?.scopeRef;
@@ -1575,6 +1597,9 @@ export function createAgentChatAdapter(options) {
1575
1597
  const engine = runConfigSelection("engine") ?? engineRef?.current;
1576
1598
  const effort = runConfigSelection("effort") ??
1577
1599
  effortRef?.current;
1600
+ const harness = hostedHarnessRef?.current
1601
+ ? (runConfigSelection("harness") ?? harnessRef?.current)
1602
+ : undefined;
1578
1603
  const requestedTurnId = (() => {
1579
1604
  const raw = runConfig?.custom && typeof runConfig.custom === "object"
1580
1605
  ? runConfig.custom.turnId
@@ -1980,6 +2005,8 @@ export function createAgentChatAdapter(options) {
1980
2005
  // "dev-frame" explicitly; the reusable in-product chat defaults to
1981
2006
  // "app" even when it is running in Desktop or inside a preview iframe.
1982
2007
  headers["x-agent-native-surface"] = surface;
2008
+ if (harness)
2009
+ headers["x-agent-native-hosted-harness"] = "1";
1983
2010
  const reconnectCurrentRun = async function* () {
1984
2011
  if (!runId)
1985
2012
  return false;
@@ -3066,6 +3093,7 @@ export function createAgentChatAdapter(options) {
3066
3093
  ...(model ? { model } : {}),
3067
3094
  ...(engine ? { engine } : {}),
3068
3095
  ...(effort ? { effort } : {}),
3096
+ ...(harness ? { harness: { runtime: harness } } : {}),
3069
3097
  ...(browserTabId ? { browserTabId } : {}),
3070
3098
  ...(scopeRef?.current ? { scope: scopeRef.current } : {}),
3071
3099
  ...(includeAttachments ? { attachments } : {}),
@@ -4,8 +4,11 @@ export declare const APP_CHAT_SIDEBAR_STATE_EVENT = "agent-native:per-app-chat-s
4
4
  export declare const APP_CHAT_SIDEBAR_STATE_MESSAGE = "agentNative.perAppChatState";
5
5
  /** Request sent by an iframe that mounted after the host announced its state. */
6
6
  export declare const APP_CHAT_SIDEBAR_STATE_REQUEST_MESSAGE = "agentNative.perAppChatStateRequest";
7
+ export type AppChatSidebarCommand = "toggle" | "open" | "close";
7
8
  export interface AppChatSidebarState {
8
9
  open: boolean;
10
+ /** True when a parent shell owns chat for this app surface. */
11
+ hosted: boolean;
9
12
  }
10
13
  export declare function buildAppChatSidebarStateMessage(open: boolean): {
11
14
  type: typeof APP_CHAT_SIDEBAR_STATE_MESSAGE;
@@ -20,4 +23,6 @@ export declare function buildAppChatSidebarStateRequest(): {
20
23
  * agent panels from changing the app's navigation chrome.
21
24
  */
22
25
  export declare function isPerAppChatStorageKey(storageKey: string | undefined): boolean;
26
+ export declare function requestPerAppChatCommand(command: AppChatSidebarCommand): boolean;
27
+ export declare function usePerAppChatState(enabled?: boolean): AppChatSidebarState;
23
28
  export declare function usePerAppChatOpen(): boolean;