@agent-native/core 0.12.4 → 0.12.6

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 (101) hide show
  1. package/dist/agent/engine/builder-engine.d.ts +3 -2
  2. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  3. package/dist/agent/engine/builder-engine.js +28 -9
  4. package/dist/agent/engine/builder-engine.js.map +1 -1
  5. package/dist/agent/engine/builtin.js +3 -3
  6. package/dist/agent/engine/builtin.js.map +1 -1
  7. package/dist/agent/engine/index.d.ts +1 -1
  8. package/dist/agent/engine/index.d.ts.map +1 -1
  9. package/dist/agent/engine/index.js +1 -1
  10. package/dist/agent/engine/index.js.map +1 -1
  11. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  12. package/dist/agent/thread-data-builder.js +2 -0
  13. package/dist/agent/thread-data-builder.js.map +1 -1
  14. package/dist/cli/templates-meta.d.ts.map +1 -1
  15. package/dist/cli/templates-meta.js +14 -0
  16. package/dist/cli/templates-meta.js.map +1 -1
  17. package/dist/client/AgentPanel.js +3 -2
  18. package/dist/client/AgentPanel.js.map +1 -1
  19. package/dist/client/CommandMenu.d.ts +1 -0
  20. package/dist/client/CommandMenu.d.ts.map +1 -1
  21. package/dist/client/CommandMenu.js +11 -3
  22. package/dist/client/CommandMenu.js.map +1 -1
  23. package/dist/client/ErrorBoundary.d.ts.map +1 -1
  24. package/dist/client/ErrorBoundary.js +15 -5
  25. package/dist/client/ErrorBoundary.js.map +1 -1
  26. package/dist/client/FeedbackButton.d.ts.map +1 -1
  27. package/dist/client/FeedbackButton.js +7 -3
  28. package/dist/client/FeedbackButton.js.map +1 -1
  29. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  30. package/dist/client/MultiTabAssistantChat.js +112 -33
  31. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  32. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  33. package/dist/client/agent-chat-adapter.js +63 -14
  34. package/dist/client/agent-chat-adapter.js.map +1 -1
  35. package/dist/client/components/icons/AgentNativeIcon.d.ts +20 -0
  36. package/dist/client/components/icons/AgentNativeIcon.d.ts.map +1 -0
  37. package/dist/client/components/icons/AgentNativeIcon.js +12 -0
  38. package/dist/client/components/icons/AgentNativeIcon.js.map +1 -0
  39. package/dist/client/composer/TiptapComposer.js +1 -1
  40. package/dist/client/composer/TiptapComposer.js.map +1 -1
  41. package/dist/client/index.d.ts +1 -0
  42. package/dist/client/index.d.ts.map +1 -1
  43. package/dist/client/index.js +1 -0
  44. package/dist/client/index.js.map +1 -1
  45. package/dist/client/notifications/NotificationsBell.d.ts +5 -1
  46. package/dist/client/notifications/NotificationsBell.d.ts.map +1 -1
  47. package/dist/client/notifications/NotificationsBell.js +2 -2
  48. package/dist/client/notifications/NotificationsBell.js.map +1 -1
  49. package/dist/client/settings/UsageSection.d.ts.map +1 -1
  50. package/dist/client/settings/UsageSection.js +41 -8
  51. package/dist/client/settings/UsageSection.js.map +1 -1
  52. package/dist/client/sharing/ShareButton.js +19 -7
  53. package/dist/client/sharing/ShareButton.js.map +1 -1
  54. package/dist/client/sharing/ShareDialog.d.ts.map +1 -1
  55. package/dist/client/sharing/ShareDialog.js +16 -6
  56. package/dist/client/sharing/ShareDialog.js.map +1 -1
  57. package/dist/client/sse-event-processor.d.ts.map +1 -1
  58. package/dist/client/sse-event-processor.js +43 -4
  59. package/dist/client/sse-event-processor.js.map +1 -1
  60. package/dist/client/use-chat-threads.d.ts +1 -1
  61. package/dist/client/use-chat-threads.d.ts.map +1 -1
  62. package/dist/client/use-chat-threads.js +2 -2
  63. package/dist/client/use-chat-threads.js.map +1 -1
  64. package/dist/client/useProductionAgent.js +2 -2
  65. package/dist/client/useProductionAgent.js.map +1 -1
  66. package/dist/extensions/routes.d.ts.map +1 -1
  67. package/dist/extensions/routes.js +4 -1
  68. package/dist/extensions/routes.js.map +1 -1
  69. package/dist/extensions/store.d.ts.map +1 -1
  70. package/dist/extensions/store.js +7 -1
  71. package/dist/extensions/store.js.map +1 -1
  72. package/dist/index.d.ts +1 -1
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +1 -1
  75. package/dist/index.js.map +1 -1
  76. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  77. package/dist/server/core-routes-plugin.js +45 -5
  78. package/dist/server/core-routes-plugin.js.map +1 -1
  79. package/dist/server/credential-provider.d.ts +3 -2
  80. package/dist/server/credential-provider.d.ts.map +1 -1
  81. package/dist/server/credential-provider.js +4 -3
  82. package/dist/server/credential-provider.js.map +1 -1
  83. package/dist/server/ssr-handler.d.ts.map +1 -1
  84. package/dist/server/ssr-handler.js +16 -6
  85. package/dist/server/ssr-handler.js.map +1 -1
  86. package/dist/sharing/actions/share-resource.d.ts +1 -0
  87. package/dist/sharing/actions/share-resource.d.ts.map +1 -1
  88. package/dist/sharing/actions/share-resource.js +65 -3
  89. package/dist/sharing/actions/share-resource.js.map +1 -1
  90. package/dist/sharing/registry.d.ts +5 -0
  91. package/dist/sharing/registry.d.ts.map +1 -1
  92. package/dist/sharing/registry.js.map +1 -1
  93. package/dist/usage/store.d.ts +16 -0
  94. package/dist/usage/store.d.ts.map +1 -1
  95. package/dist/usage/store.js +31 -0
  96. package/dist/usage/store.js.map +1 -1
  97. package/docs/content/getting-started.md +26 -0
  98. package/docs/content/sharing.md +9 -7
  99. package/docs/content/template-dispatch.md +17 -0
  100. package/docs/content/template-images.md +54 -0
  101. package/package.json +1 -1
@@ -10,6 +10,10 @@ interface NotificationsBellProps {
10
10
  * the user grants permission. Silently no-ops on denied or unsupported.
11
11
  */
12
12
  browserNotifications?: boolean;
13
+ /** Empty-state title shown when there are no notifications. */
14
+ emptyTitle?: string;
15
+ /** Optional empty-state detail text. */
16
+ emptyDescription?: string;
13
17
  }
14
18
  /**
15
19
  * Header-bar bell that shows the unread-notification count and a dropdown of
@@ -18,6 +22,6 @@ interface NotificationsBellProps {
18
22
  * the count endpoint directly so the bell updates even outside an app-state
19
23
  * change).
20
24
  */
21
- export declare function NotificationsBell({ pollMs, className, browserNotifications, }: NotificationsBellProps): import("react/jsx-runtime").JSX.Element;
25
+ export declare function NotificationsBell({ pollMs, className, browserNotifications, emptyTitle, emptyDescription, }: NotificationsBellProps): import("react/jsx-runtime").JSX.Element;
22
26
  export {};
23
27
  //# sourceMappingURL=NotificationsBell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationsBell.d.ts","sourceRoot":"","sources":["../../../src/client/notifications/NotificationsBell.tsx"],"names":[],"mappings":"AAmBA,UAAU,sBAAsB;IAC9B,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAMD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAwB,EACxB,SAAS,EACT,oBAA4B,GAC7B,EAAE,sBAAsB,2CAySxB"}
1
+ {"version":3,"file":"NotificationsBell.d.ts","sourceRoot":"","sources":["../../../src/client/notifications/NotificationsBell.tsx"],"names":[],"mappings":"AAmBA,UAAU,sBAAsB;IAC9B,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAMD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAwB,EACxB,SAAS,EACT,oBAA4B,EAC5B,UAAwC,EACxC,gBAAgB,GACjB,EAAE,sBAAsB,2CA8SxB"}
@@ -13,7 +13,7 @@ const SUPPORTS_NOTIFICATION = typeof window !== "undefined" && "Notification" in
13
13
  * the count endpoint directly so the bell updates even outside an app-state
14
14
  * change).
15
15
  */
16
- export function NotificationsBell({ pollMs = POLL_MS_DEFAULT, className, browserNotifications = false, }) {
16
+ export function NotificationsBell({ pollMs = POLL_MS_DEFAULT, className, browserNotifications = false, emptyTitle = "No app notifications yet.", emptyDescription, }) {
17
17
  const [unreadCount, setUnreadCount] = useState(0);
18
18
  const [open, setOpen] = useState(false);
19
19
  const [items, setItems] = useState(null);
@@ -191,7 +191,7 @@ export function NotificationsBell({ pollMs = POLL_MS_DEFAULT, className, browser
191
191
  e.stopPropagation();
192
192
  void dismiss(n.id);
193
193
  }, className: "absolute right-2 top-2 hidden rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground group-hover:flex", children: _jsx(IconX, { size: 12 }) })] }, n.id));
194
- })) : (_jsx("div", { className: "p-4 text-sm text-muted-foreground", children: "No notifications." })) })] })] }));
194
+ })) : (_jsxs("div", { className: "space-y-1 p-4 text-sm", children: [_jsx("p", { className: "font-medium text-foreground", children: emptyTitle }), emptyDescription ? (_jsx("p", { className: "text-xs leading-relaxed text-muted-foreground", children: emptyDescription })) : null] })) })] })] }));
195
195
  }
196
196
  // Severity color pairs — use /20 opacity backdrops that work against both
197
197
  // light and dark theme backgrounds; text uses 700/300 so it stays readable
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationsBell.js","sourceRoot":"","sources":["../../../src/client/notifications/NotificationsBell.tsx"],"names":[],"mappings":"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","sourcesContent":["import { agentNativePath, appPath } from \"../api-path.js\";\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n IconBell,\n IconBellRinging,\n IconLoader2,\n IconX,\n} from \"@tabler/icons-react\";\nimport { usePausingInterval } from \"../use-pausing-interval.js\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../components/ui/popover.js\";\nimport type {\n Notification as NotificationDto,\n NotificationSeverity,\n} from \"../../notifications/types.js\";\n\ninterface NotificationsBellProps {\n /** Poll interval in ms. Set to 0 to disable polling. Default: 10000. */\n pollMs?: number;\n /** Optional className for the outer container. */\n className?: string;\n /**\n * When true, fires a system-level `new Notification(...)` popup for each\n * new unread notification — handy when the tab is in the background.\n * Renders an \"Enable browser notifications\" prompt in the dropdown until\n * the user grants permission. Silently no-ops on denied or unsupported.\n */\n browserNotifications?: boolean;\n}\n\nconst POLL_MS_DEFAULT = 10_000;\nconst SUPPORTS_NOTIFICATION =\n typeof window !== \"undefined\" && \"Notification\" in window;\n\n/**\n * Header-bar bell that shows the unread-notification count and a dropdown of\n * recent entries. Polling keeps it in sync (the framework poll loop already\n * bumps a version counter so notifications ride on that signal, but we poll\n * the count endpoint directly so the bell updates even outside an app-state\n * change).\n */\nexport function NotificationsBell({\n pollMs = POLL_MS_DEFAULT,\n className,\n browserNotifications = false,\n}: NotificationsBellProps) {\n const [unreadCount, setUnreadCount] = useState(0);\n const [open, setOpen] = useState(false);\n const [items, setItems] = useState<NotificationDto[] | null>(null);\n // Init to \"default\" unconditionally so server and client render the same\n // HTML — reading Notification.permission at init would diverge between SSR\n // (\"denied\", no API) and hydration (\"default\"/\"granted\"), causing a mismatch\n // in templates that mount the bell outside a ClientOnly boundary. We sync\n // to the real value in a useEffect below.\n const [permission, setPermission] =\n useState<NotificationPermission>(\"default\");\n\n useEffect(() => {\n if (SUPPORTS_NOTIFICATION) setPermission(Notification.permission);\n }, []);\n // Ids already popped as browser notifications. Seeded on first run so\n // existing unread don't pop retroactively on page load.\n const seenIdsRef = useRef<Set<string> | null>(null);\n\n const loadItems = useCallback(async () => {\n try {\n const res = await fetch(\n agentNativePath(\"/_agent-native/notifications?limit=20\"),\n );\n if (!res.ok) return;\n const rows = (await res.json()) as NotificationDto[];\n setItems(rows);\n } catch {\n // best-effort\n }\n }, []);\n\n // One polling callback used by both paths. When browserNotifications is on\n // we fetch the unread list (source of truth for both the badge count AND\n // the popup loop — no second /count request), and pop Notification() for\n // any new ids. When off, we fetch just /count. The unread-list branch also\n // opts out of visibility pause so popups still fire for backgrounded tabs.\n const refresh = useCallback(async () => {\n if (browserNotifications) {\n try {\n const res = await fetch(\n agentNativePath(\"/_agent-native/notifications?unread=true&limit=20\"),\n );\n if (!res.ok) return;\n const rows = (await res.json()) as NotificationDto[];\n setUnreadCount(rows.length);\n // First run: treat everything as already seen so we don't pop\n // retroactively on page load. After that, rebuild from the current\n // unread list so ids for read/archived rows drop out — keeps the\n // set bounded to the unread fetch limit (~20).\n const prev = seenIdsRef.current;\n const seen = new Set<string>();\n for (const n of rows) {\n const alreadySeen = prev?.has(n.id) ?? true;\n seen.add(n.id);\n if (alreadySeen) continue;\n if (!SUPPORTS_NOTIFICATION) continue;\n if (Notification.permission !== \"granted\") continue;\n try {\n new Notification(n.title, { body: n.body, tag: n.id });\n } catch {\n // Safari / restricted contexts may throw even when permission\n // claims to be granted — silent no-op.\n }\n }\n seenIdsRef.current = seen;\n } catch {\n // best-effort\n }\n return;\n }\n try {\n const res = await fetch(\n agentNativePath(\"/_agent-native/notifications/count\"),\n );\n if (!res.ok) return;\n const data = (await res.json()) as { count: number };\n setUnreadCount(data.count);\n } catch {\n // best-effort\n }\n }, [browserNotifications]);\n\n usePausingInterval(\n refresh,\n pollMs,\n /* pauseWhenHidden */ !browserNotifications,\n );\n\n useEffect(() => {\n if (!open) return;\n loadItems();\n }, [open, loadItems]);\n\n const markRead = async (id: string) => {\n try {\n // `keepalive: true` lets the request survive page navigation —\n // without it, clicking a notification with a link aborts this\n // request mid-flight and the row stays unread.\n await fetch(agentNativePath(`/_agent-native/notifications/${id}/read`), {\n method: \"POST\",\n keepalive: true,\n });\n setItems((prev) =>\n prev\n ? prev.map((n) =>\n n.id === id ? { ...n, readAt: new Date().toISOString() } : n,\n )\n : prev,\n );\n refresh();\n } catch {\n // best-effort\n }\n };\n\n // Reject any URL that isn't http(s) or a same-origin relative path. Blocks\n // `javascript:` execution, `data:` URIs, and absolute redirects to phishing\n // sites. Relative paths starting with `/` are routed through `appPath()` so\n // the link works in mounted deployments (e.g. /mail subdirectory).\n const safeNotificationLink = (link: string): string | null => {\n if (link.startsWith(\"/\") && !link.startsWith(\"//\")) {\n return appPath(link);\n }\n try {\n const url = new URL(link, window.location.origin);\n if (url.protocol === \"http:\" || url.protocol === \"https:\") {\n return url.toString();\n }\n } catch {\n // fallthrough\n }\n return null;\n };\n\n const markAllRead = async () => {\n try {\n await fetch(agentNativePath(`/_agent-native/notifications/read-all`), {\n method: \"POST\",\n });\n setItems((prev) =>\n prev\n ? prev.map((n) =>\n n.readAt ? n : { ...n, readAt: new Date().toISOString() },\n )\n : prev,\n );\n setUnreadCount(0);\n } catch {\n // best-effort\n }\n };\n\n const dismiss = async (id: string) => {\n try {\n await fetch(agentNativePath(`/_agent-native/notifications/${id}`), {\n method: \"DELETE\",\n });\n setItems((prev) => (prev ? prev.filter((n) => n.id !== id) : prev));\n refresh();\n } catch {\n // best-effort\n }\n };\n\n const hasUnread = unreadCount > 0;\n const Icon = hasUnread ? IconBellRinging : IconBell;\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label={\n hasUnread ? `${unreadCount} unread notifications` : \"Notifications\"\n }\n className={\n \"an-notifications-bell__trigger relative inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent/40 hover:text-foreground\" +\n (className ? ` ${className}` : \"\")\n }\n >\n <Icon size={18} aria-hidden />\n {hasUnread ? (\n <span\n aria-hidden\n className=\"an-notifications-bell__badge absolute -right-0.5 -top-0.5 rounded-full bg-destructive px-1 text-[10px] leading-[14px] font-medium text-destructive-foreground\"\n >\n {unreadCount > 99 ? \"99+\" : unreadCount}\n </span>\n ) : null}\n </button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n sideOffset={8}\n className=\"an-notifications-bell__menu w-80 p-0\"\n >\n <div className=\"flex items-center justify-between border-b border-border px-3 py-2 text-sm font-medium\">\n <span>Notifications</span>\n {hasUnread ? (\n <button\n type=\"button\"\n onClick={markAllRead}\n className=\"text-xs text-primary hover:underline\"\n >\n Mark all read\n </button>\n ) : null}\n </div>\n {browserNotifications &&\n SUPPORTS_NOTIFICATION &&\n permission === \"default\" ? (\n <div className=\"flex items-center justify-between gap-2 border-b border-border bg-accent/40 px-3 py-2 text-xs text-foreground\">\n <span>Get a system popup for new notifications.</span>\n <button\n type=\"button\"\n onClick={async () => {\n const result = await Notification.requestPermission();\n setPermission(result);\n }}\n className=\"shrink-0 rounded bg-primary px-2 py-0.5 font-medium text-primary-foreground hover:bg-primary/90\"\n >\n Enable\n </button>\n </div>\n ) : null}\n <div className=\"max-h-96 overflow-y-auto\">\n {items === null ? (\n <div className=\"flex items-center gap-2 p-4 text-sm text-muted-foreground\">\n <IconLoader2 size={14} className=\"animate-spin\" /> Loading…\n </div>\n ) : items.length > 0 ? (\n items.map((n) => {\n const rawLink =\n typeof n.metadata?.link === \"string\" ? n.metadata.link : null;\n const link = rawLink ? safeNotificationLink(rawLink) : null;\n const onItemClick = () => {\n if (!n.readAt) void markRead(n.id);\n if (link) {\n setOpen(false);\n window.location.assign(link);\n }\n };\n return (\n <div\n key={n.id}\n className={\n \"group relative border-b border-border last:border-b-0 hover:bg-accent/40 \" +\n (n.readAt ? \"opacity-60\" : \"\")\n }\n >\n <button\n type=\"button\"\n onClick={onItemClick}\n className={\n \"flex w-full flex-col items-start gap-0.5 px-3 py-2 pr-8 text-left\" +\n (link ? \" cursor-pointer\" : \"\")\n }\n >\n <div className=\"flex w-full items-center justify-between gap-2\">\n <span className=\"truncate text-sm font-medium text-foreground\">\n {n.title}\n </span>\n <SeverityBadge severity={n.severity} />\n </div>\n {n.body ? (\n <span className=\"line-clamp-2 text-xs text-muted-foreground\">\n {n.body}\n </span>\n ) : null}\n <span className=\"text-[10px] text-muted-foreground/70\">\n {new Date(n.createdAt).toLocaleString()}\n </span>\n </button>\n <button\n type=\"button\"\n aria-label=\"Dismiss notification\"\n onClick={(e) => {\n e.stopPropagation();\n void dismiss(n.id);\n }}\n className=\"absolute right-2 top-2 hidden rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground group-hover:flex\"\n >\n <IconX size={12} />\n </button>\n </div>\n );\n })\n ) : (\n <div className=\"p-4 text-sm text-muted-foreground\">\n No notifications.\n </div>\n )}\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n\n// Severity color pairs — use /20 opacity backdrops that work against both\n// light and dark theme backgrounds; text uses 700/300 so it stays readable\n// in each mode (the `dark:` prefix is one of the few places where explicit\n// variants are necessary since these are brand-color tokens, not semantic).\nfunction SeverityBadge({ severity }: { severity: NotificationSeverity }) {\n const color =\n severity === \"critical\"\n ? \"bg-red-500/20 text-red-700 dark:text-red-300\"\n : severity === \"warning\"\n ? \"bg-amber-500/20 text-amber-700 dark:text-amber-300\"\n : \"bg-muted text-muted-foreground\";\n return (\n <span className={`rounded px-1.5 py-0.5 text-[10px] font-medium ${color}`}>\n {severity}\n </span>\n );\n}\n"]}
1
+ {"version":3,"file":"NotificationsBell.js","sourceRoot":"","sources":["../../../src/client/notifications/NotificationsBell.tsx"],"names":[],"mappings":"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","sourcesContent":["import { agentNativePath, appPath } from \"../api-path.js\";\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n IconBell,\n IconBellRinging,\n IconLoader2,\n IconX,\n} from \"@tabler/icons-react\";\nimport { usePausingInterval } from \"../use-pausing-interval.js\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../components/ui/popover.js\";\nimport type {\n Notification as NotificationDto,\n NotificationSeverity,\n} from \"../../notifications/types.js\";\n\ninterface NotificationsBellProps {\n /** Poll interval in ms. Set to 0 to disable polling. Default: 10000. */\n pollMs?: number;\n /** Optional className for the outer container. */\n className?: string;\n /**\n * When true, fires a system-level `new Notification(...)` popup for each\n * new unread notification — handy when the tab is in the background.\n * Renders an \"Enable browser notifications\" prompt in the dropdown until\n * the user grants permission. Silently no-ops on denied or unsupported.\n */\n browserNotifications?: boolean;\n /** Empty-state title shown when there are no notifications. */\n emptyTitle?: string;\n /** Optional empty-state detail text. */\n emptyDescription?: string;\n}\n\nconst POLL_MS_DEFAULT = 10_000;\nconst SUPPORTS_NOTIFICATION =\n typeof window !== \"undefined\" && \"Notification\" in window;\n\n/**\n * Header-bar bell that shows the unread-notification count and a dropdown of\n * recent entries. Polling keeps it in sync (the framework poll loop already\n * bumps a version counter so notifications ride on that signal, but we poll\n * the count endpoint directly so the bell updates even outside an app-state\n * change).\n */\nexport function NotificationsBell({\n pollMs = POLL_MS_DEFAULT,\n className,\n browserNotifications = false,\n emptyTitle = \"No app notifications yet.\",\n emptyDescription,\n}: NotificationsBellProps) {\n const [unreadCount, setUnreadCount] = useState(0);\n const [open, setOpen] = useState(false);\n const [items, setItems] = useState<NotificationDto[] | null>(null);\n // Init to \"default\" unconditionally so server and client render the same\n // HTML — reading Notification.permission at init would diverge between SSR\n // (\"denied\", no API) and hydration (\"default\"/\"granted\"), causing a mismatch\n // in templates that mount the bell outside a ClientOnly boundary. We sync\n // to the real value in a useEffect below.\n const [permission, setPermission] =\n useState<NotificationPermission>(\"default\");\n\n useEffect(() => {\n if (SUPPORTS_NOTIFICATION) setPermission(Notification.permission);\n }, []);\n // Ids already popped as browser notifications. Seeded on first run so\n // existing unread don't pop retroactively on page load.\n const seenIdsRef = useRef<Set<string> | null>(null);\n\n const loadItems = useCallback(async () => {\n try {\n const res = await fetch(\n agentNativePath(\"/_agent-native/notifications?limit=20\"),\n );\n if (!res.ok) return;\n const rows = (await res.json()) as NotificationDto[];\n setItems(rows);\n } catch {\n // best-effort\n }\n }, []);\n\n // One polling callback used by both paths. When browserNotifications is on\n // we fetch the unread list (source of truth for both the badge count AND\n // the popup loop — no second /count request), and pop Notification() for\n // any new ids. When off, we fetch just /count. The unread-list branch also\n // opts out of visibility pause so popups still fire for backgrounded tabs.\n const refresh = useCallback(async () => {\n if (browserNotifications) {\n try {\n const res = await fetch(\n agentNativePath(\"/_agent-native/notifications?unread=true&limit=20\"),\n );\n if (!res.ok) return;\n const rows = (await res.json()) as NotificationDto[];\n setUnreadCount(rows.length);\n // First run: treat everything as already seen so we don't pop\n // retroactively on page load. After that, rebuild from the current\n // unread list so ids for read/archived rows drop out — keeps the\n // set bounded to the unread fetch limit (~20).\n const prev = seenIdsRef.current;\n const seen = new Set<string>();\n for (const n of rows) {\n const alreadySeen = prev?.has(n.id) ?? true;\n seen.add(n.id);\n if (alreadySeen) continue;\n if (!SUPPORTS_NOTIFICATION) continue;\n if (Notification.permission !== \"granted\") continue;\n try {\n new Notification(n.title, { body: n.body, tag: n.id });\n } catch {\n // Safari / restricted contexts may throw even when permission\n // claims to be granted — silent no-op.\n }\n }\n seenIdsRef.current = seen;\n } catch {\n // best-effort\n }\n return;\n }\n try {\n const res = await fetch(\n agentNativePath(\"/_agent-native/notifications/count\"),\n );\n if (!res.ok) return;\n const data = (await res.json()) as { count: number };\n setUnreadCount(data.count);\n } catch {\n // best-effort\n }\n }, [browserNotifications]);\n\n usePausingInterval(\n refresh,\n pollMs,\n /* pauseWhenHidden */ !browserNotifications,\n );\n\n useEffect(() => {\n if (!open) return;\n loadItems();\n }, [open, loadItems]);\n\n const markRead = async (id: string) => {\n try {\n // `keepalive: true` lets the request survive page navigation —\n // without it, clicking a notification with a link aborts this\n // request mid-flight and the row stays unread.\n await fetch(agentNativePath(`/_agent-native/notifications/${id}/read`), {\n method: \"POST\",\n keepalive: true,\n });\n setItems((prev) =>\n prev\n ? prev.map((n) =>\n n.id === id ? { ...n, readAt: new Date().toISOString() } : n,\n )\n : prev,\n );\n refresh();\n } catch {\n // best-effort\n }\n };\n\n // Reject any URL that isn't http(s) or a same-origin relative path. Blocks\n // `javascript:` execution, `data:` URIs, and absolute redirects to phishing\n // sites. Relative paths starting with `/` are routed through `appPath()` so\n // the link works in mounted deployments (e.g. /mail subdirectory).\n const safeNotificationLink = (link: string): string | null => {\n if (link.startsWith(\"/\") && !link.startsWith(\"//\")) {\n return appPath(link);\n }\n try {\n const url = new URL(link, window.location.origin);\n if (url.protocol === \"http:\" || url.protocol === \"https:\") {\n return url.toString();\n }\n } catch {\n // fallthrough\n }\n return null;\n };\n\n const markAllRead = async () => {\n try {\n await fetch(agentNativePath(`/_agent-native/notifications/read-all`), {\n method: \"POST\",\n });\n setItems((prev) =>\n prev\n ? prev.map((n) =>\n n.readAt ? n : { ...n, readAt: new Date().toISOString() },\n )\n : prev,\n );\n setUnreadCount(0);\n } catch {\n // best-effort\n }\n };\n\n const dismiss = async (id: string) => {\n try {\n await fetch(agentNativePath(`/_agent-native/notifications/${id}`), {\n method: \"DELETE\",\n });\n setItems((prev) => (prev ? prev.filter((n) => n.id !== id) : prev));\n refresh();\n } catch {\n // best-effort\n }\n };\n\n const hasUnread = unreadCount > 0;\n const Icon = hasUnread ? IconBellRinging : IconBell;\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label={\n hasUnread ? `${unreadCount} unread notifications` : \"Notifications\"\n }\n className={\n \"an-notifications-bell__trigger relative inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent/40 hover:text-foreground\" +\n (className ? ` ${className}` : \"\")\n }\n >\n <Icon size={18} aria-hidden />\n {hasUnread ? (\n <span\n aria-hidden\n className=\"an-notifications-bell__badge absolute -right-0.5 -top-0.5 rounded-full bg-destructive px-1 text-[10px] leading-[14px] font-medium text-destructive-foreground\"\n >\n {unreadCount > 99 ? \"99+\" : unreadCount}\n </span>\n ) : null}\n </button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n sideOffset={8}\n className=\"an-notifications-bell__menu w-80 p-0\"\n >\n <div className=\"flex items-center justify-between border-b border-border px-3 py-2 text-sm font-medium\">\n <span>Notifications</span>\n {hasUnread ? (\n <button\n type=\"button\"\n onClick={markAllRead}\n className=\"text-xs text-primary hover:underline\"\n >\n Mark all read\n </button>\n ) : null}\n </div>\n {browserNotifications &&\n SUPPORTS_NOTIFICATION &&\n permission === \"default\" ? (\n <div className=\"flex items-center justify-between gap-2 border-b border-border bg-accent/40 px-3 py-2 text-xs text-foreground\">\n <span>Get a system popup for new notifications.</span>\n <button\n type=\"button\"\n onClick={async () => {\n const result = await Notification.requestPermission();\n setPermission(result);\n }}\n className=\"shrink-0 rounded bg-primary px-2 py-0.5 font-medium text-primary-foreground hover:bg-primary/90\"\n >\n Enable\n </button>\n </div>\n ) : null}\n <div className=\"max-h-96 overflow-y-auto\">\n {items === null ? (\n <div className=\"flex items-center gap-2 p-4 text-sm text-muted-foreground\">\n <IconLoader2 size={14} className=\"animate-spin\" /> Loading…\n </div>\n ) : items.length > 0 ? (\n items.map((n) => {\n const rawLink =\n typeof n.metadata?.link === \"string\" ? n.metadata.link : null;\n const link = rawLink ? safeNotificationLink(rawLink) : null;\n const onItemClick = () => {\n if (!n.readAt) void markRead(n.id);\n if (link) {\n setOpen(false);\n window.location.assign(link);\n }\n };\n return (\n <div\n key={n.id}\n className={\n \"group relative border-b border-border last:border-b-0 hover:bg-accent/40 \" +\n (n.readAt ? \"opacity-60\" : \"\")\n }\n >\n <button\n type=\"button\"\n onClick={onItemClick}\n className={\n \"flex w-full flex-col items-start gap-0.5 px-3 py-2 pr-8 text-left\" +\n (link ? \" cursor-pointer\" : \"\")\n }\n >\n <div className=\"flex w-full items-center justify-between gap-2\">\n <span className=\"truncate text-sm font-medium text-foreground\">\n {n.title}\n </span>\n <SeverityBadge severity={n.severity} />\n </div>\n {n.body ? (\n <span className=\"line-clamp-2 text-xs text-muted-foreground\">\n {n.body}\n </span>\n ) : null}\n <span className=\"text-[10px] text-muted-foreground/70\">\n {new Date(n.createdAt).toLocaleString()}\n </span>\n </button>\n <button\n type=\"button\"\n aria-label=\"Dismiss notification\"\n onClick={(e) => {\n e.stopPropagation();\n void dismiss(n.id);\n }}\n className=\"absolute right-2 top-2 hidden rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground group-hover:flex\"\n >\n <IconX size={12} />\n </button>\n </div>\n );\n })\n ) : (\n <div className=\"space-y-1 p-4 text-sm\">\n <p className=\"font-medium text-foreground\">{emptyTitle}</p>\n {emptyDescription ? (\n <p className=\"text-xs leading-relaxed text-muted-foreground\">\n {emptyDescription}\n </p>\n ) : null}\n </div>\n )}\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n\n// Severity color pairs — use /20 opacity backdrops that work against both\n// light and dark theme backgrounds; text uses 700/300 so it stays readable\n// in each mode (the `dark:` prefix is one of the few places where explicit\n// variants are necessary since these are brand-color tokens, not semantic).\nfunction SeverityBadge({ severity }: { severity: NotificationSeverity }) {\n const color =\n severity === \"critical\"\n ? \"bg-red-500/20 text-red-700 dark:text-red-300\"\n : severity === \"warning\"\n ? \"bg-amber-500/20 text-amber-700 dark:text-amber-300\"\n : \"bg-muted text-muted-foreground\";\n return (\n <span className={`rounded px-1.5 py-0.5 text-[10px] font-medium ${color}`}>\n {severity}\n </span>\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"UsageSection.d.ts","sourceRoot":"","sources":["../../../src/client/settings/UsageSection.tsx"],"names":[],"mappings":"AAkIA,wBAAgB,YAAY,4CA0K3B"}
1
+ {"version":3,"file":"UsageSection.d.ts","sourceRoot":"","sources":["../../../src/client/settings/UsageSection.tsx"],"names":[],"mappings":"AA+LA,wBAAgB,YAAY,4CA2L3B"}
@@ -8,7 +8,33 @@ const RANGES = [
8
8
  { value: 30, label: "30d" },
9
9
  { value: 90, label: "90d" },
10
10
  ];
11
- function formatCost(cents) {
11
+ const USD_BILLING = {
12
+ unit: "usd",
13
+ label: "Estimated spend",
14
+ shortLabel: "Cost",
15
+ source: "estimated-provider-cost",
16
+ };
17
+ function displayAmountFromCostCents(cents, billing) {
18
+ if (billing.unit !== "builder-credits")
19
+ return cents;
20
+ const margin = billing.hardCostMarginMultiplier ?? 1.25;
21
+ const creditsPerUsd = billing.creditsPerUsd ?? 20;
22
+ const credits = (cents / 100) * margin * creditsPerUsd;
23
+ return credits <= 0 ? 0 : Math.ceil(credits * 1000) / 1000;
24
+ }
25
+ function formatCredits(credits) {
26
+ if (!Number.isFinite(credits) || credits === 0)
27
+ return "0 credits";
28
+ const maximumFractionDigits = credits < 1 ? 3 : credits < 10 ? 2 : 1;
29
+ const value = credits.toLocaleString(undefined, {
30
+ maximumFractionDigits,
31
+ });
32
+ return `${value} ${credits === 1 ? "credit" : "credits"}`;
33
+ }
34
+ function formatSpend(cents, billing) {
35
+ if (billing.unit === "builder-credits") {
36
+ return formatCredits(displayAmountFromCostCents(cents, billing));
37
+ }
12
38
  // Sub-cent values (e.g. a single LLM call at $0.0045 = 0.45¢) — keep
13
39
  // three decimals so tiny calls don't round to 0.00¢. The prior impl
14
40
  // multiplied by 100 in this branch, overstating small costs 100×.
@@ -25,24 +51,29 @@ function formatTokens(n) {
25
51
  return `${(n / 1_000).toFixed(1)}K`;
26
52
  return String(n);
27
53
  }
28
- function BucketBars({ buckets, emptyMessage, }) {
54
+ function BucketBars({ buckets, emptyMessage, billing, }) {
29
55
  if (buckets.length === 0) {
30
56
  return (_jsx("p", { className: "text-[10px] text-muted-foreground py-1.5", children: emptyMessage }));
31
57
  }
32
- const max = Math.max(...buckets.map((b) => b.cents), 0.0001);
33
- return (_jsx("div", { className: "space-y-1", children: buckets.map((b) => (_jsxs("div", { className: "text-[10px]", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 mb-0.5", children: [_jsx("span", { className: "truncate text-foreground", title: b.key || "(none)", children: b.key || "(none)" }), _jsxs("span", { className: "shrink-0 text-muted-foreground tabular-nums", children: [formatCost(b.cents), _jsxs("span", { className: "ml-1 opacity-60", children: ["\u00B7 ", formatTokens(b.inputTokens + b.outputTokens), " tok"] })] })] }), _jsx("div", { className: "h-1 rounded-full bg-accent/40 overflow-hidden", children: _jsx("div", { className: "h-full bg-foreground/70", style: { width: `${(b.cents / max) * 100}%` } }) })] }, b.key))) }));
58
+ const max = Math.max(...buckets.map((b) => displayAmountFromCostCents(b.cents, billing)), 0.0001);
59
+ return (_jsx("div", { className: "space-y-1", children: buckets.map((b) => (_jsxs("div", { className: "text-[10px]", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 mb-0.5", children: [_jsx("span", { className: "truncate text-foreground", title: b.key || "(none)", children: b.key || "(none)" }), _jsxs("span", { className: "shrink-0 text-muted-foreground tabular-nums", children: [formatSpend(b.cents, billing), _jsxs("span", { className: "ml-1 opacity-60", children: ["\u00B7 ", formatTokens(b.inputTokens + b.outputTokens), " tok"] })] })] }), _jsx("div", { className: "h-1 rounded-full bg-accent/40 overflow-hidden", children: _jsx("div", { className: "h-full bg-foreground/70", style: {
60
+ width: `${(displayAmountFromCostCents(b.cents, billing) / max) * 100}%`,
61
+ } }) })] }, b.key))) }));
34
62
  }
35
- function DailySparkline({ days }) {
63
+ function DailySparkline({ days, billing, }) {
36
64
  if (days.length === 0)
37
65
  return null;
38
- const max = Math.max(...days.map((d) => d.cents), 0.0001);
39
- return (_jsx("div", { className: "flex items-end gap-[2px] h-8 pt-2", children: days.map((d) => (_jsx("div", { className: "flex-1 bg-foreground/60 rounded-sm min-h-[1px]", style: { height: `${Math.max(2, (d.cents / max) * 100)}%` }, title: `${d.date}: ${formatCost(d.cents)} (${d.calls} calls)` }, d.date))) }));
66
+ const max = Math.max(...days.map((d) => displayAmountFromCostCents(d.cents, billing)), 0.0001);
67
+ return (_jsx("div", { className: "flex items-end gap-[2px] h-8 pt-2", children: days.map((d) => (_jsx("div", { className: "flex-1 bg-foreground/60 rounded-sm min-h-[1px]", style: {
68
+ height: `${Math.max(2, (displayAmountFromCostCents(d.cents, billing) / max) * 100)}%`,
69
+ }, title: `${d.date}: ${formatSpend(d.cents, billing)} (${d.calls} calls)` }, d.date))) }));
40
70
  }
41
71
  export function UsageSection() {
42
72
  const [days, setDays] = useState(30);
43
73
  const [data, setData] = useState(null);
44
74
  const [loading, setLoading] = useState(false);
45
75
  const [error, setError] = useState(null);
76
+ const billing = data?.billing ?? USD_BILLING;
46
77
  const load = async (rangeDays) => {
47
78
  setLoading(true);
48
79
  setError(null);
@@ -66,6 +97,8 @@ export function UsageSection() {
66
97
  }, [days]);
67
98
  return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "flex gap-1 rounded-md border border-border p-0.5", children: RANGES.map((r) => (_jsx("button", { onClick: () => setDays(r.value), className: `px-2 py-0.5 text-[10px] rounded ${days === r.value
68
99
  ? "bg-accent text-foreground"
69
- : "text-muted-foreground hover:text-foreground"}`, children: r.label }, r.value))) }), _jsx("button", { onClick: () => load(days), className: "flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground", disabled: loading, children: loading ? (_jsx(IconLoader2, { size: 11, className: "animate-spin" })) : (_jsx(IconRefresh, { size: 11 })) })] }), error && _jsx("p", { className: "text-[10px] text-red-500", children: error }), data && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "rounded-md border border-border px-2.5 py-2", children: [_jsxs("div", { className: "flex items-baseline justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-[10px] text-muted-foreground", children: "Total spend" }), _jsx("div", { className: "text-[18px] font-semibold tabular-nums", children: formatCost(data.totalCents) })] }), _jsxs("div", { className: "text-right", children: [_jsxs("div", { className: "text-[10px] text-muted-foreground", children: [data.totalCalls, " calls"] }), _jsxs("div", { className: "text-[10px] text-muted-foreground", children: [formatTokens(data.totalInputTokens), " in \u00B7", " ", formatTokens(data.totalOutputTokens), " out"] }), data.totalCacheReadTokens > 0 && (_jsxs("div", { className: "text-[10px] text-green-500/80", children: [formatTokens(data.totalCacheReadTokens), " cached"] }))] })] }), _jsx(DailySparkline, { days: data.byDay })] }), _jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-medium text-foreground mb-1", children: "By label" }), _jsx(BucketBars, { buckets: data.byLabel, emptyMessage: "No labeled calls yet." })] }), _jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-medium text-foreground mb-1", children: "By model" }), _jsx(BucketBars, { buckets: data.byModel, emptyMessage: "No calls recorded." })] }), data.byApp.filter((b) => b.key).length > 1 && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-medium text-foreground mb-1", children: "By app" }), _jsx(BucketBars, { buckets: data.byApp, emptyMessage: "" })] })), data.recent.length > 0 && (_jsxs("details", { children: [_jsxs("summary", { className: "text-[10px] font-medium text-foreground cursor-pointer select-none hover:text-foreground/80", children: ["Recent calls (", data.recent.length, ")"] }), _jsx("div", { className: "mt-1.5 max-h-48 overflow-y-auto space-y-0.5 rounded border border-border", children: data.recent.map((r) => (_jsxs("div", { className: "flex items-center justify-between gap-2 px-2 py-1 text-[10px] border-b border-border last:border-b-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "truncate text-foreground", title: r.label, children: [r.label, r.app ? (_jsxs("span", { className: "text-muted-foreground", children: [" ", "\u00B7 ", r.app] })) : null] }), _jsxs("div", { className: "truncate text-muted-foreground", children: [new Date(r.createdAt).toLocaleString(), " \u00B7 ", r.model] })] }), _jsx("div", { className: "shrink-0 text-right tabular-nums text-muted-foreground", children: formatCost(r.cents) })] }, r.id))) })] })), _jsx("p", { className: "text-[10px] text-muted-foreground", children: "Spend is estimated from published Anthropic pricing and your own recorded token counts. Cached input is priced at ~10% of regular input." })] }))] }));
100
+ : "text-muted-foreground hover:text-foreground"}`, children: r.label }, r.value))) }), _jsx("button", { onClick: () => load(days), className: "flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground", disabled: loading, children: loading ? (_jsx(IconLoader2, { size: 11, className: "animate-spin" })) : (_jsx(IconRefresh, { size: 11 })) })] }), error && _jsx("p", { className: "text-[10px] text-red-500", children: error }), data && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "rounded-md border border-border px-2.5 py-2", children: [_jsxs("div", { className: "flex items-baseline justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-[10px] text-muted-foreground", children: billing.unit === "builder-credits"
101
+ ? "Builder.io credit spend"
102
+ : "Total spend" }), _jsx("div", { className: "text-[18px] font-semibold tabular-nums", children: formatSpend(data.totalCents, billing) })] }), _jsxs("div", { className: "text-right", children: [_jsxs("div", { className: "text-[10px] text-muted-foreground", children: [data.totalCalls, " calls"] }), _jsxs("div", { className: "text-[10px] text-muted-foreground", children: [formatTokens(data.totalInputTokens), " in \u00B7", " ", formatTokens(data.totalOutputTokens), " out"] }), data.totalCacheReadTokens > 0 && (_jsxs("div", { className: "text-[10px] text-green-500/80", children: [formatTokens(data.totalCacheReadTokens), " cached"] }))] })] }), _jsx(DailySparkline, { days: data.byDay, billing: billing })] }), _jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-medium text-foreground mb-1", children: "By label" }), _jsx(BucketBars, { buckets: data.byLabel, emptyMessage: "No labeled calls yet.", billing: billing })] }), _jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-medium text-foreground mb-1", children: "By model" }), _jsx(BucketBars, { buckets: data.byModel, emptyMessage: "No calls recorded.", billing: billing })] }), data.byApp.filter((b) => b.key).length > 1 && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-medium text-foreground mb-1", children: "By app" }), _jsx(BucketBars, { buckets: data.byApp, emptyMessage: "", billing: billing })] })), data.recent.length > 0 && (_jsxs("details", { children: [_jsxs("summary", { className: "text-[10px] font-medium text-foreground cursor-pointer select-none hover:text-foreground/80", children: ["Recent calls (", data.recent.length, ")"] }), _jsx("div", { className: "mt-1.5 max-h-48 overflow-y-auto space-y-0.5 rounded border border-border", children: data.recent.map((r) => (_jsxs("div", { className: "flex items-center justify-between gap-2 px-2 py-1 text-[10px] border-b border-border last:border-b-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "truncate text-foreground", title: r.label, children: [r.label, r.app ? (_jsxs("span", { className: "text-muted-foreground", children: [" ", "\u00B7 ", r.app] })) : null] }), _jsxs("div", { className: "truncate text-muted-foreground", children: [new Date(r.createdAt).toLocaleString(), " \u00B7 ", r.model] })] }), _jsx("div", { className: "shrink-0 text-right tabular-nums text-muted-foreground", children: formatSpend(r.cents, billing) })] }, r.id))) })] })), billing.unit === "builder-credits" ? (_jsxs("p", { className: "text-[10px] text-muted-foreground", children: ["Builder.io credits are estimated from hard token cost, a", " ", billing.hardCostMarginMultiplier ?? 1.25, "x margin, and", " ", billing.creditsPerUsd ?? 20, " credits per dollar."] })) : (_jsx("p", { className: "text-[10px] text-muted-foreground", children: "Spend is estimated from published Anthropic pricing and your own recorded token counts. Cached input is priced at ~10% of regular input." }))] }))] }));
70
103
  }
71
104
  //# sourceMappingURL=UsageSection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"UsageSection.js","sourceRoot":"","sources":["../../../src/client/settings/UsageSection.tsx"],"names":[],"mappings":"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","sourcesContent":["import { agentNativePath } from \"../api-path.js\";\nimport { useEffect, useState } from \"react\";\nimport { IconLoader2, IconRefresh } from \"@tabler/icons-react\";\n\ninterface UsageBucket {\n key: string;\n cents: number;\n calls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n}\n\ninterface DailyBucket {\n date: string;\n cents: number;\n calls: number;\n}\n\ninterface UsageRecentEntry {\n id: number;\n createdAt: number;\n label: string;\n app: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n cents: number;\n}\n\ninterface UsageSummary {\n totalCents: number;\n totalCalls: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n totalCacheReadTokens: number;\n totalCacheWriteTokens: number;\n sinceMs: number;\n byLabel: UsageBucket[];\n byModel: UsageBucket[];\n byApp: UsageBucket[];\n byDay: DailyBucket[];\n recent: UsageRecentEntry[];\n}\n\nconst RANGES = [\n { value: 1, label: \"24h\" },\n { value: 7, label: \"7d\" },\n { value: 30, label: \"30d\" },\n { value: 90, label: \"90d\" },\n];\n\nfunction formatCost(cents: number): string {\n // Sub-cent values (e.g. a single LLM call at $0.0045 = 0.45¢) — keep\n // three decimals so tiny calls don't round to 0.00¢. The prior impl\n // multiplied by 100 in this branch, overstating small costs 100×.\n if (cents < 1) return `${cents.toFixed(3)}¢`;\n if (cents < 100) return `${cents.toFixed(2)}¢`;\n return `$${(cents / 100).toFixed(2)}`;\n}\n\nfunction formatTokens(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;\n if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;\n return String(n);\n}\n\nfunction BucketBars({\n buckets,\n emptyMessage,\n}: {\n buckets: UsageBucket[];\n emptyMessage: string;\n}) {\n if (buckets.length === 0) {\n return (\n <p className=\"text-[10px] text-muted-foreground py-1.5\">{emptyMessage}</p>\n );\n }\n const max = Math.max(...buckets.map((b) => b.cents), 0.0001);\n return (\n <div className=\"space-y-1\">\n {buckets.map((b) => (\n <div key={b.key} className=\"text-[10px]\">\n <div className=\"flex items-center justify-between gap-2 mb-0.5\">\n <span\n className=\"truncate text-foreground\"\n title={b.key || \"(none)\"}\n >\n {b.key || \"(none)\"}\n </span>\n <span className=\"shrink-0 text-muted-foreground tabular-nums\">\n {formatCost(b.cents)}\n <span className=\"ml-1 opacity-60\">\n · {formatTokens(b.inputTokens + b.outputTokens)} tok\n </span>\n </span>\n </div>\n <div className=\"h-1 rounded-full bg-accent/40 overflow-hidden\">\n <div\n className=\"h-full bg-foreground/70\"\n style={{ width: `${(b.cents / max) * 100}%` }}\n />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction DailySparkline({ days }: { days: DailyBucket[] }) {\n if (days.length === 0) return null;\n const max = Math.max(...days.map((d) => d.cents), 0.0001);\n return (\n <div className=\"flex items-end gap-[2px] h-8 pt-2\">\n {days.map((d) => (\n <div\n key={d.date}\n className=\"flex-1 bg-foreground/60 rounded-sm min-h-[1px]\"\n style={{ height: `${Math.max(2, (d.cents / max) * 100)}%` }}\n title={`${d.date}: ${formatCost(d.cents)} (${d.calls} calls)`}\n />\n ))}\n </div>\n );\n}\n\nexport function UsageSection() {\n const [days, setDays] = useState(30);\n const [data, setData] = useState<UsageSummary | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const load = async (rangeDays: number) => {\n setLoading(true);\n setError(null);\n try {\n const res = await fetch(\n agentNativePath(`/_agent-native/usage?sinceDays=${rangeDays}`),\n );\n if (!res.ok) {\n throw new Error(`Failed (${res.status})`);\n }\n const json = (await res.json()) as UsageSummary;\n setData(json);\n } catch (err: any) {\n setError(err?.message || \"Failed to load usage\");\n } finally {\n setLoading(false);\n }\n };\n\n useEffect(() => {\n load(days);\n }, [days]);\n\n return (\n <div className=\"space-y-3\">\n {/* Range selector + refresh */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex gap-1 rounded-md border border-border p-0.5\">\n {RANGES.map((r) => (\n <button\n key={r.value}\n onClick={() => setDays(r.value)}\n className={`px-2 py-0.5 text-[10px] rounded ${\n days === r.value\n ? \"bg-accent text-foreground\"\n : \"text-muted-foreground hover:text-foreground\"\n }`}\n >\n {r.label}\n </button>\n ))}\n </div>\n <button\n onClick={() => load(days)}\n className=\"flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground\"\n disabled={loading}\n >\n {loading ? (\n <IconLoader2 size={11} className=\"animate-spin\" />\n ) : (\n <IconRefresh size={11} />\n )}\n </button>\n </div>\n\n {error && <p className=\"text-[10px] text-red-500\">{error}</p>}\n\n {data && (\n <>\n {/* Totals */}\n <div className=\"rounded-md border border-border px-2.5 py-2\">\n <div className=\"flex items-baseline justify-between\">\n <div>\n <div className=\"text-[10px] text-muted-foreground\">\n Total spend\n </div>\n <div className=\"text-[18px] font-semibold tabular-nums\">\n {formatCost(data.totalCents)}\n </div>\n </div>\n <div className=\"text-right\">\n <div className=\"text-[10px] text-muted-foreground\">\n {data.totalCalls} calls\n </div>\n <div className=\"text-[10px] text-muted-foreground\">\n {formatTokens(data.totalInputTokens)} in ·{\" \"}\n {formatTokens(data.totalOutputTokens)} out\n </div>\n {data.totalCacheReadTokens > 0 && (\n <div className=\"text-[10px] text-green-500/80\">\n {formatTokens(data.totalCacheReadTokens)} cached\n </div>\n )}\n </div>\n </div>\n <DailySparkline days={data.byDay} />\n </div>\n\n {/* By label */}\n <div>\n <div className=\"text-[10px] font-medium text-foreground mb-1\">\n By label\n </div>\n <BucketBars\n buckets={data.byLabel}\n emptyMessage=\"No labeled calls yet.\"\n />\n </div>\n\n {/* By model */}\n <div>\n <div className=\"text-[10px] font-medium text-foreground mb-1\">\n By model\n </div>\n <BucketBars\n buckets={data.byModel}\n emptyMessage=\"No calls recorded.\"\n />\n </div>\n\n {/* By app — only show when multiple apps contribute */}\n {data.byApp.filter((b) => b.key).length > 1 && (\n <div>\n <div className=\"text-[10px] font-medium text-foreground mb-1\">\n By app\n </div>\n <BucketBars buckets={data.byApp} emptyMessage=\"\" />\n </div>\n )}\n\n {/* Recent calls */}\n {data.recent.length > 0 && (\n <details>\n <summary className=\"text-[10px] font-medium text-foreground cursor-pointer select-none hover:text-foreground/80\">\n Recent calls ({data.recent.length})\n </summary>\n <div className=\"mt-1.5 max-h-48 overflow-y-auto space-y-0.5 rounded border border-border\">\n {data.recent.map((r) => (\n <div\n key={r.id}\n className=\"flex items-center justify-between gap-2 px-2 py-1 text-[10px] border-b border-border last:border-b-0\"\n >\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-foreground\" title={r.label}>\n {r.label}\n {r.app ? (\n <span className=\"text-muted-foreground\">\n {\" \"}\n · {r.app}\n </span>\n ) : null}\n </div>\n <div className=\"truncate text-muted-foreground\">\n {new Date(r.createdAt).toLocaleString()} · {r.model}\n </div>\n </div>\n <div className=\"shrink-0 text-right tabular-nums text-muted-foreground\">\n {formatCost(r.cents)}\n </div>\n </div>\n ))}\n </div>\n </details>\n )}\n\n <p className=\"text-[10px] text-muted-foreground\">\n Spend is estimated from published Anthropic pricing and your own\n recorded token counts. Cached input is priced at ~10% of regular\n input.\n </p>\n </>\n )}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"UsageSection.js","sourceRoot":"","sources":["../../../src/client/settings/UsageSection.tsx"],"names":[],"mappings":"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","sourcesContent":["import { agentNativePath } from \"../api-path.js\";\nimport { useEffect, useState } from \"react\";\nimport { IconLoader2, IconRefresh } from \"@tabler/icons-react\";\n\ninterface UsageBucket {\n key: string;\n cents: number;\n calls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n}\n\ninterface DailyBucket {\n date: string;\n cents: number;\n calls: number;\n}\n\ninterface UsageRecentEntry {\n id: number;\n createdAt: number;\n label: string;\n app: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n cents: number;\n}\n\ninterface UsageBillingMode {\n unit: \"usd\" | \"builder-credits\";\n label: string;\n shortLabel: string;\n source: \"estimated-provider-cost\" | \"builder-agent-credits\";\n hardCostMarginMultiplier?: number;\n creditsPerUsd?: number;\n}\n\ninterface UsageSummary {\n billing?: UsageBillingMode;\n totalCents: number;\n totalCalls: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n totalCacheReadTokens: number;\n totalCacheWriteTokens: number;\n sinceMs: number;\n byLabel: UsageBucket[];\n byModel: UsageBucket[];\n byApp: UsageBucket[];\n byDay: DailyBucket[];\n recent: UsageRecentEntry[];\n}\n\nconst RANGES = [\n { value: 1, label: \"24h\" },\n { value: 7, label: \"7d\" },\n { value: 30, label: \"30d\" },\n { value: 90, label: \"90d\" },\n];\n\nconst USD_BILLING: UsageBillingMode = {\n unit: \"usd\",\n label: \"Estimated spend\",\n shortLabel: \"Cost\",\n source: \"estimated-provider-cost\",\n};\n\nfunction displayAmountFromCostCents(\n cents: number,\n billing: UsageBillingMode,\n): number {\n if (billing.unit !== \"builder-credits\") return cents;\n const margin = billing.hardCostMarginMultiplier ?? 1.25;\n const creditsPerUsd = billing.creditsPerUsd ?? 20;\n const credits = (cents / 100) * margin * creditsPerUsd;\n return credits <= 0 ? 0 : Math.ceil(credits * 1000) / 1000;\n}\n\nfunction formatCredits(credits: number): string {\n if (!Number.isFinite(credits) || credits === 0) return \"0 credits\";\n const maximumFractionDigits = credits < 1 ? 3 : credits < 10 ? 2 : 1;\n const value = credits.toLocaleString(undefined, {\n maximumFractionDigits,\n });\n return `${value} ${credits === 1 ? \"credit\" : \"credits\"}`;\n}\n\nfunction formatSpend(cents: number, billing: UsageBillingMode): string {\n if (billing.unit === \"builder-credits\") {\n return formatCredits(displayAmountFromCostCents(cents, billing));\n }\n // Sub-cent values (e.g. a single LLM call at $0.0045 = 0.45¢) — keep\n // three decimals so tiny calls don't round to 0.00¢. The prior impl\n // multiplied by 100 in this branch, overstating small costs 100×.\n if (cents < 1) return `${cents.toFixed(3)}¢`;\n if (cents < 100) return `${cents.toFixed(2)}¢`;\n return `$${(cents / 100).toFixed(2)}`;\n}\n\nfunction formatTokens(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;\n if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;\n return String(n);\n}\n\nfunction BucketBars({\n buckets,\n emptyMessage,\n billing,\n}: {\n buckets: UsageBucket[];\n emptyMessage: string;\n billing: UsageBillingMode;\n}) {\n if (buckets.length === 0) {\n return (\n <p className=\"text-[10px] text-muted-foreground py-1.5\">{emptyMessage}</p>\n );\n }\n const max = Math.max(\n ...buckets.map((b) => displayAmountFromCostCents(b.cents, billing)),\n 0.0001,\n );\n return (\n <div className=\"space-y-1\">\n {buckets.map((b) => (\n <div key={b.key} className=\"text-[10px]\">\n <div className=\"flex items-center justify-between gap-2 mb-0.5\">\n <span\n className=\"truncate text-foreground\"\n title={b.key || \"(none)\"}\n >\n {b.key || \"(none)\"}\n </span>\n <span className=\"shrink-0 text-muted-foreground tabular-nums\">\n {formatSpend(b.cents, billing)}\n <span className=\"ml-1 opacity-60\">\n · {formatTokens(b.inputTokens + b.outputTokens)} tok\n </span>\n </span>\n </div>\n <div className=\"h-1 rounded-full bg-accent/40 overflow-hidden\">\n <div\n className=\"h-full bg-foreground/70\"\n style={{\n width: `${(displayAmountFromCostCents(b.cents, billing) / max) * 100}%`,\n }}\n />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction DailySparkline({\n days,\n billing,\n}: {\n days: DailyBucket[];\n billing: UsageBillingMode;\n}) {\n if (days.length === 0) return null;\n const max = Math.max(\n ...days.map((d) => displayAmountFromCostCents(d.cents, billing)),\n 0.0001,\n );\n return (\n <div className=\"flex items-end gap-[2px] h-8 pt-2\">\n {days.map((d) => (\n <div\n key={d.date}\n className=\"flex-1 bg-foreground/60 rounded-sm min-h-[1px]\"\n style={{\n height: `${Math.max(\n 2,\n (displayAmountFromCostCents(d.cents, billing) / max) * 100,\n )}%`,\n }}\n title={`${d.date}: ${formatSpend(d.cents, billing)} (${d.calls} calls)`}\n />\n ))}\n </div>\n );\n}\n\nexport function UsageSection() {\n const [days, setDays] = useState(30);\n const [data, setData] = useState<UsageSummary | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const billing = data?.billing ?? USD_BILLING;\n\n const load = async (rangeDays: number) => {\n setLoading(true);\n setError(null);\n try {\n const res = await fetch(\n agentNativePath(`/_agent-native/usage?sinceDays=${rangeDays}`),\n );\n if (!res.ok) {\n throw new Error(`Failed (${res.status})`);\n }\n const json = (await res.json()) as UsageSummary;\n setData(json);\n } catch (err: any) {\n setError(err?.message || \"Failed to load usage\");\n } finally {\n setLoading(false);\n }\n };\n\n useEffect(() => {\n load(days);\n }, [days]);\n\n return (\n <div className=\"space-y-3\">\n {/* Range selector + refresh */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex gap-1 rounded-md border border-border p-0.5\">\n {RANGES.map((r) => (\n <button\n key={r.value}\n onClick={() => setDays(r.value)}\n className={`px-2 py-0.5 text-[10px] rounded ${\n days === r.value\n ? \"bg-accent text-foreground\"\n : \"text-muted-foreground hover:text-foreground\"\n }`}\n >\n {r.label}\n </button>\n ))}\n </div>\n <button\n onClick={() => load(days)}\n className=\"flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground\"\n disabled={loading}\n >\n {loading ? (\n <IconLoader2 size={11} className=\"animate-spin\" />\n ) : (\n <IconRefresh size={11} />\n )}\n </button>\n </div>\n\n {error && <p className=\"text-[10px] text-red-500\">{error}</p>}\n\n {data && (\n <>\n {/* Totals */}\n <div className=\"rounded-md border border-border px-2.5 py-2\">\n <div className=\"flex items-baseline justify-between\">\n <div>\n <div className=\"text-[10px] text-muted-foreground\">\n {billing.unit === \"builder-credits\"\n ? \"Builder.io credit spend\"\n : \"Total spend\"}\n </div>\n <div className=\"text-[18px] font-semibold tabular-nums\">\n {formatSpend(data.totalCents, billing)}\n </div>\n </div>\n <div className=\"text-right\">\n <div className=\"text-[10px] text-muted-foreground\">\n {data.totalCalls} calls\n </div>\n <div className=\"text-[10px] text-muted-foreground\">\n {formatTokens(data.totalInputTokens)} in ·{\" \"}\n {formatTokens(data.totalOutputTokens)} out\n </div>\n {data.totalCacheReadTokens > 0 && (\n <div className=\"text-[10px] text-green-500/80\">\n {formatTokens(data.totalCacheReadTokens)} cached\n </div>\n )}\n </div>\n </div>\n <DailySparkline days={data.byDay} billing={billing} />\n </div>\n\n {/* By label */}\n <div>\n <div className=\"text-[10px] font-medium text-foreground mb-1\">\n By label\n </div>\n <BucketBars\n buckets={data.byLabel}\n emptyMessage=\"No labeled calls yet.\"\n billing={billing}\n />\n </div>\n\n {/* By model */}\n <div>\n <div className=\"text-[10px] font-medium text-foreground mb-1\">\n By model\n </div>\n <BucketBars\n buckets={data.byModel}\n emptyMessage=\"No calls recorded.\"\n billing={billing}\n />\n </div>\n\n {/* By app — only show when multiple apps contribute */}\n {data.byApp.filter((b) => b.key).length > 1 && (\n <div>\n <div className=\"text-[10px] font-medium text-foreground mb-1\">\n By app\n </div>\n <BucketBars\n buckets={data.byApp}\n emptyMessage=\"\"\n billing={billing}\n />\n </div>\n )}\n\n {/* Recent calls */}\n {data.recent.length > 0 && (\n <details>\n <summary className=\"text-[10px] font-medium text-foreground cursor-pointer select-none hover:text-foreground/80\">\n Recent calls ({data.recent.length})\n </summary>\n <div className=\"mt-1.5 max-h-48 overflow-y-auto space-y-0.5 rounded border border-border\">\n {data.recent.map((r) => (\n <div\n key={r.id}\n className=\"flex items-center justify-between gap-2 px-2 py-1 text-[10px] border-b border-border last:border-b-0\"\n >\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-foreground\" title={r.label}>\n {r.label}\n {r.app ? (\n <span className=\"text-muted-foreground\">\n {\" \"}\n · {r.app}\n </span>\n ) : null}\n </div>\n <div className=\"truncate text-muted-foreground\">\n {new Date(r.createdAt).toLocaleString()} · {r.model}\n </div>\n </div>\n <div className=\"shrink-0 text-right tabular-nums text-muted-foreground\">\n {formatSpend(r.cents, billing)}\n </div>\n </div>\n ))}\n </div>\n </details>\n )}\n\n {billing.unit === \"builder-credits\" ? (\n <p className=\"text-[10px] text-muted-foreground\">\n Builder.io credits are estimated from hard token cost, a{\" \"}\n {billing.hardCostMarginMultiplier ?? 1.25}x margin, and{\" \"}\n {billing.creditsPerUsd ?? 20} credits per dollar.\n </p>\n ) : (\n <p className=\"text-[10px] text-muted-foreground\">\n Spend is estimated from published Anthropic pricing and your own\n recorded token counts. Cached input is priced at ~10% of regular\n input.\n </p>\n )}\n </>\n )}\n </div>\n );\n}\n"]}
@@ -143,6 +143,7 @@ function SharePanel(props) {
143
143
  const unshare = useActionMutation("unshare-resource");
144
144
  const [email, setEmail] = useState("");
145
145
  const [role, setRole] = useState("viewer");
146
+ const [notifyPeople, setNotifyPeople] = useState(true);
146
147
  const orgMembers = useOrgMembers();
147
148
  const datalistId = `share-autocomplete-${resourceType}-${resourceId}`;
148
149
  // Optimistic overlays so clicks feel instant.
@@ -205,6 +206,8 @@ function SharePanel(props) {
205
206
  principalType: "user",
206
207
  principalId: trimmed,
207
208
  role,
209
+ notify: notifyPeople,
210
+ resourceUrl: getNotificationUrl(props.shareUrl),
208
211
  }, {
209
212
  onSuccess: () => {
210
213
  sharesQuery.refetch().then(() => {
@@ -238,6 +241,7 @@ function SharePanel(props) {
238
241
  principalType: s.principalType,
239
242
  principalId: s.principalId,
240
243
  role: next,
244
+ notify: false,
241
245
  }, {
242
246
  onSuccess: () => {
243
247
  sharesQuery.refetch().then(() => {
@@ -298,13 +302,14 @@ function SharePanel(props) {
298
302
  if (isLoading) {
299
303
  return (_jsxs("div", { children: [_jsx("div", { className: "mb-3 truncate text-base font-semibold", title: titleText, children: titleText }), _jsx("div", { className: "mb-4 h-9 rounded-md bg-muted animate-pulse" }), _jsx("div", { className: "mb-2 text-sm font-semibold", children: "People with access" }), _jsx("div", { className: "mb-4 h-7 rounded-md bg-muted animate-pulse" }), _jsx("div", { className: "mb-2 text-sm font-semibold", children: "General access" }), _jsx("div", { className: "mb-4 h-9 rounded-md bg-muted animate-pulse" }), _jsx("div", { className: "mt-2 flex justify-end", children: _jsx("button", { type: "button", onClick: onClose, className: BUTTON_PRIMARY_SM, children: "Done" }) })] }));
300
304
  }
301
- return (_jsxs("div", { children: [_jsx("div", { className: "mb-3 truncate text-base font-semibold", title: titleText, children: titleText }), canManage ? (_jsxs("div", { className: "mb-4 flex items-stretch gap-2", children: [_jsx("input", { type: "email", placeholder: "Add people by email", value: email, onChange: (e) => setEmail(e.target.value), onKeyDown: (e) => {
302
- if (e.key === "Enter")
303
- handleAdd();
304
- }, list: orgMembers.length > 0 ? datalistId : undefined, autoComplete: "off", className: "flex-1 min-w-0 h-9 rounded-md border border-input bg-background px-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background" }), orgMembers.length > 0 ? (_jsx("datalist", { id: datalistId, children: orgMembers
305
- .filter((m) => m.email !== sharesQuery.data?.ownerEmail &&
306
- !(sharesQuery.data?.shares ?? []).some((s) => s.principalType === "user" && s.principalId === m.email))
307
- .map((m) => (_jsx("option", { value: m.email, label: m.name ?? undefined }, m.email))) })) : null, _jsx(RoleSelect, { value: role, onChange: setRole })] })) : null, _jsx("div", { className: "mb-2 text-sm font-semibold", children: "People with access" }), _jsxs("ul", { className: "mb-4 flex flex-col gap-1 list-none p-0 m-0", children: [data?.ownerEmail ? (_jsxs("li", { className: "flex items-center gap-3 px-1 py-1.5 text-sm", children: [_jsx(Avatar, { label: displayName(data.ownerEmail, orgMembers) }), _jsx("span", { className: "flex-1 min-w-0 truncate", children: displayName(data.ownerEmail, orgMembers) }), _jsx("span", { className: "text-xs text-muted-foreground", children: "Owner" })] })) : null, shares.map((s) => (_jsxs("li", { className: cn("flex items-center gap-3 px-1 py-1.5 text-sm", inFlight.has(keyOf(s)) && "opacity-60"), children: [_jsx(Avatar, { label: s.principalType === "org"
305
+ return (_jsxs("div", { children: [_jsx("div", { className: "mb-3 truncate text-base font-semibold", title: titleText, children: titleText }), canManage ? (_jsxs("div", { className: "mb-4 space-y-2", children: [_jsxs("div", { className: "flex items-stretch gap-2", children: [_jsx("input", { type: "email", placeholder: "Add people by email", value: email, onChange: (e) => setEmail(e.target.value), onKeyDown: (e) => {
306
+ if (e.key === "Enter")
307
+ handleAdd();
308
+ }, list: orgMembers.length > 0 ? datalistId : undefined, autoComplete: "off", className: "flex-1 min-w-0 h-9 rounded-md border border-input bg-background px-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background" }), orgMembers.length > 0 ? (_jsx("datalist", { id: datalistId, children: orgMembers
309
+ .filter((m) => m.email !== sharesQuery.data?.ownerEmail &&
310
+ !(sharesQuery.data?.shares ?? []).some((s) => s.principalType === "user" &&
311
+ s.principalId === m.email))
312
+ .map((m) => (_jsx("option", { value: m.email, label: m.name ?? undefined }, m.email))) })) : null, _jsx(RoleSelect, { value: role, onChange: setRole })] }), _jsxs("label", { className: "inline-flex items-center gap-2 text-xs text-muted-foreground", children: [_jsx("input", { type: "checkbox", checked: notifyPeople, onChange: (e) => setNotifyPeople(e.target.checked), className: "h-4 w-4 rounded border-input accent-primary" }), "Notify people"] })] })) : null, _jsx("div", { className: "mb-2 text-sm font-semibold", children: "People with access" }), _jsxs("ul", { className: "mb-4 flex flex-col gap-1 list-none p-0 m-0", children: [data?.ownerEmail ? (_jsxs("li", { className: "flex items-center gap-3 px-1 py-1.5 text-sm", children: [_jsx(Avatar, { label: displayName(data.ownerEmail, orgMembers) }), _jsx("span", { className: "flex-1 min-w-0 truncate", children: displayName(data.ownerEmail, orgMembers) }), _jsx("span", { className: "text-xs text-muted-foreground", children: "Owner" })] })) : null, shares.map((s) => (_jsxs("li", { className: cn("flex items-center gap-3 px-1 py-1.5 text-sm", inFlight.has(keyOf(s)) && "opacity-60"), children: [_jsx(Avatar, { label: s.principalType === "org"
308
313
  ? s.principalId
309
314
  : displayName(s.principalId, orgMembers), org: s.principalType === "org" }), _jsx("span", { className: "flex-1 min-w-0 truncate", children: s.principalType === "org"
310
315
  ? s.principalId
@@ -361,6 +366,13 @@ function Avatar({ label, org }) {
361
366
  function keyOf(s) {
362
367
  return `${s.principalType}:${s.principalId}`;
363
368
  }
369
+ function getNotificationUrl(explicit) {
370
+ if (explicit)
371
+ return explicit;
372
+ if (typeof window === "undefined")
373
+ return undefined;
374
+ return window.location.href;
375
+ }
364
376
  function cap(s) {
365
377
  return s.charAt(0).toUpperCase() + s.slice(1);
366
378
  }