@polderlabs/bizar 4.4.12 → 4.5.0

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 (91) hide show
  1. package/bizar-dash/CHANGELOG.md +37 -276
  2. package/bizar-dash/dist/assets/main-CDFKHzBg.css +1 -0
  3. package/bizar-dash/dist/assets/main-NYFpS2wY.js +312 -0
  4. package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +1 -0
  5. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile--0FBIKX3.js} +2 -2
  6. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile--0FBIKX3.js.map} +1 -1
  7. package/bizar-dash/dist/assets/mobile-OgRp8VIb.js +352 -0
  8. package/bizar-dash/dist/assets/mobile-OgRp8VIb.js.map +1 -0
  9. package/bizar-dash/dist/index.html +3 -3
  10. package/bizar-dash/dist/mobile.html +2 -2
  11. package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
  12. package/bizar-dash/skills/bizar/SKILL.md +96 -0
  13. package/bizar-dash/skills/chat/SKILL.md +74 -0
  14. package/bizar-dash/skills/lightrag/SKILL.md +75 -0
  15. package/bizar-dash/skills/minimax/SKILL.md +80 -0
  16. package/bizar-dash/skills/obsidian/SKILL.md +55 -0
  17. package/bizar-dash/skills/providers/SKILL.md +75 -0
  18. package/bizar-dash/skills/sdk/SKILL.md +138 -0
  19. package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
  20. package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
  21. package/bizar-dash/skills/usage/SKILL.md +62 -0
  22. package/bizar-dash/src/server/api.mjs +12 -0
  23. package/bizar-dash/src/server/memory-lightrag.mjs +5 -2
  24. package/bizar-dash/src/server/memory-store.mjs +38 -0
  25. package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
  26. package/bizar-dash/src/server/minimax.mjs +427 -110
  27. package/bizar-dash/src/server/providers-store.mjs +966 -6
  28. package/bizar-dash/src/server/routes/config.mjs +52 -1
  29. package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
  30. package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
  31. package/bizar-dash/src/server/routes/memory.mjs +241 -1
  32. package/bizar-dash/src/server/routes/minimax.mjs +50 -57
  33. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
  34. package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
  35. package/bizar-dash/src/server/routes/providers.mjs +266 -5
  36. package/bizar-dash/src/server/routes/skills.mjs +32 -43
  37. package/bizar-dash/src/server/routes/update.mjs +340 -0
  38. package/bizar-dash/src/server/routes/usage.mjs +136 -0
  39. package/bizar-dash/src/server/serve-info.mjs +135 -4
  40. package/bizar-dash/src/server/server.mjs +4 -0
  41. package/bizar-dash/src/server/skills-store.mjs +152 -262
  42. package/bizar-dash/src/web/App.tsx +118 -29
  43. package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
  44. package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
  45. package/bizar-dash/src/web/components/Topbar.tsx +0 -1
  46. package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
  47. package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
  48. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
  49. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
  50. package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
  51. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
  52. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
  53. package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
  54. package/bizar-dash/src/web/lib/api.ts +43 -0
  55. package/bizar-dash/src/web/main.tsx +1 -0
  56. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
  57. package/bizar-dash/src/web/styles/chat.css +135 -1
  58. package/bizar-dash/src/web/styles/main.css +46 -0
  59. package/bizar-dash/src/web/styles/minimax-usage.css +471 -0
  60. package/bizar-dash/src/web/styles/settings.css +418 -0
  61. package/bizar-dash/src/web/styles/skills.css +302 -0
  62. package/bizar-dash/src/web/styles/tasks.css +288 -0
  63. package/bizar-dash/src/web/views/Chat.tsx +276 -48
  64. package/bizar-dash/src/web/views/Config.tsx +3 -2065
  65. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +620 -240
  66. package/bizar-dash/src/web/views/Settings.tsx +6 -0
  67. package/bizar-dash/src/web/views/Skills.tsx +208 -260
  68. package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
  69. package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
  70. package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
  71. package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
  72. package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
  73. package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
  74. package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
  75. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
  76. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
  77. package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
  78. package/bizar-dash/tests/skills-list.test.mjs +232 -0
  79. package/bizar-dash/tests/skills-search.test.mjs +222 -0
  80. package/bizar-dash/tests/tasks-create.test.mjs +187 -0
  81. package/bizar-dash/tests/update-check.test.mjs +127 -0
  82. package/bizar-dash/tests/update-run.test.mjs +266 -0
  83. package/cli/bin.mjs +345 -4
  84. package/cli/provision.mjs +118 -4
  85. package/config/agents/_shared/SKILLS.md +109 -0
  86. package/package.json +1 -1
  87. package/bizar-dash/dist/assets/main-BKXEqU1w.css +0 -1
  88. package/bizar-dash/dist/assets/main-EK_fzXn_.js +0 -352
  89. package/bizar-dash/dist/assets/main-EK_fzXn_.js.map +0 -1
  90. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
  91. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
@@ -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
+ }
@@ -0,0 +1,213 @@
1
+ // src/web/components/SettingsSearch.tsx — fuzzy search across Settings sections with scroll-to highlight
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { Search, X } from 'lucide-react';
4
+ import Fuse from 'fuse.js';
5
+ import { cn } from '../lib/utils';
6
+
7
+ export type SettingsSection = {
8
+ id: string;
9
+ label: string;
10
+ icon?: string;
11
+ fields: SettingsField[];
12
+ };
13
+
14
+ export type SettingsField = {
15
+ key: string; // e.g. "theme.accent"
16
+ label: string; // e.g. "Accent color"
17
+ value?: string; // current value (masked if secret)
18
+ section: string; // parent section id
19
+ };
20
+
21
+ type Props = {
22
+ sections: SettingsSection[];
23
+ onJump: (sectionId: string, fieldKey?: string) => void;
24
+ };
25
+
26
+ type Match = {
27
+ item: SettingsField | SettingsSection;
28
+ score: number;
29
+ type: 'section' | 'field';
30
+ sectionId: string;
31
+ fieldKey?: string;
32
+ };
33
+
34
+ function fuseSearch(items: SettingsSection[], query: string): Match[] {
35
+ if (!query.trim()) return [];
36
+
37
+ const sectionFuse = new Fuse(items, {
38
+ keys: ['label'],
39
+ threshold: 0.4,
40
+ includeScore: true,
41
+ });
42
+
43
+ const fieldFuse = new Fuse(
44
+ items.flatMap((s) => s.fields.map((f) => ({ ...f, _sectionId: s.id }))),
45
+ { keys: ['label', 'key'], threshold: 0.4, includeScore: true },
46
+ );
47
+
48
+ const results: Match[] = [];
49
+
50
+ for (const r of sectionFuse.search(query)) {
51
+ results.push({
52
+ item: r.item,
53
+ score: r.score ?? 1,
54
+ type: 'section',
55
+ sectionId: r.item.id,
56
+ });
57
+ }
58
+
59
+ for (const r of fieldFuse.search(query)) {
60
+ results.push({
61
+ item: r.item as SettingsField,
62
+ score: r.score ?? 1,
63
+ type: 'field',
64
+ sectionId: (r.item as SettingsField & { _sectionId: string })._sectionId,
65
+ fieldKey: r.item.key,
66
+ });
67
+ }
68
+
69
+ // Deduplicate by sectionId+fieldKey, prefer lower score
70
+ const seen = new Set<string>();
71
+ return results
72
+ .filter((r) => {
73
+ const k = `${r.sectionId}::${r.fieldKey ?? ''}`;
74
+ if (seen.has(k)) return false;
75
+ seen.add(k);
76
+ return true;
77
+ })
78
+ .sort((a, b) => a.score - b.score)
79
+ .slice(0, 8);
80
+ }
81
+
82
+ export function SettingsSearch({ sections, onJump }: Props) {
83
+ const [q, setQ] = useState('');
84
+ const [matches, setMatches] = useState<Match[]>([]);
85
+ const [activeIdx, setActiveIdx] = useState(0);
86
+ const [highlighted, setHighlighted] = useState<string | null>(null);
87
+ const inputRef = useRef<HTMLInputElement>(null);
88
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
89
+ const highlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
90
+
91
+ const search = useCallback(
92
+ (query: string) => {
93
+ if (debounceRef.current) clearTimeout(debounceRef.current);
94
+ if (!query.trim()) {
95
+ setMatches([]);
96
+ setActiveIdx(0);
97
+ return;
98
+ }
99
+ debounceRef.current = setTimeout(() => {
100
+ setMatches(fuseSearch(sections, query));
101
+ setActiveIdx(0);
102
+ }, 200);
103
+ },
104
+ [sections],
105
+ );
106
+
107
+ useEffect(() => {
108
+ search(q);
109
+ }, [q, search]);
110
+
111
+ // Cleanup timers
112
+ useEffect(() => {
113
+ return () => {
114
+ if (debounceRef.current) clearTimeout(debounceRef.current);
115
+ if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
116
+ };
117
+ }, []);
118
+
119
+ const highlightSection = (sectionId: string) => {
120
+ setHighlighted(sectionId);
121
+ if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
122
+ highlightTimerRef.current = setTimeout(() => setHighlighted(null), 1500);
123
+ const el = document.querySelector(`[data-section="${sectionId}"]`);
124
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
125
+ };
126
+
127
+ const handleKey = (e: React.KeyboardEvent) => {
128
+ if (e.key === 'Escape') {
129
+ setQ('');
130
+ inputRef.current?.blur();
131
+ } else if (e.key === 'ArrowDown') {
132
+ e.preventDefault();
133
+ setActiveIdx((i) => Math.min(i + 1, matches.length - 1));
134
+ } else if (e.key === 'ArrowUp') {
135
+ e.preventDefault();
136
+ setActiveIdx((i) => Math.max(i - 1, 0));
137
+ } else if (e.key === 'Enter' && matches[activeIdx]) {
138
+ const m = matches[activeIdx];
139
+ highlightSection(m.sectionId);
140
+ onJump(m.sectionId, m.fieldKey);
141
+ setQ('');
142
+ }
143
+ };
144
+
145
+ const handleSelect = (m: Match) => {
146
+ highlightSection(m.sectionId);
147
+ onJump(m.sectionId, m.fieldKey);
148
+ setQ('');
149
+ };
150
+
151
+ return (
152
+ <div className="settings-search">
153
+ <div className="settings-search-input-wrap">
154
+ <Search size={13} className="settings-search-icon" />
155
+ <input
156
+ ref={inputRef}
157
+ className="settings-search-input"
158
+ placeholder="Search settings…"
159
+ value={q}
160
+ onChange={(e) => setQ(e.target.value)}
161
+ onKeyDown={handleKey}
162
+ aria-label="Search settings"
163
+ />
164
+ {q && (
165
+ <button
166
+ type="button"
167
+ className="icon-btn settings-search-clear"
168
+ onClick={() => setQ('')}
169
+ aria-label="Clear search"
170
+ >
171
+ <X size={12} />
172
+ </button>
173
+ )}
174
+ </div>
175
+
176
+ {q && matches.length > 0 && (
177
+ <div className="settings-search-results">
178
+ {matches.map((m, idx) => {
179
+ const label = m.type === 'section'
180
+ ? (m.item as SettingsSection).label
181
+ : (m.item as SettingsField).label;
182
+ const key = m.type === 'section'
183
+ ? (m.item as SettingsSection).id
184
+ : (m.item as SettingsField).key;
185
+ return (
186
+ <button
187
+ key={`${m.sectionId}::${key}`}
188
+ type="button"
189
+ className={cn('settings-search-result', idx === activeIdx && 'settings-search-result-active')}
190
+ onClick={() => handleSelect(m)}
191
+ onMouseEnter={() => setActiveIdx(idx)}
192
+ >
193
+ <span className={cn('settings-search-result-type', m.type === 'section' && 'settings-search-result-type-section')}>
194
+ {m.type === 'section' ? 'section' : 'field'}
195
+ </span>
196
+ <span className="settings-search-result-label">
197
+ {label}
198
+ </span>
199
+ {m.type === 'field' && (
200
+ <span className="settings-search-result-key mono muted">{key}</span>
201
+ )}
202
+ </button>
203
+ );
204
+ })}
205
+ </div>
206
+ )}
207
+
208
+ {q && matches.length === 0 && (
209
+ <div className="settings-search-empty muted">No matching settings.</div>
210
+ )}
211
+ </div>
212
+ );
213
+ }
@@ -50,7 +50,6 @@ export const TABS: TabDef[] = [
50
50
  { id: 'schedules', label: 'Schedules', icon: Clock },
51
51
  { id: 'history', label: 'History', icon: HistoryIcon },
52
52
  { id: 'minimax', label: 'Usage', icon: Coins },
53
- { id: 'config', label: 'Config', icon: Settings2 },
54
53
  { id: 'settings', label: 'Settings', icon: Sliders },
55
54
  ];
56
55