@agent-native/core 0.125.0 → 0.127.1

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 (188) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +79 -0
  3. package/corpus/core/docs/content/external-agents-catalog.mdx +14 -11
  4. package/corpus/core/docs/content/mcp-protocol.mdx +9 -4
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/a2a/index.ts +2 -0
  7. package/corpus/core/src/agent/production-agent.ts +6 -1
  8. package/corpus/core/src/agent/run-manager.ts +37 -0
  9. package/corpus/core/src/agent/run-store.ts +18 -0
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +75 -43
  11. package/corpus/core/src/client/agent-chat.ts +19 -1
  12. package/corpus/core/src/client/chat/tool-call-display.tsx +25 -0
  13. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +23 -0
  14. package/corpus/core/src/client/settings/AgentsSection.tsx +565 -71
  15. package/corpus/core/src/client/settings/SecretsSection.tsx +71 -36
  16. package/corpus/core/src/client/use-chat-models.ts +54 -37
  17. package/corpus/core/src/localization/default-messages.ts +11 -0
  18. package/corpus/core/src/mcp/ask-app-task-handle.ts +183 -0
  19. package/corpus/core/src/mcp/builtin-tools.ts +146 -34
  20. package/corpus/core/src/server/agent-capabilities.ts +8 -1
  21. package/corpus/core/src/server/agent-chat-plugin.ts +17 -0
  22. package/corpus/core/src/server/agent-discovery.ts +1 -1
  23. package/corpus/core/src/server/agent-peer-probe.ts +133 -0
  24. package/corpus/core/src/server/core-routes-plugin.ts +67 -0
  25. package/corpus/core/src/server/email-template.ts +13 -0
  26. package/corpus/core/src/server/email.ts +40 -4
  27. package/corpus/core/src/shared/mcp-embed-headers.ts +4 -0
  28. package/corpus/core/src/sharing/actions/share-resource.ts +55 -5
  29. package/corpus/core/src/sharing/registry.ts +41 -1
  30. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
  31. package/corpus/templates/assets/actions/edit-image.ts +31 -18
  32. package/corpus/templates/assets/actions/refine-image.ts +43 -29
  33. package/corpus/templates/assets/actions/restyle-image.ts +30 -19
  34. package/corpus/templates/assets/actions/variant-slots.ts +26 -0
  35. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +85 -17
  36. package/corpus/templates/clips/changelog/2026-07-27-share-notification-emails-now-include-a-clickable-recording-.md +6 -0
  37. package/corpus/templates/clips/server/db/index.ts +28 -6
  38. package/corpus/templates/clips/server/lib/share-email-hero.ts +105 -0
  39. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +100 -12
  40. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +17 -0
  41. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +169 -22
  42. package/corpus/templates/design/app/hooks/use-question-flow.ts +21 -1
  43. package/corpus/templates/design/changelog/2026-07-28-the-model-you-pick-when-starting-a-new-design-is-now-used-fo.md +6 -0
  44. package/corpus/templates/dispatch/changelog/2026-07-28-pending-apps-are-hidden-by-default-and-app-ownership-details.md +6 -0
  45. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +5 -1
  46. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +5 -0
  47. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +31 -8
  48. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +15 -0
  49. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +78 -8
  50. package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +179 -0
  51. package/corpus/templates/slides/app/components/presentation/present-channel.ts +15 -0
  52. package/corpus/templates/slides/app/i18n/en-US.ts +10 -1
  53. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +67 -0
  54. package/corpus/templates/slides/app/lib/export-pptx-client.ts +16 -3
  55. package/corpus/templates/slides/app/pages/DeckEditor.tsx +11 -0
  56. package/corpus/templates/slides/app/pages/Presentation.tsx +11 -3
  57. package/corpus/templates/slides/changelog/2026-07-28-export-to-google-slides-now-creates-the-deck-directly-in-you.md +6 -0
  58. package/corpus/templates/slides/changelog/2026-07-28-opening-a-presentation-link-directly-no-longer-bounces-back-.md +6 -0
  59. package/corpus/templates/slides/changelog/2026-07-28-presenter-view-open-a-second-window-while-presenting-to-see-.md +6 -0
  60. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +90 -0
  61. package/corpus/toolkit/CHANGELOG.md +6 -0
  62. package/corpus/toolkit/package.json +1 -1
  63. package/corpus/toolkit/src/composer/TiptapComposer.tsx +143 -111
  64. package/corpus/toolkit/src/composer/runtime-adapters.tsx +2 -0
  65. package/dist/a2a/index.d.ts +2 -0
  66. package/dist/a2a/index.d.ts.map +1 -1
  67. package/dist/a2a/index.js +1 -0
  68. package/dist/a2a/index.js.map +1 -1
  69. package/dist/agent/production-agent.d.ts +2 -2
  70. package/dist/agent/production-agent.d.ts.map +1 -1
  71. package/dist/agent/production-agent.js +6 -3
  72. package/dist/agent/production-agent.js.map +1 -1
  73. package/dist/agent/run-manager.d.ts +11 -0
  74. package/dist/agent/run-manager.d.ts.map +1 -1
  75. package/dist/agent/run-manager.js +34 -1
  76. package/dist/agent/run-manager.js.map +1 -1
  77. package/dist/agent/run-store.d.ts +8 -0
  78. package/dist/agent/run-store.d.ts.map +1 -1
  79. package/dist/agent/run-store.js +16 -0
  80. package/dist/agent/run-store.js.map +1 -1
  81. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  82. package/dist/client/MultiTabAssistantChat.js +60 -31
  83. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  84. package/dist/client/agent-chat.d.ts +6 -0
  85. package/dist/client/agent-chat.d.ts.map +1 -1
  86. package/dist/client/agent-chat.js +13 -1
  87. package/dist/client/agent-chat.js.map +1 -1
  88. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  89. package/dist/client/chat/tool-call-display.js +14 -0
  90. package/dist/client/chat/tool-call-display.js.map +1 -1
  91. package/dist/client/resources/mcp-integration-catalog.d.ts +6 -0
  92. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  93. package/dist/client/resources/mcp-integration-catalog.js +16 -0
  94. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  95. package/dist/client/settings/AgentsSection.d.ts.map +1 -1
  96. package/dist/client/settings/AgentsSection.js +250 -30
  97. package/dist/client/settings/AgentsSection.js.map +1 -1
  98. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  99. package/dist/client/settings/SecretsSection.js +16 -2
  100. package/dist/client/settings/SecretsSection.js.map +1 -1
  101. package/dist/client/use-chat-models.d.ts.map +1 -1
  102. package/dist/client/use-chat-models.js +46 -27
  103. package/dist/client/use-chat-models.js.map +1 -1
  104. package/dist/collab/routes.d.ts +2 -2
  105. package/dist/collab/struct-routes.d.ts +1 -1
  106. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  107. package/dist/localization/default-messages.d.ts +9 -0
  108. package/dist/localization/default-messages.d.ts.map +1 -1
  109. package/dist/localization/default-messages.js +9 -0
  110. package/dist/localization/default-messages.js.map +1 -1
  111. package/dist/mcp/ask-app-task-handle.d.ts +25 -0
  112. package/dist/mcp/ask-app-task-handle.d.ts.map +1 -0
  113. package/dist/mcp/ask-app-task-handle.js +136 -0
  114. package/dist/mcp/ask-app-task-handle.js.map +1 -0
  115. package/dist/mcp/builtin-tools.d.ts +3 -3
  116. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  117. package/dist/mcp/builtin-tools.js +115 -38
  118. package/dist/mcp/builtin-tools.js.map +1 -1
  119. package/dist/observability/routes.d.ts +1 -1
  120. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  121. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  122. package/dist/secrets/routes.d.ts +9 -9
  123. package/dist/server/agent-capabilities.d.ts +7 -1
  124. package/dist/server/agent-capabilities.d.ts.map +1 -1
  125. package/dist/server/agent-capabilities.js +1 -0
  126. package/dist/server/agent-capabilities.js.map +1 -1
  127. package/dist/server/agent-chat-plugin.d.ts +7 -0
  128. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  129. package/dist/server/agent-chat-plugin.js +16 -1
  130. package/dist/server/agent-chat-plugin.js.map +1 -1
  131. package/dist/server/agent-discovery.d.ts +1 -0
  132. package/dist/server/agent-discovery.d.ts.map +1 -1
  133. package/dist/server/agent-discovery.js +1 -1
  134. package/dist/server/agent-discovery.js.map +1 -1
  135. package/dist/server/agent-peer-probe.d.ts +37 -0
  136. package/dist/server/agent-peer-probe.d.ts.map +1 -0
  137. package/dist/server/agent-peer-probe.js +90 -0
  138. package/dist/server/agent-peer-probe.js.map +1 -0
  139. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  140. package/dist/server/core-routes-plugin.js +53 -0
  141. package/dist/server/core-routes-plugin.js.map +1 -1
  142. package/dist/server/email-template.d.ts +6 -0
  143. package/dist/server/email-template.d.ts.map +1 -1
  144. package/dist/server/email-template.js +6 -0
  145. package/dist/server/email-template.js.map +1 -1
  146. package/dist/server/email.d.ts +7 -0
  147. package/dist/server/email.d.ts.map +1 -1
  148. package/dist/server/email.js +31 -6
  149. package/dist/server/email.js.map +1 -1
  150. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  151. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  152. package/dist/shared/mcp-embed-headers.js +4 -0
  153. package/dist/shared/mcp-embed-headers.js.map +1 -1
  154. package/dist/sharing/actions/share-resource.d.ts.map +1 -1
  155. package/dist/sharing/actions/share-resource.js +49 -5
  156. package/dist/sharing/actions/share-resource.js.map +1 -1
  157. package/dist/sharing/registry.d.ts +40 -1
  158. package/dist/sharing/registry.d.ts.map +1 -1
  159. package/dist/sharing/registry.js.map +1 -1
  160. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
  161. package/docs/content/external-agents-catalog.mdx +14 -11
  162. package/docs/content/mcp-protocol.mdx +9 -4
  163. package/package.json +2 -2
  164. package/src/a2a/index.ts +2 -0
  165. package/src/agent/production-agent.ts +6 -1
  166. package/src/agent/run-manager.ts +37 -0
  167. package/src/agent/run-store.ts +18 -0
  168. package/src/client/MultiTabAssistantChat.tsx +75 -43
  169. package/src/client/agent-chat.ts +19 -1
  170. package/src/client/chat/tool-call-display.tsx +25 -0
  171. package/src/client/resources/mcp-integration-catalog.ts +23 -0
  172. package/src/client/settings/AgentsSection.tsx +565 -71
  173. package/src/client/settings/SecretsSection.tsx +71 -36
  174. package/src/client/use-chat-models.ts +54 -37
  175. package/src/localization/default-messages.ts +11 -0
  176. package/src/mcp/ask-app-task-handle.ts +183 -0
  177. package/src/mcp/builtin-tools.ts +146 -34
  178. package/src/server/agent-capabilities.ts +8 -1
  179. package/src/server/agent-chat-plugin.ts +17 -0
  180. package/src/server/agent-discovery.ts +1 -1
  181. package/src/server/agent-peer-probe.ts +133 -0
  182. package/src/server/core-routes-plugin.ts +67 -0
  183. package/src/server/email-template.ts +13 -0
  184. package/src/server/email.ts +40 -4
  185. package/src/shared/mcp-embed-headers.ts +4 -0
  186. package/src/sharing/actions/share-resource.ts +55 -5
  187. package/src/sharing/registry.ts +41 -1
  188. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
@@ -1,17 +1,35 @@
1
- import { IconPlus, IconPencil, IconTrash, IconX } from "@tabler/icons-react";
1
+ import { ButtonBase as ToolkitButtonBase } from "@agent-native/toolkit/ui/button";
2
+ import {
3
+ IconPlus,
4
+ IconPencil,
5
+ IconTrash,
6
+ IconX,
7
+ IconCheck,
8
+ IconLoader2,
9
+ IconAlertTriangle,
10
+ IconExternalLink,
11
+ IconRefresh,
12
+ } from "@tabler/icons-react";
2
13
  import { useState, useEffect, useRef, useCallback } from "react";
3
14
 
15
+ import {
16
+ buildOpenRoutePath,
17
+ buildSettingsRoute,
18
+ STANDARD_SETTINGS_TABS,
19
+ } from "../../navigation/index.js";
4
20
  import {
5
21
  getRemoteAgentIdFromPath,
6
22
  isRemoteAgentPath,
23
+ REMOTE_AGENT_RESOURCE_PREFIX,
7
24
  remoteAgentResourcePath,
8
25
  } from "../../resources/metadata.js";
9
- import { agentNativePath } from "../api-path.js";
26
+ import { agentNativePath, appBasePath } from "../api-path.js";
10
27
  import {
11
28
  Tooltip,
12
29
  TooltipContent,
13
30
  TooltipTrigger,
14
31
  } from "../components/ui/tooltip.js";
32
+ import { useOrg, useSyncA2ASecret } from "../org/hooks.js";
15
33
 
16
34
  interface AgentInfo {
17
35
  id: string;
@@ -21,6 +39,61 @@ interface AgentInfo {
21
39
  description?: string;
22
40
  }
23
41
 
42
+ /** Wire shape of `GET /_agent-native/agents/probe` (single or batched result). */
43
+ interface AgentProbeResult {
44
+ url: string;
45
+ reachable: boolean;
46
+ name?: string;
47
+ description?: string;
48
+ securitySchemes?: string[];
49
+ /** Absent (not false) whenever the auth check never ran or never resolved. */
50
+ authorized?: boolean;
51
+ authError?: string;
52
+ publicSkills?: number;
53
+ error?: string;
54
+ }
55
+
56
+ function describeSkills(publicSkills: number | undefined): string | null {
57
+ if (publicSkills === undefined) return null;
58
+ // An empty public skill list only means the card advertises no anonymous-
59
+ // safe actions — the peer still has authenticated reads/writes. Saying so
60
+ // plainly avoids reading as "this agent can't do anything."
61
+ if (publicSkills === 0) return "reads require auth";
62
+ return `${publicSkills} public skill${publicSkills === 1 ? "" : "s"}`;
63
+ }
64
+
65
+ /** One-line status for the row dot tooltip. */
66
+ function describeProbeTooltip(result: AgentProbeResult): string {
67
+ if (!result.reachable) {
68
+ return `Unreachable${result.error ? `: ${result.error}` : ""}`;
69
+ }
70
+ if (result.authorized === false) {
71
+ return "Reachable, but the peer rejected our token — calls will 401 in production";
72
+ }
73
+ if (result.authorized === undefined) {
74
+ return "Reachable; couldn't verify our token";
75
+ }
76
+ return "Reachable and authorized";
77
+ }
78
+
79
+ /** Multi-clause status line for the Add popover's Check result. */
80
+ function describeCheckResult(result: AgentProbeResult): string {
81
+ if (!result.reachable) {
82
+ return result.error ?? "Not reachable";
83
+ }
84
+ const scheme = result.securitySchemes?.length
85
+ ? result.securitySchemes.join(", ")
86
+ : "no auth scheme advertised";
87
+ const authText =
88
+ result.authorized === false
89
+ ? "the peer rejected our token — calls will 401 in production"
90
+ : result.authorized === undefined
91
+ ? `couldn't verify our token${result.authError ? ` (${result.authError})` : ""}`
92
+ : "our token works";
93
+ const skills = describeSkills(result.publicSkills);
94
+ return [`Live · ${scheme}`, authText, skills].filter(Boolean).join(" · ");
95
+ }
96
+
24
97
  function AgentEditPopover({
25
98
  agent,
26
99
  onSave,
@@ -125,21 +198,68 @@ function AgentEditPopover({
125
198
  );
126
199
  }
127
200
 
201
+ type CheckState =
202
+ | { status: "idle" }
203
+ | { status: "checking" }
204
+ | { status: "done"; result: AgentProbeResult }
205
+ | { status: "error"; message: string };
206
+
207
+ interface AddedAgentInfo {
208
+ name: string;
209
+ url: string;
210
+ description: string;
211
+ }
212
+
213
+ /** Builds an absolute deep link into a peer's own Settings > Agents Add
214
+ * popover, prefilled with THIS app's own name/url/description, via the
215
+ * existing `/_agent-native/open` route's `f_*` filter-forwarding (the only
216
+ * non-reserved params the open route echoes onto the redirect URL instead of
217
+ * only stashing them server-side for `navigate` polling). No new endpoint. */
218
+ function buildPeerRegisterBackLink(peerUrl: string): string {
219
+ const selfUrl = `${window.location.origin}${appBasePath()}`;
220
+ const selfName = document.title.trim() || window.location.hostname;
221
+ const openPath = buildOpenRoutePath({
222
+ view: "settings",
223
+ to: buildSettingsRoute(STANDARD_SETTINGS_TABS.agent),
224
+ params: {
225
+ f_agentName: selfName,
226
+ f_agentUrl: selfUrl,
227
+ },
228
+ });
229
+ return `${peerUrl.replace(/\/+$/, "")}${openPath}`;
230
+ }
231
+
128
232
  function AgentAddPopover({
233
+ initialName = "",
234
+ initialUrl = "",
235
+ initialDescription = "",
236
+ secretSet,
237
+ syncSecret,
129
238
  onAdd,
130
239
  onClose,
131
240
  }: {
132
- onAdd: (name: string, url: string, description: string) => void;
241
+ initialName?: string;
242
+ initialUrl?: string;
243
+ initialDescription?: string;
244
+ secretSet: boolean | undefined;
245
+ syncSecret: ReturnType<typeof useSyncA2ASecret>;
246
+ onAdd: (name: string, url: string, description: string) => Promise<boolean>;
133
247
  onClose: () => void;
134
248
  }) {
135
- const [name, setName] = useState("");
136
- const [url, setUrl] = useState("");
137
- const [description, setDescription] = useState("");
249
+ const [name, setName] = useState(initialName);
250
+ const [url, setUrl] = useState(initialUrl);
251
+ const [description, setDescription] = useState(initialDescription);
252
+ const [check, setCheck] = useState<CheckState>({ status: "idle" });
253
+ const [added, setAdded] = useState<AddedAgentInfo | null>(null);
138
254
  const nameRef = useRef<HTMLInputElement>(null);
255
+ const urlRef = useRef<HTMLInputElement>(null);
139
256
  const popoverRef = useRef<HTMLDivElement>(null);
140
257
 
141
258
  useEffect(() => {
142
- const t = setTimeout(() => nameRef.current?.focus(), 50);
259
+ const t = setTimeout(
260
+ () => (urlRef.current?.value ? nameRef : urlRef).current?.focus(),
261
+ 50,
262
+ );
143
263
  return () => clearTimeout(t);
144
264
  }, []);
145
265
 
@@ -156,17 +276,182 @@ function AgentAddPopover({
156
276
  return () => document.removeEventListener("mousedown", handleClick);
157
277
  }, [onClose]);
158
278
 
159
- const handleAdd = () => {
160
- if (!name.trim() || !url.trim()) return;
161
- onAdd(name.trim(), url.trim(), description.trim());
279
+ const handleCheck = useCallback(async () => {
280
+ const trimmedUrl = url.trim();
281
+ if (!trimmedUrl) return;
282
+ setCheck({ status: "checking" });
283
+ try {
284
+ const res = await fetch(
285
+ agentNativePath(
286
+ `/_agent-native/agents/probe?url=${encodeURIComponent(trimmedUrl)}`,
287
+ ),
288
+ );
289
+ const body = await res.json().catch(() => null);
290
+ if (!res.ok) {
291
+ setCheck({
292
+ status: "error",
293
+ message: body?.error ?? `Check failed (${res.status})`,
294
+ });
295
+ return;
296
+ }
297
+ const result = body as AgentProbeResult;
298
+ setCheck({ status: "done", result });
299
+ if (result.reachable) {
300
+ if (!name.trim() && result.name) setName(result.name);
301
+ if (!description.trim() && result.description) {
302
+ setDescription(result.description);
303
+ }
304
+ }
305
+ } catch (err: any) {
306
+ setCheck({ status: "error", message: err?.message ?? "Check failed" });
307
+ }
308
+ }, [url, name, description]);
309
+
310
+ const handleAdd = async () => {
311
+ const trimmedName = name.trim();
312
+ const trimmedUrl = url.trim();
313
+ if (!trimmedName || !trimmedUrl) return;
314
+ const trimmedDescription = description.trim();
315
+ const ok = await onAdd(trimmedName, trimmedUrl, trimmedDescription);
316
+ if (ok) {
317
+ setAdded({
318
+ name: trimmedName,
319
+ url: trimmedUrl,
320
+ description: trimmedDescription,
321
+ });
322
+ }
162
323
  };
163
324
 
325
+ if (added) {
326
+ return (
327
+ <div
328
+ ref={popoverRef}
329
+ className="absolute end-0 top-full z-50 mt-1 w-72 rounded-lg border border-border bg-popover p-2.5 shadow-lg"
330
+ >
331
+ <div className="flex flex-col gap-1.5">
332
+ <p className="text-[10px] leading-relaxed text-muted-foreground">
333
+ Added {added.name} on your side only — registration is one-way, so
334
+ it won&apos;t know about this app until it&apos;s added there too.
335
+ </p>
336
+ <a
337
+ href={buildPeerRegisterBackLink(added.url)}
338
+ target="_blank"
339
+ rel="noopener noreferrer"
340
+ className="inline-flex cursor-pointer items-center justify-center gap-1 rounded bg-accent px-2 py-1 text-[10px] font-medium text-foreground no-underline hover:bg-accent/80"
341
+ >
342
+ <IconExternalLink size={10} />
343
+ Open {added.name}&apos;s settings
344
+ </a>
345
+ <button
346
+ onClick={onClose}
347
+ className="cursor-pointer rounded px-2 py-0.5 text-[10px] text-muted-foreground hover:text-foreground"
348
+ >
349
+ Dismiss
350
+ </button>
351
+ </div>
352
+ </div>
353
+ );
354
+ }
355
+
356
+ const result = check.status === "done" ? check.result : null;
357
+ const unreachable = result ? !result.reachable : false;
358
+ const unauthorized = result ? result.authorized === false : false;
359
+
164
360
  return (
165
361
  <div
166
362
  ref={popoverRef}
167
- className="absolute end-0 top-full z-50 mt-1 w-64 rounded-lg border border-border bg-popover p-2.5 shadow-lg"
363
+ className="absolute end-0 top-full z-50 mt-1 w-72 rounded-lg border border-border bg-popover p-2.5 shadow-lg"
168
364
  >
169
365
  <div className="flex flex-col gap-1.5">
366
+ <div className="flex gap-1">
367
+ <input
368
+ ref={urlRef}
369
+ value={url}
370
+ onChange={(e) => {
371
+ setUrl(e.target.value);
372
+ setCheck({ status: "idle" });
373
+ }}
374
+ onKeyDown={(e) => {
375
+ if (e.key === "Enter") handleCheck();
376
+ if (e.key === "Escape") onClose();
377
+ }}
378
+ className="w-full flex-1 rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
379
+ placeholder="URL (e.g. http://localhost:8085)"
380
+ />
381
+ <ToolkitButtonBase
382
+ type="button"
383
+ variant="outline"
384
+ onClick={handleCheck}
385
+ disabled={!url.trim() || check.status === "checking"}
386
+ className="inline-flex shrink-0 cursor-pointer items-center gap-1 rounded border border-border px-2 py-1 text-[10px] text-muted-foreground hover:bg-accent/40 hover:text-foreground disabled:opacity-40"
387
+ >
388
+ {check.status === "checking" ? (
389
+ <IconLoader2 size={10} className="animate-spin" />
390
+ ) : (
391
+ "Check"
392
+ )}
393
+ </ToolkitButtonBase>
394
+ </div>
395
+
396
+ {check.status === "error" && (
397
+ <p className="flex items-start gap-1 text-[10px] text-red-500">
398
+ <IconAlertTriangle size={11} className="mt-px shrink-0" />
399
+ {check.message}
400
+ </p>
401
+ )}
402
+ {result && (
403
+ <div
404
+ className={`flex items-start gap-1 text-[10px] ${
405
+ unreachable || unauthorized
406
+ ? "text-amber-600 dark:text-amber-400"
407
+ : "text-green-600 dark:text-green-500"
408
+ }`}
409
+ >
410
+ {unreachable || unauthorized ? (
411
+ <IconAlertTriangle size={11} className="mt-px shrink-0" />
412
+ ) : (
413
+ <IconCheck size={11} className="mt-px shrink-0" />
414
+ )}
415
+ <span className="leading-relaxed">
416
+ {describeCheckResult(result)}
417
+ {unreachable &&
418
+ " — the app may just not be running yet; you can still add it."}
419
+ </span>
420
+ </div>
421
+ )}
422
+ {unauthorized &&
423
+ (secretSet === true ? (
424
+ <button
425
+ onClick={() => syncSecret.mutate(undefined)}
426
+ disabled={syncSecret.isPending}
427
+ className="inline-flex cursor-pointer items-center gap-1 self-start rounded border border-border px-1.5 py-0.5 text-[10px] text-muted-foreground hover:text-foreground disabled:opacity-40"
428
+ >
429
+ {syncSecret.isPending ? (
430
+ <IconLoader2 size={10} className="animate-spin" />
431
+ ) : (
432
+ <IconRefresh size={10} />
433
+ )}
434
+ Sync secret to apps
435
+ </button>
436
+ ) : (
437
+ <p className="text-[10px] text-muted-foreground">
438
+ {secretSet === false ? (
439
+ <>
440
+ No shared secret set yet —{" "}
441
+ <a
442
+ href={buildSettingsRoute(STANDARD_SETTINGS_TABS.team)}
443
+ className="underline underline-offset-2 hover:text-foreground"
444
+ >
445
+ set one on the Team page
446
+ </a>{" "}
447
+ first.
448
+ </>
449
+ ) : (
450
+ "Ask your workspace owner to sync the shared secret."
451
+ )}
452
+ </p>
453
+ ))}
454
+
170
455
  <input
171
456
  ref={nameRef}
172
457
  value={name}
@@ -178,16 +463,6 @@ function AgentAddPopover({
178
463
  className="w-full rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
179
464
  placeholder="Name"
180
465
  />
181
- <input
182
- value={url}
183
- onChange={(e) => setUrl(e.target.value)}
184
- onKeyDown={(e) => {
185
- if (e.key === "Enter") handleAdd();
186
- if (e.key === "Escape") onClose();
187
- }}
188
- className="w-full rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
189
- placeholder="URL (e.g. http://localhost:8085)"
190
- />
191
466
  <input
192
467
  value={description}
193
468
  onChange={(e) => setDescription(e.target.value)}
@@ -201,16 +476,16 @@ function AgentAddPopover({
201
476
  <div className="flex justify-end gap-1 pt-0.5">
202
477
  <button
203
478
  onClick={onClose}
204
- className="rounded px-2 py-0.5 text-[10px] text-muted-foreground hover:text-foreground"
479
+ className="cursor-pointer rounded px-2 py-0.5 text-[10px] text-muted-foreground hover:text-foreground"
205
480
  >
206
481
  Cancel
207
482
  </button>
208
483
  <button
209
484
  onClick={handleAdd}
210
485
  disabled={!name.trim() || !url.trim()}
211
- className="rounded bg-accent px-2 py-0.5 text-[10px] font-medium text-foreground hover:bg-accent/80 disabled:opacity-40"
486
+ className="cursor-pointer rounded bg-accent px-2 py-0.5 text-[10px] font-medium text-foreground hover:bg-accent/80 disabled:opacity-40"
212
487
  >
213
- Add
488
+ {unreachable ? "Add anyway" : "Add"}
214
489
  </button>
215
490
  </div>
216
491
  </div>
@@ -218,11 +493,174 @@ function AgentAddPopover({
218
493
  );
219
494
  }
220
495
 
496
+ function A2ASecretStatusRow({
497
+ org,
498
+ syncSecret,
499
+ }: {
500
+ org: { a2aSecretSet?: boolean; allowedDomain: string | null } | undefined;
501
+ syncSecret: ReturnType<typeof useSyncA2ASecret>;
502
+ }) {
503
+ if (!org) return null;
504
+ const secretSet = org.a2aSecretSet;
505
+
506
+ if (secretSet === undefined) {
507
+ // Not an owner/admin — the server omits `a2aSecretSet` entirely for this
508
+ // role rather than reporting `false`, so this must read as "can't see
509
+ // it," never as "not set" (a claim we have no basis for).
510
+ return (
511
+ <div className="mb-2 rounded-md border border-border/60 bg-accent/20 px-2 py-1.5 text-[10px] text-muted-foreground">
512
+ Shared secret is managed by your workspace owner.
513
+ </div>
514
+ );
515
+ }
516
+
517
+ if (!secretSet) {
518
+ return (
519
+ <div className="mb-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-2 py-1.5 text-[10px] text-amber-600 dark:text-amber-400">
520
+ No shared secret set — connected apps will reject calls in production.{" "}
521
+ <a
522
+ href={buildSettingsRoute(STANDARD_SETTINGS_TABS.team)}
523
+ className="underline underline-offset-2 hover:text-amber-500"
524
+ >
525
+ Set one on the Team page
526
+ </a>
527
+ </div>
528
+ );
529
+ }
530
+
531
+ const noDomain = syncSecret.error?.message?.toLowerCase().includes("domain");
532
+ const failures = syncSecret.data?.results.filter((r) => !r.ok) ?? [];
533
+
534
+ return (
535
+ <div className="mb-2 flex flex-col gap-1 rounded-md border border-border/60 bg-accent/20 px-2 py-1.5 text-[10px]">
536
+ <div className="flex items-center justify-between gap-2">
537
+ <span className="text-muted-foreground">
538
+ Shared secret set
539
+ {org.allowedDomain ? ` for ${org.allowedDomain}` : ""}
540
+ </span>
541
+ <button
542
+ onClick={() => syncSecret.mutate(undefined)}
543
+ disabled={syncSecret.isPending}
544
+ className="inline-flex shrink-0 cursor-pointer items-center gap-1 rounded border border-border px-1.5 py-0.5 text-[10px] text-muted-foreground hover:text-foreground disabled:opacity-40"
545
+ >
546
+ {syncSecret.isPending ? (
547
+ <IconLoader2 size={10} className="animate-spin" />
548
+ ) : (
549
+ <IconRefresh size={10} />
550
+ )}
551
+ Sync to apps
552
+ </button>
553
+ </div>
554
+ {syncSecret.data && !syncSecret.isPending && (
555
+ <div className="text-muted-foreground">
556
+ Synced to {syncSecret.data.succeeded}/{syncSecret.data.total} app
557
+ {syncSecret.data.total === 1 ? "" : "s"}
558
+ {syncSecret.data.failed > 0
559
+ ? ` (${syncSecret.data.failed} failed)`
560
+ : ""}
561
+ .
562
+ {failures.length > 0 && (
563
+ <ul className="mt-0.5 list-disc ps-3 text-red-500">
564
+ {failures.map((r) => (
565
+ <li key={r.id}>
566
+ {r.name}: {r.error ?? `HTTP ${r.status ?? "?"}`}
567
+ </li>
568
+ ))}
569
+ </ul>
570
+ )}
571
+ </div>
572
+ )}
573
+ {syncSecret.error && (
574
+ <p className="text-red-500">
575
+ {syncSecret.error.message}
576
+ {noDomain && (
577
+ <>
578
+ {" "}
579
+ <a
580
+ href={buildSettingsRoute(STANDARD_SETTINGS_TABS.team)}
581
+ className="underline underline-offset-2"
582
+ >
583
+ Set the domain
584
+ </a>
585
+ </>
586
+ )}
587
+ </p>
588
+ )}
589
+ </div>
590
+ );
591
+ }
592
+
593
+ /** Query params `/_agent-native/open` forwards onto the redirect target. */
594
+ const PREFILL_PARAMS = [
595
+ "f_agentName",
596
+ "f_agentUrl",
597
+ "f_agentDescription",
598
+ ] as const;
599
+
221
600
  export function AgentsSection() {
222
601
  const [agents, setAgents] = useState<AgentInfo[]>([]);
223
602
  const [loading, setLoading] = useState(true);
224
603
  const [editingAgent, setEditingAgent] = useState<string | null>(null);
225
604
  const [showAdd, setShowAdd] = useState(false);
605
+ const [prefill, setPrefill] = useState<{
606
+ name: string;
607
+ url: string;
608
+ description: string;
609
+ } | null>(null);
610
+ const [probeById, setProbeById] = useState<Map<
611
+ string,
612
+ AgentProbeResult
613
+ > | null>(null);
614
+
615
+ const { data: org } = useOrg();
616
+ const syncSecret = useSyncA2ASecret();
617
+
618
+ // Landing from a peer's "register back" deep link (see
619
+ // buildPeerRegisterBackLink): the open route only echoes `f_*` params onto
620
+ // the redirect URL, so read those here and open the Add popover prefilled.
621
+ useEffect(() => {
622
+ if (typeof window === "undefined") return;
623
+ const params = new URLSearchParams(window.location.search);
624
+ const url = params.get("f_agentUrl");
625
+ if (!url) return;
626
+ setPrefill({
627
+ name: params.get("f_agentName") ?? "",
628
+ url,
629
+ description: params.get("f_agentDescription") ?? "",
630
+ });
631
+ setShowAdd(true);
632
+ for (const key of PREFILL_PARAMS) params.delete(key);
633
+ const query = params.toString();
634
+ window.history.replaceState(
635
+ null,
636
+ "",
637
+ `${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash}`,
638
+ );
639
+ }, []);
640
+
641
+ // One batched probe for the whole list — cheap liveness dots, not one
642
+ // request per row. A row absent from the results (never returned) stays
643
+ // dot-less rather than defaulting to a color that isn't backed by data.
644
+ useEffect(() => {
645
+ let cancelled = false;
646
+ fetch(agentNativePath("/_agent-native/agents/probe"))
647
+ .then((res) => (res.ok ? res.json() : null))
648
+ .then((data) => {
649
+ if (cancelled || !data) return;
650
+ const results = Array.isArray(data.results)
651
+ ? (data.results as Array<AgentProbeResult & { id: string }>)
652
+ : [];
653
+ const map = new Map<string, AgentProbeResult>();
654
+ for (const result of results) map.set(result.id.toLowerCase(), result);
655
+ setProbeById(map);
656
+ })
657
+ .catch(() => {
658
+ if (!cancelled) setProbeById(new Map());
659
+ });
660
+ return () => {
661
+ cancelled = true;
662
+ };
663
+ }, []);
226
664
 
227
665
  const fetchAgents = useCallback(async () => {
228
666
  try {
@@ -231,11 +669,24 @@ export function AgentsSection() {
231
669
  );
232
670
  if (!res.ok) return;
233
671
  const data = await res.json();
234
- const agentResources = (data.resources ?? []).filter(
235
- (r: { path: string }) => isRemoteAgentPath(r.path),
236
- );
672
+ // Migrating a remote agent to the canonical `remote-agents/` prefix
673
+ // leaves the legacy `agents/` row in place (resources/store.ts), so
674
+ // every migrated agent has two resources pointing at one URL. Collapse
675
+ // them the way discoverAgents does, canonical winning.
676
+ const byAgentId = new Map<string, { id: string; path: string }>();
677
+ for (const resource of (data.resources ?? []) as Array<{
678
+ id: string;
679
+ path: string;
680
+ }>) {
681
+ if (!isRemoteAgentPath(resource.path)) continue;
682
+ const agentId = getRemoteAgentIdFromPath(resource.path);
683
+ const existing = byAgentId.get(agentId);
684
+ if (existing?.path.startsWith(REMOTE_AGENT_RESOURCE_PREFIX)) continue;
685
+ byAgentId.set(agentId, resource);
686
+ }
687
+ const agentResources = [...byAgentId.values()];
237
688
  const parsed = await Promise.all(
238
- agentResources.map(async (r: { id: string; path: string }) => {
689
+ agentResources.map(async (r): Promise<AgentInfo | null> => {
239
690
  try {
240
691
  const detail = await fetch(
241
692
  agentNativePath(`/_agent-native/resources/${r.id}`),
@@ -255,7 +706,7 @@ export function AgentsSection() {
255
706
  }
256
707
  }),
257
708
  );
258
- setAgents(parsed.filter(Boolean));
709
+ setAgents(parsed.filter((agent): agent is AgentInfo => agent !== null));
259
710
  } finally {
260
711
  setLoading(false);
261
712
  }
@@ -265,7 +716,11 @@ export function AgentsSection() {
265
716
  fetchAgents();
266
717
  }, [fetchAgents]);
267
718
 
268
- const handleAdd = async (name: string, url: string, description: string) => {
719
+ const handleAdd = async (
720
+ name: string,
721
+ url: string,
722
+ description: string,
723
+ ): Promise<boolean> => {
269
724
  const id = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
270
725
  const agentJson = JSON.stringify(
271
726
  {
@@ -289,11 +744,14 @@ export function AgentsSection() {
289
744
  shared: true,
290
745
  }),
291
746
  });
292
- if (res.ok) {
293
- setShowAdd(false);
294
- fetchAgents();
295
- }
296
- } catch {}
747
+ // Deliberately don't close the popover here — a successful add shows a
748
+ // follow-up state (registration is one-way; the peer doesn't know
749
+ // about us yet) that the user dismisses explicitly.
750
+ if (res.ok) fetchAgents();
751
+ return res.ok;
752
+ } catch {
753
+ return false;
754
+ }
297
755
  };
298
756
 
299
757
  const handleSave = async (agent: AgentInfo) => {
@@ -365,13 +823,23 @@ export function AgentsSection() {
365
823
  </Tooltip>
366
824
  {showAdd && (
367
825
  <AgentAddPopover
826
+ initialName={prefill?.name}
827
+ initialUrl={prefill?.url}
828
+ initialDescription={prefill?.description}
829
+ secretSet={org?.a2aSecretSet}
830
+ syncSecret={syncSecret}
368
831
  onAdd={handleAdd}
369
- onClose={() => setShowAdd(false)}
832
+ onClose={() => {
833
+ setShowAdd(false);
834
+ setPrefill(null);
835
+ }}
370
836
  />
371
837
  )}
372
838
  </div>
373
839
  </div>
374
840
 
841
+ <A2ASecretStatusRow org={org} syncSecret={syncSecret} />
842
+
375
843
  {/* Agent list */}
376
844
  {loading ? (
377
845
  <div className="space-y-1.5">
@@ -388,42 +856,68 @@ export function AgentsSection() {
388
856
  </button>
389
857
  ) : (
390
858
  <div className="flex flex-col gap-0.5">
391
- {agents.map((agent) => (
392
- <div key={agent.id} className="group relative">
393
- <div className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 hover:bg-accent/30">
394
- <span className="text-[11px] font-medium text-foreground truncate shrink-0">
395
- {agent.name}
396
- </span>
397
- <span className="flex-1 text-[10px] text-muted-foreground/60 truncate text-end">
398
- {agent.url}
399
- </span>
400
- <Tooltip>
401
- <TooltipTrigger asChild>
402
- <button
403
- onClick={() => {
404
- setEditingAgent(
405
- editingAgent === agent.id ? null : agent.id,
406
- );
407
- setShowAdd(false);
408
- }}
409
- className="shrink-0 rounded p-0.5 text-muted-foreground opacity-0 group-hover:opacity-100 hover:text-foreground hover:bg-accent/50"
410
- >
411
- <IconPencil size={11} />
412
- </button>
413
- </TooltipTrigger>
414
- <TooltipContent>Edit agent</TooltipContent>
415
- </Tooltip>
859
+ {agents.map((agent) => {
860
+ const probe = probeById?.get(
861
+ getRemoteAgentIdFromPath(agent.path).toLowerCase(),
862
+ );
863
+ const dotState: "ok" | "warn" | null = !probe
864
+ ? null
865
+ : probe.reachable && probe.authorized !== false
866
+ ? "ok"
867
+ : "warn";
868
+ return (
869
+ <div key={agent.id} className="group relative">
870
+ <div className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 hover:bg-accent/30">
871
+ {dotState ? (
872
+ <Tooltip>
873
+ <TooltipTrigger asChild>
874
+ <span
875
+ className={`h-1.5 w-1.5 shrink-0 rounded-full ${
876
+ dotState === "ok" ? "bg-green-500" : "bg-amber-500"
877
+ }`}
878
+ />
879
+ </TooltipTrigger>
880
+ <TooltipContent>
881
+ {probe && describeProbeTooltip(probe)}
882
+ </TooltipContent>
883
+ </Tooltip>
884
+ ) : (
885
+ <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-transparent" />
886
+ )}
887
+ <span className="text-[11px] font-medium text-foreground truncate shrink-0">
888
+ {agent.name}
889
+ </span>
890
+ <span className="flex-1 text-[10px] text-muted-foreground/60 truncate text-end">
891
+ {agent.url}
892
+ </span>
893
+ <Tooltip>
894
+ <TooltipTrigger asChild>
895
+ <button
896
+ onClick={() => {
897
+ setEditingAgent(
898
+ editingAgent === agent.id ? null : agent.id,
899
+ );
900
+ setShowAdd(false);
901
+ }}
902
+ className="shrink-0 rounded p-0.5 text-muted-foreground opacity-0 group-hover:opacity-100 hover:text-foreground hover:bg-accent/50"
903
+ >
904
+ <IconPencil size={11} />
905
+ </button>
906
+ </TooltipTrigger>
907
+ <TooltipContent>Edit agent</TooltipContent>
908
+ </Tooltip>
909
+ </div>
910
+ {editingAgent === agent.id && (
911
+ <AgentEditPopover
912
+ agent={agent}
913
+ onSave={handleSave}
914
+ onDelete={handleDelete}
915
+ onClose={() => setEditingAgent(null)}
916
+ />
917
+ )}
416
918
  </div>
417
- {editingAgent === agent.id && (
418
- <AgentEditPopover
419
- agent={agent}
420
- onSave={handleSave}
421
- onDelete={handleDelete}
422
- onClose={() => setEditingAgent(null)}
423
- />
424
- )}
425
- </div>
426
- ))}
919
+ );
920
+ })}
427
921
  <button
428
922
  onClick={() => {
429
923
  setShowAdd(true);