@agent-native/core 0.98.0 → 0.98.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 (243) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +24 -0
  3. package/corpus/core/package.json +2 -2
  4. package/corpus/core/src/action-change-marker.ts +8 -1
  5. package/corpus/core/src/changelog/parse.ts +8 -3
  6. package/corpus/core/src/client/AgentPanel.tsx +2 -3
  7. package/corpus/core/src/client/AssistantChat.tsx +83 -39
  8. package/corpus/core/src/client/analytics.ts +29 -0
  9. package/corpus/core/src/client/blocks/library/wireframe-kit.tsx +16 -2
  10. package/corpus/core/src/client/chat-model-groups.ts +28 -0
  11. package/corpus/core/src/client/composer/RealtimeVoiceMode.tsx +442 -85
  12. package/corpus/core/src/client/composer/TiptapComposer.tsx +46 -29
  13. package/corpus/core/src/client/composer/VoiceButton.tsx +2 -1
  14. package/corpus/core/src/client/composer/index.ts +5 -0
  15. package/corpus/core/src/client/composer/useRealtimeVoiceMode.tsx +1206 -67
  16. package/corpus/core/src/client/index.ts +5 -0
  17. package/corpus/core/src/client/progress/RunsTray.tsx +13 -1
  18. package/corpus/core/src/client/settings/SecretsSection.tsx +387 -266
  19. package/corpus/core/src/client/settings/SettingsPanel.tsx +117 -38
  20. package/corpus/core/src/client/use-action.ts +12 -0
  21. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +8 -1
  22. package/corpus/core/src/client/use-agent-engine-configured.ts +17 -3
  23. package/corpus/core/src/client/use-db-sync.ts +84 -34
  24. package/corpus/core/src/client/use-pausing-interval.ts +14 -4
  25. package/corpus/core/src/integrations/scope-store.ts +1 -1
  26. package/corpus/core/src/integrations/usage-budget-store.ts +1 -1
  27. package/corpus/core/src/localization/default-messages.ts +44 -1
  28. package/corpus/core/src/mcp-client/workspace-servers.ts +4 -4
  29. package/corpus/core/src/secrets/crypto.ts +28 -8
  30. package/corpus/core/src/server/action-change.ts +5 -1
  31. package/corpus/core/src/server/action-routes.ts +8 -0
  32. package/corpus/core/src/server/poll.ts +1 -0
  33. package/corpus/core/src/server/realtime-voice.ts +299 -13
  34. package/corpus/core/src/styles/agent-native.css +2 -2
  35. package/corpus/core/src/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
  36. package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
  37. package/corpus/core/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
  38. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
  39. package/corpus/core/src/vite/client.ts +9 -1
  40. package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +2 -2
  41. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +10 -5
  42. package/corpus/templates/analytics/AGENTS.md +9 -13
  43. package/corpus/templates/analytics/actions/navigate.ts +59 -5
  44. package/corpus/templates/analytics/actions/view-screen.ts +3 -6
  45. package/corpus/templates/analytics/app/components/ProviderCorpusJobNotifier.tsx +20 -3
  46. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +2 -1
  47. package/corpus/templates/analytics/app/components/layout/Header.tsx +1 -1
  48. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -6
  49. package/corpus/templates/analytics/app/global.css +19 -20
  50. package/corpus/templates/analytics/app/lib/provider-corpus-job-sync.ts +26 -0
  51. package/corpus/templates/analytics/app/pages/Settings.tsx +35 -341
  52. package/corpus/templates/analytics/app/pages/monitoring/ErrorsPanel.tsx +38 -7
  53. package/corpus/templates/analytics/app/pages/monitoring/MonitoringPage.tsx +1 -1
  54. package/corpus/templates/analytics/app/pages/monitoring/errors/i18n.ts +3 -0
  55. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +333 -272
  56. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +72 -17
  57. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +297 -108
  58. package/corpus/templates/analytics/app/root.tsx +28 -8
  59. package/corpus/templates/analytics/changelog/2026-06-24-first-party-signup-and-pageview-panels-now-honor-dashboard-filters.md +2 -1
  60. package/corpus/templates/analytics/changelog/2026-06-25-referrer-dashboard-panels-show-external-domains.md +2 -1
  61. package/corpus/templates/analytics/changelog/2026-07-06-mobile-chat-header-is-more-compact-and-the-menu-button-opens-navigation.md +1 -0
  62. package/corpus/templates/analytics/changelog/2026-07-08-dashboard-history-and-chat-context.md +1 -0
  63. package/corpus/templates/analytics/changelog/2026-07-08-settings-are-cleaner-searchable-and-alerts-have-their-own-tab.md +1 -0
  64. package/corpus/templates/analytics/changelog/2026-07-08-sidebar-sections-stay-collapsed-until-opened.md +1 -0
  65. package/corpus/templates/analytics/changelog/2026-07-11-analytics-no-longer-repeatedly-restarts-background-requests-.md +6 -0
  66. package/corpus/templates/analytics/changelog/2026-07-11-analytics-only-asks-for-replay-storage-when-you-enable-sessi.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-07-11-charts-now-show-their-loading-skeleton-while-refreshing.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-07-11-realtime-voice-now-shows-a-clear-connection-indicator-comple.md +6 -0
  69. package/corpus/templates/analytics/changelog/2026-07-11-replay-event-counts-stay-hidden-until-recordings-finish-load.md +6 -0
  70. package/corpus/templates/analytics/changelog/2026-07-11-replay-storage-key-mismatches-show-actionable-setup-guidance.md +6 -0
  71. package/corpus/templates/analytics/changelog/2026-07-11-replay-timelines-group-continuous-scrolling-and-dev-tools-co.md +6 -0
  72. package/corpus/templates/analytics/changelog/2026-07-11-session-filters-now-stay-compact-in-a-settings-menu-at-mediu.md +6 -0
  73. package/corpus/templates/analytics/changelog/2026-07-11-session-replay-console-errors-now-link-to-matching-issues-or.md +6 -0
  74. package/corpus/templates/analytics/changelog/2026-07-11-session-replays-load-large-recordings-faster-and-surface-fai.md +6 -0
  75. package/corpus/templates/analytics/changelog/2026-07-11-session-replays-preserve-recorded-styles-and-viewport-layout.md +6 -0
  76. package/corpus/templates/analytics/changelog/2026-07-11-session-replays-stay-centered-instead-of-clipping-wide-recor.md +6 -0
  77. package/corpus/templates/analytics/package.json +1 -1
  78. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +1 -303
  79. package/corpus/templates/analytics/server/handlers/session-replay.ts +57 -0
  80. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +1 -1
  81. package/corpus/templates/analytics/server/lib/demo-dashboards.ts +16 -3
  82. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +12 -0
  83. package/corpus/templates/analytics/server/lib/session-replay.ts +208 -2
  84. package/corpus/templates/analytics/server/plugins/onboarding.ts +7 -79
  85. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/chunks.get.ts +1 -0
  86. package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +2 -2
  87. package/corpus/templates/assets/app/global.css +4 -4
  88. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +0 -1
  89. package/corpus/templates/assets/app/routes/library.tsx +0 -1
  90. package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +2 -2
  91. package/corpus/templates/brain/app/global.css +5 -5
  92. package/corpus/templates/brain/app/root.tsx +0 -3
  93. package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +2 -2
  94. package/corpus/templates/calendar/app/root.tsx +1 -0
  95. package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +2 -2
  96. package/corpus/templates/clips/app/hooks/use-auto-title.ts +74 -8
  97. package/corpus/templates/clips/app/hooks/use-library.ts +12 -25
  98. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +20 -22
  99. package/corpus/templates/clips/changelog/2026-07-11-generated-titles-no-longer-cause-continuous-background-refreshes.md +6 -0
  100. package/corpus/templates/clips/changelog/2026-07-11-s3-storage-setup-stays-collapsed-until-you-choose-it-as-the-.md +6 -0
  101. package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +2 -2
  102. package/corpus/templates/content/app/hooks/use-comments.ts +0 -1
  103. package/corpus/templates/content/changelog/2026-07-11-comments-now-update-from-live-collaboration-events-without-c.md +6 -0
  104. package/corpus/templates/design/app/components/layout/Header.tsx +1 -1
  105. package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +2 -2
  106. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +1 -2
  107. package/corpus/templates/macros/app/root.tsx +0 -3
  108. package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +2 -2
  109. package/corpus/templates/mail/app/hooks/use-compose-state.ts +1 -0
  110. package/corpus/templates/mail/app/root.tsx +1 -12
  111. package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +2 -2
  112. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +5 -2
  113. package/corpus/templates/plan/app/hooks/use-plans.ts +6 -13
  114. package/corpus/templates/plan/app/pages/PlansPage.tsx +5 -27
  115. package/corpus/templates/plan/changelog/2026-07-11-plan-refreshes-only-when-content-changes.md +6 -0
  116. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +4 -1
  117. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +1 -1
  118. package/corpus/templates/slides/app/components/layout/Header.tsx +1 -1
  119. package/corpus/templates/slides/app/context/DeckContext.tsx +1 -0
  120. package/corpus/templates/slides/app/hooks/use-slide-comments.ts +7 -31
  121. package/corpus/templates/slides/changelog/2026-07-11-comments-now-update-from-live-collaboration-events-without-c.md +6 -0
  122. package/dist/action-change-marker.d.ts +1 -0
  123. package/dist/action-change-marker.d.ts.map +1 -1
  124. package/dist/action-change-marker.js +7 -1
  125. package/dist/action-change-marker.js.map +1 -1
  126. package/dist/changelog/parse.d.ts +4 -2
  127. package/dist/changelog/parse.d.ts.map +1 -1
  128. package/dist/changelog/parse.js +5 -3
  129. package/dist/changelog/parse.js.map +1 -1
  130. package/dist/client/AgentPanel.d.ts.map +1 -1
  131. package/dist/client/AgentPanel.js +2 -3
  132. package/dist/client/AgentPanel.js.map +1 -1
  133. package/dist/client/AssistantChat.d.ts +7 -0
  134. package/dist/client/AssistantChat.d.ts.map +1 -1
  135. package/dist/client/AssistantChat.js +62 -40
  136. package/dist/client/AssistantChat.js.map +1 -1
  137. package/dist/client/analytics.d.ts.map +1 -1
  138. package/dist/client/analytics.js +26 -0
  139. package/dist/client/analytics.js.map +1 -1
  140. package/dist/client/blocks/library/wireframe-kit.d.ts +2 -0
  141. package/dist/client/blocks/library/wireframe-kit.d.ts.map +1 -1
  142. package/dist/client/blocks/library/wireframe-kit.js +9 -2
  143. package/dist/client/blocks/library/wireframe-kit.js.map +1 -1
  144. package/dist/client/chat-model-groups.d.ts.map +1 -1
  145. package/dist/client/chat-model-groups.js +24 -0
  146. package/dist/client/chat-model-groups.js.map +1 -1
  147. package/dist/client/composer/RealtimeVoiceMode.d.ts +40 -4
  148. package/dist/client/composer/RealtimeVoiceMode.d.ts.map +1 -1
  149. package/dist/client/composer/RealtimeVoiceMode.js +172 -42
  150. package/dist/client/composer/RealtimeVoiceMode.js.map +1 -1
  151. package/dist/client/composer/TiptapComposer.d.ts +4 -0
  152. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  153. package/dist/client/composer/TiptapComposer.js +47 -29
  154. package/dist/client/composer/TiptapComposer.js.map +1 -1
  155. package/dist/client/composer/VoiceButton.d.ts.map +1 -1
  156. package/dist/client/composer/VoiceButton.js +2 -2
  157. package/dist/client/composer/VoiceButton.js.map +1 -1
  158. package/dist/client/composer/index.d.ts +2 -2
  159. package/dist/client/composer/index.d.ts.map +1 -1
  160. package/dist/client/composer/index.js +1 -1
  161. package/dist/client/composer/index.js.map +1 -1
  162. package/dist/client/composer/useRealtimeVoiceMode.d.ts +100 -3
  163. package/dist/client/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  164. package/dist/client/composer/useRealtimeVoiceMode.js +952 -51
  165. package/dist/client/composer/useRealtimeVoiceMode.js.map +1 -1
  166. package/dist/client/index.d.ts +1 -1
  167. package/dist/client/index.d.ts.map +1 -1
  168. package/dist/client/index.js +1 -1
  169. package/dist/client/index.js.map +1 -1
  170. package/dist/client/progress/RunsTray.d.ts.map +1 -1
  171. package/dist/client/progress/RunsTray.js +10 -1
  172. package/dist/client/progress/RunsTray.js.map +1 -1
  173. package/dist/client/settings/SecretsSection.d.ts +2 -3
  174. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  175. package/dist/client/settings/SecretsSection.js +46 -22
  176. package/dist/client/settings/SecretsSection.js.map +1 -1
  177. package/dist/client/settings/SettingsPanel.d.ts +9 -0
  178. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  179. package/dist/client/settings/SettingsPanel.js +46 -12
  180. package/dist/client/settings/SettingsPanel.js.map +1 -1
  181. package/dist/client/use-action.d.ts +2 -0
  182. package/dist/client/use-action.d.ts.map +1 -1
  183. package/dist/client/use-action.js +10 -0
  184. package/dist/client/use-action.js.map +1 -1
  185. package/dist/client/use-agent-chat-home-handoff.d.ts +2 -0
  186. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  187. package/dist/client/use-agent-chat-home-handoff.js +7 -1
  188. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  189. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  190. package/dist/client/use-agent-engine-configured.js +17 -3
  191. package/dist/client/use-agent-engine-configured.js.map +1 -1
  192. package/dist/client/use-db-sync.d.ts +8 -2
  193. package/dist/client/use-db-sync.d.ts.map +1 -1
  194. package/dist/client/use-db-sync.js +57 -32
  195. package/dist/client/use-db-sync.js.map +1 -1
  196. package/dist/client/use-pausing-interval.d.ts.map +1 -1
  197. package/dist/client/use-pausing-interval.js +16 -4
  198. package/dist/client/use-pausing-interval.js.map +1 -1
  199. package/dist/integrations/scope-store.js +1 -1
  200. package/dist/integrations/scope-store.js.map +1 -1
  201. package/dist/integrations/usage-budget-store.js +1 -1
  202. package/dist/integrations/usage-budget-store.js.map +1 -1
  203. package/dist/localization/default-messages.d.ts +41 -1
  204. package/dist/localization/default-messages.d.ts.map +1 -1
  205. package/dist/localization/default-messages.js +41 -1
  206. package/dist/localization/default-messages.js.map +1 -1
  207. package/dist/mcp-client/workspace-servers.js +4 -4
  208. package/dist/mcp-client/workspace-servers.js.map +1 -1
  209. package/dist/observability/routes.d.ts +5 -5
  210. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  211. package/dist/secrets/crypto.d.ts +7 -6
  212. package/dist/secrets/crypto.d.ts.map +1 -1
  213. package/dist/secrets/crypto.js +30 -11
  214. package/dist/secrets/crypto.js.map +1 -1
  215. package/dist/secrets/routes.d.ts +9 -9
  216. package/dist/server/action-change.d.ts +1 -0
  217. package/dist/server/action-change.d.ts.map +1 -1
  218. package/dist/server/action-change.js +4 -1
  219. package/dist/server/action-change.js.map +1 -1
  220. package/dist/server/action-routes.d.ts.map +1 -1
  221. package/dist/server/action-routes.js +5 -0
  222. package/dist/server/action-routes.js.map +1 -1
  223. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  224. package/dist/server/poll.d.ts.map +1 -1
  225. package/dist/server/poll.js +1 -0
  226. package/dist/server/poll.js.map +1 -1
  227. package/dist/server/realtime-voice.d.ts +7 -0
  228. package/dist/server/realtime-voice.d.ts.map +1 -1
  229. package/dist/server/realtime-voice.js +222 -12
  230. package/dist/server/realtime-voice.js.map +1 -1
  231. package/dist/styles/agent-native.css +2 -2
  232. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
  233. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
  234. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
  235. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
  236. package/dist/vite/client.d.ts.map +1 -1
  237. package/dist/vite/client.js +6 -1
  238. package/dist/vite/client.js.map +1 -1
  239. package/package.json +2 -2
  240. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
  241. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
  242. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
  243. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * <SecretsSection /> — renders the registered secrets from the framework
3
- * secrets registry. Fetches `/_agent-native/secrets` on mount and shows a
4
- * card per secret with a masked input + Save / Rotate / Delete / Test
5
- * buttons (api-key kind) or a Connect / Disconnect button (oauth kind).
3
+ * secrets registry. Configured keys stay compact; adding or editing one
4
+ * progressively discloses its controls.
6
5
  */
7
6
 
8
7
  import {
9
8
  IconCheck,
9
+ IconChevronRight,
10
10
  IconExternalLink,
11
11
  IconLoader2,
12
12
  IconPlugConnected,
@@ -17,6 +17,14 @@ import {
17
17
  import React, { useEffect, useMemo, useState, useCallback } from "react";
18
18
 
19
19
  import { agentNativePath } from "../api-path.js";
20
+ import {
21
+ DropdownMenu,
22
+ DropdownMenuContent,
23
+ DropdownMenuItem,
24
+ DropdownMenuLabel,
25
+ DropdownMenuSeparator,
26
+ DropdownMenuTrigger,
27
+ } from "../components/ui/dropdown-menu.js";
20
28
  import {
21
29
  Tooltip,
22
30
  TooltipContent,
@@ -59,6 +67,10 @@ export function SecretsSection({ focusKey }: SecretsSectionProps) {
59
67
  const [secrets, setSecrets] = useState<SecretStatus[] | null>(null);
60
68
  const [error, setError] = useState<string | null>(null);
61
69
  const [reloadToken, setReloadToken] = useState(0);
70
+ const [openSecretKey, setOpenSecretKey] = useState<string | null>(
71
+ focusKey ?? null,
72
+ );
73
+ const [customKeyOpen, setCustomKeyOpen] = useState(false);
62
74
 
63
75
  useEffect(() => {
64
76
  let cancelled = false;
@@ -82,6 +94,13 @@ export function SecretsSection({ focusKey }: SecretsSectionProps) {
82
94
 
83
95
  const reload = useCallback(() => setReloadToken((t) => t + 1), []);
84
96
 
97
+ useEffect(() => {
98
+ if (focusKey) {
99
+ setCustomKeyOpen(false);
100
+ setOpenSecretKey(focusKey);
101
+ }
102
+ }, [focusKey]);
103
+
85
104
  if (error) {
86
105
  return (
87
106
  <p className="text-[10px] text-red-500">
@@ -99,27 +118,112 @@ export function SecretsSection({ focusKey }: SecretsSectionProps) {
99
118
  }
100
119
  if (secrets.length === 0) {
101
120
  return (
102
- <div className="space-y-2">
103
- <p className="text-[10px] text-muted-foreground">
104
- No secrets registered yet. Templates register API keys and connections
105
- via <code>registerRequiredSecret()</code>.
106
- </p>
107
- <AdHocKeysSection />
121
+ <div className="space-y-3">
122
+ <KeysHeader onCustomKey={() => setCustomKeyOpen(true)} />
123
+ <AdHocKeysSection
124
+ showForm={customKeyOpen}
125
+ onShowFormChange={setCustomKeyOpen}
126
+ showEmptyState
127
+ />
108
128
  </div>
109
129
  );
110
130
  }
111
131
 
132
+ const visibleSecrets = secrets.filter(
133
+ (secret) => secret.status !== "unset" || secret.key === openSecretKey,
134
+ );
135
+ const availableSecrets = secrets.filter(
136
+ (secret) => secret.status === "unset" && secret.key !== openSecretKey,
137
+ );
138
+
112
139
  return (
113
- <div className="space-y-2">
114
- {secrets.map((secret) => (
115
- <SecretCard
116
- key={secret.key}
117
- secret={secret}
118
- onChanged={reload}
119
- focusInput={focusKey === secret.key}
120
- />
121
- ))}
122
- <AdHocKeysSection />
140
+ <div className="space-y-3">
141
+ <KeysHeader
142
+ availableSecrets={availableSecrets}
143
+ onSecret={(key) => {
144
+ setCustomKeyOpen(false);
145
+ setOpenSecretKey(key);
146
+ }}
147
+ onCustomKey={() => {
148
+ setOpenSecretKey(null);
149
+ setCustomKeyOpen(true);
150
+ }}
151
+ />
152
+ {visibleSecrets.length > 0 && (
153
+ <div className="overflow-hidden rounded-md border border-border">
154
+ {visibleSecrets.map((secret) => (
155
+ <SecretCard
156
+ key={secret.key}
157
+ secret={secret}
158
+ onChanged={reload}
159
+ open={openSecretKey === secret.key}
160
+ onOpenChange={(open) => {
161
+ if (open) setCustomKeyOpen(false);
162
+ setOpenSecretKey(open ? secret.key : null);
163
+ }}
164
+ focusInput={openSecretKey === secret.key}
165
+ />
166
+ ))}
167
+ </div>
168
+ )}
169
+ <AdHocKeysSection
170
+ showForm={customKeyOpen}
171
+ onShowFormChange={setCustomKeyOpen}
172
+ showEmptyState={visibleSecrets.length === 0}
173
+ />
174
+ </div>
175
+ );
176
+ }
177
+
178
+ function KeysHeader({
179
+ availableSecrets = [],
180
+ onSecret,
181
+ onCustomKey,
182
+ }: {
183
+ availableSecrets?: SecretStatus[];
184
+ onSecret?: (key: string) => void;
185
+ onCustomKey: () => void;
186
+ }) {
187
+ return (
188
+ <div className="flex items-center justify-between gap-3">
189
+ <p className="text-[11px] font-medium text-foreground">Keys</p>
190
+ <DropdownMenu>
191
+ <DropdownMenuTrigger asChild>
192
+ <button
193
+ type="button"
194
+ className="inline-flex items-center gap-1 rounded-md border border-border px-2 py-1 text-[10px] font-medium text-muted-foreground transition-colors hover:bg-accent/40 hover:text-foreground"
195
+ >
196
+ <IconPlus size={11} />
197
+ New
198
+ </button>
199
+ </DropdownMenuTrigger>
200
+ <DropdownMenuContent align="end" className="w-60">
201
+ {availableSecrets.length > 0 && (
202
+ <>
203
+ <DropdownMenuLabel>Choose a key</DropdownMenuLabel>
204
+ {availableSecrets.map((secret) => (
205
+ <DropdownMenuItem
206
+ key={secret.key}
207
+ onSelect={() => onSecret?.(secret.key)}
208
+ className="flex items-center justify-between gap-3"
209
+ >
210
+ <span className="truncate">{secret.label}</span>
211
+ {secret.required && (
212
+ <span className="text-[9px] font-semibold uppercase tracking-wide text-amber-600 dark:text-amber-400">
213
+ Required
214
+ </span>
215
+ )}
216
+ </DropdownMenuItem>
217
+ ))}
218
+ <DropdownMenuSeparator />
219
+ </>
220
+ )}
221
+ <DropdownMenuItem onSelect={onCustomKey}>
222
+ <IconPlus size={14} />
223
+ Custom
224
+ </DropdownMenuItem>
225
+ </DropdownMenuContent>
226
+ </DropdownMenu>
123
227
  </div>
124
228
  );
125
229
  }
@@ -127,10 +231,18 @@ export function SecretsSection({ focusKey }: SecretsSectionProps) {
127
231
  interface SecretCardProps {
128
232
  secret: SecretStatus;
129
233
  onChanged: () => void;
234
+ open: boolean;
235
+ onOpenChange: (open: boolean) => void;
130
236
  focusInput?: boolean;
131
237
  }
132
238
 
133
- function SecretCard({ secret, onChanged, focusInput }: SecretCardProps) {
239
+ function SecretCard({
240
+ secret,
241
+ onChanged,
242
+ open,
243
+ onOpenChange,
244
+ focusInput,
245
+ }: SecretCardProps) {
134
246
  const [value, setValue] = useState("");
135
247
  const [busy, setBusy] = useState<null | "save" | "delete" | "test">(null);
136
248
  const [confirmDelete, setConfirmDelete] = useState(false);
@@ -141,10 +253,10 @@ function SecretCard({ secret, onChanged, focusInput }: SecretCardProps) {
141
253
  const inputRef = React.useRef<HTMLInputElement>(null);
142
254
 
143
255
  useEffect(() => {
144
- if (focusInput && inputRef.current) {
256
+ if (open && focusInput && inputRef.current) {
145
257
  inputRef.current.focus();
146
258
  }
147
- }, [focusInput]);
259
+ }, [focusInput, open]);
148
260
 
149
261
  const setToastAndClear = (kind: "ok" | "err", text: string, ms = 2500) => {
150
262
  setToast({ kind, text });
@@ -256,164 +368,183 @@ function SecretCard({ secret, onChanged, focusInput }: SecretCardProps) {
256
368
  const isOAuth = secret.kind === "oauth";
257
369
 
258
370
  return (
259
- <div className="rounded-md border border-border px-2.5 py-2 bg-accent/30">
260
- <div className="flex items-center justify-between gap-2">
261
- <div className="min-w-0">
262
- <div className="text-[11px] font-medium text-foreground truncate">
263
- {secret.label}
264
- </div>
371
+ <div className="border-b border-border last:border-b-0">
372
+ <button
373
+ type="button"
374
+ aria-expanded={open}
375
+ onClick={() => onOpenChange(!open)}
376
+ className="flex w-full items-center gap-2 px-2.5 py-2 text-start transition-colors hover:bg-accent/30"
377
+ >
378
+ <IconChevronRight
379
+ size={13}
380
+ className={`shrink-0 text-muted-foreground transition-transform ${open ? "rotate-90" : ""}`}
381
+ />
382
+ <span className="min-w-0 flex-1 truncate text-[11px] font-medium text-foreground">
383
+ {secret.label}
384
+ </span>
385
+ {secret.status === "set" && secret.last4 && (
386
+ <code className="text-[10px] text-muted-foreground">
387
+ ••••{secret.last4}
388
+ </code>
389
+ )}
390
+ <span className="shrink-0">{pill}</span>
391
+ </button>
392
+
393
+ {open && (
394
+ <div className="border-t border-border/60 bg-accent/20 px-3 pb-3 pt-2.5">
265
395
  {secret.description && (
266
- <p className="text-[10px] text-muted-foreground mt-0.5">
396
+ <p className="mb-2 text-[10px] leading-relaxed text-muted-foreground">
267
397
  {secret.description}
268
398
  </p>
269
399
  )}
270
- </div>
271
- <div className="shrink-0">{pill}</div>
272
- </div>
273
-
274
- {isOAuth ? (
275
- <div className="mt-2 flex items-center gap-1.5">
276
- {secret.oauthConnectUrl && (
277
- <a
278
- href={secret.oauthConnectUrl}
279
- className="inline-flex items-center gap-1 rounded px-2 py-1 text-[10px] font-medium no-underline"
280
- style={{ backgroundColor: "#00B5FF", color: "white" }}
281
- >
282
- <IconPlugConnected size={10} />
283
- {secret.status === "set" ? "Reconnect" : "Connect"}
284
- </a>
285
- )}
286
- {secret.docsUrl && (
287
- <a
288
- href={secret.docsUrl}
289
- target="_blank"
290
- rel="noopener noreferrer"
291
- className="inline-flex items-center gap-1 rounded border border-border px-2 py-1 text-[10px] no-underline text-muted-foreground hover:text-foreground"
292
- >
293
- Docs
294
- <IconExternalLink size={10} />
295
- </a>
296
- )}
297
- </div>
298
- ) : (
299
- <div className="mt-2 space-y-1.5">
300
- {secret.status === "set" && (
301
- <div className="flex items-center gap-2 text-[10px] text-muted-foreground">
302
- <span>Stored value ending in</span>
303
- <code className="rounded bg-background px-1 py-0.5 text-foreground">
304
- {secret.last4}
305
- </code>
400
+ {isOAuth ? (
401
+ <div className="mt-2 flex items-center gap-1.5">
402
+ {secret.oauthConnectUrl && (
403
+ <a
404
+ href={secret.oauthConnectUrl}
405
+ className="inline-flex items-center gap-1 rounded px-2 py-1 text-[10px] font-medium no-underline"
406
+ style={{ backgroundColor: "#00B5FF", color: "white" }}
407
+ >
408
+ <IconPlugConnected size={10} />
409
+ {secret.status === "set" ? "Reconnect" : "Connect"}
410
+ </a>
411
+ )}
412
+ {secret.docsUrl && (
413
+ <a
414
+ href={secret.docsUrl}
415
+ target="_blank"
416
+ rel="noopener noreferrer"
417
+ className="inline-flex items-center gap-1 rounded border border-border px-2 py-1 text-[10px] no-underline text-muted-foreground hover:text-foreground"
418
+ >
419
+ Docs
420
+ <IconExternalLink size={10} />
421
+ </a>
422
+ )}
306
423
  </div>
307
- )}
308
- <div className="flex gap-1.5">
309
- <input
310
- ref={inputRef}
311
- type="password"
312
- value={value}
313
- onChange={(e) => setValue(e.target.value)}
314
- onKeyDown={(e) => {
315
- if (e.key === "Enter") handleSave();
316
- }}
317
- placeholder={
318
- secret.status === "set"
319
- ? "Enter new value to rotate"
320
- : "Paste key"
321
- }
322
- className="flex-1 rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
323
- />
324
- <button
325
- type="button"
326
- onClick={handleSave}
327
- disabled={!value.trim() || busy !== null}
328
- className="inline-flex items-center gap-1 rounded px-2 py-1 text-[10px] font-medium disabled:opacity-40"
329
- style={{ backgroundColor: "#00B5FF", color: "white" }}
330
- >
331
- {busy === "save" ? (
332
- <IconLoader2 size={10} className="animate-spin" />
333
- ) : secret.status === "set" ? (
334
- <>
335
- <IconRefresh size={10} />
336
- Rotate
337
- </>
338
- ) : (
339
- "Save"
424
+ ) : (
425
+ <div className="mt-2 space-y-1.5">
426
+ {secret.status === "set" && (
427
+ <div className="flex items-center gap-2 text-[10px] text-muted-foreground">
428
+ <span>Stored value ending in</span>
429
+ <code className="rounded bg-background px-1 py-0.5 text-foreground">
430
+ {secret.last4}
431
+ </code>
432
+ </div>
340
433
  )}
341
- </button>
342
- </div>
343
- <div className="flex items-center gap-1.5">
344
- {secret.status === "set" && (
345
- <>
434
+ <div className="flex gap-1.5">
435
+ <input
436
+ ref={inputRef}
437
+ type="password"
438
+ aria-label={secret.label}
439
+ value={value}
440
+ onChange={(e) => setValue(e.target.value)}
441
+ onKeyDown={(e) => {
442
+ if (e.key === "Enter") handleSave();
443
+ }}
444
+ placeholder={
445
+ secret.status === "set"
446
+ ? "Enter new value to rotate"
447
+ : "Paste key"
448
+ }
449
+ className="flex-1 rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
450
+ />
346
451
  <button
347
452
  type="button"
348
- onClick={handleTest}
349
- disabled={busy !== null}
350
- className="inline-flex items-center gap-1 rounded border border-border px-2 py-1 text-[10px] text-muted-foreground hover:text-foreground disabled:opacity-40"
453
+ onClick={handleSave}
454
+ disabled={!value.trim() || busy !== null}
455
+ className="inline-flex items-center gap-1 rounded px-2 py-1 text-[10px] font-medium disabled:opacity-40"
456
+ style={{ backgroundColor: "#00B5FF", color: "white" }}
351
457
  >
352
- {busy === "test" ? (
458
+ {busy === "save" ? (
353
459
  <IconLoader2 size={10} className="animate-spin" />
460
+ ) : secret.status === "set" ? (
461
+ <>
462
+ <IconRefresh size={10} />
463
+ Rotate
464
+ </>
354
465
  ) : (
355
- "Test"
466
+ "Save"
356
467
  )}
357
468
  </button>
358
- <button
359
- type="button"
360
- onClick={() => setConfirmDelete(true)}
361
- disabled={busy !== null}
362
- className="inline-flex items-center gap-1 rounded border border-border px-2 py-1 text-[10px] text-muted-foreground hover:text-red-500 disabled:opacity-40"
363
- >
364
- <IconTrash size={10} />
365
- Remove
366
- </button>
367
- </>
368
- )}
369
- {secret.docsUrl && (
370
- <a
371
- href={secret.docsUrl}
372
- target="_blank"
373
- rel="noopener noreferrer"
374
- className="inline-flex items-center gap-1 rounded border border-border px-2 py-1 text-[10px] no-underline text-muted-foreground hover:text-foreground ms-auto"
375
- >
376
- Get key
377
- <IconExternalLink size={10} />
378
- </a>
379
- )}
380
- </div>
381
- {confirmDelete && (
382
- <div className="flex items-center gap-1.5 rounded border border-red-500/30 bg-red-500/10 px-2 py-1.5 text-[10px] text-red-500">
383
- <span className="min-w-0 flex-1">Remove this saved value?</span>
384
- <button
385
- type="button"
386
- onClick={handleDelete}
387
- disabled={busy !== null}
388
- className="inline-flex items-center gap-1 rounded border border-red-500/40 px-1.5 py-0.5 font-medium disabled:opacity-40"
389
- >
390
- {busy === "delete" ? (
391
- <IconLoader2 size={10} className="animate-spin" />
392
- ) : (
393
- "Confirm"
469
+ </div>
470
+ <div className="flex items-center gap-1.5">
471
+ {secret.status === "set" && (
472
+ <>
473
+ <button
474
+ type="button"
475
+ onClick={handleTest}
476
+ disabled={busy !== null}
477
+ className="inline-flex items-center gap-1 rounded border border-border px-2 py-1 text-[10px] text-muted-foreground hover:text-foreground disabled:opacity-40"
478
+ >
479
+ {busy === "test" ? (
480
+ <IconLoader2 size={10} className="animate-spin" />
481
+ ) : (
482
+ "Test"
483
+ )}
484
+ </button>
485
+ <button
486
+ type="button"
487
+ onClick={() => setConfirmDelete(true)}
488
+ disabled={busy !== null}
489
+ className="inline-flex items-center gap-1 rounded border border-border px-2 py-1 text-[10px] text-muted-foreground hover:text-red-500 disabled:opacity-40"
490
+ >
491
+ <IconTrash size={10} />
492
+ Remove
493
+ </button>
494
+ </>
394
495
  )}
395
- </button>
396
- <button
397
- type="button"
398
- onClick={() => setConfirmDelete(false)}
399
- disabled={busy !== null}
400
- className="rounded border border-border px-1.5 py-0.5 text-muted-foreground hover:text-foreground disabled:opacity-40"
401
- >
402
- Cancel
403
- </button>
496
+ {secret.docsUrl && (
497
+ <a
498
+ href={secret.docsUrl}
499
+ target="_blank"
500
+ rel="noopener noreferrer"
501
+ className="inline-flex items-center gap-1 rounded border border-border px-2 py-1 text-[10px] no-underline text-muted-foreground hover:text-foreground ms-auto"
502
+ >
503
+ Get key
504
+ <IconExternalLink size={10} />
505
+ </a>
506
+ )}
507
+ </div>
508
+ {confirmDelete && (
509
+ <div className="flex items-center gap-1.5 rounded border border-red-500/30 bg-red-500/10 px-2 py-1.5 text-[10px] text-red-500">
510
+ <span className="min-w-0 flex-1">
511
+ Remove this saved value?
512
+ </span>
513
+ <button
514
+ type="button"
515
+ onClick={handleDelete}
516
+ disabled={busy !== null}
517
+ className="inline-flex items-center gap-1 rounded border border-red-500/40 px-1.5 py-0.5 font-medium disabled:opacity-40"
518
+ >
519
+ {busy === "delete" ? (
520
+ <IconLoader2 size={10} className="animate-spin" />
521
+ ) : (
522
+ "Confirm"
523
+ )}
524
+ </button>
525
+ <button
526
+ type="button"
527
+ onClick={() => setConfirmDelete(false)}
528
+ disabled={busy !== null}
529
+ className="rounded border border-border px-1.5 py-0.5 text-muted-foreground hover:text-foreground disabled:opacity-40"
530
+ >
531
+ Cancel
532
+ </button>
533
+ </div>
534
+ )}
404
535
  </div>
405
536
  )}
406
- </div>
407
- )}
408
537
 
409
- {toast && (
410
- <p
411
- className={`mt-1.5 text-[10px] ${
412
- toast.kind === "ok" ? "text-green-500" : "text-red-500"
413
- }`}
414
- >
415
- {toast.text}
416
- </p>
538
+ {toast && (
539
+ <p
540
+ className={`mt-1.5 text-[10px] ${
541
+ toast.kind === "ok" ? "text-green-500" : "text-red-500"
542
+ }`}
543
+ >
544
+ {toast.text}
545
+ </p>
546
+ )}
547
+ </div>
417
548
  )}
418
549
  </div>
419
550
  );
@@ -433,11 +564,18 @@ interface AdHocKey {
433
564
 
434
565
  const ADHOC_ENDPOINT = agentNativePath("/_agent-native/secrets/adhoc");
435
566
 
436
- function AdHocKeysSection() {
567
+ function AdHocKeysSection({
568
+ showForm,
569
+ onShowFormChange,
570
+ showEmptyState,
571
+ }: {
572
+ showForm: boolean;
573
+ onShowFormChange: (show: boolean) => void;
574
+ showEmptyState: boolean;
575
+ }) {
437
576
  const [keys, setKeys] = useState<AdHocKey[]>([]);
438
577
  const [loading, setLoading] = useState(true);
439
578
  const [reloadToken, setReloadToken] = useState(0);
440
- const [showForm, setShowForm] = useState(false);
441
579
  const [formName, setFormName] = useState("");
442
580
  const [formValue, setFormValue] = useState("");
443
581
  const [formDescription, setFormDescription] = useState("");
@@ -489,13 +627,13 @@ function AdHocKeysSection() {
489
627
  }, [reloadToken]);
490
628
 
491
629
  const resetForm = useCallback(() => {
492
- setShowForm(false);
630
+ onShowFormChange(false);
493
631
  setFormName("");
494
632
  setFormValue("");
495
633
  setFormDescription("");
496
634
  setFormScope("user");
497
635
  setFormError(null);
498
- }, []);
636
+ }, [onShowFormChange]);
499
637
 
500
638
  const handleAdd = useCallback(async () => {
501
639
  const name = formName.trim();
@@ -524,6 +662,7 @@ function AdHocKeysSection() {
524
662
  }
525
663
  resetForm();
526
664
  showToast("ok", "Key saved");
665
+ notifySecretsChanged();
527
666
  reload();
528
667
  } catch (err: any) {
529
668
  setFormError(err?.message ?? "Failed to save");
@@ -558,6 +697,7 @@ function AdHocKeysSection() {
558
697
  }
559
698
  showToast("ok", "Key deleted");
560
699
  setConfirmDeleteName(null);
700
+ notifySecretsChanged();
561
701
  reload();
562
702
  } finally {
563
703
  setDeletingName(null);
@@ -567,30 +707,7 @@ function AdHocKeysSection() {
567
707
  );
568
708
 
569
709
  return (
570
- <div className="mt-3 space-y-2">
571
- <div className="flex items-center justify-between">
572
- <p className="text-[11px] font-medium text-foreground">
573
- Additional Keys
574
- </p>
575
- {!showForm && (
576
- <button
577
- type="button"
578
- onClick={() => setShowForm(true)}
579
- className="inline-flex items-center gap-1 rounded border border-border px-2 py-0.5 text-[10px] font-medium text-muted-foreground hover:text-foreground hover:bg-accent/40"
580
- >
581
- <IconPlus size={10} />
582
- Add Key
583
- </button>
584
- )}
585
- </div>
586
- <p className="text-[10px] text-muted-foreground/60 leading-relaxed">
587
- Keys are referenced in automations as{" "}
588
- <code className="rounded bg-background px-1 py-0.5 text-[9px]">
589
- {"${keys.KEY_NAME}"}
590
- </code>
591
- . Values are encrypted and never shown to the AI agent.
592
- </p>
593
-
710
+ <div className="space-y-2">
594
711
  {showForm && (
595
712
  <div className="rounded-md border border-border px-2.5 py-2 bg-accent/30 space-y-1.5">
596
713
  <input
@@ -601,10 +718,12 @@ function AdHocKeysSection() {
601
718
  )
602
719
  }
603
720
  className="w-full rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
604
- placeholder="KEY_NAME (e.g. SLACK_WEBHOOK)"
721
+ aria-label="Key name"
722
+ placeholder="KEY_NAME"
605
723
  />
606
724
  <input
607
725
  type="password"
726
+ aria-label="Secret value"
608
727
  value={formValue}
609
728
  onChange={(e) => setFormValue(e.target.value)}
610
729
  className="w-full rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
@@ -612,12 +731,14 @@ function AdHocKeysSection() {
612
731
  />
613
732
  <input
614
733
  value={formDescription}
734
+ aria-label="Description"
615
735
  onChange={(e) => setFormDescription(e.target.value)}
616
736
  className="w-full rounded border border-border bg-background px-2 py-1 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
617
737
  placeholder="Description (optional)"
618
738
  />
619
739
  <div className="flex items-center gap-2">
620
740
  <select
741
+ aria-label="Scope"
621
742
  value={formScope}
622
743
  onChange={(e) =>
623
744
  setFormScope(e.target.value as "user" | "workspace")
@@ -659,88 +780,88 @@ function AdHocKeysSection() {
659
780
  <IconLoader2 size={10} className="animate-spin" />
660
781
  Loading...
661
782
  </div>
662
- ) : keys.length === 0 && !showForm ? (
663
- <p className="text-[10px] text-muted-foreground">
664
- No additional keys yet.
665
- </p>
666
- ) : (
667
- keys.map((key) => (
668
- <div
669
- key={`${key.scope}-${key.name}`}
670
- className="rounded-md border border-border px-2.5 py-2 bg-accent/30"
671
- >
672
- <div className="flex items-center justify-between gap-2">
673
- <div className="min-w-0 flex-1">
674
- <div className="flex items-center gap-1.5">
675
- <span className="text-[11px] font-medium text-foreground font-mono truncate">
676
- {key.name}
677
- </span>
678
- <span
679
- className={`rounded-full px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide ${
680
- key.scope === "workspace"
681
- ? "bg-blue-500/15 text-blue-500"
682
- : "bg-accent/60 text-muted-foreground"
683
- }`}
684
- >
685
- {key.scope === "workspace" ? "workspace" : "personal"}
686
- </span>
687
- </div>
688
- {key.description && (
689
- <p className="text-[10px] text-muted-foreground mt-0.5">
690
- {key.description}
691
- </p>
692
- )}
693
- <div className="flex items-center gap-2 text-[10px] text-muted-foreground mt-0.5">
694
- <span>
695
- Ending in{" "}
696
- <code className="rounded bg-background px-1 py-0.5 text-foreground">
697
- {key.last4}
698
- </code>
699
- </span>
700
- </div>
701
- </div>
702
- <div className="shrink-0">
703
- {confirmDeleteName === key.name ? (
704
- <div className="flex items-center gap-1">
705
- <button
706
- type="button"
707
- onClick={() => handleDelete(key.name)}
708
- disabled={deletingName === key.name}
709
- className="rounded px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide bg-red-500/15 text-red-500 hover:bg-red-500/25 disabled:opacity-40"
710
- >
711
- {deletingName === key.name ? (
712
- <IconLoader2 size={10} className="animate-spin" />
713
- ) : (
714
- "Confirm"
715
- )}
716
- </button>
717
- <button
718
- type="button"
719
- onClick={() => setConfirmDeleteName(null)}
720
- className="rounded px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide bg-accent/60 text-muted-foreground hover:text-foreground"
783
+ ) : keys.length === 0 && !showForm && showEmptyState ? (
784
+ <p className="text-[10px] text-muted-foreground">No keys added yet.</p>
785
+ ) : keys.length > 0 ? (
786
+ <div className="overflow-hidden rounded-md border border-border">
787
+ {keys.map((key) => (
788
+ <div
789
+ key={`${key.scope}-${key.name}`}
790
+ className="border-b border-border px-2.5 py-2 last:border-b-0"
791
+ >
792
+ <div className="flex items-center justify-between gap-2">
793
+ <div className="min-w-0 flex-1">
794
+ <div className="flex items-center gap-1.5">
795
+ <span className="text-[11px] font-medium text-foreground font-mono truncate">
796
+ {key.name}
797
+ </span>
798
+ <span
799
+ className={`rounded-full px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide ${
800
+ key.scope === "workspace"
801
+ ? "bg-blue-500/15 text-blue-500"
802
+ : "bg-accent/60 text-muted-foreground"
803
+ }`}
721
804
  >
722
- Cancel
723
- </button>
805
+ {key.scope === "workspace" ? "workspace" : "personal"}
806
+ </span>
724
807
  </div>
725
- ) : (
726
- <Tooltip>
727
- <TooltipTrigger asChild>
808
+ {key.description && (
809
+ <p className="text-[10px] text-muted-foreground mt-0.5">
810
+ {key.description}
811
+ </p>
812
+ )}
813
+ <div className="flex items-center gap-2 text-[10px] text-muted-foreground mt-0.5">
814
+ <span>
815
+ Ending in{" "}
816
+ <code className="rounded bg-background px-1 py-0.5 text-foreground">
817
+ {key.last4}
818
+ </code>
819
+ </span>
820
+ </div>
821
+ </div>
822
+ <div className="shrink-0">
823
+ {confirmDeleteName === key.name ? (
824
+ <div className="flex items-center gap-1">
728
825
  <button
729
826
  type="button"
730
- onClick={() => setConfirmDeleteName(key.name)}
731
- className="text-muted-foreground hover:text-red-500"
827
+ onClick={() => handleDelete(key.name)}
828
+ disabled={deletingName === key.name}
829
+ className="rounded px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide bg-red-500/15 text-red-500 hover:bg-red-500/25 disabled:opacity-40"
732
830
  >
733
- <IconTrash size={12} />
831
+ {deletingName === key.name ? (
832
+ <IconLoader2 size={10} className="animate-spin" />
833
+ ) : (
834
+ "Confirm"
835
+ )}
734
836
  </button>
735
- </TooltipTrigger>
736
- <TooltipContent>Delete</TooltipContent>
737
- </Tooltip>
738
- )}
837
+ <button
838
+ type="button"
839
+ onClick={() => setConfirmDeleteName(null)}
840
+ className="rounded px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide bg-accent/60 text-muted-foreground hover:text-foreground"
841
+ >
842
+ Cancel
843
+ </button>
844
+ </div>
845
+ ) : (
846
+ <Tooltip>
847
+ <TooltipTrigger asChild>
848
+ <button
849
+ type="button"
850
+ onClick={() => setConfirmDeleteName(key.name)}
851
+ className="text-muted-foreground hover:text-red-500"
852
+ >
853
+ <IconTrash size={12} />
854
+ </button>
855
+ </TooltipTrigger>
856
+ <TooltipContent>Delete</TooltipContent>
857
+ </Tooltip>
858
+ )}
859
+ </div>
739
860
  </div>
740
861
  </div>
741
- </div>
742
- ))
743
- )}
862
+ ))}
863
+ </div>
864
+ ) : null}
744
865
 
745
866
  {toast && (
746
867
  <p