@polderlabs/bizar 4.4.13 → 4.5.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 (118) hide show
  1. package/bizar-dash/CHANGELOG.md +37 -276
  2. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +2 -0
  3. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
  4. package/bizar-dash/dist/assets/main-eWZ4NlCL.css +1 -0
  5. package/bizar-dash/dist/assets/main-usWhlPWa.js +362 -0
  6. package/bizar-dash/dist/assets/main-usWhlPWa.js.map +1 -0
  7. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile-DQLFCjwJ.js} +2 -2
  8. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile-DQLFCjwJ.js.map} +1 -1
  9. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js +352 -0
  10. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +1 -0
  11. package/bizar-dash/dist/index.html +3 -3
  12. package/bizar-dash/dist/mobile.html +2 -2
  13. package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
  14. package/bizar-dash/skills/bizar/SKILL.md +116 -0
  15. package/bizar-dash/skills/chat/SKILL.md +74 -0
  16. package/bizar-dash/skills/headroom/SKILL.md +94 -0
  17. package/bizar-dash/skills/lightrag/SKILL.md +86 -0
  18. package/bizar-dash/skills/minimax/SKILL.md +80 -0
  19. package/bizar-dash/skills/obsidian/SKILL.md +68 -0
  20. package/bizar-dash/skills/providers/SKILL.md +75 -0
  21. package/bizar-dash/skills/sdk/SKILL.md +138 -0
  22. package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
  23. package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
  24. package/bizar-dash/skills/usage/SKILL.md +62 -0
  25. package/bizar-dash/src/server/api.mjs +18 -0
  26. package/bizar-dash/src/server/headroom.mjs +645 -0
  27. package/bizar-dash/src/server/memory-lightrag.mjs +272 -2
  28. package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
  29. package/bizar-dash/src/server/memory-store.mjs +189 -0
  30. package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
  31. package/bizar-dash/src/server/minimax.mjs +196 -5
  32. package/bizar-dash/src/server/providers-store.mjs +956 -0
  33. package/bizar-dash/src/server/routes/_shared.mjs +17 -0
  34. package/bizar-dash/src/server/routes/config.mjs +52 -1
  35. package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
  36. package/bizar-dash/src/server/routes/headroom.mjs +126 -0
  37. package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
  38. package/bizar-dash/src/server/routes/memory.mjs +668 -1
  39. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
  40. package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
  41. package/bizar-dash/src/server/routes/providers.mjs +266 -5
  42. package/bizar-dash/src/server/routes/skills.mjs +32 -43
  43. package/bizar-dash/src/server/routes/update.mjs +340 -0
  44. package/bizar-dash/src/server/routes/usage.mjs +136 -0
  45. package/bizar-dash/src/server/serve-info.mjs +135 -4
  46. package/bizar-dash/src/server/server.mjs +20 -0
  47. package/bizar-dash/src/server/skills-store.mjs +152 -262
  48. package/bizar-dash/src/web/App.tsx +120 -29
  49. package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
  50. package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
  51. package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
  52. package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
  53. package/bizar-dash/src/web/components/Topbar.tsx +2 -1
  54. package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
  55. package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
  56. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
  57. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
  58. package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
  59. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
  60. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
  61. package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
  62. package/bizar-dash/src/web/lib/api.ts +43 -0
  63. package/bizar-dash/src/web/lib/types.ts +16 -0
  64. package/bizar-dash/src/web/main.tsx +2 -0
  65. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
  66. package/bizar-dash/src/web/styles/chat.css +135 -1
  67. package/bizar-dash/src/web/styles/main.css +46 -0
  68. package/bizar-dash/src/web/styles/memory.css +955 -0
  69. package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
  70. package/bizar-dash/src/web/styles/settings.css +418 -0
  71. package/bizar-dash/src/web/styles/skills.css +302 -0
  72. package/bizar-dash/src/web/styles/tasks.css +288 -0
  73. package/bizar-dash/src/web/views/Chat.tsx +276 -48
  74. package/bizar-dash/src/web/views/Config.tsx +3 -2065
  75. package/bizar-dash/src/web/views/Memory.tsx +140 -0
  76. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
  77. package/bizar-dash/src/web/views/Overview.tsx +3 -0
  78. package/bizar-dash/src/web/views/Settings.tsx +36 -0
  79. package/bizar-dash/src/web/views/Skills.tsx +208 -260
  80. package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
  81. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
  82. package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
  83. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
  84. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +354 -0
  85. package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
  86. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
  87. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
  88. package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
  89. package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
  90. package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
  91. package/bizar-dash/tests/headroom-install.test.mjs +173 -0
  92. package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
  93. package/bizar-dash/tests/headroom-status.test.mjs +117 -0
  94. package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
  95. package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
  96. package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
  97. package/bizar-dash/tests/memory-tab.test.mjs +322 -0
  98. package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
  99. package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
  100. package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
  101. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
  102. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
  103. package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
  104. package/bizar-dash/tests/skills-list.test.mjs +232 -0
  105. package/bizar-dash/tests/skills-search.test.mjs +222 -0
  106. package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
  107. package/bizar-dash/tests/tasks-create.test.mjs +187 -0
  108. package/bizar-dash/tests/update-check.test.mjs +127 -0
  109. package/bizar-dash/tests/update-run.test.mjs +266 -0
  110. package/cli/bin.mjs +247 -1
  111. package/cli/provision.mjs +118 -4
  112. package/config/agents/_shared/SKILLS.md +109 -0
  113. package/package.json +1 -1
  114. package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
  115. package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
  116. package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
  117. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
  118. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
@@ -7,8 +7,8 @@ import { ModalProvider, useModal } from './components/Modal';
7
7
  import { ToastProvider, useToast } from './components/Toast';
8
8
  import { SearchModal } from './components/SearchModal';
9
9
  import { Notifications } from './components/Notifications';
10
+ import { api, ApiError } from './lib/api';
10
11
  import { CommandDialog, type DialogDescriptor } from './components/CommandDialog';
11
- import { api } from './lib/api';
12
12
  import { Ws } from './lib/ws';
13
13
  import {
14
14
  applyTheme,
@@ -34,6 +34,7 @@ import { Mods } from './views/Mods';
34
34
  import { ModView, type ModView as ModViewType } from './views/ModView';
35
35
  import { Schedules } from './views/Schedules';
36
36
  import { Skills } from './views/Skills';
37
+ import { Memory } from './views/Memory';
37
38
  import { History } from './views/History';
38
39
  import { MiniMaxUsage } from './views/MiniMaxUsage';
39
40
  import { BackgroundAgents } from './views/BackgroundAgents';
@@ -67,6 +68,7 @@ const VIEW_MAP: Record<string, (p: ViewProps) => React.ReactNode> = {
67
68
  chat: Chat,
68
69
  agents: Agents,
69
70
  artifacts: Artifacts,
71
+ memory: Memory,
70
72
  tasks: Tasks,
71
73
  activity: Activity,
72
74
  background: BackgroundAgents,
@@ -79,7 +81,7 @@ const VIEW_MAP: Record<string, (p: ViewProps) => React.ReactNode> = {
79
81
  minimax: MiniMaxUsage,
80
82
  };
81
83
 
82
- const VERSION = 'v3.21.0';
84
+ const VERSION = 'v4.5.0';
83
85
 
84
86
  /**
85
87
  * Render the active view. If `activeTab` matches a built-in tab id,
@@ -113,6 +115,67 @@ function renderActiveView(
113
115
  return <V {...viewProps} />;
114
116
  }
115
117
 
118
+ // v3.6.2 — Inline token-entry form shown on the boot-error screen when
119
+ // the server returns 401 (reverse-proxy / Tailscale Serve scenario).
120
+ // The user pastes the token, it is saved to localStorage, and boot is
121
+ // retried. The form closes on Escape or on successful save.
122
+ function TokenEntryForm({ onSaved }: { onSaved: () => void }) {
123
+ const [value, setValue] = useState('');
124
+ const inputRef = useRef<HTMLInputElement>(null);
125
+
126
+ // Auto-focus the input on mount
127
+ useEffect(() => {
128
+ inputRef.current?.focus();
129
+ }, []);
130
+
131
+ const submit = () => {
132
+ const tok = value.trim();
133
+ if (!tok) return;
134
+ api.setToken(tok);
135
+ onSaved();
136
+ };
137
+
138
+ const handleKeyDown = (e: React.KeyboardEvent) => {
139
+ if (e.key === 'Escape') {
140
+ e.preventDefault();
141
+ onSaved();
142
+ }
143
+ if (e.key === 'Enter') {
144
+ e.preventDefault();
145
+ submit();
146
+ }
147
+ };
148
+
149
+ return (
150
+ <form
151
+ className="token-entry-form"
152
+ onSubmit={(e) => { e.preventDefault(); submit(); }}
153
+ >
154
+ <div className="token-entry-row">
155
+ <input
156
+ ref={inputRef}
157
+ type="password"
158
+ className="input mono"
159
+ value={value}
160
+ onChange={(e) => setValue(e.target.value)}
161
+ placeholder="Paste auth token"
162
+ spellCheck={false}
163
+ autoComplete="off"
164
+ onKeyDown={handleKeyDown}
165
+ />
166
+ <Button type="submit" variant="primary" size="sm" disabled={!value.trim()}>
167
+ Save &amp; retry
168
+ </Button>
169
+ </div>
170
+ <p className="token-entry-hint">
171
+ Where do I find this token? Check the file{' '}
172
+ <code>~/.config/bizar/dashboard-secret</code> on the machine
173
+ running the dashboard, or look for it in the server output.
174
+ </p>
175
+ </form>
176
+ );
177
+ }
178
+
116
179
  export function App() {
117
180
  return (
118
181
  <ToastProvider>
@@ -133,6 +196,7 @@ function Shell() {
133
196
  const [settings, setSettings] = useState<Settings | null>(null);
134
197
  const [wsStatus, setWsStatus] = useState<WsStatus>('connecting');
135
198
  const [bootError, setBootError] = useState<string | null>(null);
199
+ const [authPromptOpen, setAuthPromptOpen] = useState(false);
136
200
  const [searchOpen, setSearchOpen] = useState(false);
137
201
  const [stuckAgents, setStuckAgents] = useState<{ name: string }[]>([]);
138
202
  const [stuckBannerDismissed, setStuckBannerDismissed] = useState(false);
@@ -196,9 +260,11 @@ function Shell() {
196
260
  return () => mql.removeEventListener('change', handler);
197
261
  }, [settings?.theme?.mode]);
198
262
 
199
- // Initial fetch runs once on mount
200
- useEffect(() => {
263
+ // v3.6.2Boot logic extracted to retryBoot so it can be re-invoked
264
+ // after the user pastes a token in the inline recovery form.
265
+ const retryBoot = useCallback(async () => {
201
266
  let cancelled = false;
267
+
202
268
  const loadBootData = () =>
203
269
  Promise.all([
204
270
  api.get<Snapshot>('/snapshot').catch(() => null),
@@ -216,42 +282,59 @@ function Shell() {
216
282
  if (stuck?.stuck) setStuckAgents(stuck.stuck);
217
283
  };
218
284
 
219
- (async () => {
220
- try {
221
- const auth = await api.probeAuthStatus();
222
- const [snap, set, stuck] = await loadBootData();
223
- if (cancelled) return;
285
+ try {
286
+ const auth = await api.probeAuthStatus();
287
+ const [snap, set, stuck] = await loadBootData();
288
+ if (cancelled) return;
224
289
 
225
- applyBootData(snap, set, stuck);
226
- if (snap || set) return;
290
+ applyBootData(snap, set, stuck);
291
+ if (snap || set) {
292
+ setBootError(null);
293
+ return;
294
+ }
227
295
 
228
- if (!auth.loopback) {
229
- setBootError('Dashboard server unreachable.');
230
- return;
231
- }
296
+ if (!auth.loopback) {
297
+ setBootError('Dashboard server unreachable.');
298
+ return;
299
+ }
232
300
 
233
- await new Promise((resolve) => setTimeout(resolve, 1000));
234
- if (cancelled) return;
301
+ await new Promise((resolve) => setTimeout(resolve, 1000));
302
+ if (cancelled) return;
235
303
 
236
- const [retrySnap, retrySet, retryStuck] = await loadBootData();
237
- if (cancelled) return;
304
+ const [retrySnap, retrySet, retryStuck] = await loadBootData();
305
+ if (cancelled) return;
238
306
 
239
- applyBootData(retrySnap, retrySet, retryStuck);
240
- if (!retrySnap && !retrySet) {
241
- setBootError('Dashboard server unreachable.');
242
- }
243
- } catch (err) {
244
- if (cancelled) return;
245
- const msg = (err as Error)?.message ?? 'unknown error';
307
+ applyBootData(retrySnap, retrySet, retryStuck);
308
+ if (!retrySnap && !retrySet) {
309
+ setBootError('Dashboard server unreachable.');
310
+ }
311
+ } catch (err) {
312
+ if (cancelled) return;
313
+ const msg = (err as Error)?.message ?? 'unknown error';
314
+ // v3.6.2 — If we got a 401, the user needs to supply a token.
315
+ // Surface the form inline instead of just the error message.
316
+ if (err instanceof ApiError && err.status === 401) {
246
317
  setBootError(msg);
247
- toast.error(`Failed to load: ${msg}`);
318
+ setAuthPromptOpen(true);
319
+ toast.error(`Auth required: ${msg}`);
320
+ return;
248
321
  }
249
- })();
322
+ setBootError(msg);
323
+ toast.error(`Failed to load: ${msg}`);
324
+ }
325
+ }, [toast]);
250
326
 
327
+ // Initial fetch — runs once on mount
328
+ useEffect(() => {
329
+ let cancelled = false;
330
+ const id = setTimeout(() => {
331
+ if (!cancelled) retryBoot();
332
+ }, 0);
251
333
  return () => {
252
334
  cancelled = true;
335
+ clearTimeout(id);
253
336
  };
254
- }, [toast]);
337
+ }, [retryBoot]);
255
338
 
256
339
  // Apply defaultTab ONCE on mount only — must NOT re-fire on toast changes
257
340
  useEffect(() => {
@@ -695,6 +778,14 @@ function Shell() {
695
778
  <div className="boot-error">
696
779
  <h2>Dashboard unavailable</h2>
697
780
  <p>{bootError}</p>
781
+ {authPromptOpen && (
782
+ <TokenEntryForm
783
+ onSaved={() => {
784
+ setAuthPromptOpen(false);
785
+ retryBoot();
786
+ }}
787
+ />
788
+ )}
698
789
  <p className="boot-error-hint">
699
790
  Make sure the Bizar dashboard server is running. Try{' '}
700
791
  <code>bizar-dash start</code> in your terminal.
@@ -0,0 +1,247 @@
1
+ // src/web/components/EnvVarManager.tsx — manage BIZAR_* env vars from ~/.config/bizar/env.json
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import { Plus, Pencil, Trash2, Eye, EyeOff, RefreshCw, KeyRound, CheckCircle, X } from 'lucide-react';
4
+ import { Button } from './Button';
5
+ import { useToast } from './Toast';
6
+ import { api } from '../lib/api';
7
+ import { cn } from '../lib/utils';
8
+
9
+ export type EnvVarEntry = {
10
+ name: string;
11
+ value: string; // masked on GET, full on edit
12
+ createdAt: string;
13
+ source: string;
14
+ };
15
+
16
+ type Props = {
17
+ onError?: (msg: string) => void;
18
+ };
19
+
20
+ export function EnvVarManager({ onError }: Props) {
21
+ const toast = useToast();
22
+ const [vars, setVars] = useState<EnvVarEntry[]>([]);
23
+ const [loading, setLoading] = useState(false);
24
+ const [saving, setSaving] = useState(false);
25
+ // Which rows have the value revealed (edit mode shows real value)
26
+ const [revealed, setRevealed] = useState<Set<string>>(new Set());
27
+ // Currently editing name (null = none)
28
+ const [editing, setEditing] = useState<string | null>(null);
29
+ const [editValue, setEditValue] = useState('');
30
+ // New var form
31
+ const [showAdd, setShowAdd] = useState(false);
32
+ const [newName, setNewName] = useState('');
33
+ const [newValue, setNewValue] = useState('');
34
+
35
+ const load = useCallback(async () => {
36
+ setLoading(true);
37
+ try {
38
+ const r = await api.get<EnvVarEntry[]>('/env-vars');
39
+ setVars(r);
40
+ } catch (err) {
41
+ toast.error(`Load failed: ${(err as Error).message}`);
42
+ onError?.((err as Error).message);
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }, [toast, onError]);
47
+
48
+ useEffect(() => { load(); }, [load]);
49
+
50
+ const toggleReveal = (name: string) => {
51
+ setRevealed((prev) => {
52
+ const next = new Set(prev);
53
+ if (next.has(name)) next.delete(name);
54
+ else next.add(name);
55
+ return next;
56
+ });
57
+ };
58
+
59
+ const handleAdd = async () => {
60
+ const name = newName.trim().toUpperCase();
61
+ const value = newValue.trim();
62
+ if (!name || !/^BIZAR_[A-Z0-9_]+$/.test(name)) {
63
+ toast.warning('Name must match BIZAR_<NAME> (e.g. BIZAR_MINIMAX_KEY)');
64
+ return;
65
+ }
66
+ if (!value) {
67
+ toast.warning('Value is required.');
68
+ return;
69
+ }
70
+ if (vars.some((v) => v.name === name)) {
71
+ toast.warning(`${name} already exists.`);
72
+ return;
73
+ }
74
+ setSaving(true);
75
+ try {
76
+ await api.post('/env-vars', { name, value });
77
+ toast.success(`${name} created.`);
78
+ setShowAdd(false);
79
+ setNewName('');
80
+ setNewValue('');
81
+ await load();
82
+ } catch (err) {
83
+ toast.error(`Create failed: ${(err as Error).message}`);
84
+ } finally {
85
+ setSaving(false);
86
+ }
87
+ };
88
+
89
+ const startEdit = (v: EnvVarEntry) => {
90
+ setEditing(v.name);
91
+ setEditValue(v.value === v.value.replace(/\*/g, '') ? v.value : '');
92
+ setRevealed((prev) => { const next = new Set(prev); next.add(v.name); return next; });
93
+ };
94
+
95
+ const handleUpdate = async (name: string) => {
96
+ if (!editValue.trim()) {
97
+ toast.warning('Value cannot be empty.');
98
+ return;
99
+ }
100
+ setSaving(true);
101
+ try {
102
+ await api.put(`/env-vars/${encodeURIComponent(name)}`, { value: editValue.trim() });
103
+ toast.success(`${name} updated.`);
104
+ setEditing(null);
105
+ await load();
106
+ } catch (err) {
107
+ toast.error(`Update failed: ${(err as Error).message}`);
108
+ } finally {
109
+ setSaving(false);
110
+ }
111
+ };
112
+
113
+ const handleDelete = async (name: string) => {
114
+ if (!confirm(`Delete ${name}? This cannot be undone.`)) return;
115
+ try {
116
+ await api.del(`/env-vars/${encodeURIComponent(name)}`);
117
+ toast.success(`${name} deleted.`);
118
+ await load();
119
+ } catch (err) {
120
+ toast.error(`Delete failed: ${(err as Error).message}`);
121
+ }
122
+ };
123
+
124
+ if (loading) {
125
+ return <div className="muted" style={{ padding: '16px 0', fontSize: 13 }}>Loading env vars…</div>;
126
+ }
127
+
128
+ return (
129
+ <div className="env-var-manager">
130
+ <div className="env-var-toolbar">
131
+ <Button variant="primary" size="sm" onClick={() => setShowAdd(true)}>
132
+ <Plus size={12} /> New variable
133
+ </Button>
134
+ <Button variant="ghost" size="sm" onClick={load} title="Reload">
135
+ <RefreshCw size={12} />
136
+ </Button>
137
+ </div>
138
+
139
+ {vars.length === 0 && !showAdd && (
140
+ <div className="env-var-empty">
141
+ <KeyRound size={20} />
142
+ <span>No BIZAR_* env vars yet.</span>
143
+ <span style={{ fontSize: 11 }}>Create one to store API keys securely.</span>
144
+ </div>
145
+ )}
146
+
147
+ {/* New var form */}
148
+ {showAdd && (
149
+ <div className="env-var-add-form">
150
+ <div className="env-var-add-row">
151
+ <input
152
+ className="input mono"
153
+ style={{ width: 240 }}
154
+ placeholder="BIZAR_NAME"
155
+ value={newName}
156
+ onChange={(e) => setNewName(e.target.value.toUpperCase())}
157
+ onKeyDown={(e) => { if (e.key === 'Enter') handleAdd(); if (e.key === 'Escape') setShowAdd(false); }}
158
+ autoFocus
159
+ />
160
+ <input
161
+ className="input mono"
162
+ type="password"
163
+ placeholder="value (API key, token, etc.)"
164
+ value={newValue}
165
+ onChange={(e) => setNewValue(e.target.value)}
166
+ onKeyDown={(e) => { if (e.key === 'Enter') handleAdd(); if (e.key === 'Escape') setShowAdd(false); }}
167
+ />
168
+ <Button variant="primary" size="sm" onClick={handleAdd} disabled={saving}>
169
+ <CheckCircle size={12} /> Create
170
+ </Button>
171
+ <Button variant="ghost" size="sm" onClick={() => { setShowAdd(false); setNewName(''); setNewValue(''); }}>
172
+ <X size={12} />
173
+ </Button>
174
+ </div>
175
+ <p style={{ fontSize: 11, color: 'var(--text-dim)', margin: 0 }}>
176
+ Names must match <code>BIZAR_<var>NAME</var></code> (uppercase letters, digits, underscores).
177
+ </p>
178
+ </div>
179
+ )}
180
+
181
+ {/* Var list */}
182
+ {vars.length > 0 && (
183
+ <div className="env-var-list">
184
+ {vars.map((v) => {
185
+ const isEditing = editing === v.name;
186
+ const isRevealed = revealed.has(v.name);
187
+ return (
188
+ <div key={v.name} className={cn('env-var-row', isEditing && 'env-var-row-editing')}>
189
+ <span className="env-var-name mono">{v.name}</span>
190
+ {isEditing ? (
191
+ <input
192
+ className="input mono"
193
+ type="password"
194
+ style={{ flex: 1, minWidth: 200 }}
195
+ value={editValue}
196
+ onChange={(e) => setEditValue(e.target.value)}
197
+ onKeyDown={(e) => {
198
+ if (e.key === 'Enter') handleUpdate(v.name);
199
+ if (e.key === 'Escape') setEditing(null);
200
+ }}
201
+ autoFocus
202
+ />
203
+ ) : (
204
+ <span className={cn('env-var-value mono', !isRevealed && 'env-var-value-masked')}>
205
+ {isRevealed ? v.value : v.value}
206
+ </span>
207
+ )}
208
+ <span className="env-var-source muted">{v.source}</span>
209
+ <div className="env-var-actions">
210
+ {!isEditing && (
211
+ <button
212
+ type="button"
213
+ className="icon-btn"
214
+ title={isRevealed ? 'Hide' : 'Reveal'}
215
+ onClick={() => toggleReveal(v.name)}
216
+ >
217
+ {isRevealed ? <EyeOff size={12} /> : <Eye size={12} />}
218
+ </button>
219
+ )}
220
+ {isEditing ? (
221
+ <>
222
+ <Button variant="primary" size="sm" onClick={() => handleUpdate(v.name)} disabled={saving}>
223
+ Save
224
+ </Button>
225
+ <Button variant="ghost" size="sm" onClick={() => setEditing(null)}>
226
+ Cancel
227
+ </Button>
228
+ </>
229
+ ) : (
230
+ <>
231
+ <button type="button" className="icon-btn" title="Edit" onClick={() => startEdit(v)}>
232
+ <Pencil size={12} />
233
+ </button>
234
+ <button type="button" className="icon-btn icon-btn-danger" title="Delete" onClick={() => handleDelete(v.name)}>
235
+ <Trash2 size={12} />
236
+ </button>
237
+ </>
238
+ )}
239
+ </div>
240
+ </div>
241
+ );
242
+ })}
243
+ </div>
244
+ )}
245
+ </div>
246
+ );
247
+ }