@elizaos/client 1.5.5-alpha.10

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 (209) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +350 -0
  3. package/dist/assets/empty-module-CLMscLYw.js +1 -0
  4. package/dist/assets/main-BBZ_3lkn.css +5999 -0
  5. package/dist/assets/main-C5zNUkXH.js +7 -0
  6. package/dist/assets/main-Dz64ENQg.js +614 -0
  7. package/dist/assets/react-vendor-DM5m98rr.js +545 -0
  8. package/dist/assets/ui-vendor-BQCqNqg0.js +1 -0
  9. package/dist/elizaos-avatar.png +0 -0
  10. package/dist/elizaos-icon.png +0 -0
  11. package/dist/elizaos-logo-light.png +0 -0
  12. package/dist/elizaos.webp +0 -0
  13. package/dist/favicon.ico +0 -0
  14. package/dist/images/agents/agent1.png +0 -0
  15. package/dist/images/agents/agent2.png +0 -0
  16. package/dist/images/agents/agent3.png +0 -0
  17. package/dist/images/agents/agent4.png +0 -0
  18. package/dist/images/agents/agent5.png +0 -0
  19. package/dist/index.html +14 -0
  20. package/index.html +24 -0
  21. package/package.json +159 -0
  22. package/postcss.config.js +3 -0
  23. package/public/elizaos-avatar.png +0 -0
  24. package/public/elizaos-icon.png +0 -0
  25. package/public/elizaos-logo-light.png +0 -0
  26. package/public/elizaos.webp +0 -0
  27. package/public/favicon.ico +0 -0
  28. package/public/images/agents/agent1.png +0 -0
  29. package/public/images/agents/agent2.png +0 -0
  30. package/public/images/agents/agent3.png +0 -0
  31. package/public/images/agents/agent4.png +0 -0
  32. package/public/images/agents/agent5.png +0 -0
  33. package/src/App.tsx +222 -0
  34. package/src/components/AgentDetailsPanel.tsx +147 -0
  35. package/src/components/ChatInputArea.tsx +196 -0
  36. package/src/components/ChatMessageListComponent.tsx +139 -0
  37. package/src/components/actionTool.tsx +186 -0
  38. package/src/components/add-agent-card.tsx +77 -0
  39. package/src/components/agent-action-viewer.tsx +816 -0
  40. package/src/components/agent-avatar-stack.tsx +121 -0
  41. package/src/components/agent-card.cy.tsx +259 -0
  42. package/src/components/agent-card.tsx +177 -0
  43. package/src/components/agent-creator.tsx +142 -0
  44. package/src/components/agent-log-viewer.tsx +645 -0
  45. package/src/components/agent-memory-edit-overlay.tsx +461 -0
  46. package/src/components/agent-memory-viewer.tsx +504 -0
  47. package/src/components/agent-settings.tsx +270 -0
  48. package/src/components/agent-sidebar.tsx +178 -0
  49. package/src/components/api-key-dialog.tsx +113 -0
  50. package/src/components/app-sidebar.tsx +685 -0
  51. package/src/components/array-input.tsx +116 -0
  52. package/src/components/audio-recorder.tsx +292 -0
  53. package/src/components/avatar-panel.tsx +141 -0
  54. package/src/components/character-form.tsx +1138 -0
  55. package/src/components/chat.tsx +1813 -0
  56. package/src/components/combobox.tsx +187 -0
  57. package/src/components/confirmation-dialog.tsx +59 -0
  58. package/src/components/connection-error-banner.tsx +101 -0
  59. package/src/components/connection-status.cy.tsx +73 -0
  60. package/src/components/connection-status.tsx +155 -0
  61. package/src/components/copy-button.tsx +35 -0
  62. package/src/components/delete-button.tsx +24 -0
  63. package/src/components/env-settings.tsx +261 -0
  64. package/src/components/group-card.tsx +160 -0
  65. package/src/components/group-panel.tsx +543 -0
  66. package/src/components/input-copy.tsx +21 -0
  67. package/src/components/logs-page.tsx +41 -0
  68. package/src/components/media-content.tsx +385 -0
  69. package/src/components/memory-graph.tsx +170 -0
  70. package/src/components/missing-secrets-dialog.tsx +72 -0
  71. package/src/components/onboarding-tour.tsx +247 -0
  72. package/src/components/page-title.tsx +8 -0
  73. package/src/components/plugins-panel.tsx +383 -0
  74. package/src/components/profile-card.tsx +66 -0
  75. package/src/components/profile-overlay.tsx +283 -0
  76. package/src/components/retry-button.tsx +28 -0
  77. package/src/components/secret-panel.tsx +1505 -0
  78. package/src/components/server-management.tsx +264 -0
  79. package/src/components/split-button.tsx +148 -0
  80. package/src/components/stop-agent-button.tsx +99 -0
  81. package/src/components/ui/alert-dialog.cy.tsx +333 -0
  82. package/src/components/ui/alert-dialog.tsx +115 -0
  83. package/src/components/ui/alert.tsx +49 -0
  84. package/src/components/ui/avatar.cy.tsx +180 -0
  85. package/src/components/ui/avatar.tsx +57 -0
  86. package/src/components/ui/badge.cy.tsx +146 -0
  87. package/src/components/ui/badge.tsx +43 -0
  88. package/src/components/ui/button.cy.tsx +177 -0
  89. package/src/components/ui/button.tsx +56 -0
  90. package/src/components/ui/card.cy.tsx +160 -0
  91. package/src/components/ui/card.tsx +73 -0
  92. package/src/components/ui/chat/animated-markdown.tsx +59 -0
  93. package/src/components/ui/chat/chat-bubble.tsx +178 -0
  94. package/src/components/ui/chat/chat-container.tsx +51 -0
  95. package/src/components/ui/chat/chat-input.cy.tsx +169 -0
  96. package/src/components/ui/chat/chat-input.tsx +47 -0
  97. package/src/components/ui/chat/chat-message-list.tsx +61 -0
  98. package/src/components/ui/chat/chat-tts-button.tsx +199 -0
  99. package/src/components/ui/chat/code-block.tsx +79 -0
  100. package/src/components/ui/chat/expandable-chat.tsx +131 -0
  101. package/src/components/ui/chat/hooks/useAutoScroll.ts +86 -0
  102. package/src/components/ui/chat/markdown.tsx +209 -0
  103. package/src/components/ui/chat/message-loading.tsx +48 -0
  104. package/src/components/ui/checkbox.cy.tsx +170 -0
  105. package/src/components/ui/checkbox.tsx +30 -0
  106. package/src/components/ui/collapsible.cy.tsx +283 -0
  107. package/src/components/ui/collapsible.tsx +9 -0
  108. package/src/components/ui/command.cy.tsx +313 -0
  109. package/src/components/ui/command.tsx +143 -0
  110. package/src/components/ui/dialog.cy.tsx +279 -0
  111. package/src/components/ui/dialog.tsx +104 -0
  112. package/src/components/ui/dropdown-menu.cy.tsx +273 -0
  113. package/src/components/ui/dropdown-menu.tsx +281 -0
  114. package/src/components/ui/input.cy.tsx +82 -0
  115. package/src/components/ui/input.tsx +27 -0
  116. package/src/components/ui/label.cy.tsx +157 -0
  117. package/src/components/ui/label.tsx +19 -0
  118. package/src/components/ui/resizable.tsx +42 -0
  119. package/src/components/ui/scroll-area.cy.tsx +242 -0
  120. package/src/components/ui/scroll-area.tsx +46 -0
  121. package/src/components/ui/select.cy.tsx +277 -0
  122. package/src/components/ui/select.tsx +155 -0
  123. package/src/components/ui/separator.cy.tsx +145 -0
  124. package/src/components/ui/separator.tsx +29 -0
  125. package/src/components/ui/sheet.cy.tsx +324 -0
  126. package/src/components/ui/sheet.tsx +119 -0
  127. package/src/components/ui/sidebar.tsx +734 -0
  128. package/src/components/ui/skeleton.cy.tsx +149 -0
  129. package/src/components/ui/skeleton.tsx +17 -0
  130. package/src/components/ui/split-button.cy.tsx +274 -0
  131. package/src/components/ui/split-button.tsx +112 -0
  132. package/src/components/ui/switch.tsx +28 -0
  133. package/src/components/ui/tabs.cy.tsx +271 -0
  134. package/src/components/ui/tabs.tsx +53 -0
  135. package/src/components/ui/textarea.cy.tsx +136 -0
  136. package/src/components/ui/textarea.tsx +26 -0
  137. package/src/components/ui/toast.cy.tsx +209 -0
  138. package/src/components/ui/toast.tsx +126 -0
  139. package/src/components/ui/toaster.tsx +29 -0
  140. package/src/components/ui/tooltip.cy.tsx +244 -0
  141. package/src/components/ui/tooltip.tsx +30 -0
  142. package/src/config/agent-templates.ts +349 -0
  143. package/src/config/voice-models.ts +181 -0
  144. package/src/constants.ts +23 -0
  145. package/src/context/AuthContext.tsx +44 -0
  146. package/src/context/ConnectionContext.tsx +194 -0
  147. package/src/entry.tsx +9 -0
  148. package/src/hooks/__tests__/use-agent-tab-state.test.ts +137 -0
  149. package/src/hooks/__tests__/use-agent-update.test.tsx +250 -0
  150. package/src/hooks/__tests__/use-character-convert.test.ts +102 -0
  151. package/src/hooks/__tests__/use-panel-width-state.test.ts +243 -0
  152. package/src/hooks/__tests__/use-sidebar-state.test.ts +117 -0
  153. package/src/hooks/use-agent-management.ts +130 -0
  154. package/src/hooks/use-agent-tab-state.ts +74 -0
  155. package/src/hooks/use-agent-update.ts +469 -0
  156. package/src/hooks/use-character-convert.ts +138 -0
  157. package/src/hooks/use-confirmation.ts +55 -0
  158. package/src/hooks/use-delete-agent.ts +123 -0
  159. package/src/hooks/use-dm-channels.ts +198 -0
  160. package/src/hooks/use-elevenlabs-voices.ts +83 -0
  161. package/src/hooks/use-file-upload.ts +224 -0
  162. package/src/hooks/use-mobile.tsx +19 -0
  163. package/src/hooks/use-onboarding.tsx +49 -0
  164. package/src/hooks/use-panel-width-state.ts +147 -0
  165. package/src/hooks/use-partial-update.ts +288 -0
  166. package/src/hooks/use-plugin-details.ts +462 -0
  167. package/src/hooks/use-plugins.ts +119 -0
  168. package/src/hooks/use-query-hooks.ts +1263 -0
  169. package/src/hooks/use-server-agents.ts +62 -0
  170. package/src/hooks/use-server-version.tsx +47 -0
  171. package/src/hooks/use-sidebar-state.ts +50 -0
  172. package/src/hooks/use-socket-chat.ts +264 -0
  173. package/src/hooks/use-toast.ts +260 -0
  174. package/src/hooks/use-version.tsx +64 -0
  175. package/src/index.css +146 -0
  176. package/src/lib/api-client-config.ts +53 -0
  177. package/src/lib/api-type-mappers.ts +196 -0
  178. package/src/lib/export-utils.ts +123 -0
  179. package/src/lib/logger.ts +19 -0
  180. package/src/lib/media-utils.ts +170 -0
  181. package/src/lib/pca.test.ts +17 -0
  182. package/src/lib/pca.ts +52 -0
  183. package/src/lib/socketio-manager.ts +664 -0
  184. package/src/lib/utils.ts +168 -0
  185. package/src/main.tsx +16 -0
  186. package/src/mocks/empty-module.ts +12 -0
  187. package/src/mocks/node-module.ts +57 -0
  188. package/src/polyfills.ts +37 -0
  189. package/src/routes/agent-detail.tsx +30 -0
  190. package/src/routes/agent-list.tsx +27 -0
  191. package/src/routes/agent-settings.tsx +48 -0
  192. package/src/routes/character-detail.tsx +52 -0
  193. package/src/routes/character-form.tsx +79 -0
  194. package/src/routes/character-list.tsx +38 -0
  195. package/src/routes/chat.tsx +128 -0
  196. package/src/routes/createAgent.tsx +13 -0
  197. package/src/routes/group-new.tsx +50 -0
  198. package/src/routes/group.tsx +29 -0
  199. package/src/routes/home.tsx +218 -0
  200. package/src/routes/not-found.tsx +71 -0
  201. package/src/test/setup.ts +154 -0
  202. package/src/types/crypto-browserify.d.ts +4 -0
  203. package/src/types/index.ts +13 -0
  204. package/src/types/rooms.ts +8 -0
  205. package/src/types.ts +84 -0
  206. package/src/vite-env.d.ts +40 -0
  207. package/tailwind.config.ts +90 -0
  208. package/tsconfig.json +10 -0
  209. package/vite.config.ts +102 -0
@@ -0,0 +1,1505 @@
1
+ import { Button } from '@/components/ui/button';
2
+ import { Input } from '@/components/ui/input';
3
+ import { Textarea } from '@/components/ui/textarea';
4
+ import { ScrollArea } from '@/components/ui/scroll-area';
5
+ import { Badge } from '@/components/ui/badge';
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from '@/components/ui/dialog';
14
+ import type { Agent } from '@elizaos/core';
15
+ import { decryptObjectValues, getSalt } from '@elizaos/core';
16
+ import {
17
+ Check,
18
+ CloudUpload,
19
+ Eye,
20
+ EyeOff,
21
+ X,
22
+ AlertCircle,
23
+ FileText,
24
+ Copy,
25
+ Trash2,
26
+ Edit3,
27
+ Globe,
28
+ } from 'lucide-react';
29
+ import {
30
+ useEffect,
31
+ useRef,
32
+ useState,
33
+ useImperativeHandle,
34
+ forwardRef,
35
+ useCallback,
36
+ useMemo,
37
+ } from 'react';
38
+ import { useRequiredSecrets } from '@/hooks/use-plugin-details';
39
+ import { Alert, AlertDescription } from '@/components/ui/alert';
40
+ import { Skeleton } from '@/components/ui/skeleton';
41
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
42
+ import { createElizaClient } from '@/lib/api-client-config';
43
+
44
+ type EnvVariable = {
45
+ name: string;
46
+ value: string;
47
+ isNew?: boolean;
48
+ isModified?: boolean;
49
+ isDeleted?: boolean;
50
+ isRequired?: boolean;
51
+ description?: string;
52
+ example?: string;
53
+ plugin?: string;
54
+ };
55
+
56
+ interface SecretPanelProps {
57
+ characterValue: Agent;
58
+ onChange?: (secrets: Record<string, string | null>) => void;
59
+ }
60
+
61
+ export interface SecretPanelRef {
62
+ getSecrets: () => Record<string, string | null>;
63
+ validateSecrets: () => { isValid: boolean; missingSecrets: string[] };
64
+ }
65
+
66
+ export const SecretPanel = forwardRef<SecretPanelRef, SecretPanelProps>(
67
+ ({ characterValue, onChange }, ref) => {
68
+ const [envs, setEnvs] = useState<EnvVariable[]>([]);
69
+ const [name, setName] = useState('');
70
+ const [value, setValue] = useState('');
71
+ const [showPassword, setShowPassword] = useState(false);
72
+ const [editingIndex, setEditingIndex] = useState<number | null>(null);
73
+ const [editedValue, setEditedValue] = useState('');
74
+ const [isDragging, setIsDragging] = useState(false);
75
+ const [deletedKeys, setDeletedKeys] = useState<string[]>([]);
76
+ const [visibleSecrets, setVisibleSecrets] = useState<Set<number>>(new Set());
77
+ const [globalEnvs, setGlobalEnvs] = useState<Record<string, string>>({});
78
+ const [isLoadingGlobalEnvs, setIsLoadingGlobalEnvs] = useState(true);
79
+
80
+ // Raw editor modal state
81
+ const [rawEditorOpen, setRawEditorOpen] = useState(false);
82
+ const [rawEditorContent, setRawEditorContent] = useState('');
83
+
84
+ const lastAgentIdRef = useRef<string | null>(null);
85
+ const lastSecretsRef = useRef<string>('');
86
+ const dropRef = useRef<HTMLDivElement>(null);
87
+ const lastRequiredSecretsKeyRef = useRef<string>('');
88
+
89
+ // Get required secrets based on enabled plugins
90
+ const enabledPlugins = useMemo(() => characterValue?.plugins || [], [characterValue?.plugins]);
91
+ const { requiredSecrets, isLoading: isLoadingSecrets } = useRequiredSecrets(enabledPlugins);
92
+
93
+ // Fetch global environment variables
94
+ useEffect(() => {
95
+ const fetchGlobalEnvs = async () => {
96
+ try {
97
+ setIsLoadingGlobalEnvs(true);
98
+ const elizaClient = createElizaClient();
99
+ const data = await elizaClient.system.getEnvironment();
100
+ setGlobalEnvs(data || {});
101
+ } catch (error) {
102
+ console.error('Failed to fetch global environment variables:', error);
103
+ setGlobalEnvs({});
104
+ } finally {
105
+ setIsLoadingGlobalEnvs(false);
106
+ }
107
+ };
108
+
109
+ fetchGlobalEnvs();
110
+ }, []);
111
+
112
+ // Function to get current secrets
113
+ const getCurrentSecrets = useCallback(() => {
114
+ const currentSecrets: Record<string, string | null> = {};
115
+
116
+ envs.forEach(({ name, value }) => {
117
+ currentSecrets[name] = value;
118
+ });
119
+
120
+ deletedKeys.forEach((key) => {
121
+ currentSecrets[key] = null;
122
+ });
123
+
124
+ return currentSecrets;
125
+ }, [envs, deletedKeys]);
126
+
127
+ // Function to validate if all required secrets are provided
128
+ const validateSecrets = useCallback(() => {
129
+ const currentSecrets = getCurrentSecrets();
130
+
131
+ // If we're still loading required secrets, check against current envs marked as required
132
+ const secretsToCheck = isLoadingSecrets
133
+ ? envs.filter((env) => env.isRequired)
134
+ : requiredSecrets;
135
+
136
+ const missingSecrets = secretsToCheck
137
+ .filter((secret) => {
138
+ const secretName = typeof secret === 'object' && 'name' in secret ? secret.name : secret;
139
+ const value = currentSecrets[secretName];
140
+
141
+ // Check if the value exists in current secrets
142
+ if (value && value.trim() !== '') {
143
+ return false;
144
+ }
145
+
146
+ // Check if the value exists in global environment
147
+ if (globalEnvs[secretName] && globalEnvs[secretName].trim() !== '') {
148
+ return false;
149
+ }
150
+
151
+ return true;
152
+ })
153
+ .map((secret) => (typeof secret === 'object' && 'name' in secret ? secret.name : secret));
154
+
155
+ return {
156
+ isValid: missingSecrets.length === 0,
157
+ missingSecrets,
158
+ };
159
+ }, [getCurrentSecrets, requiredSecrets, isLoadingSecrets, envs, globalEnvs]);
160
+
161
+ // Expose methods to get current secrets state and validate
162
+ useImperativeHandle(
163
+ ref,
164
+ () => ({
165
+ getSecrets: getCurrentSecrets,
166
+ validateSecrets,
167
+ }),
168
+ [getCurrentSecrets, validateSecrets]
169
+ );
170
+
171
+ // Toggle visibility for a specific secret
172
+ const toggleSecretVisibility = (index: number) => {
173
+ setVisibleSecrets((prev) => {
174
+ const newSet = new Set(prev);
175
+ if (newSet.has(index)) {
176
+ newSet.delete(index);
177
+ } else {
178
+ newSet.add(index);
179
+ }
180
+ return newSet;
181
+ });
182
+ };
183
+
184
+ // Convert envs to raw editor format - include ALL variables, even empty required ones
185
+ const envsToRawText = useCallback(() => {
186
+ const lines: string[] = [];
187
+
188
+ // Add a header comment
189
+ lines.push('# Environment Variables');
190
+ lines.push('');
191
+
192
+ // Group by required vs optional
193
+ const requiredEnvs = envs.filter((env) => env.isRequired);
194
+ const optionalEnvs = envs.filter(
195
+ (env) => !env.isRequired && env.value && env.value.trim() !== ''
196
+ );
197
+
198
+ // Add required secrets section
199
+ if (requiredEnvs.length > 0) {
200
+ lines.push('# Required Secrets');
201
+ requiredEnvs.forEach((env) => {
202
+ if (env.plugin) {
203
+ lines.push(`# Required by ${env.plugin}`);
204
+ }
205
+ if (env.description) {
206
+ lines.push(`# ${env.description}`);
207
+ }
208
+ lines.push(`${env.name}=${env.value || ''}`);
209
+ lines.push('');
210
+ });
211
+ }
212
+
213
+ // Add optional secrets section
214
+ if (optionalEnvs.length > 0) {
215
+ lines.push('# Optional Variables');
216
+ optionalEnvs.forEach((env) => {
217
+ lines.push(`${env.name}=${env.value}`);
218
+ });
219
+ }
220
+
221
+ return lines.join('\n');
222
+ }, [envs]);
223
+
224
+ // Parse raw editor content back to envs
225
+ const parseRawText = useCallback((text: string) => {
226
+ const lines = text.split('\n');
227
+ const parsedEnvs: Record<string, string> = {};
228
+
229
+ for (const line of lines) {
230
+ const trimmedLine = line.trim();
231
+ if (!trimmedLine || trimmedLine.startsWith('#')) continue;
232
+
233
+ const [key, ...rest] = trimmedLine.split('=');
234
+ const val = rest
235
+ .join('=')
236
+ .trim()
237
+ .replace(/^['"]|['"]$/g, '');
238
+ if (key && key.trim()) {
239
+ parsedEnvs[key.trim()] = val;
240
+ }
241
+ }
242
+
243
+ return parsedEnvs;
244
+ }, []);
245
+
246
+ // Handle raw editor save - this should trigger the parent onChange
247
+ const handleRawEditorSave = () => {
248
+ const parsedEnvs = parseRawText(rawEditorContent);
249
+
250
+ // Create a new envs array with updates from raw editor
251
+ const newEnvs = envs
252
+ .map((env) => {
253
+ if (parsedEnvs.hasOwnProperty(env.name)) {
254
+ const cleanValue = parsedEnvs[env.name].startsWith('process.env.')
255
+ ? ''
256
+ : parsedEnvs[env.name];
257
+ return { ...env, value: cleanValue, isModified: env.value !== cleanValue };
258
+ }
259
+ // Keep required secrets even if not in parsed content
260
+ if (env.isRequired) {
261
+ return { ...env, value: '', isModified: env.value !== '' };
262
+ }
263
+ // Remove non-required secrets that aren't in parsed content
264
+ return null;
265
+ })
266
+ .filter(Boolean) as EnvVariable[];
267
+
268
+ // Add new secrets from parsed content
269
+ Object.entries(parsedEnvs).forEach(([key, value]) => {
270
+ const exists = newEnvs.some((env) => env.name === key);
271
+ if (!exists) {
272
+ const cleanValue = value.startsWith('process.env.') ? '' : value;
273
+ const reqSecret = requiredSecrets.find((s) => s.name === key);
274
+ newEnvs.push({
275
+ name: key,
276
+ value: cleanValue,
277
+ isNew: true,
278
+ isModified: false,
279
+ isDeleted: false,
280
+ isRequired: reqSecret?.required || false,
281
+ description: reqSecret?.description,
282
+ example: reqSecret?.example,
283
+ plugin: reqSecret?.plugin,
284
+ });
285
+ }
286
+ });
287
+
288
+ // Sort: required secrets first, then alphabetically
289
+ newEnvs.sort((a, b) => {
290
+ if (a.isRequired && !b.isRequired) return -1;
291
+ if (!a.isRequired && b.isRequired) return 1;
292
+ return a.name.localeCompare(b.name);
293
+ });
294
+
295
+ setEnvs(newEnvs);
296
+ setRawEditorOpen(false);
297
+
298
+ // The useEffect watching envs will trigger onChange to parent
299
+ };
300
+
301
+ // Open raw editor modal
302
+ const openRawEditor = () => {
303
+ setRawEditorContent(envsToRawText());
304
+ setRawEditorOpen(true);
305
+ };
306
+
307
+ // Copy env content to clipboard
308
+ const copyEnvContent = () => {
309
+ navigator.clipboard.writeText(rawEditorContent);
310
+ };
311
+
312
+ // Load initial secrets from characterValue and merge with required secrets
313
+ useEffect(() => {
314
+ // Skip if still loading secrets or global envs
315
+ if (isLoadingSecrets || isLoadingGlobalEnvs) return;
316
+
317
+ // Only reset if we're switching to a different agent or this is the first load
318
+ // or if envs is empty (meaning we haven't initialized yet)
319
+ if (
320
+ characterValue.id !== lastAgentIdRef.current ||
321
+ !lastAgentIdRef.current ||
322
+ envs.length === 0
323
+ ) {
324
+ // Decrypt secrets from the server using the core decryption function
325
+ const salt = getSalt();
326
+ const decryptedSecretsRaw = characterValue?.settings?.secrets || {};
327
+ // Ensure we're working with a plain object
328
+ const decryptedSecrets =
329
+ typeof decryptedSecretsRaw === 'object' &&
330
+ !Array.isArray(decryptedSecretsRaw) &&
331
+ decryptedSecretsRaw !== null
332
+ ? decryptObjectValues(decryptedSecretsRaw, salt)
333
+ : {};
334
+
335
+ const existingSecrets = Object.entries(decryptedSecrets).map(([name, value]) => {
336
+ // Filter out process.env values - these should not be stored as actual values
337
+ const stringValue = String(value);
338
+ const cleanValue = stringValue.startsWith('process.env.') ? '' : stringValue;
339
+
340
+ return {
341
+ name,
342
+ value: cleanValue,
343
+ isNew: false,
344
+ isModified: false,
345
+ isDeleted: false,
346
+ isRequired: false,
347
+ description: undefined,
348
+ example: undefined,
349
+ plugin: undefined,
350
+ };
351
+ });
352
+
353
+ // Create a map for quick lookup
354
+ const existingSecretsMap = new Map(existingSecrets.map((s) => [s.name, s]));
355
+
356
+ // Add required secrets that don't exist yet
357
+ const allSecrets: EnvVariable[] = [...existingSecrets];
358
+
359
+ requiredSecrets.forEach((reqSecret) => {
360
+ if (!existingSecretsMap.has(reqSecret.name)) {
361
+ // Check if this secret exists in global environment
362
+ // Don't populate the value - let the UI show it's using global
363
+ allSecrets.push({
364
+ name: reqSecret.name,
365
+ value: '', // Keep empty to show it's using global
366
+ isNew: true,
367
+ isModified: false,
368
+ isDeleted: false,
369
+ isRequired: true,
370
+ description: reqSecret.description,
371
+ example: reqSecret.example,
372
+ plugin: reqSecret.plugin,
373
+ });
374
+ } else {
375
+ // Update existing secret with required metadata
376
+ const existingIndex = allSecrets.findIndex((s) => s.name === reqSecret.name);
377
+ if (existingIndex !== -1) {
378
+ // Keep existing value as is - don't auto-populate from global
379
+ allSecrets[existingIndex] = {
380
+ ...allSecrets[existingIndex],
381
+ isRequired: true,
382
+ description: reqSecret.description,
383
+ example: reqSecret.example,
384
+ plugin: reqSecret.plugin,
385
+ };
386
+ }
387
+ }
388
+ });
389
+
390
+ // Sort: required secrets first, then alphabetically
391
+ allSecrets.sort((a, b) => {
392
+ if (a.isRequired && !b.isRequired) return -1;
393
+ if (!a.isRequired && b.isRequired) return 1;
394
+ return a.name.localeCompare(b.name);
395
+ });
396
+
397
+ setEnvs(allSecrets);
398
+ setDeletedKeys([]); // Reset deleted keys when loading new data
399
+ lastAgentIdRef.current = characterValue.id || 'new-agent';
400
+
401
+ // Also reset the required secrets tracking
402
+ const requiredSecretsKey = requiredSecrets
403
+ .map((s) => s.name)
404
+ .sort()
405
+ .join(',');
406
+ lastRequiredSecretsKeyRef.current = requiredSecretsKey;
407
+ }
408
+ }, [
409
+ characterValue.id,
410
+ characterValue.settings?.secrets,
411
+ requiredSecrets,
412
+ isLoadingSecrets,
413
+ globalEnvs,
414
+ isLoadingGlobalEnvs,
415
+ ]);
416
+
417
+ // Sync secrets when plugins change (not just when agent changes)
418
+ useEffect(() => {
419
+ // Skip only if still loading secrets
420
+ if (isLoadingSecrets) return;
421
+
422
+ // Create a stable key for comparison
423
+ const requiredSecretsKey = requiredSecrets
424
+ .map((s) => s.name)
425
+ .sort()
426
+ .join(',');
427
+
428
+ // Only update if the required secrets actually changed
429
+ if (requiredSecretsKey === lastRequiredSecretsKeyRef.current) return;
430
+ lastRequiredSecretsKeyRef.current = requiredSecretsKey;
431
+
432
+ // Get current required secret names
433
+ const currentRequiredNames = new Set(requiredSecrets.map((s) => s.name));
434
+
435
+ // Update existing envs
436
+ setEnvs((prevEnvs) => {
437
+ const updatedEnvs = prevEnvs
438
+ .map((env) => {
439
+ // Check if this secret is still required
440
+ const isStillRequired = currentRequiredNames.has(env.name);
441
+ const reqSecret = requiredSecrets.find((s) => s.name === env.name);
442
+
443
+ // If it was required but no longer is, remove it entirely (regardless of value)
444
+ if (env.isRequired && !isStillRequired) {
445
+ // Mark this secret for deletion in parent component immediately
446
+ setTimeout(() => {
447
+ setDeletedKeys((prev) => (prev.includes(env.name) ? prev : [...prev, env.name]));
448
+ }, 0);
449
+ return null; // Mark for removal from envs array
450
+ }
451
+
452
+ // Update metadata if still required
453
+ if (isStillRequired && reqSecret) {
454
+ return {
455
+ ...env,
456
+ isRequired: true,
457
+ description: reqSecret.description,
458
+ example: reqSecret.example,
459
+ plugin: reqSecret.plugin,
460
+ };
461
+ }
462
+
463
+ // Keep non-required envs that have values
464
+ return {
465
+ ...env,
466
+ isRequired: isStillRequired,
467
+ description: isStillRequired ? env.description : undefined,
468
+ example: isStillRequired ? env.example : undefined,
469
+ plugin: isStillRequired ? env.plugin : undefined,
470
+ };
471
+ })
472
+ .filter(Boolean) as EnvVariable[]; // Remove nulls
473
+
474
+ // Add new required secrets that don't exist
475
+ const existingNames = new Set(updatedEnvs.map((e) => e.name));
476
+ requiredSecrets.forEach((reqSecret) => {
477
+ if (!existingNames.has(reqSecret.name)) {
478
+ // Don't auto-populate from global - let UI show it's using global
479
+ updatedEnvs.push({
480
+ name: reqSecret.name,
481
+ value: '', // Keep empty to show it's using global
482
+ isNew: true,
483
+ isModified: false,
484
+ isDeleted: false,
485
+ isRequired: true,
486
+ description: reqSecret.description,
487
+ example: reqSecret.example,
488
+ plugin: reqSecret.plugin,
489
+ });
490
+ }
491
+ });
492
+
493
+ // Sort: required secrets first, then alphabetically
494
+ updatedEnvs.sort((a, b) => {
495
+ if (a.isRequired && !b.isRequired) return -1;
496
+ if (!a.isRequired && b.isRequired) return 1;
497
+ return a.name.localeCompare(b.name);
498
+ });
499
+
500
+ return updatedEnvs;
501
+ });
502
+ }, [requiredSecrets, isLoadingSecrets, globalEnvs]);
503
+
504
+ // Force cleanup of non-required secrets (alternative approach)
505
+ useEffect(() => {
506
+ const currentRequiredNames = new Set(requiredSecrets.map((s) => s.name));
507
+
508
+ setEnvs((prevEnvs) => {
509
+ const cleanedEnvs = prevEnvs.filter((env) => {
510
+ const shouldKeep =
511
+ currentRequiredNames.has(env.name) ||
512
+ (!env.isRequired && env.value && env.value.trim() !== '');
513
+
514
+ if (!shouldKeep && env.isRequired) {
515
+ // Mark for deletion
516
+ setDeletedKeys((prev) => (prev.includes(env.name) ? prev : [...prev, env.name]));
517
+ }
518
+
519
+ return shouldKeep;
520
+ });
521
+
522
+ return cleanedEnvs;
523
+ });
524
+ }, [requiredSecrets]);
525
+
526
+ // Notify parent of changes
527
+ useEffect(() => {
528
+ if (!onChange) return;
529
+
530
+ // Create a debounced version to avoid rapid fire updates
531
+ const timeoutId = setTimeout(() => {
532
+ const secrets = getCurrentSecrets();
533
+
534
+ // Also check for secrets that should be removed
535
+ // Get all env names that are currently in the list
536
+ const currentEnvNames = new Set(envs.map((e) => e.name));
537
+
538
+ // Check if there are any secrets in the character settings that are no longer in our envs
539
+ // This happens when a plugin is removed and its required secrets are cleaned up
540
+ const characterSecrets = characterValue?.settings?.secrets || {};
541
+ Object.keys(characterSecrets).forEach((secretName) => {
542
+ if (!currentEnvNames.has(secretName) && !deletedKeys.includes(secretName)) {
543
+ // Mark this secret for deletion
544
+ secrets[secretName] = null;
545
+ }
546
+ });
547
+
548
+ // Only call onChange if secrets actually changed
549
+ // Sort keys to ensure consistent comparison
550
+ const sortedSecrets = Object.keys(secrets)
551
+ .sort()
552
+ .reduce(
553
+ (acc, key) => {
554
+ acc[key] = secrets[key];
555
+ return acc;
556
+ },
557
+ {} as Record<string, string | null>
558
+ );
559
+ const secretsString = JSON.stringify(sortedSecrets);
560
+
561
+ if (secretsString !== lastSecretsRef.current) {
562
+ lastSecretsRef.current = secretsString;
563
+ onChange(secrets);
564
+ }
565
+ }, 100); // 100ms debounce
566
+
567
+ return () => clearTimeout(timeoutId);
568
+ }, [envs, deletedKeys, onChange, characterValue?.settings?.secrets, getCurrentSecrets]);
569
+
570
+ const handleFileUpload = useCallback(
571
+ (file: File) => {
572
+ const reader = new FileReader();
573
+ reader.onload = (event) => {
574
+ const text = event.target?.result as string;
575
+ const lines = text.split('\n');
576
+
577
+ const newEnvs: Record<string, string> = {};
578
+ for (const line of lines) {
579
+ const trimmedLine = line.trim();
580
+ if (!trimmedLine || trimmedLine.startsWith('#')) continue;
581
+
582
+ const [key, ...rest] = trimmedLine.split('=');
583
+ const val = rest
584
+ .join('=')
585
+ .trim()
586
+ .replace(/^['"]|['"]$/g, '');
587
+ if (key) newEnvs[key.trim()] = val;
588
+ }
589
+
590
+ setEnvs((prev) => {
591
+ const merged = new Map(
592
+ prev.map(({ name, value, ...rest }) => [name, { value, ...rest }])
593
+ );
594
+ for (const [key, val] of Object.entries(newEnvs)) {
595
+ // Filter out process.env values
596
+ const cleanValue = val.startsWith('process.env.') ? '' : val;
597
+ const existing = merged.get(key);
598
+ if (existing) {
599
+ merged.set(key, { ...existing, value: cleanValue, isModified: true });
600
+ } else {
601
+ const reqSecret = requiredSecrets.find((s) => s.name === key);
602
+ merged.set(key, {
603
+ value: cleanValue,
604
+ isNew: true,
605
+ isModified: false,
606
+ isRequired: reqSecret?.required || false,
607
+ description: reqSecret?.description,
608
+ example: reqSecret?.example,
609
+ plugin: reqSecret?.plugin,
610
+ });
611
+ }
612
+ }
613
+ const result = Array.from(merged.entries()).map(([name, data]) => ({
614
+ name,
615
+ ...data,
616
+ }));
617
+
618
+ // Sort: required secrets first, then alphabetically
619
+ result.sort((a, b) => {
620
+ if (a.isRequired && !b.isRequired) return -1;
621
+ if (!a.isRequired && b.isRequired) return 1;
622
+ return a.name.localeCompare(b.name);
623
+ });
624
+
625
+ return result;
626
+ });
627
+
628
+ if (deletedKeys.length > 0) {
629
+ setDeletedKeys((prev) => prev.filter((key) => !Object.keys(newEnvs).includes(key)));
630
+ }
631
+ };
632
+ reader.readAsText(file);
633
+ },
634
+ [requiredSecrets, deletedKeys]
635
+ );
636
+
637
+ useEffect(() => {
638
+ const drop = dropRef.current;
639
+ if (!drop) {
640
+ return;
641
+ }
642
+
643
+ const preventDefaults = (e: Event) => {
644
+ e.preventDefault();
645
+ e.stopPropagation();
646
+ };
647
+
648
+ const highlight = () => {
649
+ setIsDragging(true);
650
+ };
651
+
652
+ const unhighlight = () => {
653
+ setIsDragging(false);
654
+ };
655
+
656
+ const handleDrop = (e: DragEvent) => {
657
+ preventDefaults(e);
658
+ unhighlight();
659
+
660
+ const file = e.dataTransfer?.files?.[0];
661
+ if (file && file.name.endsWith('.env')) {
662
+ handleFileUpload(file);
663
+ }
664
+ };
665
+
666
+ ['dragenter', 'dragover'].forEach((event) => drop.addEventListener(event, highlight));
667
+ ['dragleave', 'drop'].forEach((event) => drop.addEventListener(event, unhighlight));
668
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((event) =>
669
+ drop.addEventListener(event, preventDefaults)
670
+ );
671
+ drop.addEventListener('drop', handleDrop);
672
+
673
+ return () => {
674
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((event) =>
675
+ drop.removeEventListener(event, preventDefaults)
676
+ );
677
+ ['dragenter', 'dragover'].forEach((event) => drop.removeEventListener(event, highlight));
678
+ ['dragleave', 'drop'].forEach((event) => drop.removeEventListener(event, unhighlight));
679
+ drop.removeEventListener('drop', handleDrop);
680
+ };
681
+ }, [handleFileUpload]);
682
+
683
+ const addEnv = () => {
684
+ if (name && value) {
685
+ const exists = envs.some((env) => env.name === name);
686
+
687
+ // Filter out process.env values
688
+ const cleanValue = value.startsWith('process.env.') ? '' : value;
689
+
690
+ if (!exists) {
691
+ if (deletedKeys.includes(name)) {
692
+ setDeletedKeys(deletedKeys.filter((key) => key !== name));
693
+ }
694
+
695
+ const reqSecret = requiredSecrets.find((s) => s.name === name);
696
+ const newEnv: EnvVariable = {
697
+ name,
698
+ value: cleanValue,
699
+ isNew: true,
700
+ isRequired: reqSecret?.required || false,
701
+ description: reqSecret?.description,
702
+ example: reqSecret?.example,
703
+ plugin: reqSecret?.plugin,
704
+ };
705
+
706
+ const updatedEnvs = [...envs, newEnv];
707
+ // Sort: required secrets first, then alphabetically
708
+ updatedEnvs.sort((a, b) => {
709
+ if (a.isRequired && !b.isRequired) return -1;
710
+ if (!a.isRequired && b.isRequired) return 1;
711
+ return a.name.localeCompare(b.name);
712
+ });
713
+
714
+ setEnvs(updatedEnvs);
715
+ setName('');
716
+ setValue('');
717
+ } else {
718
+ setEnvs(
719
+ envs.map((env) =>
720
+ env.name === name ? { ...env, value: cleanValue, isModified: true } : env
721
+ )
722
+ );
723
+ setName('');
724
+ setValue('');
725
+ }
726
+ }
727
+ };
728
+
729
+ const startEditing = (index: number) => {
730
+ // Close any other editing
731
+ if (editingIndex !== null && editingIndex !== index) {
732
+ setEditingIndex(null);
733
+ }
734
+
735
+ setEditingIndex(index);
736
+ setEditedValue(envs[index].value);
737
+ // Show the secret when editing
738
+ setVisibleSecrets((prev) => new Set(prev).add(index));
739
+ };
740
+
741
+ const saveEdit = (index: number) => {
742
+ const updatedEnvs = [...envs];
743
+ // Filter out process.env values
744
+ const cleanValue = editedValue.startsWith('process.env.') ? '' : editedValue;
745
+
746
+ if (updatedEnvs[index].value !== cleanValue) {
747
+ updatedEnvs[index].value = cleanValue;
748
+ updatedEnvs[index].isModified = true;
749
+ }
750
+ setEnvs(updatedEnvs);
751
+ setEditingIndex(null);
752
+ setEditedValue('');
753
+ // Hide the secret after saving
754
+ setVisibleSecrets((prev) => {
755
+ const newSet = new Set(prev);
756
+ newSet.delete(index);
757
+ return newSet;
758
+ });
759
+ };
760
+
761
+ const cancelEdit = () => {
762
+ const currentIndex = editingIndex;
763
+ setEditingIndex(null);
764
+ setEditedValue('');
765
+ // Hide the secret after canceling
766
+ if (currentIndex !== null) {
767
+ setVisibleSecrets((prev) => {
768
+ const newSet = new Set(prev);
769
+ newSet.delete(currentIndex);
770
+ return newSet;
771
+ });
772
+ }
773
+ };
774
+
775
+ const removeEnv = (index: number) => {
776
+ const keyToRemove = envs[index].name;
777
+
778
+ setDeletedKeys([...deletedKeys, keyToRemove]);
779
+
780
+ setEnvs(envs.filter((_, i) => i !== index));
781
+ setEditingIndex(null);
782
+ };
783
+
784
+ // Get missing required secrets (considering global envs)
785
+ const missingRequiredSecrets = requiredSecrets.filter((reqSecret) => {
786
+ const env = envs.find((e) => e.name === reqSecret.name);
787
+ const hasLocalValue = env && env.value && env.value.trim() !== '';
788
+ const hasGlobalValue = globalEnvs[reqSecret.name] && globalEnvs[reqSecret.name].trim() !== '';
789
+ return !hasLocalValue && !hasGlobalValue;
790
+ });
791
+
792
+ // Check if a secret exists in global environment
793
+ const isInGlobalEnv = useCallback(
794
+ (secretName: string) => {
795
+ return globalEnvs[secretName] && globalEnvs[secretName].trim() !== '';
796
+ },
797
+ [globalEnvs]
798
+ );
799
+
800
+ // Custom scrollbar styles for input containers - visible scrollbar like Railway
801
+ const scrollbarContainerClass = 'scrollbar-visible';
802
+
803
+ return (
804
+ <div className="w-full h-full flex flex-col">
805
+ {/* Header */}
806
+ <div className="flex items-center justify-between p-4 border-b">
807
+ <div className="flex items-center gap-2">
808
+ <h2 className="text-lg font-semibold">Variables</h2>
809
+ <Badge variant="secondary" className="text-xs">
810
+ {envs.filter((env) => env.value && env.value.trim() !== '').length} Service Variables
811
+ </Badge>
812
+ </div>
813
+ <Button
814
+ type="button"
815
+ variant="outline"
816
+ size="sm"
817
+ onClick={openRawEditor}
818
+ className="flex items-center gap-2"
819
+ >
820
+ <FileText className="w-4 h-4" />
821
+ Raw Editor
822
+ </Button>
823
+ </div>
824
+
825
+ {/* Content */}
826
+ <ScrollArea className="flex-1">
827
+ <div className="p-4 space-y-4">
828
+ {/* Show loading state for required secrets */}
829
+ {(isLoadingSecrets || isLoadingGlobalEnvs) && (
830
+ <div className="space-y-2">
831
+ <Skeleton className="h-4 w-48" />
832
+ <Skeleton className="h-4 w-64" />
833
+ </div>
834
+ )}
835
+
836
+ {/* Show alert if there are missing required secrets */}
837
+ {!isLoadingSecrets && !isLoadingGlobalEnvs && missingRequiredSecrets.length > 0 && (
838
+ <Alert>
839
+ <AlertCircle className="h-4 w-4" />
840
+ <AlertDescription>
841
+ <strong>Missing required secrets:</strong>
842
+ <ul className="mt-2 space-y-1">
843
+ {missingRequiredSecrets.map((secret) => (
844
+ <li key={secret.name} className="text-sm">
845
+ • <code className="font-mono">{secret.name}</code>
846
+ {secret.plugin && (
847
+ <span className="text-muted-foreground ml-1">
848
+ (required by {secret.plugin})
849
+ </span>
850
+ )}
851
+ </li>
852
+ ))}
853
+ </ul>
854
+ </AlertDescription>
855
+ </Alert>
856
+ )}
857
+
858
+ {/* Show info about global environment variables */}
859
+ {!isLoadingSecrets &&
860
+ !isLoadingGlobalEnvs &&
861
+ requiredSecrets.some(
862
+ (secret) =>
863
+ isInGlobalEnv(secret.name) && !envs.find((e) => e.name === secret.name)?.value
864
+ ) && (
865
+ <Alert className="border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20">
866
+ <Globe className="h-4 w-4 text-blue-600 dark:text-blue-400" />
867
+ <AlertDescription className="text-blue-800 dark:text-blue-200">
868
+ <strong>Global environment variables detected:</strong>
869
+ <p className="text-sm mt-1">
870
+ Some required secrets are automatically using values from your global
871
+ environment settings. To override any of these for this specific agent, simply
872
+ enter a new value in the field.
873
+ </p>
874
+ </AlertDescription>
875
+ </Alert>
876
+ )}
877
+
878
+ {/* Add new variable form */}
879
+ <div className="space-y-2">
880
+ {/* Desktop layout */}
881
+ <div className="hidden md:grid md:grid-cols-[minmax(200px,1fr)_2fr_auto] gap-2 items-end">
882
+ <div className="space-y-1">
883
+ <label
884
+ htmlFor="new-secret-name"
885
+ className="text-xs font-medium text-muted-foreground uppercase"
886
+ >
887
+ Name
888
+ </label>
889
+ <Input
890
+ id="new-secret-name"
891
+ placeholder="VARIABLE_NAME"
892
+ value={name}
893
+ onChange={(e) => setName(e.target.value)}
894
+ className="font-mono text-sm"
895
+ onKeyDown={(e) => {
896
+ if (e.key === 'Enter') {
897
+ e.preventDefault();
898
+ addEnv();
899
+ }
900
+ }}
901
+ />
902
+ </div>
903
+ <div className="space-y-1">
904
+ <label
905
+ htmlFor="new-secret-value"
906
+ className="text-xs font-medium text-muted-foreground uppercase"
907
+ >
908
+ Value
909
+ </label>
910
+ <div className="relative">
911
+ <div className={`rounded border ${scrollbarContainerClass}`}>
912
+ <Input
913
+ id="new-secret-value"
914
+ type={showPassword ? 'text' : 'password'}
915
+ placeholder="Enter value..."
916
+ value={value}
917
+ onChange={(e) => setValue(e.target.value)}
918
+ className="font-mono text-sm pr-10 border-0 focus-visible:ring-0 w-auto min-w-full rounded"
919
+ style={{ minWidth: '100%' }}
920
+ onKeyDown={(e) => {
921
+ if (e.key === 'Enter') {
922
+ e.preventDefault();
923
+ addEnv();
924
+ }
925
+ }}
926
+ />
927
+ </div>
928
+ <Button
929
+ type="button"
930
+ variant="ghost"
931
+ size="sm"
932
+ className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0"
933
+ onClick={() => setShowPassword(!showPassword)}
934
+ >
935
+ {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
936
+ </Button>
937
+ </div>
938
+ </div>
939
+ <Button type="button" onClick={addEnv} size="sm">
940
+ Add
941
+ </Button>
942
+ </div>
943
+
944
+ {/* Mobile layout */}
945
+ <div className="md:hidden space-y-3">
946
+ <div className="space-y-1">
947
+ <label
948
+ htmlFor="new-secret-name-mobile"
949
+ className="text-xs font-medium text-muted-foreground uppercase"
950
+ >
951
+ Name
952
+ </label>
953
+ <Input
954
+ id="new-secret-name-mobile"
955
+ placeholder="VARIABLE_NAME"
956
+ value={name}
957
+ onChange={(e) => setName(e.target.value)}
958
+ className="font-mono text-sm w-full"
959
+ onKeyDown={(e) => {
960
+ if (e.key === 'Enter') {
961
+ e.preventDefault();
962
+ // Focus on value input on mobile
963
+ document.getElementById('new-secret-value-mobile')?.focus();
964
+ }
965
+ }}
966
+ />
967
+ </div>
968
+ <div className="space-y-1">
969
+ <label
970
+ htmlFor="new-secret-value-mobile"
971
+ className="text-xs font-medium text-muted-foreground uppercase"
972
+ >
973
+ Value
974
+ </label>
975
+ <div className="relative">
976
+ <div className={`rounded border ${scrollbarContainerClass}`}>
977
+ <Input
978
+ id="new-secret-value-mobile"
979
+ type={showPassword ? 'text' : 'password'}
980
+ placeholder="Enter value..."
981
+ value={value}
982
+ onChange={(e) => setValue(e.target.value)}
983
+ className="font-mono text-sm pr-10 border-0 focus-visible:ring-0 w-auto min-w-full rounded"
984
+ style={{ minWidth: '100%' }}
985
+ onKeyDown={(e) => {
986
+ if (e.key === 'Enter') {
987
+ e.preventDefault();
988
+ addEnv();
989
+ }
990
+ }}
991
+ />
992
+ </div>
993
+ <Button
994
+ type="button"
995
+ variant="ghost"
996
+ size="sm"
997
+ className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0"
998
+ onClick={() => setShowPassword(!showPassword)}
999
+ >
1000
+ {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
1001
+ </Button>
1002
+ </div>
1003
+ </div>
1004
+ <Button type="button" onClick={addEnv} size="sm" className="w-full">
1005
+ Add Variable
1006
+ </Button>
1007
+ </div>
1008
+ </div>
1009
+
1010
+ {/* Variables Table */}
1011
+ {envs.length > 0 && (
1012
+ <TooltipProvider>
1013
+ {/* Desktop Table Layout */}
1014
+ <div className="hidden md:block border rounded-lg overflow-hidden relative">
1015
+ <div className="bg-muted/50 px-4 py-2 border-b">
1016
+ <div className="grid grid-cols-[minmax(200px,1fr)_2fr_auto] gap-4 text-xs font-medium text-muted-foreground uppercase">
1017
+ <div>Name</div>
1018
+ <div>Value</div>
1019
+ <div className="text-center">Actions</div>
1020
+ </div>
1021
+ </div>
1022
+
1023
+ {/* Scrollable content area for variables */}
1024
+ <div className="max-h-[400px] overflow-y-auto">
1025
+ {envs.map((env, index) => (
1026
+ <div
1027
+ key={`${env.name}-${index}`}
1028
+ className={`grid grid-cols-[minmax(200px,1fr)_2fr_auto] gap-4 items-center px-4 py-3 border-b last:border-b-0 hover:bg-muted/10 transition-colors ${
1029
+ env.isRequired &&
1030
+ (!env.value || env.value.trim() === '') &&
1031
+ !isInGlobalEnv(env.name)
1032
+ ? 'bg-red-500/5'
1033
+ : ''
1034
+ }`}
1035
+ >
1036
+ {/* Name Column */}
1037
+ <div className="pr-2">
1038
+ <div className="flex items-start gap-2 flex-wrap">
1039
+ <code className="font-mono text-sm font-medium break-words cursor-default">
1040
+ {env.name}
1041
+ </code>
1042
+ {env.isRequired && (
1043
+ <Badge variant="outline" className="text-xs shrink-0">
1044
+ Required
1045
+ </Badge>
1046
+ )}
1047
+ {isInGlobalEnv(env.name) && (
1048
+ <Tooltip>
1049
+ <TooltipTrigger asChild>
1050
+ <Badge
1051
+ variant="secondary"
1052
+ className="text-xs shrink-0 flex items-center gap-1"
1053
+ >
1054
+ <Globe className="w-3 h-3" />
1055
+ Global
1056
+ </Badge>
1057
+ </TooltipTrigger>
1058
+ <TooltipContent>
1059
+ <p>
1060
+ {env.value
1061
+ ? 'Overriding global value'
1062
+ : 'Value pulled from global environment. Add new value to override.'}
1063
+ </p>
1064
+ </TooltipContent>
1065
+ </Tooltip>
1066
+ )}
1067
+ </div>
1068
+ {env.description && (
1069
+ <div className="text-xs text-muted-foreground mt-1 line-clamp-2">
1070
+ {env.description}
1071
+ </div>
1072
+ )}
1073
+ {env.plugin && (
1074
+ <div className="text-xs text-muted-foreground truncate">
1075
+ Required by: {env.plugin}
1076
+ </div>
1077
+ )}
1078
+ </div>
1079
+
1080
+ {/* Value Column */}
1081
+ <div className="min-w-0">
1082
+ {editingIndex === index ? (
1083
+ <div className="flex items-center gap-2">
1084
+ <div className="relative flex-1">
1085
+ <div className={`rounded border ${scrollbarContainerClass}`}>
1086
+ <Input
1087
+ value={editedValue}
1088
+ onChange={(e) => setEditedValue(e.target.value)}
1089
+ className="font-mono text-sm border-0 focus-visible:ring-0 w-auto min-w-full rounded"
1090
+ style={{ minWidth: '100%' }}
1091
+ type={visibleSecrets.has(index) ? 'text' : 'password'}
1092
+ placeholder={
1093
+ isInGlobalEnv(env.name) && !env.value
1094
+ ? `Using global (enter to override)`
1095
+ : env.example || 'Enter value...'
1096
+ }
1097
+ autoFocus
1098
+ onKeyDown={(e) => {
1099
+ if (e.key === 'Enter') {
1100
+ e.preventDefault();
1101
+ saveEdit(index);
1102
+ } else if (e.key === 'Escape') {
1103
+ cancelEdit();
1104
+ }
1105
+ }}
1106
+ />
1107
+ </div>
1108
+ </div>
1109
+ <div className="flex items-center gap-1">
1110
+ <Button
1111
+ type="button"
1112
+ variant="ghost"
1113
+ size="sm"
1114
+ onClick={() => toggleSecretVisibility(index)}
1115
+ className="h-8 w-8 p-0"
1116
+ >
1117
+ {visibleSecrets.has(index) ? (
1118
+ <EyeOff className="w-4 h-4" />
1119
+ ) : (
1120
+ <Eye className="w-4 h-4" />
1121
+ )}
1122
+ </Button>
1123
+ <Button
1124
+ type="button"
1125
+ variant="ghost"
1126
+ size="sm"
1127
+ onClick={() => saveEdit(index)}
1128
+ className="h-8 w-8 p-0 text-green-600 hover:text-green-700"
1129
+ >
1130
+ <Check className="w-4 h-4" />
1131
+ </Button>
1132
+ <Button
1133
+ type="button"
1134
+ variant="ghost"
1135
+ size="sm"
1136
+ onClick={cancelEdit}
1137
+ className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
1138
+ >
1139
+ <X className="w-4 h-4" />
1140
+ </Button>
1141
+ </div>
1142
+ </div>
1143
+ ) : (
1144
+ <div className="flex items-center gap-2 group">
1145
+ <Tooltip>
1146
+ <TooltipTrigger asChild>
1147
+ <div
1148
+ className={`flex-1 border rounded px-3 py-1 ${scrollbarContainerClass}`}
1149
+ >
1150
+ {visibleSecrets.has(index) ? (
1151
+ <code className="font-mono text-sm whitespace-nowrap block">
1152
+ {env.value || (
1153
+ <span className="text-muted-foreground italic">
1154
+ {isInGlobalEnv(env.name)
1155
+ ? 'Using global value'
1156
+ : env.example
1157
+ ? `e.g. ${env.example}`
1158
+ : 'Not set'}
1159
+ </span>
1160
+ )}
1161
+ </code>
1162
+ ) : env.value ? (
1163
+ <code className="font-mono text-sm text-muted-foreground block">
1164
+ ••••••••
1165
+ </code>
1166
+ ) : (
1167
+ <span className="text-muted-foreground italic text-sm block">
1168
+ {isInGlobalEnv(env.name) ? 'Using global value' : 'Not set'}
1169
+ </span>
1170
+ )}
1171
+ </div>
1172
+ </TooltipTrigger>
1173
+ {isInGlobalEnv(env.name) && (
1174
+ <TooltipContent className="max-w-xs">
1175
+ <div className="space-y-1">
1176
+ <p className="font-semibold">Global Environment Variable</p>
1177
+ <p className="text-sm">
1178
+ {env.value
1179
+ ? "You've entered a custom value for this agent. This overrides the global environment setting. Clear the field to use the global value."
1180
+ : 'This secret is currently using the value from your global environment settings. The actual value is hidden for security. Enter a value here if you want to override it for this agent only.'}
1181
+ </p>
1182
+ </div>
1183
+ </TooltipContent>
1184
+ )}
1185
+ </Tooltip>
1186
+ <Button
1187
+ type="button"
1188
+ variant="ghost"
1189
+ size="sm"
1190
+ onClick={() => toggleSecretVisibility(index)}
1191
+ className="h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
1192
+ >
1193
+ {visibleSecrets.has(index) ? (
1194
+ <EyeOff className="w-4 h-4" />
1195
+ ) : (
1196
+ <Eye className="w-4 h-4" />
1197
+ )}
1198
+ </Button>
1199
+ </div>
1200
+ )}
1201
+ </div>
1202
+
1203
+ {/* Actions Column */}
1204
+ <div className="flex items-center justify-center gap-1">
1205
+ <Button
1206
+ type="button"
1207
+ variant="ghost"
1208
+ size="sm"
1209
+ onClick={() => startEditing(index)}
1210
+ disabled={editingIndex !== null && editingIndex !== index}
1211
+ className="h-8 w-8 p-0"
1212
+ >
1213
+ <Edit3 className="w-4 h-4" />
1214
+ </Button>
1215
+ <Button
1216
+ type="button"
1217
+ variant="ghost"
1218
+ size="sm"
1219
+ onClick={() => removeEnv(index)}
1220
+ disabled={editingIndex !== null}
1221
+ className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
1222
+ >
1223
+ <Trash2 className="w-4 h-4" />
1224
+ </Button>
1225
+ </div>
1226
+ </div>
1227
+ ))}
1228
+ </div>
1229
+ </div>
1230
+
1231
+ {/* Mobile Card Layout */}
1232
+ <div className="md:hidden space-y-3">
1233
+ {envs.map((env, index) => (
1234
+ <div
1235
+ key={`${env.name}-${index}-mobile`}
1236
+ className={`border rounded-lg p-4 space-y-3 ${
1237
+ env.isRequired && (!env.value || env.value.trim() === '')
1238
+ ? 'border-red-500/50 bg-red-500/5'
1239
+ : ''
1240
+ }`}
1241
+ >
1242
+ {/* Header with name and required badge */}
1243
+ <div className="space-y-1">
1244
+ <div className="flex items-start justify-between gap-2">
1245
+ <div className="flex-1 min-w-0">
1246
+ <div className="flex items-center gap-2 flex-wrap">
1247
+ <code className="font-mono text-sm font-medium break-all">
1248
+ {env.name}
1249
+ </code>
1250
+ {env.isRequired && (
1251
+ <Badge variant="outline" className="text-xs">
1252
+ Required
1253
+ </Badge>
1254
+ )}
1255
+ {isInGlobalEnv(env.name) && (
1256
+ <Badge
1257
+ variant="secondary"
1258
+ className="text-xs flex items-center gap-1"
1259
+ >
1260
+ <Globe className="w-3 h-3" />
1261
+ Global
1262
+ </Badge>
1263
+ )}
1264
+ </div>
1265
+ {env.description && (
1266
+ <div className="text-xs text-muted-foreground mt-1">
1267
+ {env.description}
1268
+ </div>
1269
+ )}
1270
+ {env.plugin && (
1271
+ <div className="text-xs text-muted-foreground">
1272
+ Required by: {env.plugin}
1273
+ </div>
1274
+ )}
1275
+ </div>
1276
+ {/* Actions on mobile */}
1277
+ <div className="flex items-center gap-1">
1278
+ <Button
1279
+ type="button"
1280
+ variant="ghost"
1281
+ size="sm"
1282
+ onClick={() => startEditing(index)}
1283
+ disabled={editingIndex !== null && editingIndex !== index}
1284
+ className="h-8 w-8 p-0"
1285
+ >
1286
+ <Edit3 className="w-4 h-4" />
1287
+ </Button>
1288
+ <Button
1289
+ type="button"
1290
+ variant="ghost"
1291
+ size="sm"
1292
+ onClick={() => removeEnv(index)}
1293
+ disabled={editingIndex !== null}
1294
+ className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
1295
+ >
1296
+ <Trash2 className="w-4 h-4" />
1297
+ </Button>
1298
+ </div>
1299
+ </div>
1300
+ </div>
1301
+
1302
+ {/* Value section */}
1303
+ <div className="space-y-1">
1304
+ <div className="text-xs font-medium text-muted-foreground uppercase">
1305
+ Value
1306
+ </div>
1307
+ {editingIndex === index ? (
1308
+ <div className="space-y-2">
1309
+ <div className="relative">
1310
+ <div className={`rounded border ${scrollbarContainerClass}`}>
1311
+ <Input
1312
+ value={editedValue}
1313
+ onChange={(e) => setEditedValue(e.target.value)}
1314
+ className="font-mono text-sm pr-10 border-0 focus-visible:ring-0 w-auto min-w-full rounded"
1315
+ style={{ minWidth: '100%' }}
1316
+ type={visibleSecrets.has(index) ? 'text' : 'password'}
1317
+ placeholder={
1318
+ isInGlobalEnv(env.name) && !env.value
1319
+ ? `Using global (enter to override)`
1320
+ : env.example || 'Enter value...'
1321
+ }
1322
+ autoFocus
1323
+ onKeyDown={(e) => {
1324
+ if (e.key === 'Enter') {
1325
+ e.preventDefault();
1326
+ saveEdit(index);
1327
+ } else if (e.key === 'Escape') {
1328
+ cancelEdit();
1329
+ }
1330
+ }}
1331
+ />
1332
+ </div>
1333
+ <Button
1334
+ type="button"
1335
+ variant="ghost"
1336
+ size="sm"
1337
+ onClick={() => toggleSecretVisibility(index)}
1338
+ className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0"
1339
+ >
1340
+ {visibleSecrets.has(index) ? (
1341
+ <EyeOff className="w-4 h-4" />
1342
+ ) : (
1343
+ <Eye className="w-4 h-4" />
1344
+ )}
1345
+ </Button>
1346
+ </div>
1347
+ <div className="flex items-center gap-2">
1348
+ <Button
1349
+ type="button"
1350
+ variant="outline"
1351
+ size="sm"
1352
+ onClick={() => saveEdit(index)}
1353
+ className="flex-1"
1354
+ >
1355
+ <Check className="w-4 h-4 mr-2" />
1356
+ Save
1357
+ </Button>
1358
+ <Button
1359
+ type="button"
1360
+ variant="ghost"
1361
+ size="sm"
1362
+ onClick={cancelEdit}
1363
+ className="flex-1"
1364
+ >
1365
+ <X className="w-4 h-4 mr-2" />
1366
+ Cancel
1367
+ </Button>
1368
+ </div>
1369
+ </div>
1370
+ ) : (
1371
+ <div className="flex items-center gap-2">
1372
+ <Tooltip>
1373
+ <TooltipTrigger asChild>
1374
+ <div
1375
+ className={`flex-1 border rounded px-3 py-1 ${scrollbarContainerClass}`}
1376
+ >
1377
+ {visibleSecrets.has(index) ? (
1378
+ <code className="font-mono text-sm whitespace-nowrap block">
1379
+ {env.value || (
1380
+ <span className="text-muted-foreground italic">
1381
+ {isInGlobalEnv(env.name)
1382
+ ? 'Using global value'
1383
+ : env.example
1384
+ ? `e.g. ${env.example}`
1385
+ : 'Not set'}
1386
+ </span>
1387
+ )}
1388
+ </code>
1389
+ ) : env.value ? (
1390
+ <code className="font-mono text-sm text-muted-foreground block">
1391
+ ••••••••
1392
+ </code>
1393
+ ) : (
1394
+ <span className="text-muted-foreground italic text-sm block">
1395
+ {isInGlobalEnv(env.name) ? 'Using global value' : 'Not set'}
1396
+ </span>
1397
+ )}
1398
+ </div>
1399
+ </TooltipTrigger>
1400
+ {isInGlobalEnv(env.name) && (
1401
+ <TooltipContent className="max-w-xs">
1402
+ <div className="space-y-1">
1403
+ <p className="font-semibold">Global Environment Variable</p>
1404
+ <p className="text-sm">
1405
+ {env.value
1406
+ ? "You've entered a custom value for this agent. This overrides the global environment setting. Clear the field to use the global value."
1407
+ : 'This secret is currently using the value from your global environment settings. The actual value is hidden for security. Enter a value here if you want to override it for this agent only.'}
1408
+ </p>
1409
+ </div>
1410
+ </TooltipContent>
1411
+ )}
1412
+ </Tooltip>
1413
+ <Button
1414
+ type="button"
1415
+ variant="ghost"
1416
+ size="sm"
1417
+ onClick={() => toggleSecretVisibility(index)}
1418
+ className="h-8 w-8 p-0"
1419
+ >
1420
+ {visibleSecrets.has(index) ? (
1421
+ <EyeOff className="w-4 h-4" />
1422
+ ) : (
1423
+ <Eye className="w-4 h-4" />
1424
+ )}
1425
+ </Button>
1426
+ </div>
1427
+ )}
1428
+ </div>
1429
+ </div>
1430
+ ))}
1431
+ </div>
1432
+ </TooltipProvider>
1433
+ )}
1434
+
1435
+ {/* File Upload Area */}
1436
+ <div
1437
+ ref={dropRef}
1438
+ className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
1439
+ isDragging
1440
+ ? 'border-primary bg-primary/5'
1441
+ : 'border-muted-foreground/25 hover:border-muted-foreground/50'
1442
+ }`}
1443
+ onClick={() => document.getElementById('env-upload')?.click()}
1444
+ >
1445
+ <CloudUpload className="w-8 h-8 mx-auto mb-2 text-muted-foreground" />
1446
+ <div className="text-sm text-muted-foreground">
1447
+ Drag & drop <code className="bg-muted px-1 rounded">.env</code> file or click to
1448
+ select
1449
+ </div>
1450
+ <input
1451
+ id="env-upload"
1452
+ type="file"
1453
+ accept="*/*"
1454
+ className="hidden"
1455
+ onChange={(e) => {
1456
+ const file = e.target.files?.[0];
1457
+ if (file && file.name.endsWith('.env')) {
1458
+ handleFileUpload(file);
1459
+ }
1460
+ }}
1461
+ />
1462
+ </div>
1463
+ </div>
1464
+ </ScrollArea>
1465
+
1466
+ {/* Raw Editor Modal */}
1467
+ <Dialog open={rawEditorOpen} onOpenChange={setRawEditorOpen}>
1468
+ <DialogContent className="max-w-3xl h-[600px] flex flex-col">
1469
+ <DialogHeader>
1470
+ <DialogTitle>Raw Editor</DialogTitle>
1471
+ <DialogDescription>
1472
+ Add, edit, or delete your project variables in .env format
1473
+ </DialogDescription>
1474
+ </DialogHeader>
1475
+ <div className="flex-1 border rounded-lg overflow-hidden">
1476
+ <Textarea
1477
+ value={rawEditorContent}
1478
+ onChange={(e) => setRawEditorContent(e.target.value)}
1479
+ placeholder="# Environment Variables&#10;&#10;# Required Secrets&#10;API_KEY=your_api_key_here&#10;&#10;# Optional Variables&#10;DEBUG=true&#10;&#10;# Comments are supported"
1480
+ className="w-full h-full resize-none border-0 font-mono text-sm focus-visible:ring-0"
1481
+ />
1482
+ </div>
1483
+ <DialogFooter>
1484
+ <Button variant="outline" size="sm" onClick={copyEnvContent}>
1485
+ <Copy className="w-4 h-4 mr-2" />
1486
+ Copy ENV
1487
+ </Button>
1488
+ <Button variant="outline" size="sm" onClick={() => setRawEditorOpen(false)}>
1489
+ Cancel
1490
+ </Button>
1491
+ <Button size="sm" onClick={handleRawEditorSave}>
1492
+ Update Variables
1493
+ </Button>
1494
+ </DialogFooter>
1495
+ </DialogContent>
1496
+ </Dialog>
1497
+ </div>
1498
+ );
1499
+ }
1500
+ );
1501
+
1502
+ SecretPanel.displayName = 'SecretPanel';
1503
+
1504
+ // Also provide a default export for backward compatibility
1505
+ export default SecretPanel;