@agent-native/core 0.157.6 → 0.157.8

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 (93) 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/lib/meeting-content.ts +28 -0
  14. package/corpus/templates/clips/actions/list-meetings.ts +33 -11
  15. package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
  16. package/corpus/templates/clips/app/lib/meeting-history-query.ts +11 -0
  17. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +47 -21
  18. package/corpus/templates/clips/netlify.toml +4 -0
  19. package/corpus/templates/content/netlify.toml +3 -0
  20. package/corpus/templates/design/app/i18n-data.ts +2 -0
  21. package/corpus/templates/design/netlify.toml +3 -0
  22. package/corpus/templates/dispatch/netlify.toml +3 -0
  23. package/corpus/templates/factory/netlify.toml +3 -0
  24. package/corpus/templates/forms/netlify.toml +3 -0
  25. package/corpus/templates/macros/netlify.toml +3 -0
  26. package/corpus/templates/mail/agent-native.config.ts +5 -0
  27. package/corpus/templates/mail/netlify.toml +3 -0
  28. package/corpus/templates/plan/netlify.toml +3 -0
  29. package/corpus/templates/slides/netlify.toml +3 -0
  30. package/dist/agent/harness/acp-adapter.js +17 -2
  31. package/dist/agent/harness/ai-sdk-adapter.js +14 -3
  32. package/dist/agent/harness/background.js +10 -5
  33. package/dist/agent/harness/hosted.d.ts +20 -0
  34. package/dist/agent/harness/hosted.js +100 -0
  35. package/dist/agent/harness/runner.js +100 -77
  36. package/dist/agent/observational-memory/observer.js +12 -0
  37. package/dist/agent/production-agent.d.ts +3 -0
  38. package/dist/agent/production-agent.js +74 -4
  39. package/dist/agent/thread-data-builder.d.ts +12 -0
  40. package/dist/agent/thread-data-builder.js +23 -0
  41. package/dist/agent/types.d.ts +6 -0
  42. package/dist/client/AgentPanel.d.ts +5 -1
  43. package/dist/client/AgentPanel.js +113 -17
  44. package/dist/client/AgentTaskCard.d.ts +2 -2
  45. package/dist/client/AgentTaskCard.js +8 -22
  46. package/dist/client/AssistantChat.d.ts +8 -0
  47. package/dist/client/AssistantChat.js +106 -59
  48. package/dist/client/MultiTabAssistantChat.js +50 -6
  49. package/dist/client/NewWorkspaceAppFlow.js +7 -1
  50. package/dist/client/agent-chat-adapter.d.ts +6 -0
  51. package/dist/client/agent-chat-adapter.js +36 -8
  52. package/dist/client/app-chat-sidebar.d.ts +5 -0
  53. package/dist/client/app-chat-sidebar.js +53 -9
  54. package/dist/client/chat/composer-draft.d.ts +4 -0
  55. package/dist/client/chat/composer-draft.js +63 -0
  56. package/dist/client/chat-first/apps-rail.js +10 -1
  57. package/dist/client/chat-first/primary-nav.js +10 -5
  58. package/dist/client/frame-protocol.d.ts +1 -0
  59. package/dist/client/hooks/index.d.ts +1 -1
  60. package/dist/client/hooks/index.js +1 -1
  61. package/dist/client/i18n.js +5 -2
  62. package/dist/client/progress/RunsTray.js +129 -16
  63. package/dist/client/settings/SettingsRow.js +1 -1
  64. package/dist/client/use-chat-threads.js +11 -6
  65. package/dist/config.d.ts +11 -0
  66. package/dist/config.js +52 -0
  67. package/dist/extensions/actions.js +22 -1
  68. package/dist/hosted-harness/actions/get-hosted-harness-config.d.ts +2 -0
  69. package/dist/hosted-harness/actions/get-hosted-harness-config.js +13 -0
  70. package/dist/hosted-harness/actions/set-hosted-harness-enabled.d.ts +4 -0
  71. package/dist/hosted-harness/actions/set-hosted-harness-enabled.js +32 -0
  72. package/dist/localization/default-messages.d.ts +4 -1
  73. package/dist/localization/default-messages.js +5 -2
  74. package/dist/notifications/routes.d.ts +1 -1
  75. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  76. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  77. package/dist/server/action-discovery.js +12 -0
  78. package/dist/server/agent-chat-plugin.js +24 -4
  79. package/dist/server/agent-teams.d.ts +20 -12
  80. package/dist/server/agent-teams.js +69 -35
  81. package/dist/server/hosted-harness-policy.d.ts +17 -0
  82. package/dist/server/hosted-harness-policy.js +38 -0
  83. package/dist/server/poll.d.ts +4 -4
  84. package/dist/server/poll.js +45 -13
  85. package/dist/server/realtime-token.d.ts +1 -1
  86. package/dist/server/request-context.d.ts +4 -0
  87. package/dist/styles/agent-native.css +39 -0
  88. package/dist/templates/chat/agent-native.config.ts +5 -0
  89. package/dist/templates/chat/netlify.toml +3 -0
  90. package/dist/vite/action-types-plugin.js +8 -0
  91. package/package.json +5 -3
  92. package/src/templates/chat/agent-native.config.ts +5 -0
  93. package/src/templates/chat/netlify.toml +3 -0
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState } from "react";
2
+ import { getFramePostMessageTargetOrigin } from "./frame.js";
2
3
  /** Custom event used by top-level hosts such as Electron webviews. */
3
4
  export const APP_CHAT_SIDEBAR_STATE_EVENT = "agent-native:per-app-chat-state";
4
5
  /** postMessage sent from a per-app chat host to its embedded app. */
@@ -8,7 +9,7 @@ export const APP_CHAT_SIDEBAR_STATE_REQUEST_MESSAGE = "agentNative.perAppChatSta
8
9
  export function buildAppChatSidebarStateMessage(open) {
9
10
  return {
10
11
  type: APP_CHAT_SIDEBAR_STATE_MESSAGE,
11
- data: { open },
12
+ data: { open, hosted: true },
12
13
  };
13
14
  }
14
15
  export function buildAppChatSidebarStateRequest() {
@@ -22,22 +23,62 @@ export function buildAppChatSidebarStateRequest() {
22
23
  export function isPerAppChatStorageKey(storageKey) {
23
24
  return (storageKey === "desktop-app-chat" || storageKey === "dispatch-app-chat");
24
25
  }
25
- export function usePerAppChatOpen() {
26
- const [open, setOpen] = useState(false);
26
+ function readDesktopChatBridge() {
27
+ if (typeof window === "undefined")
28
+ return null;
29
+ const bridge = window.agentNativeDesktop;
30
+ return bridge?.chat ?? null;
31
+ }
32
+ function readInitialPerAppChatState() {
33
+ return {
34
+ open: false,
35
+ hosted: readDesktopChatBridge() !== null,
36
+ };
37
+ }
38
+ export function requestPerAppChatCommand(command) {
39
+ if (typeof window === "undefined")
40
+ return false;
41
+ const desktopCommand = readDesktopChatBridge()?.[command];
42
+ if (desktopCommand) {
43
+ desktopCommand();
44
+ return true;
45
+ }
46
+ if (window.parent === window)
47
+ return false;
48
+ const data = command === "toggle" ? undefined : { open: command === "open" };
49
+ window.parent.postMessage({
50
+ type: "agentNative.toggleSidebar",
51
+ ...(data ? { data } : {}),
52
+ }, getFramePostMessageTargetOrigin() ?? "*");
53
+ return true;
54
+ }
55
+ export function usePerAppChatState(enabled = true) {
56
+ const [state, setState] = useState(() => enabled ? readInitialPerAppChatState() : { open: false, hosted: false });
27
57
  useEffect(() => {
58
+ if (!enabled)
59
+ return;
28
60
  const applyState = (value) => {
29
- if (typeof value === "boolean")
30
- setOpen(value);
61
+ if (!value || typeof value !== "object")
62
+ return;
63
+ const next = value;
64
+ if (typeof next.open !== "boolean")
65
+ return;
66
+ setState({
67
+ open: next.open,
68
+ // Older hosts sent only `open`; a typed host-state message is still
69
+ // enough to establish that the parent owns this app's chat.
70
+ hosted: next.hosted !== false,
71
+ });
31
72
  };
32
73
  const handleCustomEvent = (event) => {
33
- applyState(event.detail?.open);
74
+ applyState(event.detail);
34
75
  };
35
76
  const handleMessage = (event) => {
36
77
  if (window.parent === window || event.source !== window.parent)
37
78
  return;
38
79
  if (event.data?.type !== APP_CHAT_SIDEBAR_STATE_MESSAGE)
39
80
  return;
40
- applyState(event.data.data?.open);
81
+ applyState(event.data.data);
41
82
  };
42
83
  window.addEventListener(APP_CHAT_SIDEBAR_STATE_EVENT, handleCustomEvent);
43
84
  window.addEventListener("message", handleMessage);
@@ -48,6 +89,9 @@ export function usePerAppChatOpen() {
48
89
  window.removeEventListener(APP_CHAT_SIDEBAR_STATE_EVENT, handleCustomEvent);
49
90
  window.removeEventListener("message", handleMessage);
50
91
  };
51
- }, []);
52
- return open;
92
+ }, [enabled]);
93
+ return state;
94
+ }
95
+ export function usePerAppChatOpen() {
96
+ return usePerAppChatState().open;
53
97
  }
@@ -0,0 +1,4 @@
1
+ export declare function assistantChatComposerDraftKey(scope?: string | null): string | null;
2
+ export declare function readAssistantChatComposerDraft(scope?: string | null): string | null;
3
+ export declare function writeAssistantChatComposerDraft(scope: string | null | undefined, text: string): void;
4
+ export declare function clearAssistantChatComposerDraft(scope?: string | null): void;
@@ -0,0 +1,63 @@
1
+ const ASSISTANT_CHAT_COMPOSER_DRAFT_PREFIX = "agent-chat-composer-text:";
2
+ export function assistantChatComposerDraftKey(scope) {
3
+ const normalizedScope = scope?.trim();
4
+ return normalizedScope
5
+ ? `${ASSISTANT_CHAT_COMPOSER_DRAFT_PREFIX}${encodeURIComponent(normalizedScope)}`
6
+ : null;
7
+ }
8
+ function getComposerDraftStorage() {
9
+ if (typeof window === "undefined")
10
+ return null;
11
+ try {
12
+ return window.localStorage;
13
+ }
14
+ catch {
15
+ // coercion-ok: browser storage may be unavailable; treat it as absent.
16
+ return null;
17
+ }
18
+ }
19
+ export function readAssistantChatComposerDraft(scope) {
20
+ const key = assistantChatComposerDraftKey(scope);
21
+ const storage = getComposerDraftStorage();
22
+ if (!key || !storage)
23
+ return null;
24
+ try {
25
+ const draft = storage.getItem(key);
26
+ return draft && draft.trim().length > 0 ? draft : null;
27
+ }
28
+ catch {
29
+ // coercion-ok: browser storage may be unavailable; treat it as absent.
30
+ return null;
31
+ }
32
+ }
33
+ export function writeAssistantChatComposerDraft(scope, text) {
34
+ const key = assistantChatComposerDraftKey(scope);
35
+ const storage = getComposerDraftStorage();
36
+ if (!key || !storage)
37
+ return;
38
+ try {
39
+ if (text.trim().length > 0) {
40
+ storage.setItem(key, text);
41
+ }
42
+ else {
43
+ storage.removeItem(key);
44
+ }
45
+ }
46
+ catch {
47
+ // coercion-ok: browser storage may be unavailable; keep the live editor authoritative.
48
+ // The live editor remains the source of truth when browser storage is unavailable.
49
+ }
50
+ }
51
+ export function clearAssistantChatComposerDraft(scope) {
52
+ const key = assistantChatComposerDraftKey(scope);
53
+ const storage = getComposerDraftStorage();
54
+ if (!key || !storage)
55
+ return;
56
+ try {
57
+ storage.removeItem(key);
58
+ }
59
+ catch {
60
+ // coercion-ok: browser storage may be unavailable; keep the live editor authoritative.
61
+ // The live editor remains the source of truth when browser storage is unavailable.
62
+ }
63
+ }
@@ -53,7 +53,16 @@ export const ChatFirstAppsRail = memo(function ChatFirstAppsRail({ apps, activeA
53
53
  .map((id) => appsById.get(id))
54
54
  .filter((app) => Boolean(app));
55
55
  }, [apps, layout]);
56
- const visibleApps = showAllApps ? orderedApps : orderedApps.slice(0, 5);
56
+ const visibleApps = useMemo(() => {
57
+ if (showAllApps)
58
+ return orderedApps;
59
+ const defaultApps = orderedApps.slice(0, 5);
60
+ if (!activeAppId || defaultApps.some((app) => app.id === activeAppId)) {
61
+ return defaultApps;
62
+ }
63
+ const activeApp = orderedApps.find((app) => app.id === activeAppId);
64
+ return activeApp ? [...defaultApps, activeApp] : defaultApps;
65
+ }, [activeAppId, orderedApps, showAllApps]);
57
66
  const hasMoreApps = orderedApps.length > visibleApps.length;
58
67
  const createTrigger = createAppTrigger ??
59
68
  (onCreateApp ? (_jsx("button", { type: "button", className: "flex size-6 items-center justify-center rounded text-sidebar-foreground/55 hover:bg-sidebar-accent hover:text-sidebar-foreground", onClick: onCreateApp, "aria-label": copy("createWorkspaceApp"), title: copy("createWorkspaceApp"), children: _jsx(IconPlus, { size: 14, "aria-hidden": "true" }) })) : null);
@@ -2,16 +2,21 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { IconClock, IconPlus, IconPlugConnected, IconSearch, } from "@tabler/icons-react";
3
3
  import { defaultChatFirstCopy } from "./copy.js";
4
4
  export function ChatFirstPrimaryNavigation({ onNewChat, onOpenIntegrations, onOpenScheduled, onSearch, activeTab, collapsed = false, stickyNewChat = false, copy = defaultChatFirstCopy, }) {
5
- const tabClassName = (tab) => `flex h-8 w-full items-center gap-2 rounded-md px-2 text-[13px] font-medium transition-[background-color,color] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${activeTab === tab
5
+ const tabClassName = (tab) => `flex h-8 w-full items-center gap-2 rounded-md text-[13px] font-medium transition-[background-color,color] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${collapsed ? "justify-center px-0" : "px-2"} ${activeTab === tab
6
6
  ? "bg-sidebar-accent text-sidebar-accent-foreground"
7
7
  : "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"}`;
8
- const renderTab = (tab, content, onSelect, className) => (_jsx("button", { type: "button", role: "tab", "aria-selected": activeTab === tab, "aria-label": tab === "new-chat" ? copy("newChat") : undefined, title: tab === "new-chat" && collapsed ? copy("newChat") : undefined, className: [tabClassName(tab), className].filter(Boolean).join(" "), onClick: onSelect, children: content }));
8
+ const tabLabel = (tab) => tab === "new-chat"
9
+ ? copy("newChat")
10
+ : tab === "integrations"
11
+ ? copy("integrations")
12
+ : copy("scheduled");
13
+ const renderTab = (tab, content, onSelect, className) => (_jsx("button", { type: "button", role: "tab", "aria-selected": activeTab === tab, "aria-label": collapsed || tab === "new-chat" ? tabLabel(tab) : undefined, title: collapsed ? tabLabel(tab) : undefined, className: [tabClassName(tab), className].filter(Boolean).join(" "), onClick: onSelect, children: content }));
9
14
  const newChatTab = onNewChat
10
15
  ? renderTab("new-chat", _jsxs(_Fragment, { children: [_jsx(IconPlus, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { className: collapsed ? "sr-only" : undefined, children: copy("newChat") })] }), onNewChat, "code-agents-primary-new-chat")
11
16
  : null;
12
- const integrationsTab = renderTab("integrations", _jsxs(_Fragment, { children: [_jsx(IconPlugConnected, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { children: copy("integrations") })] }), onOpenIntegrations);
13
- const scheduledTab = renderTab("scheduled", _jsxs(_Fragment, { children: [_jsx(IconClock, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { children: copy("scheduled") })] }), onOpenScheduled);
14
- const searchAction = onSearch ? (_jsxs("button", { type: "button", className: "mt-px flex h-8 w-full items-center gap-2 rounded-md px-2 text-[13px] font-medium text-sidebar-foreground/80 transition-[background-color,color] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: onSearch, children: [_jsx(IconSearch, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { children: copy("search") })] })) : null;
17
+ const integrationsTab = renderTab("integrations", _jsxs(_Fragment, { children: [_jsx(IconPlugConnected, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { className: collapsed ? "sr-only" : undefined, children: copy("integrations") })] }), onOpenIntegrations);
18
+ const scheduledTab = renderTab("scheduled", _jsxs(_Fragment, { children: [_jsx(IconClock, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { className: collapsed ? "sr-only" : undefined, children: copy("scheduled") })] }), onOpenScheduled);
19
+ const searchAction = onSearch ? (_jsxs("button", { type: "button", "aria-label": collapsed ? copy("search") : undefined, title: collapsed ? copy("search") : undefined, className: `mt-px flex h-8 w-full items-center gap-2 rounded-md text-[13px] font-medium text-sidebar-foreground/80 transition-[background-color,color] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${collapsed ? "justify-center px-0" : "px-2"}`, onClick: onSearch, children: [_jsx(IconSearch, { size: 15, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { className: collapsed ? "sr-only" : undefined, children: copy("search") })] })) : null;
15
20
  if (stickyNewChat) {
16
21
  return (_jsxs(_Fragment, { children: [newChatTab ? (_jsx("div", { className: "code-agents-primary-new-chat-shell", children: newChatTab })) : null, _jsxs("div", { className: "code-agents-nav-list", "aria-label": "Agent navigation", children: [_jsxs("div", { role: "tablist", "aria-label": "Primary navigation", className: "grid gap-px", children: [integrationsTab, scheduledTab] }), searchAction] })] }));
17
22
  }
@@ -54,6 +54,7 @@ export interface PerAppChatSidebarStateMessage {
54
54
  type: "agentNative.perAppChatState";
55
55
  data: {
56
56
  open: boolean;
57
+ hosted: boolean;
57
58
  };
58
59
  }
59
60
  /** The embedded app asks a host for the current per-app chat state on mount. */
@@ -8,7 +8,7 @@ export { notifySessionInvalidated, useSession, type AuthSession, } from "../use-
8
8
  export { ACTION_KEEPALIVE_BODY_BUDGET_BYTES, callAction, tryCallActionKeepalive, useActionQuery, useActionMutation, type ActionRegistry, type ClientActionCallOptions, type ClientActionMethod, type KeepaliveActionCallRejectionReason, type KeepaliveActionCallResult, } from "../use-action.js";
9
9
  export { createAgentNativeQueryClient } from "../create-query-client.js";
10
10
  export { AppProviders, type AppProvidersProps } from "../app-providers.js";
11
- export { APP_CHAT_SIDEBAR_STATE_EVENT, APP_CHAT_SIDEBAR_STATE_MESSAGE, APP_CHAT_SIDEBAR_STATE_REQUEST_MESSAGE, buildAppChatSidebarStateMessage, buildAppChatSidebarStateRequest, isPerAppChatStorageKey, usePerAppChatOpen, type AppChatSidebarState, } from "../app-chat-sidebar.js";
11
+ export { APP_CHAT_SIDEBAR_STATE_EVENT, APP_CHAT_SIDEBAR_STATE_MESSAGE, APP_CHAT_SIDEBAR_STATE_REQUEST_MESSAGE, buildAppChatSidebarStateMessage, buildAppChatSidebarStateRequest, isPerAppChatStorageKey, requestPerAppChatCommand, usePerAppChatState, usePerAppChatOpen, type AppChatSidebarCommand, type AppChatSidebarState, } from "../app-chat-sidebar.js";
12
12
  export { usePinchZoom, type UsePinchZoomOptions } from "../use-pinch-zoom.js";
13
13
  export { useAvatarUrl, uploadAvatar, invalidateAvatarCache, } from "../use-avatar.js";
14
14
  export { usePollLoop, type UsePollLoopOptions, type UsePollLoopHandle, } from "../use-poll-loop.js";
@@ -8,7 +8,7 @@ export { notifySessionInvalidated, useSession, } from "../use-session.js";
8
8
  export { ACTION_KEEPALIVE_BODY_BUDGET_BYTES, callAction, tryCallActionKeepalive, useActionQuery, useActionMutation, } from "../use-action.js";
9
9
  export { createAgentNativeQueryClient } from "../create-query-client.js";
10
10
  export { AppProviders } from "../app-providers.js";
11
- export { APP_CHAT_SIDEBAR_STATE_EVENT, APP_CHAT_SIDEBAR_STATE_MESSAGE, APP_CHAT_SIDEBAR_STATE_REQUEST_MESSAGE, buildAppChatSidebarStateMessage, buildAppChatSidebarStateRequest, isPerAppChatStorageKey, usePerAppChatOpen, } from "../app-chat-sidebar.js";
11
+ export { APP_CHAT_SIDEBAR_STATE_EVENT, APP_CHAT_SIDEBAR_STATE_MESSAGE, APP_CHAT_SIDEBAR_STATE_REQUEST_MESSAGE, buildAppChatSidebarStateMessage, buildAppChatSidebarStateRequest, isPerAppChatStorageKey, requestPerAppChatCommand, usePerAppChatState, usePerAppChatOpen, } from "../app-chat-sidebar.js";
12
12
  export { usePinchZoom } from "../use-pinch-zoom.js";
13
13
  export { useAvatarUrl, uploadAvatar, invalidateAvatarCache, } from "../use-avatar.js";
14
14
  export { usePollLoop, } from "../use-poll-loop.js";
@@ -451,6 +451,9 @@ const CORE_FALLBACK_MESSAGES = {
451
451
  "runsTray.statusDone": "Done",
452
452
  "runsTray.statusFailed": "Failed",
453
453
  "runsTray.statusStopped": "Stopped",
454
+ "runsTray.statusNeedsApproval": "Needs approval",
455
+ "runsTray.statusNeedsInput": "Needs input",
456
+ "runsTray.statusPaused": "Paused",
454
457
  "runsTray.updatedJustNow": "Updated just now",
455
458
  "runsTray.finishedJustNow": "Finished just now",
456
459
  "runsTray.updatedMinutes": "Updated {{count}}m ago",
@@ -459,8 +462,8 @@ const CORE_FALLBACK_MESSAGES = {
459
462
  "runsTray.finishedHours": "Finished {{count}}h ago",
460
463
  "runsTray.updatedDate": "Updated {{date}}",
461
464
  "runsTray.finishedDate": "Finished {{date}}",
462
- "agentTask.backgroundTask": "Background task",
463
- "agentTask.stop": "Stop background task",
465
+ "agentTask.spawnedAgent": "Spawned agent",
466
+ "agentTask.stop": "Stop spawned agent",
464
467
  "agentTask.openThread": "Open task thread",
465
468
  "codeRequired.fallbackDetail": "Edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way you like.",
466
469
  "codeRequired.defaultFeature": "Make the requested code changes to this app",
@@ -24,20 +24,37 @@ function useRunsTrayState({ pollMs = 3000, limit = 5, hideWhenIdle = true, showR
24
24
  const includeRecent = showRecent ?? !hideWhenIdle;
25
25
  const runsVersion = useChangeVersion("runs");
26
26
  const refresh = useCallback(async (signal) => {
27
- try {
28
- const query = new URLSearchParams({ limit: String(limit) });
29
- if (!includeRecent)
30
- query.set("active", "true");
31
- const res = await fetch(agentNativePath(`/_agent-native/runs?${query.toString()}`), { signal });
32
- if (!res.ok)
33
- return;
34
- const rows = (await res.json());
35
- setRuns(rows);
27
+ const query = new URLSearchParams({ limit: String(limit) });
28
+ if (!includeRecent)
29
+ query.set("active", "true");
30
+ const [legacyResult, backgroundResult] = await Promise.allSettled([
31
+ fetch(agentNativePath(`/_agent-native/runs?${query.toString()}`), {
32
+ signal,
33
+ }),
34
+ fetch(agentNativePath(`/_agent-native/agent-chat/runs/list?${new URLSearchParams({ limit: String(limit) }).toString()}`), { signal }),
35
+ ]);
36
+ const rows = legacyResult.status === "fulfilled" && legacyResult.value.ok
37
+ ? await readLegacyRuns(legacyResult.value)
38
+ : null;
39
+ const backgroundRows = backgroundResult.status === "fulfilled" && backgroundResult.value.ok
40
+ ? await readBackgroundRuns(backgroundResult.value)
41
+ : null;
42
+ const hasSuccessfulResponse = rows !== null || backgroundRows !== null;
43
+ if (!hasSuccessfulResponse) {
44
+ return;
36
45
  }
37
- catch {
38
- // coercion-ok: `runs` keeps its last good value rather than being
39
- // cleared to an empty tray; the next poll tick retries.
46
+ const merged = new Map();
47
+ for (const row of rows ?? [])
48
+ merged.set(row.id, row);
49
+ for (const row of backgroundRows ?? []) {
50
+ const normalized = normalizeBackgroundRun(row);
51
+ if (includeRecent || normalized.status === "running") {
52
+ merged.set(normalized.id, normalized);
53
+ }
40
54
  }
55
+ setRuns([...merged.values()]
56
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
57
+ .slice(0, limit));
41
58
  }, [includeRecent, limit]);
42
59
  useEffect(() => {
43
60
  void refresh();
@@ -54,7 +71,10 @@ function useRunsTrayState({ pollMs = 3000, limit = 5, hideWhenIdle = true, showR
54
71
  enabled: pollMs > 0 || hasActiveRun,
55
72
  });
56
73
  const dismissRun = useCallback(async (runId) => {
74
+ const existing = runs.find((run) => run.id === runId);
57
75
  setRuns((current) => current.filter((run) => run.id !== runId));
76
+ if (existing && isBackgroundRun(existing))
77
+ return;
58
78
  try {
59
79
  const res = await fetch(agentNativePath(`/_agent-native/runs/${runId}`), {
60
80
  method: "DELETE",
@@ -66,7 +86,7 @@ function useRunsTrayState({ pollMs = 3000, limit = 5, hideWhenIdle = true, showR
66
86
  catch {
67
87
  refresh();
68
88
  }
69
- }, [refresh]);
89
+ }, [refresh, runs]);
70
90
  const stopRun = useCallback(async (runId) => {
71
91
  // Optimistic: mark as cancelled immediately so the UI is responsive.
72
92
  setRuns((current) => current.map((run) => run.id === runId
@@ -179,6 +199,67 @@ function RunsTrayContent({ runs, hasRuns, activeCount, terminalCount, onDismiss,
179
199
  ? t("runsTray.summaryRecent", { count: runs.length })
180
200
  : t("runsTray.noTrackedWorkYet") })] }), hasRuns ? (_jsx("div", { className: "max-h-96 divide-y divide-border overflow-y-auto", children: runs.map((run) => (_jsx(RunRow, { run: run, onDismiss: onDismiss, onStop: onStop, onOpenThread: onOpenThread }, run.id))) })) : (_jsxs("div", { className: "px-3 py-6 text-center", children: [_jsx(IconClock, { size: 22, className: "mx-auto text-muted-foreground/50", "aria-hidden": true }), _jsx("div", { className: "mt-2 text-sm font-medium text-foreground", children: t("runsTray.noRecentRuns") }), _jsx("p", { className: "mt-1 text-xs leading-relaxed text-muted-foreground", children: t("runsTray.emptyDescription") })] }))] }));
181
201
  }
202
+ async function readLegacyRuns(response) {
203
+ try {
204
+ const body = await response.json();
205
+ return Array.isArray(body) ? body : null;
206
+ }
207
+ catch {
208
+ // coercion-ok: malformed responses return null, distinct from an empty run list.
209
+ return null;
210
+ }
211
+ }
212
+ async function readBackgroundRuns(response) {
213
+ try {
214
+ const body = await response.json();
215
+ if (!body || typeof body !== "object" || !("runs" in body))
216
+ return null;
217
+ const runs = body.runs;
218
+ return Array.isArray(runs) ? runs : null;
219
+ }
220
+ catch {
221
+ // coercion-ok: malformed responses return null, distinct from an empty run list.
222
+ return null;
223
+ }
224
+ }
225
+ function normalizeBackgroundRun(run) {
226
+ const metadata = run.metadata ?? {};
227
+ const threadId = run.sourceRecord?.threadId ??
228
+ (typeof metadata.threadId === "string" ? metadata.threadId : undefined);
229
+ const status = run.status === "errored"
230
+ ? "failed"
231
+ : run.status === "queued" ||
232
+ run.status === "running" ||
233
+ run.status === "needs-input" ||
234
+ run.status === "needs-approval"
235
+ ? "running"
236
+ : "succeeded";
237
+ return {
238
+ id: run.id,
239
+ owner: "",
240
+ title: run.title,
241
+ step: run.needsApproval
242
+ ? "Needs approval"
243
+ : (run.subtitle ?? (run.status === "paused" ? "Paused" : run.phase)),
244
+ percent: null,
245
+ status,
246
+ startedAt: run.createdAt,
247
+ updatedAt: run.updatedAt,
248
+ completedAt: status === "running" ? null : run.updatedAt,
249
+ metadata: {
250
+ ...metadata,
251
+ kind: run.kind,
252
+ source: run.source,
253
+ sourceLabel: run.sourceLabel,
254
+ backgroundStatus: run.status,
255
+ backgroundGoalId: run.goalId,
256
+ needsApproval: run.needsApproval,
257
+ needsInput: run.needsInput,
258
+ ...(run.surfaceUrl ? { surfaceUrl: run.surfaceUrl } : {}),
259
+ ...(threadId ? { threadId } : {}),
260
+ },
261
+ };
262
+ }
182
263
  function getRunThreadId(run) {
183
264
  const metadata = run.metadata ?? {};
184
265
  const direct = typeof metadata.threadId === "string"
@@ -223,13 +304,20 @@ function isAgentTeamRun(run) {
223
304
  run.metadata !== null &&
224
305
  run.metadata.kind === "agent-team");
225
306
  }
307
+ function isBackgroundRun(run) {
308
+ return (typeof run.metadata === "object" &&
309
+ run.metadata !== null &&
310
+ typeof run.metadata.backgroundStatus ===
311
+ "string");
312
+ }
226
313
  function RunRow({ run, onDismiss, onStop, onOpenThread, }) {
227
314
  const t = useT();
228
315
  const { formatDate } = useFormatters();
229
316
  const threadId = getRunThreadId(run);
230
317
  const isRunning = run.status === "running";
231
- const canStop = isRunning && isAgentTeamRun(run);
232
- return (_jsxs("div", { className: "flex flex-col gap-1.5 px-3 py-2.5 text-sm", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate font-medium text-foreground", children: run.title }), run.step ? (_jsx("div", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: run.step })) : null] }), _jsx(StatusPill, { status: run.status })] }), run.percent != null || isRunning ? (_jsx("div", { className: "h-1 w-full overflow-hidden rounded bg-muted", children: run.percent != null ? (_jsx("div", { className: cn("h-full origin-left transition-transform duration-200 ease-[var(--ease-collapse)]", run.status === "failed"
318
+ const canStop = isRunning && (isAgentTeamRun(run) || isBackgroundRun(run));
319
+ const backgroundStatus = getBackgroundStatus(run);
320
+ return (_jsxs("div", { className: "flex flex-col gap-1.5 px-3 py-2.5 text-sm", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate font-medium text-foreground", children: run.title }), run.step ? (_jsx("div", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: run.step })) : null] }), _jsx(StatusPill, { status: run.status, backgroundStatus: backgroundStatus })] }), run.percent != null || isRunning ? (_jsx("div", { className: "h-1 w-full overflow-hidden rounded bg-muted", children: run.percent != null ? (_jsx("div", { className: cn("h-full origin-left transition-transform duration-200 ease-[var(--ease-collapse)]", run.status === "failed"
233
321
  ? "bg-destructive"
234
322
  : run.status === "cancelled"
235
323
  ? "bg-muted-foreground/50"
@@ -250,12 +338,37 @@ const STATUS_PILL_STYLES = {
250
338
  failed: "bg-destructive/10 text-destructive",
251
339
  cancelled: "bg-muted text-muted-foreground",
252
340
  };
253
- function StatusPill({ status }) {
341
+ function StatusPill({ status, backgroundStatus, }) {
254
342
  const t = useT();
343
+ if (backgroundStatus === "needs-approval") {
344
+ return (_jsxs("span", { className: "inline-flex h-5 shrink-0 items-center gap-1 rounded-md bg-primary/10 px-1.5 text-[10px] font-medium text-primary", children: [_jsx(IconAlertCircle, { size: 12, "aria-hidden": true }), t("runsTray.statusNeedsApproval", { defaultValue: "Needs approval" })] }));
345
+ }
346
+ if (backgroundStatus === "needs-input") {
347
+ return (_jsxs("span", { className: "inline-flex h-5 shrink-0 items-center gap-1 rounded-md bg-primary/10 px-1.5 text-[10px] font-medium text-primary", children: [_jsx(IconAlertCircle, { size: 12, "aria-hidden": true }), t("runsTray.statusNeedsInput", { defaultValue: "Needs input" })] }));
348
+ }
349
+ if (backgroundStatus === "paused") {
350
+ return (_jsxs("span", { className: "inline-flex h-5 shrink-0 items-center gap-1 rounded-md bg-muted px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx(IconClock, { size: 12, "aria-hidden": true }), t("runsTray.statusPaused", { defaultValue: "Paused" })] }));
351
+ }
255
352
  const { Icon, className } = STATUS_GLYPHS[status];
256
353
  const spinClass = status === "running" ? " animate-spin" : "";
257
354
  return (_jsxs("span", { className: cn("inline-flex h-5 shrink-0 items-center gap-1 rounded-md px-1.5 text-[10px] font-medium", STATUS_PILL_STYLES[status]), children: [_jsx(Icon, { size: 12, className: `${className}${spinClass}`, "aria-hidden": true }), t(STATUS_COPY_KEYS[status])] }));
258
355
  }
356
+ function getBackgroundStatus(run) {
357
+ const value = run.metadata?.backgroundStatus;
358
+ return typeof value === "string" && isBackgroundStatus(value)
359
+ ? value
360
+ : undefined;
361
+ }
362
+ function isBackgroundStatus(value) {
363
+ return (value === "queued" ||
364
+ value === "running" ||
365
+ value === "paused" ||
366
+ value === "needs-input" ||
367
+ value === "needs-approval" ||
368
+ value === "completed" ||
369
+ value === "errored" ||
370
+ value === "unknown");
371
+ }
259
372
  // dark: variants only where there's no semantic token for the colour
260
373
  // (e.g. success green isn't in shadcn's default palette).
261
374
  const STATUS_GLYPHS = {
@@ -12,5 +12,5 @@ export function SettingsGroup({ id, title, description, variant = "default", cla
12
12
  }
13
13
  /** One setting: label and description on the start side, control on the end. */
14
14
  export function SettingsRow({ id, label, description, icon, status, control, children, className, }) {
15
- return (_jsxs("div", { id: id, className: cn("scroll-mt-16 px-5 py-4 sm:px-6", className), children: [_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "flex min-w-0 gap-3", children: [icon && (_jsx("span", { className: "mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-background text-muted-foreground [&>svg]:size-[18px]", children: icon })), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium text-foreground", children: label }), status] }), description && (_jsx("p", { className: "mt-1 max-w-xl text-sm leading-6 text-muted-foreground", children: description }))] })] }), control && (_jsx("div", { className: "shrink-0 sm:ms-4 sm:text-end", children: control }))] }), children && _jsx("div", { className: "mt-4", children: children })] }));
15
+ return (_jsxs("div", { id: id, className: cn("agent-native-settings-row scroll-mt-16 px-5 py-4 sm:px-6", className), children: [_jsxs("div", { className: "agent-native-settings-row__layout flex flex-col gap-3", children: [_jsxs("div", { className: "agent-native-settings-row__main flex min-w-0 flex-1 gap-3", children: [icon && (_jsx("span", { className: "mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-background text-muted-foreground [&>svg]:size-[18px]", children: icon })), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium text-foreground", children: label }), status] }), description && (_jsx("p", { className: "mt-1 max-w-xl text-sm leading-6 text-muted-foreground", children: description }))] })] }), control && (_jsx("div", { className: "agent-native-settings-row__control max-w-full shrink-0", children: control }))] }), children && _jsx("div", { className: "mt-4", children: children })] }));
16
16
  }
@@ -467,10 +467,9 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
467
467
  // it; the server hasn't seen it yet because there's no POST anymore,
468
468
  // the row gets written when the user sends a message.
469
469
  // - savedId is set but not on the current page → look it up directly. A
470
- // found thread stays active. An auto-creating surface replaces a confirmed
471
- // 404 with a fresh local tab rather than reusing an inaccessible server id.
472
- // A list-only surface keeps the saved id as an optimistic empty tab so a
473
- // never-posted tab remains selectable after reload.
470
+ // found thread stays active. An unavailable lookup keeps the saved id so
471
+ // the detail surface can preserve cached state and offer recovery instead
472
+ // of replacing a shared/reopened conversation with a blank local tab.
474
473
  // - No savedId → synthesize a fresh local id (no POST; server creates the
475
474
  // row on first message). The server may contain chats from another
476
475
  // branch, preview, or project that shares the same user/database, so
@@ -512,7 +511,12 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
512
511
  const restoredIsUnavailable = restoredThread === null && lookupRestored && !restoredOnPage;
513
512
  const restoredBelongsElsewhere = Boolean(restoredThread &&
514
513
  !threadCanStayVisibleInScope(restoredThread.scope ?? null, scopeRef.current));
515
- const restoredNeedsReplacement = restoredBelongsElsewhere || (restoredIsUnavailable && autoCreate);
514
+ // Keep the saved id when the direct lookup says 404. AssistantChat owns
515
+ // the detail restore and can show a retryable error while preserving any
516
+ // cached transcript or composer draft. Replacing the id here silently
517
+ // turns a shared/reopened conversation into a new blank chat after the
518
+ // slower lookup finishes.
519
+ const restoredNeedsReplacement = restoredBelongsElsewhere;
516
520
  if (restoredNeedsReplacement)
517
521
  setActiveThreadId(null);
518
522
  const savedId = restoredNeedsReplacement ? null : restoredId;
@@ -534,7 +538,8 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
534
538
  }
535
539
  else if (savedId &&
536
540
  !newlyCreatedRef.current.has(savedId) &&
537
- !loadedHasSavedId) {
541
+ !loadedHasSavedId &&
542
+ !restoredIsUnavailable) {
538
543
  // The tab the user left open isn't a server thread and we didn't
539
544
  // create it this session (newlyCreatedRef was wiped by the
540
545
  // reload). Treat it as the empty tab it is — keep its id and
package/dist/config.d.ts CHANGED
@@ -59,6 +59,16 @@ export interface AgentNativeChangelogConfig {
59
59
  /** Whether agents should create and surface user-facing changelog entries. */
60
60
  enabled?: boolean;
61
61
  }
62
+ export type AgentNativeHarnessRuntime = "claude-code" | "codex" | "pi" | "opencode";
63
+ export interface AgentNativeHarnessConfig {
64
+ /** Optionally narrow the hosted harness picker to these runtimes. */
65
+ runtimes?: AgentNativeHarnessRuntime[];
66
+ }
67
+ /**
68
+ * The intentionally small app-level switch for hosted tools-only harnesses.
69
+ * `true` enables every supported runtime; an object narrows the picker.
70
+ */
71
+ export type AgentNativeHarnessSetting = boolean | AgentNativeHarnessConfig;
62
72
  export interface AgentNativeConfig {
63
73
  version?: typeof AGENT_NATIVE_CONFIG_VERSION;
64
74
  onboarding?: AgentNativeOnboardingConfig;
@@ -67,6 +77,7 @@ export interface AgentNativeConfig {
67
77
  instructions?: AgentNativeInstructionsConfig;
68
78
  translations?: AgentNativeTranslationsConfig;
69
79
  changelog?: AgentNativeChangelogConfig;
80
+ harness?: AgentNativeHarnessSetting;
70
81
  }
71
82
  export interface AgentNativeConfigContext {
72
83
  command: "serve" | "build";
package/dist/config.js CHANGED
@@ -30,6 +30,7 @@ export function normalizeAgentNativeConfig(input, source = "agent-native config"
30
30
  const instructionsValue = input.instructions;
31
31
  const translationsValue = input.translations;
32
32
  const changelogValue = input.changelog;
33
+ const harnessValue = input.harness;
33
34
  const normalized = {
34
35
  ...(input.version === undefined
35
36
  ? {}
@@ -57,6 +58,9 @@ export function normalizeAgentNativeConfig(input, source = "agent-native config"
57
58
  if (changelogValue !== undefined) {
58
59
  normalized.changelog = normalizeChangelogConfig(changelogValue, `${source}.changelog`);
59
60
  }
61
+ if (harnessValue !== undefined) {
62
+ normalized.harness = normalizeHarnessConfig(harnessValue, `${source}.harness`);
63
+ }
60
64
  return normalized;
61
65
  }
62
66
  export function mergeAgentNativeConfigs(base, override) {
@@ -128,6 +132,7 @@ export function mergeAgentNativeConfigs(base, override) {
128
132
  ...override.changelog,
129
133
  }
130
134
  : undefined,
135
+ harness: mergeHarnessSettings(base.harness, override.harness),
131
136
  };
132
137
  }
133
138
  export function resolveAgentNativeConfig(input, context) {
@@ -254,6 +259,47 @@ function normalizeChangelogConfig(value, source) {
254
259
  }
255
260
  return value.enabled === undefined ? {} : { enabled: value.enabled };
256
261
  }
262
+ function normalizeHarnessConfig(value, source) {
263
+ if (typeof value === "boolean")
264
+ return value;
265
+ if (!isRecord(value)) {
266
+ throw new Error(`${source} must be a boolean or object`);
267
+ }
268
+ if ("enabled" in value || "ui" in value) {
269
+ throw new Error(`${source} must be true or an object with runtimes`);
270
+ }
271
+ const runtimes = value.runtimes;
272
+ if (runtimes !== undefined) {
273
+ if (!Array.isArray(runtimes) ||
274
+ runtimes.some((runtime) => !isAgentNativeHarnessRuntime(runtime))) {
275
+ throw new Error(`${source}.runtimes must contain only "claude-code", "codex", "pi", or "opencode"`);
276
+ }
277
+ }
278
+ return {
279
+ ...(runtimes === undefined
280
+ ? {}
281
+ : { runtimes: [...new Set(runtimes)] }),
282
+ };
283
+ }
284
+ function mergeHarnessSettings(base, override) {
285
+ if (override === undefined)
286
+ return base;
287
+ if (typeof override === "boolean")
288
+ return override;
289
+ if (typeof base !== "object" || base === null)
290
+ return override;
291
+ if (base.runtimes === undefined && override.runtimes === undefined) {
292
+ return {};
293
+ }
294
+ return {
295
+ runtimes: [
296
+ ...new Set([
297
+ ...(base.runtimes ?? []),
298
+ ...(override.runtimes ?? []),
299
+ ]),
300
+ ],
301
+ };
302
+ }
257
303
  function normalizeRelativeFilePath(value, source) {
258
304
  const normalized = value.trim().replaceAll("\\", "/");
259
305
  if (!normalized ||
@@ -286,6 +332,12 @@ function isFirstRunMode(value) {
286
332
  value === "connect" ||
287
333
  value === "connect-and-integrations");
288
334
  }
335
+ function isAgentNativeHarnessRuntime(value) {
336
+ return (value === "claude-code" ||
337
+ value === "codex" ||
338
+ value === "pi" ||
339
+ value === "opencode");
340
+ }
289
341
  function isRecord(value) {
290
342
  return !!value && typeof value === "object" && !Array.isArray(value);
291
343
  }