@polderlabs/bizar 5.0.0 → 5.0.2
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.
- package/bizar-dash/dist/assets/{icons-CFqu2M-c.js → icons-Bo0iH9EC.js} +166 -121
- package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
- package/bizar-dash/dist/assets/main-C1cpttnv.js +19 -0
- package/bizar-dash/dist/assets/main-C1cpttnv.js.map +1 -0
- package/bizar-dash/dist/assets/main-DTkNlLrw.css +1 -0
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js +2 -0
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js → mobile-DD-FZrTC.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js.map → mobile-DD-FZrTC.js.map} +1 -1
- package/bizar-dash/dist/index.html +4 -4
- package/bizar-dash/dist/mobile.html +3 -3
- package/bizar-dash/node_modules/.package-lock.json +6 -0
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/package-lock.json +6 -0
- package/bizar-dash/src/server/api.mjs +7 -0
- package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
- package/bizar-dash/src/server/memory-store.mjs +46 -0
- package/bizar-dash/src/server/providers-store.mjs +11 -4
- package/bizar-dash/src/server/routes/_shared.mjs +2 -2
- package/bizar-dash/src/server/routes/doctor.mjs +71 -0
- package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
- package/bizar-dash/src/server/routes/memory.mjs +3 -0
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
- package/bizar-dash/src/server/routes/schedules.mjs +55 -0
- package/bizar-dash/src/server/serve-info.mjs +172 -0
- package/bizar-dash/src/web/App.tsx +48 -11
- package/bizar-dash/src/web/components/AutosaveField.tsx +50 -0
- package/bizar-dash/src/web/components/DoctorPanel.tsx +160 -0
- package/bizar-dash/src/web/components/EnvVarManager.tsx +245 -62
- package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
- package/bizar-dash/src/web/components/SettingsNav.tsx +101 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +25 -1
- package/bizar-dash/src/web/components/StatusBadge.tsx +33 -2
- package/bizar-dash/src/web/components/Topbar.tsx +11 -0
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +64 -0
- package/bizar-dash/src/web/components/chat/Composer.tsx +1 -1
- package/bizar-dash/src/web/components/chat/useChat.ts +118 -1
- package/bizar-dash/src/web/hooks/useAutosave.ts +107 -0
- package/bizar-dash/src/web/lib/types.ts +134 -0
- package/bizar-dash/src/web/styles/chat.css +8 -5
- package/bizar-dash/src/web/styles/main.css +77 -2
- package/bizar-dash/src/web/styles/settings.css +265 -0
- package/bizar-dash/src/web/views/Chat.tsx +15 -1
- package/bizar-dash/src/web/views/Doctor.tsx +317 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +1 -0
- package/bizar-dash/src/web/views/Schedules.tsx +94 -12
- package/bizar-dash/src/web/views/Settings.tsx +76 -46
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
- package/bizar-dash/src/web/views/settings/AgentSection.tsx +23 -12
- package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +5 -6
- package/bizar-dash/src/web/views/settings/GeneralSection.tsx +38 -15
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +92 -8
- package/bizar-dash/tests/autosave.test.tsx +276 -0
- package/bizar-dash/tests/chat-composer.test.tsx +140 -0
- package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
- package/bizar-dash/tests/diagnostics-store.test.mjs +206 -0
- package/bizar-dash/tests/env-vars-extended.test.mjs +190 -0
- package/bizar-dash/tests/layout-spacing.test.mjs +101 -0
- package/bizar-dash/tests/memory-default-vault.test.mjs +98 -0
- package/bizar-dash/tests/minimax-bar.test.tsx +113 -0
- package/bizar-dash/tests/minimax-models.test.mjs +123 -0
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
- package/bizar-dash/tests/routes-doctor.test.mjs +159 -0
- package/bizar-dash/tests/schedules-templates.test.mjs +144 -0
- package/bizar-dash/tests/settings-layout.test.tsx +129 -0
- package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
- package/bizar-dash/tests/settings-nav.test.tsx +126 -0
- package/cli/commands/dash.mjs +5 -1
- package/cli/commands/minimax.mjs +5 -2
- package/cli/tests/minimax-cli.test.mjs +79 -0
- package/config/agents/frigg.md +1 -1
- package/config/agents/heimdall.md +1 -1
- package/config/agents/mimir.md +1 -1
- package/config/agents/quick.md +1 -1
- package/config/agents/semble-search.md +1 -1
- package/config/agents/vor.md +1 -1
- package/config/opencode.json +28 -21
- package/config/opencode.json.template +8 -1
- package/package.json +1 -1
- package/plugins/bizar/index.ts +77 -0
- package/plugins/bizar/src/compaction.d.mts +48 -0
- package/plugins/bizar/src/compaction.mjs +192 -0
- package/plugins/bizar/tests/compaction.test.ts +264 -0
- package/templates/schedules/daily-backup.json +12 -0
- package/templates/schedules/daily-cleanup.json +12 -0
- package/templates/schedules/hourly-health-check.json +12 -0
- package/templates/schedules/webhook-on-push.json +13 -0
- package/templates/schedules/weekly-digest.json +13 -0
- package/bizar-dash/dist/assets/icons-CFqu2M-c.js.map +0 -1
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js +0 -16
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +0 -1
- package/bizar-dash/dist/assets/main-ZAfGKENE.css +0 -1
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +0 -2
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +0 -1
- package/bizar-dash/src/web/views/settings/BackupSection.tsx +0 -16
- package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +0 -16
- package/bizar-dash/src/web/views/settings/SkillsSection.tsx +0 -16
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
RefreshCw,
|
|
14
14
|
History,
|
|
15
15
|
ChevronDown,
|
|
16
|
+
LayoutTemplate,
|
|
16
17
|
} from 'lucide-react';
|
|
17
18
|
import { Card, CardTitle, CardMeta } from '../components/Card';
|
|
18
19
|
import { Button } from '../components/Button';
|
|
@@ -23,6 +24,8 @@ import { useModal } from '../components/Modal';
|
|
|
23
24
|
import { api } from '../lib/api';
|
|
24
25
|
import { formatRelative } from '../lib/utils';
|
|
25
26
|
import type { Schedule, Settings, Snapshot } from '../lib/types';
|
|
27
|
+
import { ScheduleTemplateCard } from '../components/ScheduleTemplateCard';
|
|
28
|
+
import type { ScheduleTemplate } from '../components/ScheduleTemplateCard';
|
|
26
29
|
|
|
27
30
|
type Props = {
|
|
28
31
|
snapshot: Snapshot;
|
|
@@ -221,6 +224,9 @@ export function Schedules({ snapshot, refreshSnapshot }: Props) {
|
|
|
221
224
|
const modal = useModal();
|
|
222
225
|
const [schedules, setSchedules] = useState<Schedule[]>(snapshot.schedules || []);
|
|
223
226
|
const [loading, setLoading] = useState(!snapshot.schedules);
|
|
227
|
+
const [activeSection, setActiveSection] = useState<'schedules' | 'templates'>('schedules');
|
|
228
|
+
const [templates, setTemplates] = useState<ScheduleTemplate[]>([]);
|
|
229
|
+
const [templatesLoading, setTemplatesLoading] = useState(false);
|
|
224
230
|
|
|
225
231
|
const reload = async () => {
|
|
226
232
|
try {
|
|
@@ -235,6 +241,18 @@ export function Schedules({ snapshot, refreshSnapshot }: Props) {
|
|
|
235
241
|
}
|
|
236
242
|
};
|
|
237
243
|
|
|
244
|
+
const loadTemplates = async () => {
|
|
245
|
+
setTemplatesLoading(true);
|
|
246
|
+
try {
|
|
247
|
+
const r = await api.get<{ templates: ScheduleTemplate[] }>('/schedules/templates');
|
|
248
|
+
setTemplates(r.templates || []);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
toast.error(`Templates load failed: ${(err as Error).message}`);
|
|
251
|
+
} finally {
|
|
252
|
+
setTemplatesLoading(false);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
238
256
|
useEffect(() => {
|
|
239
257
|
if (snapshot.schedules?.length || snapshot.schedules) {
|
|
240
258
|
setSchedules(snapshot.schedules || []);
|
|
@@ -245,6 +263,12 @@ export function Schedules({ snapshot, refreshSnapshot }: Props) {
|
|
|
245
263
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
246
264
|
}, [snapshot.schedules]);
|
|
247
265
|
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (activeSection === 'templates' && templates.length === 0) {
|
|
268
|
+
loadTemplates();
|
|
269
|
+
}
|
|
270
|
+
}, [activeSection]);
|
|
271
|
+
|
|
248
272
|
const openEditor = (initial?: Schedule) => {
|
|
249
273
|
modal.open({
|
|
250
274
|
title: initial ? `Edit schedule: ${initial.name}` : 'New schedule',
|
|
@@ -294,6 +318,20 @@ export function Schedules({ snapshot, refreshSnapshot }: Props) {
|
|
|
294
318
|
}
|
|
295
319
|
};
|
|
296
320
|
|
|
321
|
+
const onUseTemplate = async (template: ScheduleTemplate) => {
|
|
322
|
+
try {
|
|
323
|
+
const saved = await api.post<Schedule>('/schedules/from-template', {
|
|
324
|
+
templateId: template.id,
|
|
325
|
+
});
|
|
326
|
+
setSchedules((cur) => [...cur, saved]);
|
|
327
|
+
setActiveSection('schedules');
|
|
328
|
+
toast.success(`Schedule "${saved.name}" created from template.`);
|
|
329
|
+
await refreshSnapshot();
|
|
330
|
+
} catch (err) {
|
|
331
|
+
toast.error(`Failed to create schedule: ${(err as Error).message}`);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
297
335
|
if (loading) {
|
|
298
336
|
return (
|
|
299
337
|
<div className="view-loading"><Spinner size="lg" /></div>
|
|
@@ -304,25 +342,69 @@ export function Schedules({ snapshot, refreshSnapshot }: Props) {
|
|
|
304
342
|
<div className="view view-schedules">
|
|
305
343
|
<header className="view-header">
|
|
306
344
|
<div className="view-header-text">
|
|
307
|
-
<
|
|
308
|
-
<
|
|
309
|
-
|
|
345
|
+
<div className="view-title-row">
|
|
346
|
+
<h2 className="view-title">
|
|
347
|
+
<Clock size={18} /> Schedules
|
|
348
|
+
</h2>
|
|
349
|
+
<div className="view-section-tabs">
|
|
350
|
+
<button
|
|
351
|
+
type="button"
|
|
352
|
+
className={`tab-btn ${activeSection === 'schedules' ? 'tab-btn-active' : ''}`}
|
|
353
|
+
onClick={() => setActiveSection('schedules')}
|
|
354
|
+
>
|
|
355
|
+
Schedules ({schedules.length})
|
|
356
|
+
</button>
|
|
357
|
+
<button
|
|
358
|
+
type="button"
|
|
359
|
+
className={`tab-btn ${activeSection === 'templates' ? 'tab-btn-active' : ''}`}
|
|
360
|
+
onClick={() => setActiveSection('templates')}
|
|
361
|
+
>
|
|
362
|
+
<LayoutTemplate size={14} /> Templates
|
|
363
|
+
</button>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
310
366
|
<p className="view-subtitle">
|
|
311
|
-
|
|
312
|
-
|
|
367
|
+
{activeSection === 'schedules'
|
|
368
|
+
? <>Recurring tasks for <strong>{snapshot.activeProject?.name || '(none)'}</strong>. Service daemon runs them at the right time.</>
|
|
369
|
+
: 'Example schedules you can use as a starting point.'}
|
|
313
370
|
</p>
|
|
314
371
|
</div>
|
|
315
372
|
<div className="view-actions">
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
373
|
+
{activeSection === 'schedules' && (
|
|
374
|
+
<>
|
|
375
|
+
<Button variant="secondary" size="sm" onClick={reload}>
|
|
376
|
+
<RefreshCw size={14} /> Refresh
|
|
377
|
+
</Button>
|
|
378
|
+
<Button variant="primary" size="sm" onClick={() => openEditor()}>
|
|
379
|
+
<Plus size={14} /> New schedule
|
|
380
|
+
</Button>
|
|
381
|
+
</>
|
|
382
|
+
)}
|
|
383
|
+
{activeSection === 'templates' && (
|
|
384
|
+
<Button variant="secondary" size="sm" onClick={loadTemplates} disabled={templatesLoading}>
|
|
385
|
+
<RefreshCw size={14} /> Refresh
|
|
386
|
+
</Button>
|
|
387
|
+
)}
|
|
322
388
|
</div>
|
|
323
389
|
</header>
|
|
324
390
|
|
|
325
|
-
{
|
|
391
|
+
{activeSection === 'templates' ? (
|
|
392
|
+
templatesLoading ? (
|
|
393
|
+
<div className="view-loading"><Spinner size="lg" /></div>
|
|
394
|
+
) : templates.length === 0 ? (
|
|
395
|
+
<EmptyState
|
|
396
|
+
icon={<LayoutTemplate size={32} />}
|
|
397
|
+
title="No templates"
|
|
398
|
+
message="No schedule templates found. Templates are loaded from templates/schedules/."
|
|
399
|
+
/>
|
|
400
|
+
) : (
|
|
401
|
+
<div className="schedule-template-grid">
|
|
402
|
+
{templates.map((t) => (
|
|
403
|
+
<ScheduleTemplateCard key={t.id} template={t} onUse={onUseTemplate} />
|
|
404
|
+
))}
|
|
405
|
+
</div>
|
|
406
|
+
)
|
|
407
|
+
) : schedules.length === 0 ? (
|
|
326
408
|
<EmptyState
|
|
327
409
|
icon={<Clock size={32} />}
|
|
328
410
|
title="No schedules"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/views/Settings.tsx — v4 settings shell: routes to focused sub-components.
|
|
2
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
|
3
3
|
import { Sliders, Save, RefreshCw, RotateCcw } from 'lucide-react';
|
|
4
4
|
import { Button } from '../components/Button';
|
|
5
5
|
import { useToast } from '../components/Toast';
|
|
@@ -18,12 +18,8 @@ import { AgentSection } from './settings/AgentSection';
|
|
|
18
18
|
import { SystemLlmSection } from './settings/SystemLlmSection';
|
|
19
19
|
import { HeadroomSection } from './settings/HeadroomSection';
|
|
20
20
|
import { ActivitySection } from './settings/ActivitySection';
|
|
21
|
-
import { EnvVarsSection } from './settings/EnvVarsSection';
|
|
22
|
-
import { ProvidersSection } from './settings/ProvidersSection';
|
|
23
|
-
import { MemorySection } from './settings/MemorySection';
|
|
24
|
-
import { SkillsSection } from './settings/SkillsSection';
|
|
25
|
-
import { BackupSection } from './settings/BackupSection';
|
|
26
21
|
import { WorkspacesSection } from './settings/WorkspacesSection';
|
|
22
|
+
import { EnvVarsSection } from './settings/EnvVarsSection';
|
|
27
23
|
|
|
28
24
|
type Props = {
|
|
29
25
|
snapshot: Snapshot;
|
|
@@ -31,6 +27,12 @@ type Props = {
|
|
|
31
27
|
activeTab: string;
|
|
32
28
|
setActiveTab: (id: string) => void;
|
|
33
29
|
refreshSnapshot: () => Promise<void>;
|
|
30
|
+
/** v4.9.0 — When true, navigation uses the sidebar instead of the subnav bar. */
|
|
31
|
+
settingsMode?: boolean;
|
|
32
|
+
/** v4.9.0 — The active section. Null means "show all". Used when settingsMode is true. */
|
|
33
|
+
settingsActiveSection?: string | null;
|
|
34
|
+
/** v4.9.0 — Called when the user selects a section via the sidebar. */
|
|
35
|
+
setSettingsActiveSection?: (id: string | null) => void;
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
const SECTION_LINKS = [
|
|
@@ -38,6 +40,7 @@ const SECTION_LINKS = [
|
|
|
38
40
|
{ id: 'updates', label: 'Updates' },
|
|
39
41
|
{ id: 'layout', label: 'Layout' },
|
|
40
42
|
{ id: 'general', label: 'General' },
|
|
43
|
+
{ id: 'env-vars', label: 'Env Vars' },
|
|
41
44
|
{ id: 'network', label: 'Network' },
|
|
42
45
|
{ id: 'notifications', label: 'Notifications' },
|
|
43
46
|
{ id: 'auth', label: 'Auth' },
|
|
@@ -48,11 +51,6 @@ const SECTION_LINKS = [
|
|
|
48
51
|
{ id: 'headroom', label: 'Headroom' },
|
|
49
52
|
{ id: 'activity-log', label: 'Activity' },
|
|
50
53
|
{ id: 'about', label: 'About' },
|
|
51
|
-
{ id: 'env-vars', label: 'Env Vars' },
|
|
52
|
-
{ id: 'providers', label: 'Providers' },
|
|
53
|
-
{ id: 'memory', label: 'Memory' },
|
|
54
|
-
{ id: 'skills', label: 'Skills' },
|
|
55
|
-
{ id: 'backup', label: 'Backup' },
|
|
56
54
|
{ id: 'workspaces', label: 'Workspaces' },
|
|
57
55
|
] as const;
|
|
58
56
|
|
|
@@ -83,6 +81,9 @@ const SETTINGS_SECTIONS: SettingsSection[] = [
|
|
|
83
81
|
{ key: 'defaultAgent', label: 'Default agent', section: 'general' },
|
|
84
82
|
{ key: 'defaultModel', label: 'Model override', section: 'general' },
|
|
85
83
|
] },
|
|
84
|
+
{ id: 'env-vars', label: 'Environment Variables', fields: [
|
|
85
|
+
{ key: 'env-vars.count', label: 'Env var count', section: 'env-vars' },
|
|
86
|
+
] },
|
|
86
87
|
{ id: 'network', label: 'Network', fields: [
|
|
87
88
|
{ key: 'tailscale.enabled', label: 'Tailscale Serve', section: 'network' },
|
|
88
89
|
{ key: 'tailscale.port', label: 'Tailscale port', section: 'network' },
|
|
@@ -131,25 +132,9 @@ const SETTINGS_SECTIONS: SettingsSection[] = [
|
|
|
131
132
|
{ key: 'about.homepage', label: 'Homepage', section: 'about' },
|
|
132
133
|
{ key: 'about.license', label: 'License', section: 'about' },
|
|
133
134
|
] },
|
|
134
|
-
{ id: 'env-vars', label: 'Env Vars', fields: [
|
|
135
|
-
{ key: 'env.add', label: 'Add variable', section: 'env-vars' },
|
|
136
|
-
] },
|
|
137
|
-
{ id: 'providers', label: 'Providers', fields: [
|
|
138
|
-
{ key: 'providers.list', label: 'Provider list', section: 'providers' },
|
|
139
|
-
] },
|
|
140
|
-
{ id: 'memory', label: 'Memory', fields: [
|
|
141
|
-
{ key: 'memory.config', label: 'Memory config', section: 'memory' },
|
|
142
|
-
] },
|
|
143
|
-
{ id: 'skills', label: 'Skills', fields: [
|
|
144
|
-
{ key: 'skills.paths', label: 'Skill paths', section: 'skills' },
|
|
145
|
-
] },
|
|
146
|
-
{ id: 'backup', label: 'Backup', fields: [
|
|
147
|
-
{ key: 'backup.create', label: 'Create backup', section: 'backup' },
|
|
148
|
-
{ key: 'backup.restore', label: 'Restore backup', section: 'backup' },
|
|
149
|
-
] },
|
|
150
135
|
];
|
|
151
136
|
|
|
152
|
-
function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
|
|
137
|
+
function SettingsViewInner({ settings: initial, refreshSnapshot, settingsMode, settingsActiveSection, setSettingsActiveSection, setActiveTab }: Props) {
|
|
153
138
|
const toast = useToast();
|
|
154
139
|
const [settings, setSettings] = useState<Settings>(initial);
|
|
155
140
|
const [dirty, setDirty] = useState(false);
|
|
@@ -158,10 +143,31 @@ function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
|
|
|
158
143
|
const [tailscaleDraft, setTailscaleDraft] = useState({ port: 4321, https: true, hostname: '' });
|
|
159
144
|
const [authStatus, setAuthStatus] = useState<{ required: boolean; loopback: boolean; peer: string } | null>(null);
|
|
160
145
|
|
|
146
|
+
// v4.9.0 — Exit settings mode by switching back to overview tab.
|
|
147
|
+
const handleExitSettings = () => { setActiveTab('overview'); };
|
|
148
|
+
|
|
161
149
|
useEffect(() => { setSettings(initial); setDirty(false); if (initial.theme) applyThemeTokens(initial.theme); }, [initial]);
|
|
162
150
|
useEffect(() => { if (!tailscale) return; setTailscaleDraft({ port: tailscale.settings.port, https: tailscale.settings.https !== false, hostname: tailscale.settings.hostname || '' }); }, [tailscale]);
|
|
163
151
|
useEffect(() => { api.get<TailscaleStatus>('/tailscale/status').then(setTailscale).catch(() => undefined); }, []);
|
|
164
152
|
|
|
153
|
+
const autoSaveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
154
|
+
|
|
155
|
+
const autoSave = useCallback(async (key: keyof Settings, value: Settings[keyof Settings]) => {
|
|
156
|
+
setSettings((cur) => ({ ...cur, [key]: value }));
|
|
157
|
+
if (autoSaveTimeoutRef.current) clearTimeout(autoSaveTimeoutRef.current);
|
|
158
|
+
autoSaveTimeoutRef.current = setTimeout(async () => {
|
|
159
|
+
try {
|
|
160
|
+
await api.put('/settings', { ...settings, [key]: value });
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.error('autoSave failed', err);
|
|
163
|
+
}
|
|
164
|
+
}, 300);
|
|
165
|
+
}, [settings]);
|
|
166
|
+
|
|
167
|
+
useEffect(() => () => {
|
|
168
|
+
if (autoSaveTimeoutRef.current) clearTimeout(autoSaveTimeoutRef.current);
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
165
171
|
const patchTheme = (patch: Partial<Settings['theme']>) => { setSettings((cur) => { const next = { ...cur, theme: { ...cur.theme, ...patch } }; applyThemeTokens(next.theme); return next; }); setDirty(true); };
|
|
166
172
|
const patchUi = (patch: Partial<Settings['ui']>) => { setSettings((cur) => ({ ...cur, ui: { ...cur.ui, ...patch } })); setDirty(true); };
|
|
167
173
|
const patchTop = <K extends keyof Settings>(key: K, value: Settings[K]) => { setSettings((cur) => ({ ...cur, [key]: value })); setDirty(true); };
|
|
@@ -169,19 +175,36 @@ function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
|
|
|
169
175
|
const patchAgents = (patch: Partial<Settings['agents']>) => { setSettings((cur) => ({ ...cur, agents: { ...cur.agents, ...patch } })); setDirty(true); };
|
|
170
176
|
const patchDashboard = (patch: Partial<Settings['dashboard']>) => { setSettings((cur) => ({ ...cur, dashboard: { ...cur.dashboard, ...patch } })); setDirty(true); };
|
|
171
177
|
|
|
172
|
-
|
|
178
|
+
// v4.9.0 — In settingsMode, section navigation is driven by the sidebar via
|
|
179
|
+
// settingsActiveSection (shared state at App level). Outside settingsMode,
|
|
180
|
+
// fall back to local hash-based state for backwards compatibility.
|
|
181
|
+
const [localActiveSection, setLocalActiveSection] = useState<string | null>(() => {
|
|
173
182
|
if (typeof window === 'undefined') return null;
|
|
174
183
|
const hash = window.location.hash.replace(/^#settings-/, '');
|
|
175
184
|
return SECTION_LINKS.some((s) => s.id === hash) ? hash : null;
|
|
176
185
|
});
|
|
177
186
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
// The effective active section: use sidebar state in settingsMode, local state otherwise.
|
|
188
|
+
const activeSection = settingsMode ? (settingsActiveSection ?? null) : localActiveSection;
|
|
189
|
+
|
|
190
|
+
// v4.9.0 — In settingsMode, update the shared App-level state. Otherwise update local state.
|
|
191
|
+
const handleJumpSection = (id: string | null) => {
|
|
192
|
+
if (settingsMode) {
|
|
193
|
+
setSettingsActiveSection?.(id);
|
|
194
|
+
} else {
|
|
195
|
+
setLocalActiveSection(id);
|
|
196
|
+
try {
|
|
197
|
+
history.replaceState(null, '', id ? `#settings-${id}` : window.location.pathname);
|
|
198
|
+
if (id) { const el = document.getElementById(`settings-${id}`); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
|
|
199
|
+
else window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
200
|
+
} catch { /* ignore */ }
|
|
201
|
+
}
|
|
202
|
+
if (id) {
|
|
203
|
+
const el = document.getElementById(`settings-${id}`);
|
|
204
|
+
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
205
|
+
} else {
|
|
206
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
207
|
+
}
|
|
185
208
|
};
|
|
186
209
|
|
|
187
210
|
const onTailscaleToggle = async () => {
|
|
@@ -222,6 +245,7 @@ function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
|
|
|
222
245
|
const sp = { settings, patchTheme, patchUi, patchTop, patchNotifications, patchAgents, patchDashboard };
|
|
223
246
|
const about = settings.about || { version: '3.0.4', homepage: 'https://github.com/DrB0rk/BizarHarness', license: 'MIT' };
|
|
224
247
|
|
|
248
|
+
// v4.9.0 — showAll means no section is filtered (activeSection is null).
|
|
225
249
|
const showAll = activeSection === null;
|
|
226
250
|
const sectionOf = (id: string) => activeSection === id;
|
|
227
251
|
|
|
@@ -241,31 +265,37 @@ function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
|
|
|
241
265
|
</div>
|
|
242
266
|
</header>
|
|
243
267
|
|
|
244
|
-
|
|
268
|
+
{settingsMode && (
|
|
269
|
+
<div className="settings-mode-banner">
|
|
270
|
+
<span>You're in settings mode. All sections are visible in the sidebar.</span>
|
|
271
|
+
<Button variant="secondary" size="sm" onClick={handleExitSettings}>← Back to main view</Button>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
<SettingsSearch sections={SETTINGS_SECTIONS} onJump={handleJumpSection} />
|
|
245
276
|
|
|
277
|
+
{/* v4.9.0 — Subnav removed; section navigation is now driven by the sidebar */}
|
|
278
|
+
{/*
|
|
246
279
|
<nav className="settings-subnav" aria-label="Settings sections">
|
|
247
|
-
<button type="button" className={cn('settings-subnav-button', 'settings-subnav-button-all', showAll && 'settings-subnav-button-active')} onClick={() =>
|
|
280
|
+
<button type="button" className={cn('settings-subnav-button', 'settings-subnav-button-all', showAll && 'settings-subnav-button-active')} onClick={() => handleJumpSection(null)} title="Show all settings sections">All</button>
|
|
248
281
|
{SECTION_LINKS.map((s) => (
|
|
249
|
-
<button key={s.id} type="button" className={cn('settings-subnav-button', activeSection === s.id && 'settings-subnav-button-active')} onClick={() =>
|
|
282
|
+
<button key={s.id} type="button" className={cn('settings-subnav-button', activeSection === s.id && 'settings-subnav-button-active')} onClick={() => handleJumpSection(s.id)}>{s.label}</button>
|
|
250
283
|
))}
|
|
251
284
|
</nav>
|
|
285
|
+
*/}
|
|
252
286
|
|
|
253
287
|
<div className={cn('settings-grid', activeSection && 'settings-grid-filtered')} data-active-section={activeSection || undefined}>
|
|
254
288
|
{(showAll || sectionOf('theme')) && <ThemeSection {...sp} />}
|
|
255
289
|
{(showAll || sectionOf('updates')) && <UpdatesSection />}
|
|
256
|
-
{(showAll || sectionOf('layout') || sectionOf('general')) && <GeneralSection {...sp} />}
|
|
290
|
+
{(showAll || sectionOf('layout') || sectionOf('general')) && <GeneralSection {...sp} autoSave={autoSave} />}
|
|
291
|
+
{(showAll || sectionOf('env-vars')) && <EnvVarsSection />}
|
|
257
292
|
{(showAll || sectionOf('network') || sectionOf('service') || sectionOf('tailscale')) && <NetworkSection tailscale={tailscale} tailscaleDraft={tailscaleDraft} setTailscaleDraft={setTailscaleDraft} onTailscaleToggle={onTailscaleToggle} />}
|
|
258
293
|
{(showAll || sectionOf('notifications')) && <NotificationsSection {...sp} />}
|
|
259
294
|
{(showAll || sectionOf('auth')) && <AuthSection settings={settings} authStatus={authStatus} setAuthStatus={setAuthStatus} />}
|
|
260
|
-
{(showAll || sectionOf('agents') || sectionOf('dashboard') || sectionOf('background')) && <AgentSection {...sp} />}
|
|
295
|
+
{(showAll || sectionOf('agents') || sectionOf('dashboard') || sectionOf('background')) && <AgentSection {...sp} autoSave={autoSave} />}
|
|
261
296
|
{(showAll || sectionOf('system-llm')) && <SystemLlmSection {...sp} />}
|
|
262
297
|
{(showAll || sectionOf('headroom')) && <HeadroomSection settings={settings} setSettings={setSettings} setDirty={setDirty} />}
|
|
263
298
|
{(showAll || sectionOf('activity-log') || sectionOf('about')) && <ActivitySection about={about} />}
|
|
264
|
-
{(showAll || sectionOf('env-vars')) && <EnvVarsSection />}
|
|
265
|
-
{(showAll || sectionOf('providers')) && <ProvidersSection />}
|
|
266
|
-
{(showAll || sectionOf('memory')) && <MemorySection />}
|
|
267
|
-
{(showAll || sectionOf('skills')) && <SkillsSection />}
|
|
268
|
-
{(showAll || sectionOf('backup')) && <BackupSection />}
|
|
269
299
|
{(showAll || sectionOf('workspaces')) && <WorkspacesSection />}
|
|
270
300
|
</div>
|
|
271
301
|
</div>
|