@agent-native/dispatch 0.15.28 → 0.16.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 (80) hide show
  1. package/dist/actions/create-browser-chat-session.d.ts +15 -0
  2. package/dist/actions/create-browser-chat-session.d.ts.map +1 -0
  3. package/dist/actions/create-browser-chat-session.js +75 -0
  4. package/dist/actions/create-browser-chat-session.js.map +1 -0
  5. package/dist/actions/delete-destination.d.ts +12 -12
  6. package/dist/actions/index.d.ts.map +1 -1
  7. package/dist/actions/index.js +2 -0
  8. package/dist/actions/index.js.map +1 -1
  9. package/dist/actions/provider-api-register.d.ts +13 -13
  10. package/dist/actions/upsert-destination.d.ts +12 -12
  11. package/dist/actions/view-screen.js +4 -2
  12. package/dist/actions/view-screen.js.map +1 -1
  13. package/dist/components/create-app-popover.d.ts.map +1 -1
  14. package/dist/components/create-app-popover.js +7 -0
  15. package/dist/components/create-app-popover.js.map +1 -1
  16. package/dist/components/layout/Layout.d.ts.map +1 -1
  17. package/dist/components/layout/Layout.js +3 -4
  18. package/dist/components/layout/Layout.js.map +1 -1
  19. package/dist/config.d.ts +5 -0
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js.map +1 -1
  22. package/dist/hooks/use-navigation-state.js +2 -0
  23. package/dist/hooks/use-navigation-state.js.map +1 -1
  24. package/dist/lib/automations.d.ts.map +1 -1
  25. package/dist/lib/automations.js +5 -4
  26. package/dist/lib/automations.js.map +1 -1
  27. package/dist/lib/browser-chat-bridge.d.ts +8 -0
  28. package/dist/lib/browser-chat-bridge.d.ts.map +1 -0
  29. package/dist/lib/browser-chat-bridge.js +48 -0
  30. package/dist/lib/browser-chat-bridge.js.map +1 -0
  31. package/dist/lib/browser-chat-protocol.d.ts +33 -0
  32. package/dist/lib/browser-chat-protocol.d.ts.map +1 -0
  33. package/dist/lib/browser-chat-protocol.js +82 -0
  34. package/dist/lib/browser-chat-protocol.js.map +1 -0
  35. package/dist/routes/index.d.ts.map +1 -1
  36. package/dist/routes/index.js +2 -0
  37. package/dist/routes/index.js.map +1 -1
  38. package/dist/routes/pages/browser-chat.d.ts +5 -0
  39. package/dist/routes/pages/browser-chat.d.ts.map +1 -0
  40. package/dist/routes/pages/browser-chat.js +43 -0
  41. package/dist/routes/pages/browser-chat.js.map +1 -0
  42. package/dist/routes/pages/browser-connect.d.ts +5 -0
  43. package/dist/routes/pages/browser-connect.d.ts.map +1 -0
  44. package/dist/routes/pages/browser-connect.js +72 -0
  45. package/dist/routes/pages/browser-connect.js.map +1 -0
  46. package/dist/server/lib/browser-extension-allowlist.d.ts +9 -0
  47. package/dist/server/lib/browser-extension-allowlist.d.ts.map +1 -0
  48. package/dist/server/lib/browser-extension-allowlist.js +39 -0
  49. package/dist/server/lib/browser-extension-allowlist.js.map +1 -0
  50. package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
  51. package/dist/server/lib/usage-metrics-store.js +2 -1
  52. package/dist/server/lib/usage-metrics-store.js.map +1 -1
  53. package/package.json +3 -3
  54. package/src/actions/create-browser-chat-session.spec.ts +122 -0
  55. package/src/actions/create-browser-chat-session.ts +99 -0
  56. package/src/actions/index.ts +2 -0
  57. package/src/actions/view-screen.ts +5 -2
  58. package/src/components/create-app-popover.spec.tsx +53 -0
  59. package/src/components/create-app-popover.tsx +49 -0
  60. package/src/components/layout/Layout.spec.tsx +0 -1
  61. package/src/components/layout/Layout.tsx +2 -6
  62. package/src/config.ts +5 -0
  63. package/src/hooks/use-navigation-state.spec.ts +7 -0
  64. package/src/hooks/use-navigation-state.ts +1 -0
  65. package/src/lib/automations.spec.ts +64 -0
  66. package/src/lib/automations.ts +5 -3
  67. package/src/lib/browser-chat-bridge.spec.ts +151 -0
  68. package/src/lib/browser-chat-bridge.ts +79 -0
  69. package/src/lib/browser-chat-protocol.spec.ts +113 -0
  70. package/src/lib/browser-chat-protocol.ts +124 -0
  71. package/src/routes/index.spec.ts +6 -0
  72. package/src/routes/index.ts +2 -0
  73. package/src/routes/pages/browser-chat.spec.tsx +121 -0
  74. package/src/routes/pages/browser-chat.tsx +89 -0
  75. package/src/routes/pages/browser-connect.spec.tsx +137 -0
  76. package/src/routes/pages/browser-connect.tsx +143 -0
  77. package/src/server/lib/browser-extension-allowlist.spec.ts +76 -0
  78. package/src/server/lib/browser-extension-allowlist.ts +58 -0
  79. package/src/server/lib/usage-metrics-store.spec.ts +23 -2
  80. package/src/server/lib/usage-metrics-store.ts +2 -1
@@ -0,0 +1,39 @@
1
+ const CHROME_EXTENSION_ID = /^[a-p]{32}$/;
2
+ const MAX_BROWSER_EXTENSION_IDS = 64;
3
+ export function resolveAllowedBrowserExtensionIds(configIds = [], envValue = process.env.AGENT_NATIVE_BROWSER_EXTENSION_IDS) {
4
+ const ids = [...configIds, ...(envValue?.split(",") ?? [])]
5
+ .map((value) => value.trim())
6
+ .filter(Boolean);
7
+ const invalid = ids.find((value) => !CHROME_EXTENSION_ID.test(value));
8
+ if (invalid) {
9
+ throw new Error("Browser extension allowlist contains an invalid Chrome extension id.");
10
+ }
11
+ const uniqueIds = new Set(ids);
12
+ if (uniqueIds.size > MAX_BROWSER_EXTENSION_IDS) {
13
+ throw new Error(`Browser extension allowlist cannot exceed ${MAX_BROWSER_EXTENSION_IDS} ids.`);
14
+ }
15
+ return uniqueIds;
16
+ }
17
+ function isLoopbackOrigin(value) {
18
+ if (!value)
19
+ return false;
20
+ try {
21
+ const url = new URL(value);
22
+ return ((url.protocol === "http:" || url.protocol === "https:") &&
23
+ (url.hostname === "localhost" ||
24
+ url.hostname === "127.0.0.1" ||
25
+ url.hostname === "[::1]"));
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ export function isBrowserExtensionIdAllowed(options) {
32
+ if (!CHROME_EXTENSION_ID.test(options.extensionId))
33
+ return false;
34
+ const configured = resolveAllowedBrowserExtensionIds(options.configIds, options.envValue);
35
+ if (configured.has(options.extensionId))
36
+ return true;
37
+ return (options.nodeEnv !== "production" && isLoopbackOrigin(options.requestOrigin));
38
+ }
39
+ //# sourceMappingURL=browser-extension-allowlist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-extension-allowlist.js","sourceRoot":"","sources":["../../../src/server/lib/browser-extension-allowlist.ts"],"names":[],"mappings":"AAAA,MAAM,mBAAmB,GAAG,aAAa,CAAC;AAC1C,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAErC,MAAM,UAAU,iCAAiC,CAC/C,SAAS,GAAsB,EAAE,EACjC,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,kCAAkC;IAEzD,MAAM,GAAG,GAAG,CAAC,GAAG,SAAS,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;SACxD,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,OAAO,CAAC,CAAC;IACnB,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,sEAAsE,CACvE,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,SAAS,CAAC,IAAI,GAAG,yBAAyB,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CACb,6CAA6C,yBAAyB,OAAO,CAC9E,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAyB;IACjD,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QAC3B,OAAO,CACL,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;YACvD,CAAC,GAAG,CAAC,QAAQ,KAAK,WAAW;gBAC3B,GAAG,CAAC,QAAQ,KAAK,WAAW;gBAC5B,GAAG,CAAC,QAAQ,KAAK,OAAO,CAAC,CAC5B,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,OAM3C;IACC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC;QAAE,OAAO,KAAK,CAAC;IACjE,MAAM,UAAU,GAAG,iCAAiC,CAClD,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,QAAQ,CACjB,CAAC;IACF,IAAI,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IAErD,OAAO,CACL,OAAO,CAAC,OAAO,KAAK,YAAY,IAAI,gBAAgB,CAAC,OAAO,CAAC,aAAa,CAAC,CAC5E,CAAC;AACJ,CAAC","sourcesContent":["const CHROME_EXTENSION_ID = /^[a-p]{32}$/;\nconst MAX_BROWSER_EXTENSION_IDS = 64;\n\nexport function resolveAllowedBrowserExtensionIds(\n configIds: readonly string[] = [],\n envValue = process.env.AGENT_NATIVE_BROWSER_EXTENSION_IDS,\n): Set<string> {\n const ids = [...configIds, ...(envValue?.split(\",\") ?? [])]\n .map((value) => value.trim())\n .filter(Boolean);\n const invalid = ids.find((value) => !CHROME_EXTENSION_ID.test(value));\n if (invalid) {\n throw new Error(\n \"Browser extension allowlist contains an invalid Chrome extension id.\",\n );\n }\n const uniqueIds = new Set(ids);\n if (uniqueIds.size > MAX_BROWSER_EXTENSION_IDS) {\n throw new Error(\n `Browser extension allowlist cannot exceed ${MAX_BROWSER_EXTENSION_IDS} ids.`,\n );\n }\n return uniqueIds;\n}\n\nfunction isLoopbackOrigin(value: string | undefined): boolean {\n if (!value) return false;\n try {\n const url = new URL(value);\n return (\n (url.protocol === \"http:\" || url.protocol === \"https:\") &&\n (url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\" ||\n url.hostname === \"[::1]\")\n );\n } catch {\n return false;\n }\n}\n\nexport function isBrowserExtensionIdAllowed(options: {\n extensionId: string;\n configIds?: readonly string[];\n envValue?: string;\n nodeEnv?: string;\n requestOrigin?: string;\n}): boolean {\n if (!CHROME_EXTENSION_ID.test(options.extensionId)) return false;\n const configured = resolveAllowedBrowserExtensionIds(\n options.configIds,\n options.envValue,\n );\n if (configured.has(options.extensionId)) return true;\n\n return (\n options.nodeEnv !== \"production\" && isLoopbackOrigin(options.requestOrigin)\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"usage-metrics-store.d.ts","sourceRoot":"","sources":["../../../src/server/lib/usage-metrics-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"usage-metrics-store.d.ts","sourceRoot":"","sources":["../../../src/server/lib/usage-metrics-store.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { detectEngineFromEnv, detectEngineFromUserSecrets, getAgentEngineEntry, isAgentEngineSettingConfigured, isStoredEngineUsable, registerBuiltinEngines, } from "@agent-native/core/agent/engine";
2
2
  import { getDbExec } from "@agent-native/core/db";
3
3
  import { getSetting } from "@agent-native/core/settings";
4
+ import { ForbiddenError } from "@agent-native/core/sharing";
4
5
  import { getUsageSummary, usageBillingForEngine, } from "@agent-native/core/usage";
5
6
  import { listWorkspaceApps, } from "./app-creation-store.js";
6
7
  import { currentOrgId, currentOwnerEmail } from "./dispatch-store.js";
@@ -208,7 +209,7 @@ async function assertCanViewMetrics() {
208
209
  if (!orgId) {
209
210
  return { viewerEmail, orgId, role };
210
211
  }
211
- throw new Error("Only organization owners and admins can view workspace usage metrics.");
212
+ throw new ForbiddenError("Only organization owners and admins can view workspace usage metrics.");
212
213
  }
213
214
  export async function listDispatchUsageMetrics(input) {
214
215
  const { viewerEmail, orgId, role } = await assertCanViewMetrics();
@@ -1 +1 @@
1
- {"version":3,"file":"usage-metrics-store.js","sourceRoot":"","sources":["../../../src/server/lib/usage-metrics-store.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n detectEngineFromEnv,\n detectEngineFromUserSecrets,\n getAgentEngineEntry,\n isAgentEngineSettingConfigured,\n isStoredEngineUsable,\n registerBuiltinEngines,\n} from \"@agent-native/core/agent/engine\";\nimport { getDbExec } from \"@agent-native/core/db\";\nimport { getSetting } from \"@agent-native/core/settings\";\nimport {\n getUsageSummary,\n usageBillingForEngine,\n type UsageBillingMode,\n} from \"@agent-native/core/usage\";\n\nimport {\n listWorkspaceApps,\n type WorkspaceAppSummary,\n} from \"./app-creation-store.js\";\nimport { currentOrgId, currentOwnerEmail } from \"./dispatch-store.js\";\n\nconst DAY_MS = 86_400_000;\n\nregisterBuiltinEngines();\n\nexport interface UsageMetricBucket {\n key: string;\n label: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n activeUsers: number;\n lastActiveAt: number | null;\n}\n\nexport interface UserUsageMetric extends UsageMetricBucket {\n ownerEmail: string;\n chatThreads: number;\n chatMessages: number;\n lastChatAt: number | null;\n topApp: string | null;\n role: string | null;\n}\n\nexport interface AppAccessMetric {\n id: string;\n name: string;\n path: string;\n status: WorkspaceAppSummary[\"status\"];\n statusLabel?: string;\n isDispatch: boolean;\n accessModel: \"workspace\" | \"solo\";\n accessLabel: string;\n accessUsers: number;\n usersWithUsage: number;\n usageCalls: number;\n chatCalls: number;\n costCents: number;\n lastActiveAt: number | null;\n}\n\nexport interface DailyUsageMetric {\n date: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n activeUsers: number;\n}\n\nexport interface RecentUsageMetric {\n id: number;\n createdAt: number;\n ownerEmail: string;\n app: string;\n label: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n costCents: number;\n}\n\nexport interface DispatchUsageMetrics {\n billing: UsageBillingMode;\n sinceMs: number;\n sinceDays: number;\n generatedAt: number;\n access: {\n viewerEmail: string;\n orgId: string | null;\n role: string | null;\n scope: \"organization\" | \"solo\";\n totalUsers: number;\n };\n totals: {\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n activeUsers: number;\n chatThreads: number;\n chatMessages: number;\n workspaceApps: number;\n };\n byApp: UsageMetricBucket[];\n byUser: UserUsageMetric[];\n byLabel: UsageMetricBucket[];\n byModel: UsageMetricBucket[];\n daily: DailyUsageMetric[];\n appAccess: AppAccessMetric[];\n recent: RecentUsageMetric[];\n}\n\ninterface MemberRecord {\n email: string;\n role: string | null;\n joinedAt: number | null;\n}\n\ninterface ChatStats {\n threads: number;\n messages: number;\n lastChatAt: number | null;\n}\n\nfunction numberField(row: Record<string, unknown>, key: string): number {\n return Number(row[key] ?? 0) || 0;\n}\n\nfunction stringField(row: Record<string, unknown>, key: string): string {\n return String(row[key] ?? \"\");\n}\n\nfunction nullableNumberField(\n row: Record<string, unknown>,\n key: string,\n): number | null {\n const value = row[key];\n if (value == null) return null;\n const numberValue = Number(value);\n return Number.isFinite(numberValue) ? numberValue : null;\n}\n\nfunction labelForKey(value: string): string {\n const trimmed = value.trim();\n return trimmed || \"Unattributed\";\n}\n\nfunction normalizeAppKey(value: string | null | undefined): string {\n const raw = (value ?? \"\").trim().toLowerCase();\n if (!raw) return \"unattributed\";\n return raw.replace(/^agent-native-/, \"\");\n}\n\nfunction envEmails(name: string): string[] {\n return (process.env[name] ?? \"\")\n .split(\",\")\n .map((value) => value.trim().toLowerCase())\n .filter(Boolean);\n}\n\nfunction isEnvAdmin(email: string): boolean {\n const normalized = email.trim().toLowerCase();\n return [\n ...envEmails(\"DISPATCH_ADMIN_EMAILS\"),\n ...envEmails(\"WORKSPACE_OWNER_EMAIL\"),\n ...envEmails(\"DISPATCH_DEFAULT_OWNER_EMAIL\"),\n ].includes(normalized);\n}\n\nasync function detectUsageEngineName(): Promise<string | null> {\n try {\n const stored = (await getSetting(\"agent-engine\")) as {\n engine?: string;\n } | null;\n if (isAgentEngineSettingConfigured(stored)) {\n return (stored as { engine: string }).engine;\n }\n if (stored && typeof stored.engine === \"string\") {\n const entry = getAgentEngineEntry(stored.engine);\n if (entry && isStoredEngineUsable(stored, entry)) {\n return stored.engine;\n }\n }\n\n const detectedFromUser = await detectEngineFromUserSecrets();\n if (detectedFromUser) return detectedFromUser.name;\n\n return detectEngineFromEnv()?.name ?? null;\n } catch {\n return null;\n }\n}\n\nasync function queryRows<T extends Record<string, unknown>>(\n sql: string,\n args: unknown[] = [],\n): Promise<T[]> {\n try {\n const result = await getDbExec().execute({ sql, args });\n return result.rows as T[];\n } catch {\n return [];\n }\n}\n\nasync function initializeUsageMetricsTable(sinceMs: number): Promise<void> {\n try {\n // Initializes token_usage on fresh deployments before the read-only\n // aggregate queries below. The fake owner avoids changing visible data.\n await getUsageSummary({ ownerEmail: \"__dispatch_metrics_init__\", sinceMs });\n } catch {\n // Metrics should still render an empty state if usage storage is locked,\n // stale, or unavailable; each aggregate read below is already best-effort.\n }\n}\n\nasync function getViewerOrgRole(\n orgId: string | null,\n email: string,\n): Promise<string | null> {\n if (!orgId) return null;\n const rows = await queryRows<{ role?: string }>(\n `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,\n [orgId, email.toLowerCase()],\n );\n const role = rows[0]?.role;\n return typeof role === \"string\" ? role : null;\n}\n\nasync function listOrgMembers(orgId: string | null): Promise<MemberRecord[]> {\n if (!orgId) return [];\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT email, role, joined_at AS joined_at FROM org_members WHERE org_id = ? ORDER BY joined_at ASC`,\n [orgId],\n );\n return rows\n .map((row) => ({\n email: stringField(row, \"email\").trim(),\n role: stringField(row, \"role\") || null,\n joinedAt: nullableNumberField(row, \"joined_at\"),\n }))\n .filter((member) => member.email);\n}\n\nasync function listSignedInUsers(): Promise<MemberRecord[]> {\n const authRows = await queryRows<Record<string, unknown>>(\n `SELECT email, created_at AS joined_at FROM \"user\" ORDER BY created_at ASC`,\n );\n if (authRows.length > 0) {\n return authRows\n .map((row) => ({\n email: stringField(row, \"email\").trim(),\n role: null,\n joinedAt: nullableNumberField(row, \"joined_at\"),\n }))\n .filter((member) => member.email);\n }\n\n const usageRows = await queryRows<{ email?: string }>(\n `SELECT DISTINCT owner_email AS email FROM token_usage`,\n );\n const threadRows = await queryRows<{ email?: string }>(\n `SELECT DISTINCT owner_email AS email FROM chat_threads`,\n );\n const emails = new Set<string>();\n for (const row of [...usageRows, ...threadRows]) {\n if (row.email) emails.add(row.email);\n }\n return [...emails].sort().map((email) => ({\n email,\n role: null,\n joinedAt: null,\n }));\n}\n\nfunction usageScope(\n sinceMs: number,\n memberEmails: string[],\n): { where: string; args: unknown[] } {\n if (memberEmails.length === 0) {\n return { where: \"created_at >= ?\", args: [sinceMs] };\n }\n const placeholders = memberEmails.map(() => \"?\").join(\", \");\n return {\n where: `created_at >= ? AND owner_email IN (${placeholders})`,\n args: [sinceMs, ...memberEmails],\n };\n}\n\nfunction threadScope(\n sinceMs: number,\n memberEmails: string[],\n): { where: string; args: unknown[] } {\n if (memberEmails.length === 0) {\n return { where: \"updated_at >= ?\", args: [sinceMs] };\n }\n const placeholders = memberEmails.map(() => \"?\").join(\", \");\n return {\n where: `updated_at >= ? AND owner_email IN (${placeholders})`,\n args: [sinceMs, ...memberEmails],\n };\n}\n\nfunction bucketFromRow(row: Record<string, unknown>): UsageMetricBucket {\n const key = stringField(row, \"k\");\n return {\n key,\n label: labelForKey(key),\n costCents: numberField(row, \"cost_x100\") / 100,\n calls: numberField(row, \"calls\"),\n chatCalls: numberField(row, \"chat_calls\"),\n inputTokens: numberField(row, \"input_tokens\"),\n outputTokens: numberField(row, \"output_tokens\"),\n cacheReadTokens: numberField(row, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(row, \"cache_write_tokens\"),\n activeUsers: numberField(row, \"active_users\"),\n lastActiveAt: nullableNumberField(row, \"last_active_at\"),\n };\n}\n\nasync function usageBuckets(\n columnExpression: string,\n where: string,\n args: unknown[],\n limit: number,\n): Promise<UsageMetricBucket[]> {\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT ${columnExpression} AS k,\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,\n COUNT(*) AS calls,\n SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,\n COALESCE(SUM(input_tokens), 0) AS input_tokens,\n COALESCE(SUM(output_tokens), 0) AS output_tokens,\n COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,\n COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,\n COUNT(DISTINCT owner_email) AS active_users,\n MAX(created_at) AS last_active_at\n FROM token_usage\n WHERE ${where}\n GROUP BY ${columnExpression}\n ORDER BY cost_x100 DESC\n LIMIT ?`,\n [...args, limit],\n );\n return rows.map(bucketFromRow);\n}\n\nasync function loadChatStats(\n where: string,\n args: unknown[],\n): Promise<Map<string, ChatStats>> {\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT owner_email AS owner_email,\n COUNT(*) AS threads,\n COALESCE(SUM(message_count), 0) AS messages,\n MAX(updated_at) AS last_chat_at\n FROM chat_threads\n WHERE ${where}\n GROUP BY owner_email`,\n args,\n );\n return new Map(\n rows.map((row) => [\n stringField(row, \"owner_email\"),\n {\n threads: numberField(row, \"threads\"),\n messages: numberField(row, \"messages\"),\n lastChatAt: nullableNumberField(row, \"last_chat_at\"),\n },\n ]),\n );\n}\n\nasync function assertCanViewMetrics(): Promise<{\n viewerEmail: string;\n orgId: string | null;\n role: string | null;\n}> {\n const viewerEmail = currentOwnerEmail();\n const orgId = currentOrgId();\n const role = await getViewerOrgRole(orgId, viewerEmail);\n if (isEnvAdmin(viewerEmail) || role === \"owner\" || role === \"admin\") {\n return { viewerEmail, orgId, role };\n }\n if (!orgId) {\n return { viewerEmail, orgId, role };\n }\n throw new Error(\n \"Only organization owners and admins can view workspace usage metrics.\",\n );\n}\n\nexport async function listDispatchUsageMetrics(input: {\n sinceDays?: number;\n}): Promise<DispatchUsageMetrics> {\n const { viewerEmail, orgId, role } = await assertCanViewMetrics();\n const sinceDays = Math.max(1, Math.min(365, input.sinceDays ?? 30));\n const sinceMs = Date.now() - sinceDays * DAY_MS;\n const billing = usageBillingForEngine(await detectUsageEngineName());\n\n await initializeUsageMetricsTable(sinceMs);\n\n const rawMembers = orgId\n ? await listOrgMembers(orgId)\n : await listSignedInUsers();\n const members =\n orgId && rawMembers.length === 0\n ? [{ email: viewerEmail, role, joinedAt: null }]\n : rawMembers;\n const memberEmails = orgId ? members.map((member) => member.email) : [];\n const memberByEmail = new Map(\n members.map((member) => [member.email.toLowerCase(), member]),\n );\n const usage = usageScope(sinceMs, memberEmails);\n const threads = threadScope(sinceMs, memberEmails);\n\n const [apps, totalsRows, byApp, byUserBase, byLabel, byModel, chatStats] =\n await Promise.all([\n listWorkspaceApps({ includeAgentCards: false }),\n queryRows<Record<string, unknown>>(\n `SELECT\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,\n COUNT(*) AS calls,\n SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,\n COALESCE(SUM(input_tokens), 0) AS input_tokens,\n COALESCE(SUM(output_tokens), 0) AS output_tokens,\n COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,\n COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,\n COUNT(DISTINCT owner_email) AS active_users\n FROM token_usage\n WHERE ${usage.where}`,\n usage.args,\n ),\n usageBuckets(\n `COALESCE(NULLIF(app, ''), 'unattributed')`,\n usage.where,\n usage.args,\n 20,\n ),\n usageBuckets(\"owner_email\", usage.where, usage.args, 50),\n usageBuckets(\n `COALESCE(NULLIF(label, ''), 'chat')`,\n usage.where,\n usage.args,\n 20,\n ),\n usageBuckets(\n `COALESCE(NULLIF(model, ''), 'unknown')`,\n usage.where,\n usage.args,\n 20,\n ),\n loadChatStats(threads.where, threads.args),\n ]);\n\n const topAppRows = await queryRows<Record<string, unknown>>(\n `SELECT owner_email AS owner_email,\n COALESCE(NULLIF(app, ''), 'unattributed') AS app,\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100\n FROM token_usage\n WHERE ${usage.where}\n GROUP BY owner_email, COALESCE(NULLIF(app, ''), 'unattributed')\n ORDER BY owner_email ASC, cost_x100 DESC`,\n usage.args,\n );\n const topAppByUser = new Map<string, string>();\n for (const row of topAppRows) {\n const email = stringField(row, \"owner_email\");\n if (!topAppByUser.has(email)) {\n topAppByUser.set(email, stringField(row, \"app\"));\n }\n }\n\n const byUserMap = new Map<string, UserUsageMetric>();\n for (const bucket of byUserBase) {\n const ownerEmail = bucket.key;\n const stats = chatStats.get(ownerEmail) ?? {\n threads: 0,\n messages: 0,\n lastChatAt: null,\n };\n const member = memberByEmail.get(ownerEmail.toLowerCase());\n byUserMap.set(ownerEmail, {\n ...bucket,\n ownerEmail,\n chatThreads: stats.threads,\n chatMessages: stats.messages,\n lastChatAt: stats.lastChatAt,\n topApp: topAppByUser.get(ownerEmail) ?? null,\n role: member?.role ?? null,\n });\n }\n for (const [ownerEmail, stats] of chatStats) {\n if (byUserMap.has(ownerEmail)) continue;\n const member = memberByEmail.get(ownerEmail.toLowerCase());\n byUserMap.set(ownerEmail, {\n key: ownerEmail,\n label: ownerEmail,\n ownerEmail,\n costCents: 0,\n calls: 0,\n chatCalls: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n activeUsers: 1,\n lastActiveAt: stats.lastChatAt,\n chatThreads: stats.threads,\n chatMessages: stats.messages,\n lastChatAt: stats.lastChatAt,\n topApp: null,\n role: member?.role ?? null,\n });\n }\n\n const dayRows = await queryRows<Record<string, unknown>>(\n `SELECT created_at, owner_email, label, cost_cents_x100\n FROM token_usage\n WHERE ${usage.where}\n ORDER BY created_at ASC`,\n usage.args,\n );\n const dailyMap = new Map<\n string,\n { costX100: number; calls: number; chatCalls: number; users: Set<string> }\n >();\n for (const row of dayRows) {\n const date = new Date(numberField(row, \"created_at\"))\n .toISOString()\n .slice(0, 10);\n const current = dailyMap.get(date) ?? {\n costX100: 0,\n calls: 0,\n chatCalls: 0,\n users: new Set<string>(),\n };\n current.costX100 += numberField(row, \"cost_cents_x100\");\n current.calls += 1;\n if (stringField(row, \"label\") === \"chat\") current.chatCalls += 1;\n current.users.add(stringField(row, \"owner_email\"));\n dailyMap.set(date, current);\n }\n const daily = [...dailyMap.entries()]\n .map(([date, value]) => ({\n date,\n costCents: value.costX100 / 100,\n calls: value.calls,\n chatCalls: value.chatCalls,\n activeUsers: value.users.size,\n }))\n .sort((a, b) => a.date.localeCompare(b.date));\n\n const recentRows = await queryRows<Record<string, unknown>>(\n `SELECT id, created_at, owner_email, app, label, model,\n input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,\n cost_cents_x100\n FROM token_usage\n WHERE ${usage.where}\n ORDER BY created_at DESC\n LIMIT 50`,\n usage.args,\n );\n const recent = recentRows.map((row) => ({\n id: numberField(row, \"id\"),\n createdAt: numberField(row, \"created_at\"),\n ownerEmail: stringField(row, \"owner_email\"),\n app: stringField(row, \"app\") || \"unattributed\",\n label: stringField(row, \"label\") || \"chat\",\n model: stringField(row, \"model\") || \"unknown\",\n inputTokens: numberField(row, \"input_tokens\"),\n outputTokens: numberField(row, \"output_tokens\"),\n cacheReadTokens: numberField(row, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(row, \"cache_write_tokens\"),\n costCents: numberField(row, \"cost_cents_x100\") / 100,\n }));\n\n const appUsageByKey = new Map(\n byApp.map((bucket) => [normalizeAppKey(bucket.key), bucket]),\n );\n const accessUsers = members.length || byUserMap.size;\n const accessModel = orgId ? \"workspace\" : \"solo\";\n const accessLabel = orgId ? \"Workspace members\" : \"Signed-in users\";\n const appAccess = apps.map((app) => {\n const usageBucket = appUsageByKey.get(normalizeAppKey(app.id));\n return {\n id: app.id,\n name: app.name,\n path: app.path,\n status: app.status,\n statusLabel: app.statusLabel,\n isDispatch: app.isDispatch,\n accessModel,\n accessLabel,\n accessUsers,\n usersWithUsage: usageBucket?.activeUsers ?? 0,\n usageCalls: usageBucket?.calls ?? 0,\n chatCalls: usageBucket?.chatCalls ?? 0,\n costCents: usageBucket?.costCents ?? 0,\n lastActiveAt: usageBucket?.lastActiveAt ?? null,\n } satisfies AppAccessMetric;\n });\n\n const totals = totalsRows[0] ?? {};\n const chatThreadTotals = [...chatStats.values()].reduce(\n (acc, value) => ({\n threads: acc.threads + value.threads,\n messages: acc.messages + value.messages,\n }),\n { threads: 0, messages: 0 },\n );\n\n return {\n billing,\n sinceMs,\n sinceDays,\n generatedAt: Date.now(),\n access: {\n viewerEmail,\n orgId,\n role,\n scope: orgId ? \"organization\" : \"solo\",\n totalUsers: accessUsers,\n },\n totals: {\n costCents: numberField(totals, \"cost_x100\") / 100,\n calls: numberField(totals, \"calls\"),\n chatCalls: numberField(totals, \"chat_calls\"),\n inputTokens: numberField(totals, \"input_tokens\"),\n outputTokens: numberField(totals, \"output_tokens\"),\n cacheReadTokens: numberField(totals, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(totals, \"cache_write_tokens\"),\n activeUsers: numberField(totals, \"active_users\"),\n chatThreads: chatThreadTotals.threads,\n chatMessages: chatThreadTotals.messages,\n workspaceApps: apps.filter((app) => !app.isDispatch).length,\n },\n byApp,\n byUser: [...byUserMap.values()].sort((a, b) => {\n if (b.costCents !== a.costCents) return b.costCents - a.costCents;\n return (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0);\n }),\n byLabel,\n byModel,\n daily,\n appAccess,\n recent,\n };\n}\n"]}
1
+ {"version":3,"file":"usage-metrics-store.js","sourceRoot":"","sources":["../../../src/server/lib/usage-metrics-store.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n detectEngineFromEnv,\n detectEngineFromUserSecrets,\n getAgentEngineEntry,\n isAgentEngineSettingConfigured,\n isStoredEngineUsable,\n registerBuiltinEngines,\n} from \"@agent-native/core/agent/engine\";\nimport { getDbExec } from \"@agent-native/core/db\";\nimport { getSetting } from \"@agent-native/core/settings\";\nimport { ForbiddenError } from \"@agent-native/core/sharing\";\nimport {\n getUsageSummary,\n usageBillingForEngine,\n type UsageBillingMode,\n} from \"@agent-native/core/usage\";\n\nimport {\n listWorkspaceApps,\n type WorkspaceAppSummary,\n} from \"./app-creation-store.js\";\nimport { currentOrgId, currentOwnerEmail } from \"./dispatch-store.js\";\n\nconst DAY_MS = 86_400_000;\n\nregisterBuiltinEngines();\n\nexport interface UsageMetricBucket {\n key: string;\n label: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n activeUsers: number;\n lastActiveAt: number | null;\n}\n\nexport interface UserUsageMetric extends UsageMetricBucket {\n ownerEmail: string;\n chatThreads: number;\n chatMessages: number;\n lastChatAt: number | null;\n topApp: string | null;\n role: string | null;\n}\n\nexport interface AppAccessMetric {\n id: string;\n name: string;\n path: string;\n status: WorkspaceAppSummary[\"status\"];\n statusLabel?: string;\n isDispatch: boolean;\n accessModel: \"workspace\" | \"solo\";\n accessLabel: string;\n accessUsers: number;\n usersWithUsage: number;\n usageCalls: number;\n chatCalls: number;\n costCents: number;\n lastActiveAt: number | null;\n}\n\nexport interface DailyUsageMetric {\n date: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n activeUsers: number;\n}\n\nexport interface RecentUsageMetric {\n id: number;\n createdAt: number;\n ownerEmail: string;\n app: string;\n label: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n costCents: number;\n}\n\nexport interface DispatchUsageMetrics {\n billing: UsageBillingMode;\n sinceMs: number;\n sinceDays: number;\n generatedAt: number;\n access: {\n viewerEmail: string;\n orgId: string | null;\n role: string | null;\n scope: \"organization\" | \"solo\";\n totalUsers: number;\n };\n totals: {\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n activeUsers: number;\n chatThreads: number;\n chatMessages: number;\n workspaceApps: number;\n };\n byApp: UsageMetricBucket[];\n byUser: UserUsageMetric[];\n byLabel: UsageMetricBucket[];\n byModel: UsageMetricBucket[];\n daily: DailyUsageMetric[];\n appAccess: AppAccessMetric[];\n recent: RecentUsageMetric[];\n}\n\ninterface MemberRecord {\n email: string;\n role: string | null;\n joinedAt: number | null;\n}\n\ninterface ChatStats {\n threads: number;\n messages: number;\n lastChatAt: number | null;\n}\n\nfunction numberField(row: Record<string, unknown>, key: string): number {\n return Number(row[key] ?? 0) || 0;\n}\n\nfunction stringField(row: Record<string, unknown>, key: string): string {\n return String(row[key] ?? \"\");\n}\n\nfunction nullableNumberField(\n row: Record<string, unknown>,\n key: string,\n): number | null {\n const value = row[key];\n if (value == null) return null;\n const numberValue = Number(value);\n return Number.isFinite(numberValue) ? numberValue : null;\n}\n\nfunction labelForKey(value: string): string {\n const trimmed = value.trim();\n return trimmed || \"Unattributed\";\n}\n\nfunction normalizeAppKey(value: string | null | undefined): string {\n const raw = (value ?? \"\").trim().toLowerCase();\n if (!raw) return \"unattributed\";\n return raw.replace(/^agent-native-/, \"\");\n}\n\nfunction envEmails(name: string): string[] {\n return (process.env[name] ?? \"\")\n .split(\",\")\n .map((value) => value.trim().toLowerCase())\n .filter(Boolean);\n}\n\nfunction isEnvAdmin(email: string): boolean {\n const normalized = email.trim().toLowerCase();\n return [\n ...envEmails(\"DISPATCH_ADMIN_EMAILS\"),\n ...envEmails(\"WORKSPACE_OWNER_EMAIL\"),\n ...envEmails(\"DISPATCH_DEFAULT_OWNER_EMAIL\"),\n ].includes(normalized);\n}\n\nasync function detectUsageEngineName(): Promise<string | null> {\n try {\n const stored = (await getSetting(\"agent-engine\")) as {\n engine?: string;\n } | null;\n if (isAgentEngineSettingConfigured(stored)) {\n return (stored as { engine: string }).engine;\n }\n if (stored && typeof stored.engine === \"string\") {\n const entry = getAgentEngineEntry(stored.engine);\n if (entry && isStoredEngineUsable(stored, entry)) {\n return stored.engine;\n }\n }\n\n const detectedFromUser = await detectEngineFromUserSecrets();\n if (detectedFromUser) return detectedFromUser.name;\n\n return detectEngineFromEnv()?.name ?? null;\n } catch {\n return null;\n }\n}\n\nasync function queryRows<T extends Record<string, unknown>>(\n sql: string,\n args: unknown[] = [],\n): Promise<T[]> {\n try {\n const result = await getDbExec().execute({ sql, args });\n return result.rows as T[];\n } catch {\n return [];\n }\n}\n\nasync function initializeUsageMetricsTable(sinceMs: number): Promise<void> {\n try {\n // Initializes token_usage on fresh deployments before the read-only\n // aggregate queries below. The fake owner avoids changing visible data.\n await getUsageSummary({ ownerEmail: \"__dispatch_metrics_init__\", sinceMs });\n } catch {\n // Metrics should still render an empty state if usage storage is locked,\n // stale, or unavailable; each aggregate read below is already best-effort.\n }\n}\n\nasync function getViewerOrgRole(\n orgId: string | null,\n email: string,\n): Promise<string | null> {\n if (!orgId) return null;\n const rows = await queryRows<{ role?: string }>(\n `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,\n [orgId, email.toLowerCase()],\n );\n const role = rows[0]?.role;\n return typeof role === \"string\" ? role : null;\n}\n\nasync function listOrgMembers(orgId: string | null): Promise<MemberRecord[]> {\n if (!orgId) return [];\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT email, role, joined_at AS joined_at FROM org_members WHERE org_id = ? ORDER BY joined_at ASC`,\n [orgId],\n );\n return rows\n .map((row) => ({\n email: stringField(row, \"email\").trim(),\n role: stringField(row, \"role\") || null,\n joinedAt: nullableNumberField(row, \"joined_at\"),\n }))\n .filter((member) => member.email);\n}\n\nasync function listSignedInUsers(): Promise<MemberRecord[]> {\n const authRows = await queryRows<Record<string, unknown>>(\n `SELECT email, created_at AS joined_at FROM \"user\" ORDER BY created_at ASC`,\n );\n if (authRows.length > 0) {\n return authRows\n .map((row) => ({\n email: stringField(row, \"email\").trim(),\n role: null,\n joinedAt: nullableNumberField(row, \"joined_at\"),\n }))\n .filter((member) => member.email);\n }\n\n const usageRows = await queryRows<{ email?: string }>(\n `SELECT DISTINCT owner_email AS email FROM token_usage`,\n );\n const threadRows = await queryRows<{ email?: string }>(\n `SELECT DISTINCT owner_email AS email FROM chat_threads`,\n );\n const emails = new Set<string>();\n for (const row of [...usageRows, ...threadRows]) {\n if (row.email) emails.add(row.email);\n }\n return [...emails].sort().map((email) => ({\n email,\n role: null,\n joinedAt: null,\n }));\n}\n\nfunction usageScope(\n sinceMs: number,\n memberEmails: string[],\n): { where: string; args: unknown[] } {\n if (memberEmails.length === 0) {\n return { where: \"created_at >= ?\", args: [sinceMs] };\n }\n const placeholders = memberEmails.map(() => \"?\").join(\", \");\n return {\n where: `created_at >= ? AND owner_email IN (${placeholders})`,\n args: [sinceMs, ...memberEmails],\n };\n}\n\nfunction threadScope(\n sinceMs: number,\n memberEmails: string[],\n): { where: string; args: unknown[] } {\n if (memberEmails.length === 0) {\n return { where: \"updated_at >= ?\", args: [sinceMs] };\n }\n const placeholders = memberEmails.map(() => \"?\").join(\", \");\n return {\n where: `updated_at >= ? AND owner_email IN (${placeholders})`,\n args: [sinceMs, ...memberEmails],\n };\n}\n\nfunction bucketFromRow(row: Record<string, unknown>): UsageMetricBucket {\n const key = stringField(row, \"k\");\n return {\n key,\n label: labelForKey(key),\n costCents: numberField(row, \"cost_x100\") / 100,\n calls: numberField(row, \"calls\"),\n chatCalls: numberField(row, \"chat_calls\"),\n inputTokens: numberField(row, \"input_tokens\"),\n outputTokens: numberField(row, \"output_tokens\"),\n cacheReadTokens: numberField(row, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(row, \"cache_write_tokens\"),\n activeUsers: numberField(row, \"active_users\"),\n lastActiveAt: nullableNumberField(row, \"last_active_at\"),\n };\n}\n\nasync function usageBuckets(\n columnExpression: string,\n where: string,\n args: unknown[],\n limit: number,\n): Promise<UsageMetricBucket[]> {\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT ${columnExpression} AS k,\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,\n COUNT(*) AS calls,\n SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,\n COALESCE(SUM(input_tokens), 0) AS input_tokens,\n COALESCE(SUM(output_tokens), 0) AS output_tokens,\n COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,\n COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,\n COUNT(DISTINCT owner_email) AS active_users,\n MAX(created_at) AS last_active_at\n FROM token_usage\n WHERE ${where}\n GROUP BY ${columnExpression}\n ORDER BY cost_x100 DESC\n LIMIT ?`,\n [...args, limit],\n );\n return rows.map(bucketFromRow);\n}\n\nasync function loadChatStats(\n where: string,\n args: unknown[],\n): Promise<Map<string, ChatStats>> {\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT owner_email AS owner_email,\n COUNT(*) AS threads,\n COALESCE(SUM(message_count), 0) AS messages,\n MAX(updated_at) AS last_chat_at\n FROM chat_threads\n WHERE ${where}\n GROUP BY owner_email`,\n args,\n );\n return new Map(\n rows.map((row) => [\n stringField(row, \"owner_email\"),\n {\n threads: numberField(row, \"threads\"),\n messages: numberField(row, \"messages\"),\n lastChatAt: nullableNumberField(row, \"last_chat_at\"),\n },\n ]),\n );\n}\n\nasync function assertCanViewMetrics(): Promise<{\n viewerEmail: string;\n orgId: string | null;\n role: string | null;\n}> {\n const viewerEmail = currentOwnerEmail();\n const orgId = currentOrgId();\n const role = await getViewerOrgRole(orgId, viewerEmail);\n if (isEnvAdmin(viewerEmail) || role === \"owner\" || role === \"admin\") {\n return { viewerEmail, orgId, role };\n }\n if (!orgId) {\n return { viewerEmail, orgId, role };\n }\n throw new ForbiddenError(\n \"Only organization owners and admins can view workspace usage metrics.\",\n );\n}\n\nexport async function listDispatchUsageMetrics(input: {\n sinceDays?: number;\n}): Promise<DispatchUsageMetrics> {\n const { viewerEmail, orgId, role } = await assertCanViewMetrics();\n const sinceDays = Math.max(1, Math.min(365, input.sinceDays ?? 30));\n const sinceMs = Date.now() - sinceDays * DAY_MS;\n const billing = usageBillingForEngine(await detectUsageEngineName());\n\n await initializeUsageMetricsTable(sinceMs);\n\n const rawMembers = orgId\n ? await listOrgMembers(orgId)\n : await listSignedInUsers();\n const members =\n orgId && rawMembers.length === 0\n ? [{ email: viewerEmail, role, joinedAt: null }]\n : rawMembers;\n const memberEmails = orgId ? members.map((member) => member.email) : [];\n const memberByEmail = new Map(\n members.map((member) => [member.email.toLowerCase(), member]),\n );\n const usage = usageScope(sinceMs, memberEmails);\n const threads = threadScope(sinceMs, memberEmails);\n\n const [apps, totalsRows, byApp, byUserBase, byLabel, byModel, chatStats] =\n await Promise.all([\n listWorkspaceApps({ includeAgentCards: false }),\n queryRows<Record<string, unknown>>(\n `SELECT\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,\n COUNT(*) AS calls,\n SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,\n COALESCE(SUM(input_tokens), 0) AS input_tokens,\n COALESCE(SUM(output_tokens), 0) AS output_tokens,\n COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,\n COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,\n COUNT(DISTINCT owner_email) AS active_users\n FROM token_usage\n WHERE ${usage.where}`,\n usage.args,\n ),\n usageBuckets(\n `COALESCE(NULLIF(app, ''), 'unattributed')`,\n usage.where,\n usage.args,\n 20,\n ),\n usageBuckets(\"owner_email\", usage.where, usage.args, 50),\n usageBuckets(\n `COALESCE(NULLIF(label, ''), 'chat')`,\n usage.where,\n usage.args,\n 20,\n ),\n usageBuckets(\n `COALESCE(NULLIF(model, ''), 'unknown')`,\n usage.where,\n usage.args,\n 20,\n ),\n loadChatStats(threads.where, threads.args),\n ]);\n\n const topAppRows = await queryRows<Record<string, unknown>>(\n `SELECT owner_email AS owner_email,\n COALESCE(NULLIF(app, ''), 'unattributed') AS app,\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100\n FROM token_usage\n WHERE ${usage.where}\n GROUP BY owner_email, COALESCE(NULLIF(app, ''), 'unattributed')\n ORDER BY owner_email ASC, cost_x100 DESC`,\n usage.args,\n );\n const topAppByUser = new Map<string, string>();\n for (const row of topAppRows) {\n const email = stringField(row, \"owner_email\");\n if (!topAppByUser.has(email)) {\n topAppByUser.set(email, stringField(row, \"app\"));\n }\n }\n\n const byUserMap = new Map<string, UserUsageMetric>();\n for (const bucket of byUserBase) {\n const ownerEmail = bucket.key;\n const stats = chatStats.get(ownerEmail) ?? {\n threads: 0,\n messages: 0,\n lastChatAt: null,\n };\n const member = memberByEmail.get(ownerEmail.toLowerCase());\n byUserMap.set(ownerEmail, {\n ...bucket,\n ownerEmail,\n chatThreads: stats.threads,\n chatMessages: stats.messages,\n lastChatAt: stats.lastChatAt,\n topApp: topAppByUser.get(ownerEmail) ?? null,\n role: member?.role ?? null,\n });\n }\n for (const [ownerEmail, stats] of chatStats) {\n if (byUserMap.has(ownerEmail)) continue;\n const member = memberByEmail.get(ownerEmail.toLowerCase());\n byUserMap.set(ownerEmail, {\n key: ownerEmail,\n label: ownerEmail,\n ownerEmail,\n costCents: 0,\n calls: 0,\n chatCalls: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n activeUsers: 1,\n lastActiveAt: stats.lastChatAt,\n chatThreads: stats.threads,\n chatMessages: stats.messages,\n lastChatAt: stats.lastChatAt,\n topApp: null,\n role: member?.role ?? null,\n });\n }\n\n const dayRows = await queryRows<Record<string, unknown>>(\n `SELECT created_at, owner_email, label, cost_cents_x100\n FROM token_usage\n WHERE ${usage.where}\n ORDER BY created_at ASC`,\n usage.args,\n );\n const dailyMap = new Map<\n string,\n { costX100: number; calls: number; chatCalls: number; users: Set<string> }\n >();\n for (const row of dayRows) {\n const date = new Date(numberField(row, \"created_at\"))\n .toISOString()\n .slice(0, 10);\n const current = dailyMap.get(date) ?? {\n costX100: 0,\n calls: 0,\n chatCalls: 0,\n users: new Set<string>(),\n };\n current.costX100 += numberField(row, \"cost_cents_x100\");\n current.calls += 1;\n if (stringField(row, \"label\") === \"chat\") current.chatCalls += 1;\n current.users.add(stringField(row, \"owner_email\"));\n dailyMap.set(date, current);\n }\n const daily = [...dailyMap.entries()]\n .map(([date, value]) => ({\n date,\n costCents: value.costX100 / 100,\n calls: value.calls,\n chatCalls: value.chatCalls,\n activeUsers: value.users.size,\n }))\n .sort((a, b) => a.date.localeCompare(b.date));\n\n const recentRows = await queryRows<Record<string, unknown>>(\n `SELECT id, created_at, owner_email, app, label, model,\n input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,\n cost_cents_x100\n FROM token_usage\n WHERE ${usage.where}\n ORDER BY created_at DESC\n LIMIT 50`,\n usage.args,\n );\n const recent = recentRows.map((row) => ({\n id: numberField(row, \"id\"),\n createdAt: numberField(row, \"created_at\"),\n ownerEmail: stringField(row, \"owner_email\"),\n app: stringField(row, \"app\") || \"unattributed\",\n label: stringField(row, \"label\") || \"chat\",\n model: stringField(row, \"model\") || \"unknown\",\n inputTokens: numberField(row, \"input_tokens\"),\n outputTokens: numberField(row, \"output_tokens\"),\n cacheReadTokens: numberField(row, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(row, \"cache_write_tokens\"),\n costCents: numberField(row, \"cost_cents_x100\") / 100,\n }));\n\n const appUsageByKey = new Map(\n byApp.map((bucket) => [normalizeAppKey(bucket.key), bucket]),\n );\n const accessUsers = members.length || byUserMap.size;\n const accessModel = orgId ? \"workspace\" : \"solo\";\n const accessLabel = orgId ? \"Workspace members\" : \"Signed-in users\";\n const appAccess = apps.map((app) => {\n const usageBucket = appUsageByKey.get(normalizeAppKey(app.id));\n return {\n id: app.id,\n name: app.name,\n path: app.path,\n status: app.status,\n statusLabel: app.statusLabel,\n isDispatch: app.isDispatch,\n accessModel,\n accessLabel,\n accessUsers,\n usersWithUsage: usageBucket?.activeUsers ?? 0,\n usageCalls: usageBucket?.calls ?? 0,\n chatCalls: usageBucket?.chatCalls ?? 0,\n costCents: usageBucket?.costCents ?? 0,\n lastActiveAt: usageBucket?.lastActiveAt ?? null,\n } satisfies AppAccessMetric;\n });\n\n const totals = totalsRows[0] ?? {};\n const chatThreadTotals = [...chatStats.values()].reduce(\n (acc, value) => ({\n threads: acc.threads + value.threads,\n messages: acc.messages + value.messages,\n }),\n { threads: 0, messages: 0 },\n );\n\n return {\n billing,\n sinceMs,\n sinceDays,\n generatedAt: Date.now(),\n access: {\n viewerEmail,\n orgId,\n role,\n scope: orgId ? \"organization\" : \"solo\",\n totalUsers: accessUsers,\n },\n totals: {\n costCents: numberField(totals, \"cost_x100\") / 100,\n calls: numberField(totals, \"calls\"),\n chatCalls: numberField(totals, \"chat_calls\"),\n inputTokens: numberField(totals, \"input_tokens\"),\n outputTokens: numberField(totals, \"output_tokens\"),\n cacheReadTokens: numberField(totals, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(totals, \"cache_write_tokens\"),\n activeUsers: numberField(totals, \"active_users\"),\n chatThreads: chatThreadTotals.threads,\n chatMessages: chatThreadTotals.messages,\n workspaceApps: apps.filter((app) => !app.isDispatch).length,\n },\n byApp,\n byUser: [...byUserMap.values()].sort((a, b) => {\n if (b.costCents !== a.costCents) return b.costCents - a.costCents;\n return (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0);\n }),\n byLabel,\n byModel,\n daily,\n appAccess,\n recent,\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.15.28",
3
+ "version": "0.16.1",
4
4
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -84,7 +84,7 @@
84
84
  "tailwind-merge": "^3.5.0",
85
85
  "vaul": "^1.1.2",
86
86
  "zod": "^4.3.6",
87
- "@agent-native/toolkit": "0.10.11"
87
+ "@agent-native/toolkit": "0.11.1"
88
88
  },
89
89
  "devDependencies": {
90
90
  "@react-router/dev": "^8.1.0",
@@ -98,7 +98,7 @@
98
98
  "typescript-7": "npm:typescript@^7.0.2",
99
99
  "vite": "8.1.0",
100
100
  "vitest": "^4.1.5",
101
- "@agent-native/core": "0.129.0"
101
+ "@agent-native/core": "0.131.2"
102
102
  },
103
103
  "peerDependencies": {
104
104
  "@agent-native/core": ">=0.8.0",
@@ -0,0 +1,122 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const server = vi.hoisted(() => ({
4
+ buildStartPath: vi.fn(
5
+ (ticket: string) => `/_agent-native/embed/start?ticket=${ticket}`,
6
+ ),
7
+ createTicket: vi.fn(),
8
+ getContext: vi.fn(),
9
+ getEmail: vi.fn(),
10
+ }));
11
+ const dispatch = vi.hoisted(() => ({
12
+ getConfig: vi.fn(),
13
+ }));
14
+ const integrations = vi.hoisted(() => ({
15
+ createRemoteDevice: vi.fn(),
16
+ }));
17
+
18
+ vi.mock("@agent-native/core/server", () => ({
19
+ buildEmbedStartPath: server.buildStartPath,
20
+ createEmbedSessionTicket: server.createTicket,
21
+ getRequestContext: server.getContext,
22
+ getRequestUserEmail: server.getEmail,
23
+ withConfiguredAppBasePath: (origin: string) => origin,
24
+ }));
25
+ vi.mock("@agent-native/core/integrations", () => ({
26
+ createRemoteDevice: integrations.createRemoteDevice,
27
+ }));
28
+ vi.mock("../server/index.js", () => ({
29
+ getDispatchConfig: dispatch.getConfig,
30
+ }));
31
+
32
+ import action from "./create-browser-chat-session.js";
33
+
34
+ const nonce = "browser-chat-nonce-1234567890";
35
+ const extensionId = "abcdefghijklmnopabcdefghijklmnop";
36
+ const parentOrigin = `chrome-extension://${extensionId}`;
37
+
38
+ describe("create-browser-chat-session", () => {
39
+ beforeEach(() => {
40
+ server.createTicket.mockReset();
41
+ server.getContext.mockReturnValue({
42
+ orgId: "org-example",
43
+ requestOrigin: "https://dispatch.example.com",
44
+ });
45
+ server.getEmail.mockReturnValue("user@example.com");
46
+ dispatch.getConfig.mockReturnValue({
47
+ browserExtensionIds: [extensionId],
48
+ });
49
+ integrations.createRemoteDevice.mockResolvedValue({
50
+ device: { id: "remote-device-example" },
51
+ token: "anr_example",
52
+ });
53
+ });
54
+
55
+ it("mints a one-time ticket bound to the bridge nonce and parent origin", async () => {
56
+ server.createTicket.mockResolvedValue({
57
+ ticket: "ticket-example",
58
+ ticketHash: "hash-example",
59
+ expiresAt: 12345,
60
+ });
61
+
62
+ await expect(action.run({ nonce, extensionId })).resolves.toEqual({
63
+ startPath: "/_agent-native/embed/start?ticket=ticket-example",
64
+ expiresAt: 12345,
65
+ parentOrigin,
66
+ remoteDevice: {
67
+ id: "remote-device-example",
68
+ token: "anr_example",
69
+ },
70
+ relayBaseUrl: "https://dispatch.example.com",
71
+ });
72
+ expect(server.createTicket).toHaveBeenCalledWith({
73
+ ownerEmail: "user@example.com",
74
+ orgId: "org-example",
75
+ targetPath:
76
+ "/browser-chat?browserChatNonce=browser-chat-nonce-1234567890&browserChatParentOrigin=chrome-extension%3A%2F%2Fabcdefghijklmnopabcdefghijklmnop",
77
+ scope: "browser-chat",
78
+ ttlSeconds: 60,
79
+ });
80
+ expect(action.agentTool).toBe(false);
81
+ expect(action.toolCallable).toBe(false);
82
+ expect(integrations.createRemoteDevice).toHaveBeenCalledWith({
83
+ ownerEmail: "user@example.com",
84
+ orgId: "org-example",
85
+ label: "Agent Native for Chrome",
86
+ platform: "chrome-extension",
87
+ metadata: {
88
+ browserExtension: { extensionId },
89
+ computerCapabilities: {
90
+ browser: {
91
+ observe: true,
92
+ control: true,
93
+ provider: "agent-native-chrome-extension",
94
+ },
95
+ },
96
+ },
97
+ });
98
+ });
99
+
100
+ it("fails closed without an authenticated Dispatch identity", async () => {
101
+ server.getEmail.mockReturnValue(undefined);
102
+
103
+ await expect(action.run({ nonce, extensionId })).rejects.toThrow(
104
+ "Sign in to Dispatch",
105
+ );
106
+ expect(server.createTicket).not.toHaveBeenCalled();
107
+ });
108
+
109
+ it("rejects an extension id that is not configured in production", async () => {
110
+ const previousNodeEnv = process.env.NODE_ENV;
111
+ process.env.NODE_ENV = "production";
112
+ dispatch.getConfig.mockReturnValue({ browserExtensionIds: [] });
113
+ try {
114
+ await expect(action.run({ nonce, extensionId })).rejects.toThrow(
115
+ "not allowed",
116
+ );
117
+ } finally {
118
+ process.env.NODE_ENV = previousNodeEnv;
119
+ }
120
+ expect(server.createTicket).not.toHaveBeenCalled();
121
+ });
122
+ });
@@ -0,0 +1,99 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { createRemoteDevice } from "@agent-native/core/integrations";
3
+ import {
4
+ buildEmbedStartPath,
5
+ createEmbedSessionTicket,
6
+ getRequestContext,
7
+ getRequestUserEmail,
8
+ withConfiguredAppBasePath,
9
+ } from "@agent-native/core/server";
10
+ import { z } from "zod";
11
+
12
+ import {
13
+ BROWSER_CHAT_NONCE_QUERY_PARAM,
14
+ BROWSER_CHAT_PARENT_ORIGIN_QUERY_PARAM,
15
+ browserChatExtensionIdSchema,
16
+ browserChatNonceSchema,
17
+ } from "../lib/browser-chat-protocol.js";
18
+ import { isBrowserExtensionIdAllowed } from "../server/lib/browser-extension-allowlist.js";
19
+
20
+ export default defineAction({
21
+ description:
22
+ "Create a one-time authenticated Dispatch browser-chat embed session for an approved Agent-Native Chrome extension.",
23
+ schema: z
24
+ .object({
25
+ nonce: browserChatNonceSchema,
26
+ extensionId: browserChatExtensionIdSchema,
27
+ })
28
+ .strict(),
29
+ readOnly: false,
30
+ parallelSafe: true,
31
+ agentTool: false,
32
+ toolCallable: false,
33
+ run: async ({ nonce, extensionId }) => {
34
+ const ownerEmail = getRequestUserEmail()?.trim();
35
+ if (!ownerEmail) {
36
+ throw new Error(
37
+ "Sign in to Dispatch before connecting the browser extension.",
38
+ );
39
+ }
40
+
41
+ const requestContext = getRequestContext();
42
+ const { browserExtensionIds } = await import("../server/index.js").then(
43
+ (module) => module.getDispatchConfig(),
44
+ );
45
+ if (
46
+ !isBrowserExtensionIdAllowed({
47
+ extensionId,
48
+ configIds: browserExtensionIds,
49
+ nodeEnv: process.env.NODE_ENV,
50
+ requestOrigin: requestContext?.requestOrigin,
51
+ })
52
+ ) {
53
+ throw new Error(
54
+ "This browser extension is not allowed to connect to Dispatch.",
55
+ );
56
+ }
57
+
58
+ const parentOrigin = `chrome-extension://${extensionId}`;
59
+ const requestOrigin = requestContext?.requestOrigin;
60
+ if (!requestOrigin) {
61
+ throw new Error("Dispatch could not resolve its browser relay URL.");
62
+ }
63
+ const query = new URLSearchParams({
64
+ [BROWSER_CHAT_NONCE_QUERY_PARAM]: nonce,
65
+ [BROWSER_CHAT_PARENT_ORIGIN_QUERY_PARAM]: parentOrigin,
66
+ });
67
+ const session = await createEmbedSessionTicket({
68
+ ownerEmail,
69
+ orgId: requestContext?.orgId ?? null,
70
+ targetPath: `/browser-chat?${query}`,
71
+ scope: "browser-chat",
72
+ ttlSeconds: 60,
73
+ });
74
+ const remote = await createRemoteDevice({
75
+ ownerEmail,
76
+ orgId: requestContext?.orgId ?? null,
77
+ label: "Agent Native for Chrome",
78
+ platform: "chrome-extension",
79
+ metadata: {
80
+ browserExtension: { extensionId },
81
+ computerCapabilities: {
82
+ browser: {
83
+ observe: true,
84
+ control: true,
85
+ provider: "agent-native-chrome-extension",
86
+ },
87
+ },
88
+ },
89
+ });
90
+
91
+ return {
92
+ startPath: buildEmbedStartPath(session.ticket),
93
+ expiresAt: session.expiresAt,
94
+ parentOrigin,
95
+ remoteDevice: { id: remote.device.id, token: remote.token },
96
+ relayBaseUrl: withConfiguredAppBasePath(requestOrigin),
97
+ };
98
+ },
99
+ });
@@ -6,6 +6,7 @@ import approveVaultRequest from "./approve-vault-request.js";
6
6
  import archiveWorkspaceApp from "./archive-workspace-app.js";
7
7
  import askApp from "./ask_app.js";
8
8
  import askAppStatus from "./ask_app_status.js";
9
+ import createBrowserChatSession from "./create-browser-chat-session.js";
9
10
  import createDreamReport from "./create-dream-report.js";
10
11
  import createLinkToken from "./create-link-token.js";
11
12
  import createPylonTicket from "./create-pylon-ticket.js";
@@ -111,6 +112,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
111
112
  "create-workspace-resource-grant": createWorkspaceResourceGrant,
112
113
  "create-workspace-resource": createWorkspaceResource,
113
114
  "create-dream-report": createDreamReport,
115
+ "create-browser-chat-session": createBrowserChatSession,
114
116
  create_embed_session: createEmbedSession,
115
117
  "delete-staged-dataset": deleteStagedDataset,
116
118
  "delete-destination": deleteDestination,
@@ -68,9 +68,12 @@ export default defineAction({
68
68
  approvalPolicy: overview.settings,
69
69
  };
70
70
  if (navigation) screen.navigation = navigation;
71
- if (navigation?.view === "chat") {
71
+ if (navigation?.view === "chat" || navigation?.view === "browser-chat") {
72
72
  screen.chatSurface = {
73
- view: "full-page Dispatch chat",
73
+ view:
74
+ navigation.view === "browser-chat"
75
+ ? "embedded browser chat"
76
+ : "full-page Dispatch chat",
74
77
  purpose:
75
78
  "Create apps, manage workspace resources, route work to connected agents, and continue Dispatch conversations.",
76
79
  };
@@ -246,4 +246,57 @@ describe("CreateAppFlow", () => {
246
246
  expect(() => findButton(container, "Connect Builder")).toThrow();
247
247
  expect(() => findButton(container, "Try again")).toThrow();
248
248
  });
249
+
250
+ it("replaces the composer with Builder branch progress and success states", async () => {
251
+ let resolveBuilderRequest: ((response: Response) => void) | undefined;
252
+ fetchSpy.mockImplementation(async (input: RequestInfo | URL) => {
253
+ const url = String(input);
254
+ if (url.includes("get-vault-access-settings")) {
255
+ return jsonResponse({ mode: "all-apps" });
256
+ }
257
+ if (
258
+ url.includes("list-vault-secret-options") ||
259
+ url.includes("list-workspace-resource-options")
260
+ ) {
261
+ return jsonResponse([]);
262
+ }
263
+ if (url.includes("start-workspace-app-creation")) {
264
+ return new Promise<Response>((resolve) => {
265
+ resolveBuilderRequest = resolve;
266
+ });
267
+ }
268
+ return jsonResponse({ error: `Unexpected URL: ${url}` }, 404);
269
+ });
270
+
271
+ await renderAndSubmit("Build a quality dashboard");
272
+
273
+ await act(async () => {
274
+ await vi.waitFor(() => {
275
+ expect(container.textContent).toContain("Creating your Builder branch");
276
+ });
277
+ });
278
+ expect(container.textContent).not.toContain("Dispatch keys");
279
+
280
+ await act(async () => {
281
+ resolveBuilderRequest?.(
282
+ jsonResponse({
283
+ mode: "builder",
284
+ appId: "quality-dashboard",
285
+ url: "https://branch.example.test",
286
+ }),
287
+ );
288
+ });
289
+
290
+ await act(async () => {
291
+ await vi.waitFor(() => {
292
+ expect(container.textContent).toContain("Your Builder branch is ready");
293
+ });
294
+ });
295
+ const branchLink = Array.from(container.querySelectorAll("a")).find(
296
+ (candidate) => candidate.textContent?.includes("Open Builder branch"),
297
+ );
298
+ expect(branchLink?.getAttribute("href")).toBe(
299
+ "https://branch.example.test",
300
+ );
301
+ });
249
302
  });
@@ -362,6 +362,55 @@ export function CreateAppFlow({
362
362
  }
363
363
 
364
364
  const submitWithSelectedAccess = () => submit(prompt);
365
+ const isCreatingBuilderBranch =
366
+ isSubmitting && !isInBuilderFrame() && !isDevMode;
367
+
368
+ if (branchUrl) {
369
+ return (
370
+ <div
371
+ className={`flex min-h-[260px] flex-col items-center justify-center gap-5 px-6 py-8 text-center ${className}`}
372
+ >
373
+ <span className="flex size-11 items-center justify-center rounded-full bg-primary/10 text-primary">
374
+ <IconCheck className="size-5" aria-hidden="true" />
375
+ </span>
376
+ <div className="space-y-2">
377
+ <h2 className="text-base font-semibold text-foreground">
378
+ Your Builder branch is ready
379
+ </h2>
380
+ <p className="text-sm leading-6 text-muted-foreground">
381
+ Continue building and editing your app in Builder.
382
+ </p>
383
+ </div>
384
+ <Button asChild className="w-full sm:w-auto">
385
+ <a href={branchUrl} target="_blank" rel="noreferrer">
386
+ Open Builder branch <IconArrowUpRight aria-hidden="true" />
387
+ </a>
388
+ </Button>
389
+ </div>
390
+ );
391
+ }
392
+
393
+ if (isCreatingBuilderBranch) {
394
+ return (
395
+ <div
396
+ className={`flex min-h-[260px] flex-col items-center justify-center gap-4 px-6 py-8 text-center ${className}`}
397
+ aria-live="polite"
398
+ >
399
+ <IconLoader2
400
+ className="size-7 animate-spin text-muted-foreground"
401
+ aria-hidden="true"
402
+ />
403
+ <div className="space-y-2">
404
+ <h2 className="text-base font-semibold text-foreground">
405
+ Creating your Builder branch
406
+ </h2>
407
+ <p className="text-sm leading-6 text-muted-foreground">
408
+ This usually takes a few seconds.
409
+ </p>
410
+ </div>
411
+ </div>
412
+ );
413
+ }
365
414
 
366
415
  return (
367
416
  <div className={`flex flex-col gap-3 ${className}`}>