@agent-native/core 0.19.0 → 0.19.3

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 (95) hide show
  1. package/dist/a2a/caller-auth.d.ts +1 -0
  2. package/dist/a2a/caller-auth.d.ts.map +1 -1
  3. package/dist/a2a/caller-auth.js +1 -1
  4. package/dist/a2a/caller-auth.js.map +1 -1
  5. package/dist/agent/production-agent.d.ts +1 -1
  6. package/dist/agent/production-agent.d.ts.map +1 -1
  7. package/dist/agent/production-agent.js +34 -2
  8. package/dist/agent/production-agent.js.map +1 -1
  9. package/dist/cli/code-agent-executor.d.ts.map +1 -1
  10. package/dist/cli/code-agent-executor.js +47 -256
  11. package/dist/cli/code-agent-executor.js.map +1 -1
  12. package/dist/cli/connect.d.ts +3 -2
  13. package/dist/cli/connect.d.ts.map +1 -1
  14. package/dist/cli/connect.js +12 -8
  15. package/dist/cli/connect.js.map +1 -1
  16. package/dist/cli/mcp-config-writers.d.ts +3 -3
  17. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  18. package/dist/cli/mcp-config-writers.js +19 -8
  19. package/dist/cli/mcp-config-writers.js.map +1 -1
  20. package/dist/client/AgentPanel.d.ts +3 -1
  21. package/dist/client/AgentPanel.d.ts.map +1 -1
  22. package/dist/client/AgentPanel.js +4 -4
  23. package/dist/client/AgentPanel.js.map +1 -1
  24. package/dist/client/AssistantChat.d.ts +3 -0
  25. package/dist/client/AssistantChat.d.ts.map +1 -1
  26. package/dist/client/AssistantChat.js +11 -3
  27. package/dist/client/AssistantChat.js.map +1 -1
  28. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  29. package/dist/client/MultiTabAssistantChat.js +4 -1
  30. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  31. package/dist/client/dynamic-suggestions.d.ts +43 -0
  32. package/dist/client/dynamic-suggestions.d.ts.map +1 -0
  33. package/dist/client/dynamic-suggestions.js +344 -0
  34. package/dist/client/dynamic-suggestions.js.map +1 -0
  35. package/dist/client/index.d.ts +1 -0
  36. package/dist/client/index.d.ts.map +1 -1
  37. package/dist/client/index.js +1 -0
  38. package/dist/client/index.js.map +1 -1
  39. package/dist/client/settings/SettingsPanel.js +2 -2
  40. package/dist/client/settings/SettingsPanel.js.map +1 -1
  41. package/dist/coding-tools/index.d.ts +31 -0
  42. package/dist/coding-tools/index.d.ts.map +1 -0
  43. package/dist/coding-tools/index.js +411 -0
  44. package/dist/coding-tools/index.js.map +1 -0
  45. package/dist/mcp/build-server.d.ts +33 -1
  46. package/dist/mcp/build-server.d.ts.map +1 -1
  47. package/dist/mcp/build-server.js +33 -10
  48. package/dist/mcp/build-server.js.map +1 -1
  49. package/dist/mcp/builtin-tools.d.ts +3 -1
  50. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  51. package/dist/mcp/builtin-tools.js +115 -26
  52. package/dist/mcp/builtin-tools.js.map +1 -1
  53. package/dist/mcp/connect-route.d.ts.map +1 -1
  54. package/dist/mcp/connect-route.js +382 -74
  55. package/dist/mcp/connect-route.js.map +1 -1
  56. package/dist/mcp/org-directory.d.ts +83 -0
  57. package/dist/mcp/org-directory.d.ts.map +1 -0
  58. package/dist/mcp/org-directory.js +201 -0
  59. package/dist/mcp/org-directory.js.map +1 -0
  60. package/dist/mcp/server.d.ts +38 -1
  61. package/dist/mcp/server.d.ts.map +1 -1
  62. package/dist/mcp/server.js +222 -77
  63. package/dist/mcp/server.js.map +1 -1
  64. package/dist/scripts/dev/index.d.ts +6 -4
  65. package/dist/scripts/dev/index.d.ts.map +1 -1
  66. package/dist/scripts/dev/index.js +28 -13
  67. package/dist/scripts/dev/index.js.map +1 -1
  68. package/dist/server/agent-chat-plugin.d.ts +6 -6
  69. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  70. package/dist/server/agent-chat-plugin.js +65 -32
  71. package/dist/server/agent-chat-plugin.js.map +1 -1
  72. package/dist/server/agent-teams.js +2 -2
  73. package/dist/server/agent-teams.js.map +1 -1
  74. package/dist/server/agents-bundle.d.ts +3 -3
  75. package/dist/server/agents-bundle.js +5 -5
  76. package/dist/server/agents-bundle.js.map +1 -1
  77. package/dist/server/auth.d.ts +8 -0
  78. package/dist/server/auth.d.ts.map +1 -1
  79. package/dist/server/auth.js +8 -1
  80. package/dist/server/auth.js.map +1 -1
  81. package/dist/server/sentry.d.ts.map +1 -1
  82. package/dist/server/sentry.js +17 -2
  83. package/dist/server/sentry.js.map +1 -1
  84. package/dist/vite/client.d.ts.map +1 -1
  85. package/dist/vite/client.js +1 -0
  86. package/dist/vite/client.js.map +1 -1
  87. package/docs/content/client.md +15 -0
  88. package/docs/content/code-agents-ui.md +11 -1
  89. package/docs/content/drop-in-agent.md +3 -1
  90. package/docs/content/external-agents.md +27 -6
  91. package/docs/content/frames.md +1 -1
  92. package/docs/content/mcp-clients.md +2 -0
  93. package/docs/content/mcp-protocol.md +4 -2
  94. package/docs/content/migration-workbench.md +5 -0
  95. package/package.json +1 -1
@@ -28,11 +28,11 @@
28
28
  */
29
29
  import { getMethod, getHeader } from "h3";
30
30
  import { readBody } from "../server/h3-helpers.js";
31
- import { getSession, getConfiguredLoginHtml } from "../server/auth.js";
31
+ import { getSession, getConfiguredLoginHtml, isLoopbackRequest, } from "../server/auth.js";
32
32
  import { signA2AToken } from "../a2a/client.js";
33
33
  import { getOrgDomain } from "../org/context.js";
34
34
  import { randomUUID } from "node:crypto";
35
- import { recordMintedToken, listTokens, revokeToken, createDeviceCode, getDeviceCode, approveDeviceCode, claimDeviceCodeForMint, finishDeviceCodeMint, releaseDeviceCodeMint, expireDeviceCode, MCP_CONNECT_SCOPE, DEFAULT_TOKEN_TTL_DAYS, MIN_TOKEN_TTL_DAYS, MAX_TOKEN_TTL_DAYS, DEVICE_CODE_TTL_MS, } from "./connect-store.js";
35
+ import { recordMintedToken, listTokens, revokeToken, createDeviceCode, getDeviceCode, approveDeviceCode, consumeDeviceCode, claimDeviceCodeForMint, finishDeviceCodeMint, releaseDeviceCodeMint, expireDeviceCode, MCP_CONNECT_SCOPE, DEFAULT_TOKEN_TTL_DAYS, MIN_TOKEN_TTL_DAYS, MAX_TOKEN_TTL_DAYS, DEVICE_CODE_TTL_MS, } from "./connect-store.js";
36
36
  /** Device-flow poll interval hint (seconds). */
37
37
  const DEVICE_POLL_INTERVAL_S = 3;
38
38
  // Human-typable user code: 8 base32 chars, dashed XXXX-XXXX.
@@ -89,6 +89,17 @@ function appLabel(origin, options) {
89
89
  function serverName(origin, options) {
90
90
  return `agent-native-${appLabel(origin, options)}`;
91
91
  }
92
+ function canUseDevOpenConnect(event) {
93
+ // Loopback determined from the real socket peer (isLoopbackRequest →
94
+ // getRequestIP without xForwardedFor), NOT a parsed `Host` header — the
95
+ // header is client-controlled, and it also handles IPv6 `::1`. A
96
+ // misconfigured public deploy with no secret thus can't unlock dev-open
97
+ // by spoofing `Host: localhost`.
98
+ return (isLoopbackRequest(event) &&
99
+ !process.env.A2A_SECRET?.trim() &&
100
+ !process.env.ACCESS_TOKEN?.trim() &&
101
+ !process.env.ACCESS_TOKENS?.trim());
102
+ }
92
103
  function escapeHtml(s) {
93
104
  return s
94
105
  .replace(/&/g, "&")
@@ -142,17 +153,23 @@ async function mintConnectToken(params) {
142
153
  });
143
154
  return { token, jti };
144
155
  }
145
- function mcpResultPayload(appUrl, token, options) {
156
+ function mcpResultPayload(appUrl, options, auth) {
146
157
  const mcpUrl = `${appUrl}/_agent-native/mcp`;
147
158
  const name = serverName(appUrl, options);
159
+ const headers = {};
160
+ if (auth.token)
161
+ headers.Authorization = `Bearer ${auth.token}`;
162
+ if (!auth.token && auth.ownerEmail) {
163
+ headers["X-Agent-Native-Owner-Email"] = auth.ownerEmail;
164
+ }
148
165
  return {
149
- token,
166
+ token: auth.token ?? "",
150
167
  mcpUrl,
151
168
  serverName: name,
152
169
  mcpServerEntry: {
153
170
  type: "http",
154
171
  url: mcpUrl,
155
- headers: { Authorization: `Bearer ${token}` },
172
+ ...(Object.keys(headers).length ? { headers } : {}),
156
173
  },
157
174
  cli: `agent-native connect ${appUrl}`,
158
175
  };
@@ -160,11 +177,25 @@ function mcpResultPayload(appUrl, token, options) {
160
177
  // ---------------------------------------------------------------------------
161
178
  // Connect page (server-rendered HTML string)
162
179
  // ---------------------------------------------------------------------------
180
+ function agentNativeMarkSvg(className, gradientId) {
181
+ return `<svg class="${className}" width="114" height="66" viewBox="0 0 114 66" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
182
+ <path d="M24.5537 65.7695H0L15.0859 39.4619L37.708 0L60.4912 39.4619H39.6396L24.5537 65.7695Z" fill="white"/>
183
+ <path d="M89.446 0H114L76.2921 65.7704H51.7383L89.446 0Z" fill="url(#${gradientId})"/>
184
+ <defs>
185
+ <linearGradient id="${gradientId}" x1="101.702" y1="67.4791" x2="113.672" y2="-37.4275" gradientUnits="userSpaceOnUse">
186
+ <stop stop-color="#00B5FF"/>
187
+ <stop offset="1" stop-color="#48FFE4"/>
188
+ </linearGradient>
189
+ </defs>
190
+ </svg>`;
191
+ }
163
192
  function renderConnectPage(params) {
164
193
  const { origin, connectBasePath, email, appName, userCode } = params;
165
194
  const safeOrigin = escapeHtml(origin);
166
195
  const safeEmail = escapeHtml(email);
167
196
  const safeApp = escapeHtml(appName);
197
+ const brandMarkSvg = agentNativeMarkSvg("brand-mark", "agent-native-connect-brand-gradient");
198
+ const flowMarkSvg = agentNativeMarkSvg("flow-mark", "agent-native-connect-flow-gradient");
168
199
  const safeUserCode = userCode && USER_CODE_RE.test(userCode) ? escapeHtml(userCode) : "";
169
200
  return `<!DOCTYPE html>
170
201
  <html lang="en">
@@ -176,119 +207,315 @@ function renderConnectPage(params) {
176
207
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
177
208
  :root {
178
209
  color-scheme: dark;
179
- --bg: #09090b; --panel: #141417; --border: rgba(255,255,255,0.1);
180
- --text: #f4f4f5; --muted: #a1a1aa; --subtle: #71717a;
210
+ --bg: #09090b; --panel: #121214; --panel-2: #0c0c0e;
211
+ --panel-soft: rgba(255,255,255,0.025);
212
+ --border: rgba(255,255,255,0.075); --border-strong: rgba(255,255,255,0.14);
213
+ --text: #f7f7f8; --muted: #a1a1aa; --subtle: #74747d;
181
214
  --accent: #f4f4f5; --accent-fg: #09090b;
215
+ --ring: rgba(250,250,250,0.55);
182
216
  --error: #fca5a5; --error-bg: rgba(127,29,29,0.18);
183
- --ok: #86efac; --ok-bg: rgba(20,83,45,0.2);
217
+ --ok: #86efac; --ok-bg: rgba(20,83,45,0.12); --ok-border: rgba(134,239,172,0.18);
184
218
  }
219
+ html, body { -webkit-font-smoothing: antialiased; }
185
220
  body {
186
221
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
187
- background: linear-gradient(180deg, #111114 0%, var(--bg) 58%);
222
+ background: linear-gradient(180deg, #101013 0%, var(--bg) 58%);
188
223
  color: var(--text); display: flex; align-items: center;
189
- justify-content: center; min-height: 100vh; padding: 1rem;
224
+ justify-content: center; min-height: 100vh; padding: 1.5rem 1rem;
190
225
  }
191
226
  .card {
192
- width: 100%; max-width: 520px; padding: 2rem;
227
+ width: 100%; max-width: 440px;
193
228
  background: var(--panel); border: 1px solid var(--border);
194
- border-radius: 12px; box-shadow: 0 24px 80px rgba(0,0,0,0.35);
195
- }
196
- h1 { font-size: 1.35rem; font-weight: 650; margin-bottom: 0.35rem; }
197
- .sub { color: var(--muted); font-size: 0.9rem; margin-bottom: 1.25rem; }
198
- .row { color: var(--subtle); font-size: 0.8rem; margin-bottom: 1.25rem; }
199
- .code-callout {
200
- border: 1px solid var(--border); border-radius: 8px; padding: 0.85rem 1rem;
201
- margin-bottom: 1.25rem; background: rgba(255,255,255,0.03);
202
- }
203
- .code-callout .label { font-size: 0.72rem; color: var(--subtle);
204
- text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 0.35rem; }
205
- .code-callout .value { font-size: 1.5rem; font-weight: 700;
229
+ border-radius: 8px; box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset,
230
+ 0 30px 90px rgba(0,0,0,0.5);
231
+ padding: 1.25rem;
232
+ }
233
+ .topbar {
234
+ display: flex; align-items: center; justify-content: space-between;
235
+ gap: 0.75rem; margin-bottom: 1.75rem;
236
+ }
237
+ .brand-lockup {
238
+ display: flex; align-items: center; gap: 0.55rem;
239
+ color: var(--muted); font-size: 0.78rem; font-weight: 600;
240
+ }
241
+ .brand-mark { width: 18px; height: auto; display: block; }
242
+ .app-pill {
243
+ max-width: 50%; border: 1px solid var(--border);
244
+ border-radius: 999px; padding: 0.28rem 0.55rem;
245
+ color: var(--subtle); font-size: 0.72rem; line-height: 1;
246
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
247
+ }
248
+ .hero { padding: 0 0.75rem; text-align: center; }
249
+ .flow {
250
+ display: flex; align-items: center; justify-content: center;
251
+ gap: 0; margin: 0 auto 1.1rem; width: fit-content;
252
+ }
253
+ .flow .tile {
254
+ width: 42px; height: 42px; border-radius: 8px;
255
+ display: flex; align-items: center; justify-content: center;
256
+ background: var(--panel-2); border: 1px solid var(--border-strong);
257
+ color: var(--text); flex-shrink: 0;
258
+ }
259
+ .flow-mark { width: 26px; height: auto; display: block; }
260
+ .flow .agent-symbol {
206
261
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
207
- letter-spacing: 0.08em; }
262
+ font-size: 0.95rem; font-weight: 700; letter-spacing: -0.04em;
263
+ }
264
+ .flow .conn {
265
+ width: 30px; height: 1px; flex-shrink: 0;
266
+ background: linear-gradient(90deg, transparent, var(--border-strong), transparent);
267
+ background-position: center;
268
+ }
269
+ .eyebrow {
270
+ text-align: center; font-size: 0.72rem; font-weight: 600;
271
+ letter-spacing: 0.08em; text-transform: uppercase;
272
+ color: var(--subtle); margin-bottom: 0.55rem;
273
+ }
274
+ h1 {
275
+ text-align: center; font-size: 1.45rem; font-weight: 680;
276
+ line-height: 1.25; margin-bottom: 0.55rem;
277
+ letter-spacing: -0.01em;
278
+ }
279
+ .sub {
280
+ text-align: center; color: var(--muted); font-size: 0.9rem;
281
+ line-height: 1.5; margin: 0 auto 0.9rem; max-width: 36ch;
282
+ }
283
+ .identity {
284
+ display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
285
+ gap: 0.25rem 0.45rem; color: var(--subtle); font-size: 0.78rem;
286
+ line-height: 1.35; margin: 0 auto 1.4rem; max-width: 34ch;
287
+ }
288
+ .identity strong { color: var(--muted); font-weight: 600; }
289
+ .identity .origin { overflow-wrap: anywhere; }
290
+ .device-strip {
291
+ display: flex; align-items: center; justify-content: space-between;
292
+ gap: 0.75rem; border: 1px solid var(--border);
293
+ border-radius: 8px; padding: 0.55rem 0.65rem; margin: 0 0 0.9rem;
294
+ background: var(--panel-soft); color: var(--muted);
295
+ }
296
+ .device-strip .label {
297
+ font-size: 0.76rem; font-weight: 560; color: var(--subtle);
298
+ }
299
+ .device-strip .value {
300
+ font-size: 0.9rem; font-weight: 700;
301
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
302
+ letter-spacing: 0.08em; color: var(--text);
303
+ }
208
304
  button {
209
305
  cursor: pointer; font: inherit; font-weight: 600; border: none;
210
- border-radius: 8px; padding: 0.7rem 1.1rem;
306
+ border-radius: 8px; padding: 0.78rem 1rem;
307
+ }
308
+ button:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
309
+ .primary {
310
+ background: var(--accent); color: var(--accent-fg); width: 100%;
311
+ font-size: 0.95rem;
211
312
  }
212
- .primary { background: var(--accent); color: var(--accent-fg); width: 100%; }
213
- .primary:disabled { opacity: 0.6; cursor: default; }
313
+ .primary:hover:not(:disabled) { background: #e4e4e7; }
314
+ .primary:disabled { opacity: 0.55; cursor: default; }
214
315
  .ghost {
215
316
  background: transparent; color: var(--muted);
216
- border: 1px solid var(--border); padding: 0.35rem 0.7rem;
217
- font-size: 0.78rem; font-weight: 500;
317
+ border: 1px solid var(--border-strong); padding: 0.35rem 0.7rem;
318
+ font-size: 0.78rem; font-weight: 500; border-radius: 8px;
218
319
  }
320
+ .ghost:hover:not(:disabled) { color: var(--text); border-color: var(--subtle); }
219
321
  pre {
220
- background: #0c0c0e; border: 1px solid var(--border); border-radius: 8px;
322
+ background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
221
323
  padding: 0.9rem; font-size: 0.78rem; line-height: 1.5; overflow-x: auto;
222
324
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
223
325
  color: #d4d4d8; margin: 0.5rem 0 1rem;
224
326
  }
225
- .field { margin-bottom: 1rem; }
226
- .field label { display: block; font-size: 0.8rem; color: var(--muted);
327
+ /* Advanced disclosure */
328
+ .advanced { margin: 0 0 1rem; }
329
+ .advanced > summary {
330
+ list-style: none; cursor: pointer; user-select: none;
331
+ display: flex; align-items: center; justify-content: center; gap: 0.35rem;
332
+ color: var(--subtle); font-size: 0.8rem; font-weight: 500;
333
+ padding: 0.5rem 0; text-align: center;
334
+ }
335
+ .advanced > summary::-webkit-details-marker { display: none; }
336
+ .advanced > summary:hover { color: var(--muted); }
337
+ .advanced > summary:focus-visible { outline: 2px solid var(--ring);
338
+ outline-offset: 2px; border-radius: 6px; }
339
+ .advanced > summary .chev {
340
+ width: 7px; height: 7px; border-right: 1.5px solid currentColor;
341
+ border-bottom: 1.5px solid currentColor; transform: rotate(45deg);
342
+ transition: transform 0.15s ease; margin-top: -3px;
343
+ }
344
+ .advanced[open] > summary .chev { transform: rotate(225deg); margin-top: 2px; }
345
+ .advanced-body {
346
+ padding: 0.85rem 0.1rem 0.25rem;
347
+ }
348
+ .field { margin-bottom: 0.9rem; }
349
+ .field:last-child { margin-bottom: 0; }
350
+ .field label { display: block; font-size: 0.78rem; color: var(--muted);
227
351
  margin-bottom: 0.35rem; }
228
352
  .field input {
229
- width: 100%; padding: 0.55rem 0.7rem; font: inherit; color: var(--text);
230
- background: #0c0c0e; border: 1px solid var(--border); border-radius: 6px;
231
- }
232
- .inline { display: flex; gap: 0.5rem; }
233
- .inline input { flex: 1; }
234
- .tokens { margin-top: 1.75rem; border-top: 1px solid var(--border);
235
- padding-top: 1.25rem; }
236
- .tokens h2 { font-size: 0.95rem; font-weight: 600; margin-bottom: 0.75rem; }
353
+ width: 100%; padding: 0.6rem 0.7rem; font: inherit; color: var(--text);
354
+ background: var(--panel-2); border: 1px solid var(--border-strong);
355
+ border-radius: 8px;
356
+ }
357
+ .field input:focus-visible {
358
+ outline: none; border-color: var(--ring);
359
+ box-shadow: 0 0 0 3px rgba(250,250,250,0.12);
360
+ }
361
+ .connections {
362
+ margin-top: 1.1rem; border-top: 1px solid var(--border);
363
+ padding-top: 0.35rem;
364
+ }
365
+ .connections > summary {
366
+ list-style: none; cursor: pointer; user-select: none;
367
+ display: flex; align-items: center; gap: 0.55rem;
368
+ min-height: 2.2rem; color: var(--muted); font-size: 0.82rem;
369
+ }
370
+ .connections > summary::-webkit-details-marker { display: none; }
371
+ .connections > summary:focus-visible {
372
+ outline: 2px solid var(--ring); outline-offset: 2px; border-radius: 6px;
373
+ }
374
+ .connections-title { font-weight: 600; color: var(--muted); }
375
+ .connections-state {
376
+ margin-left: auto; color: var(--subtle); font-size: 0.73rem;
377
+ border: 1px solid var(--border); border-radius: 999px;
378
+ padding: 0.18rem 0.45rem; line-height: 1;
379
+ }
380
+ .connections .chev {
381
+ width: 7px; height: 7px; border-right: 1.5px solid currentColor;
382
+ border-bottom: 1.5px solid currentColor; transform: rotate(45deg);
383
+ transition: transform 0.15s ease; margin: -3px 0 0 0.15rem;
384
+ }
385
+ .connections[open] .chev { transform: rotate(225deg); margin-top: 2px; }
386
+ .token-list { padding-top: 0.4rem; }
237
387
  .tok { display: flex; align-items: center; justify-content: space-between;
238
- gap: 0.75rem; padding: 0.55rem 0; border-bottom: 1px solid var(--border);
388
+ gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid var(--border);
239
389
  font-size: 0.83rem; }
240
390
  .tok:last-child { border-bottom: none; }
241
- .tok .meta { color: var(--subtle); font-size: 0.74rem; }
391
+ .tok .meta { color: var(--subtle); font-size: 0.74rem; margin-top: 0.1rem; }
242
392
  .tok.revoked { opacity: 0.45; }
243
- .msg { font-size: 0.83rem; padding: 0.6rem 0.8rem; border-radius: 6px;
244
- margin-bottom: 1rem; display: none; }
245
- .msg.err { display: block; color: var(--error); background: var(--error-bg); }
246
- .msg.ok { display: block; color: var(--ok); background: var(--ok-bg); }
393
+ .empty-state {
394
+ color: var(--subtle); font-size: 0.78rem; line-height: 1.45;
395
+ padding: 0.3rem 0 0.45rem;
396
+ }
397
+ .msg { font-size: 0.83rem; padding: 0.7rem 0.8rem; border-radius: 8px;
398
+ margin-bottom: 0.9rem; display: none; line-height: 1.4; }
399
+ .msg.err { display: block; color: var(--error); background: var(--error-bg);
400
+ border: 1px solid rgba(252,165,165,0.16); }
401
+ .msg.ok { display: block; color: var(--ok); background: var(--ok-bg);
402
+ border: 1px solid var(--ok-border); }
403
+ .result-panel { padding-top: 0.15rem; }
404
+ .result-title {
405
+ color: var(--text); font-size: 0.95rem; font-weight: 650;
406
+ text-align: center; margin-bottom: 0.35rem;
407
+ }
408
+ .result-copy {
409
+ color: var(--muted); font-size: 0.83rem; line-height: 1.45;
410
+ text-align: center; margin: 0 auto 0.85rem; max-width: 34ch;
411
+ }
412
+ .section-label {
413
+ color: var(--subtle); font-size: 0.7rem; font-weight: 650;
414
+ letter-spacing: 0.08em; text-transform: uppercase; margin-top: 0.85rem;
415
+ }
416
+ @media (max-width: 480px) {
417
+ body { align-items: flex-start; padding: 0.75rem; }
418
+ .card { padding: 1rem; }
419
+ .hero { padding: 0; }
420
+ .topbar { margin-bottom: 1.35rem; }
421
+ h1 { font-size: 1.3rem; }
422
+ .app-pill { max-width: 46%; }
423
+ pre { font-size: 0.72rem; }
424
+ }
247
425
  .hidden { display: none !important; }
248
426
  </style>
249
427
  </head>
250
428
  <body>
251
429
  <div class="card">
252
- <h1>Connect an external agent</h1>
253
- <div class="sub">Mint a personal token for <strong>${safeApp}</strong> so a coding agent (Claude Code, Codex, Cowork) can act as you.</div>
254
- <div class="row">Signed in as ${safeEmail} &middot; ${safeOrigin}</div>
430
+ <div class="topbar">
431
+ <div class="brand-lockup">
432
+ ${brandMarkSvg}
433
+ <span>Agent Native</span>
434
+ </div>
435
+ <div class="app-pill" title="${safeApp}">${safeApp}</div>
436
+ </div>
437
+
438
+ <div class="hero">
439
+ <!-- "Connect an external agent" is kept as the accessible consent label. -->
440
+ <div class="flow" role="img" aria-label="Connect an external agent to ${safeApp}">
441
+ <span class="tile" aria-hidden="true">
442
+ ${flowMarkSvg}
443
+ </span>
444
+ <span class="conn" aria-hidden="true"></span>
445
+ <span class="tile" aria-hidden="true">
446
+ <span class="agent-symbol">&lt;/&gt;</span>
447
+ </span>
448
+ </div>
449
+
450
+ <div class="eyebrow">Connect an external agent</div>
451
+ <h1>${safeUserCode ? `Authorize ${safeApp} from your terminal?` : `Connect ${safeApp} to an agent`}</h1>
452
+ <p class="sub">Allow Claude Code, Codex, or Cowork to use ${safeApp} with your account. You can revoke access anytime.</p>
453
+ <p class="identity">
454
+ <span>Signed in as <strong>${safeEmail}</strong></span>
455
+ <span aria-hidden="true">&middot;</span>
456
+ <span class="origin">${safeOrigin}</span>
457
+ </p>
458
+ </div>
255
459
 
256
- <div id="codeCallout" class="code-callout ${safeUserCode ? "" : "hidden"}">
257
- <div class="label">Authorizing device code</div>
258
- <div class="value" id="userCodeValue">${safeUserCode}</div>
460
+ <div id="codeCallout" class="device-strip ${safeUserCode ? "" : "hidden"}">
461
+ <span class="label">Device code</span>
462
+ <span class="value" id="userCodeValue">${safeUserCode}</span>
259
463
  </div>
260
464
 
261
465
  <div id="msg" class="msg"></div>
262
466
 
263
467
  <div id="mintForm">
264
- <div class="field">
265
- <label for="label">Label (optional)</label>
266
- <input id="label" type="text" placeholder="e.g. Claude Code on my laptop" maxlength="120" />
267
- </div>
268
- <div class="field">
269
- <label for="ttl">Expires in (days, 1–365)</label>
270
- <input id="ttl" type="number" min="1" max="365" value="${DEFAULT_TOKEN_TTL_DAYS}" />
271
- </div>
272
468
  <button id="authorizeBtn" class="primary">${safeUserCode ? "Authorize device" : "Create connection token"}</button>
469
+ <details class="advanced">
470
+ <summary>
471
+ Advanced options
472
+ <span class="chev" aria-hidden="true"></span>
473
+ </summary>
474
+ <div class="advanced-body">
475
+ <div class="field">
476
+ <label for="label">Label (optional)</label>
477
+ <input id="label" type="text" placeholder="e.g. Claude Code on my laptop" maxlength="120" />
478
+ </div>
479
+ <div class="field">
480
+ <label for="ttl">Expires in (days, 1–365)</label>
481
+ <input id="ttl" type="number" min="1" max="365" value="${DEFAULT_TOKEN_TTL_DAYS}" />
482
+ </div>
483
+ </div>
484
+ </details>
273
485
  </div>
274
486
 
275
- <div id="result" class="hidden">
276
- <p class="sub" id="resultMsg">Token created. Paste this into your agent's MCP config:</p>
487
+ <div id="result" class="result-panel hidden">
488
+ <div class="result-title">Connection token created</div>
489
+ <p class="result-copy" id="resultMsg">Paste this into your agent's MCP config. The token is shown only once.</p>
490
+ <div class="section-label">MCP config</div>
277
491
  <pre id="mcpJson"></pre>
278
- <p class="sub">Or from a terminal:</p>
279
- <pre id="cliLine"></pre>
492
+ <details class="advanced">
493
+ <summary>
494
+ Terminal alternative
495
+ <span class="chev" aria-hidden="true"></span>
496
+ </summary>
497
+ <div class="advanced-body">
498
+ <pre id="cliLine"></pre>
499
+ </div>
500
+ </details>
280
501
  </div>
281
502
 
282
- <div class="tokens">
283
- <h2>Your connections</h2>
284
- <div id="tokenList"><div class="meta">Loading…</div></div>
285
- </div>
503
+ <details id="connections" class="connections">
504
+ <summary>
505
+ <span class="connections-title">Existing connections</span>
506
+ <span id="connectionsState" class="connections-state">Checking</span>
507
+ <span class="chev" aria-hidden="true"></span>
508
+ </summary>
509
+ <div id="tokenList" class="token-list"><div class="empty-state">Checking connections...</div></div>
510
+ </details>
286
511
  </div>
287
512
  <script>
288
513
  (function () {
289
514
  var BASE = ${JSON.stringify(joinAppPath(connectBasePath, "/_agent-native/mcp/connect"))};
290
515
  var USER_CODE = ${JSON.stringify(safeUserCode || null)};
291
516
  var msgEl = document.getElementById("msg");
517
+ var connectionsEl = document.getElementById("connections");
518
+ var connectionsStateEl = document.getElementById("connectionsState");
292
519
  function showMsg(text, kind) {
293
520
  msgEl.textContent = text;
294
521
  msgEl.className = "msg " + (kind || "err");
@@ -321,10 +548,23 @@ function renderConnectPage(params) {
321
548
  var listEl = document.getElementById("tokenList");
322
549
  try {
323
550
  var res = await fetch(BASE + "/tokens", { credentials: "same-origin" });
324
- if (!res.ok) { listEl.innerHTML = '<div class="meta">Could not load.</div>'; return; }
551
+ if (!res.ok) {
552
+ connectionsStateEl.textContent = "Unavailable";
553
+ connectionsEl.open = true;
554
+ listEl.innerHTML = '<div class="empty-state">Could not load connections.</div>';
555
+ return;
556
+ }
325
557
  var data = await res.json();
326
558
  var tokens = (data && data.tokens) || [];
327
- if (!tokens.length) { listEl.innerHTML = '<div class="meta">No connections yet.</div>'; return; }
559
+ if (!tokens.length) {
560
+ connectionsStateEl.textContent = "None";
561
+ connectionsEl.open = false;
562
+ listEl.innerHTML = '<div class="empty-state">Created connections will appear here for revoking later.</div>';
563
+ return;
564
+ }
565
+ var activeCount = tokens.filter(function (t) { return !t.revokedAt; }).length;
566
+ connectionsStateEl.textContent = activeCount === 1 ? "1 active" : activeCount + " active";
567
+ connectionsEl.open = true;
328
568
  listEl.innerHTML = "";
329
569
  tokens.forEach(function (t) {
330
570
  var div = document.createElement("div");
@@ -354,7 +594,9 @@ function renderConnectPage(params) {
354
594
  listEl.appendChild(div);
355
595
  });
356
596
  } catch (e) {
357
- listEl.innerHTML = '<div class="meta">Could not load.</div>';
597
+ connectionsStateEl.textContent = "Unavailable";
598
+ connectionsEl.open = true;
599
+ listEl.innerHTML = '<div class="empty-state">Could not load connections.</div>';
358
600
  }
359
601
  }
360
602
 
@@ -372,9 +614,57 @@ function renderConnectPage(params) {
372
614
  showMsg((a.data && a.data.error) || "Could not authorize this device code.");
373
615
  return;
374
616
  }
375
- showMsg("Device authorized. You can return to your terminal — it will connect automatically.", "ok");
617
+ showMsg("Device authorized finishing connection… you can return to your terminal.", "ok");
376
618
  btn.classList.add("hidden");
377
619
  document.getElementById("mintForm").classList.add("hidden");
620
+ var cc = document.getElementById("codeCallout");
621
+ if (cc) cc.classList.add("hidden");
622
+ // The token is minted a few seconds later, when the CLI next polls
623
+ // /device/poll — so a single loadTokens() here runs BEFORE the row
624
+ // exists and the list would wrongly read "No connections yet" until
625
+ // a manual reload. Snapshot the EXISTING non-revoked token ids first
626
+ // so we announce "Connected" only when THIS device's freshly-minted
627
+ // token appears — a user who already has tokens must not get a false
628
+ // success the instant they authorize.
629
+ var priorIds = {};
630
+ try {
631
+ var pr = await fetch(BASE + "/tokens", { credentials: "same-origin" });
632
+ if (pr.ok) {
633
+ var pd = await pr.json();
634
+ ((pd && pd.tokens) || []).forEach(function (t) {
635
+ if (!t.revokedAt) priorIds[t.id] = true;
636
+ });
637
+ }
638
+ } catch (e) {}
639
+ loadTokens();
640
+ var tries = 0;
641
+ var iv = setInterval(async function () {
642
+ tries++;
643
+ try {
644
+ var res = await fetch(BASE + "/tokens", { credentials: "same-origin" });
645
+ if (res.ok) {
646
+ var data = await res.json();
647
+ var fresh = ((data && data.tokens) || []).filter(function (t) {
648
+ return !t.revokedAt && !priorIds[t.id];
649
+ });
650
+ if (fresh.length > 0) {
651
+ clearInterval(iv);
652
+ showMsg("Connected. This device can now act as you — manage or revoke it below.", "ok");
653
+ loadTokens();
654
+ return;
655
+ }
656
+ }
657
+ } catch (e) {}
658
+ if (tries >= 30) {
659
+ // No new token appeared in the window — e.g. the loopback
660
+ // dev-open path writes a header-only config and never mints.
661
+ // Don't claim "Connected" (we couldn't confirm a device token);
662
+ // keep the "authorized" message and just refresh the list.
663
+ clearInterval(iv);
664
+ loadTokens();
665
+ }
666
+ }, 2000);
667
+ return;
378
668
  } else {
379
669
  var m = await postJson("/token", { label: label, ttlDays: ttlDays });
380
670
  if (!m.ok) {
@@ -459,6 +749,9 @@ export async function handleMcpConnect(event, subpath, options = {}) {
459
749
  if (!session?.email)
460
750
  return json({ error: "Unauthorized" }, 401);
461
751
  if (!process.env.A2A_SECRET) {
752
+ if (canUseDevOpenConnect(event)) {
753
+ return json(mcpResultPayload(appUrl, options, { ownerEmail: session.email }));
754
+ }
462
755
  return json({
463
756
  error: "This deployment has no A2A_SECRET configured, so connect tokens cannot be minted.",
464
757
  }, 503);
@@ -475,7 +768,7 @@ export async function handleMcpConnect(event, subpath, options = {}) {
475
768
  label,
476
769
  ttlDays,
477
770
  });
478
- return json(mcpResultPayload(appUrl, token, options));
771
+ return json(mcpResultPayload(appUrl, options, { token }));
479
772
  }
480
773
  catch {
481
774
  return json({ error: "Failed to mint token." }, 500);
@@ -557,6 +850,21 @@ export async function handleMcpConnect(event, subpath, options = {}) {
557
850
  }
558
851
  // status === "approved" && ownerEmail bound → mint exactly once.
559
852
  if (!process.env.A2A_SECRET) {
853
+ if (canUseDevOpenConnect(event)) {
854
+ const consumed = await consumeDeviceCode(deviceCode, `dev-open-${randomUUID()}`);
855
+ if (!consumed) {
856
+ const fresh = await getDeviceCode(deviceCode);
857
+ if (fresh?.status === "consumed")
858
+ return json({ status: "consumed" });
859
+ return json({ status: "pending" });
860
+ }
861
+ return json({
862
+ status: "approved",
863
+ ...mcpResultPayload(appUrl, options, {
864
+ ownerEmail: row.ownerEmail,
865
+ }),
866
+ });
867
+ }
560
868
  return json({ status: "error", error: "A2A_SECRET not configured" }, 503);
561
869
  }
562
870
  try {
@@ -594,7 +902,7 @@ export async function handleMcpConnect(event, subpath, options = {}) {
594
902
  }
595
903
  return json({
596
904
  status: "approved",
597
- ...mcpResultPayload(appUrl, token, options),
905
+ ...mcpResultPayload(appUrl, options, { token }),
598
906
  });
599
907
  }
600
908
  catch {