@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.
Files changed (97) hide show
  1. package/bizar-dash/dist/assets/{icons-CFqu2M-c.js → icons-Bo0iH9EC.js} +166 -121
  2. package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
  3. package/bizar-dash/dist/assets/main-C1cpttnv.js +19 -0
  4. package/bizar-dash/dist/assets/main-C1cpttnv.js.map +1 -0
  5. package/bizar-dash/dist/assets/main-DTkNlLrw.css +1 -0
  6. package/bizar-dash/dist/assets/mobile-BtxQJftK.js +2 -0
  7. package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
  8. package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js → mobile-DD-FZrTC.js} +1 -1
  9. package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js.map → mobile-DD-FZrTC.js.map} +1 -1
  10. package/bizar-dash/dist/index.html +4 -4
  11. package/bizar-dash/dist/mobile.html +3 -3
  12. package/bizar-dash/node_modules/.package-lock.json +6 -0
  13. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  14. package/bizar-dash/package-lock.json +6 -0
  15. package/bizar-dash/src/server/api.mjs +7 -0
  16. package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
  17. package/bizar-dash/src/server/memory-store.mjs +46 -0
  18. package/bizar-dash/src/server/providers-store.mjs +11 -4
  19. package/bizar-dash/src/server/routes/_shared.mjs +2 -2
  20. package/bizar-dash/src/server/routes/doctor.mjs +71 -0
  21. package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
  22. package/bizar-dash/src/server/routes/memory.mjs +3 -0
  23. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
  24. package/bizar-dash/src/server/routes/schedules.mjs +55 -0
  25. package/bizar-dash/src/server/serve-info.mjs +172 -0
  26. package/bizar-dash/src/web/App.tsx +48 -11
  27. package/bizar-dash/src/web/components/AutosaveField.tsx +50 -0
  28. package/bizar-dash/src/web/components/DoctorPanel.tsx +160 -0
  29. package/bizar-dash/src/web/components/EnvVarManager.tsx +245 -62
  30. package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
  31. package/bizar-dash/src/web/components/SettingsNav.tsx +101 -0
  32. package/bizar-dash/src/web/components/Sidebar.tsx +25 -1
  33. package/bizar-dash/src/web/components/StatusBadge.tsx +33 -2
  34. package/bizar-dash/src/web/components/Topbar.tsx +11 -0
  35. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +64 -0
  36. package/bizar-dash/src/web/components/chat/Composer.tsx +1 -1
  37. package/bizar-dash/src/web/components/chat/useChat.ts +118 -1
  38. package/bizar-dash/src/web/hooks/useAutosave.ts +107 -0
  39. package/bizar-dash/src/web/lib/types.ts +134 -0
  40. package/bizar-dash/src/web/styles/chat.css +8 -5
  41. package/bizar-dash/src/web/styles/main.css +77 -2
  42. package/bizar-dash/src/web/styles/settings.css +265 -0
  43. package/bizar-dash/src/web/views/Chat.tsx +15 -1
  44. package/bizar-dash/src/web/views/Doctor.tsx +317 -0
  45. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +1 -0
  46. package/bizar-dash/src/web/views/Schedules.tsx +94 -12
  47. package/bizar-dash/src/web/views/Settings.tsx +76 -46
  48. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
  49. package/bizar-dash/src/web/views/settings/AgentSection.tsx +23 -12
  50. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +5 -6
  51. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +38 -15
  52. package/bizar-dash/src/web/views/settings/MemorySection.tsx +92 -8
  53. package/bizar-dash/tests/autosave.test.tsx +276 -0
  54. package/bizar-dash/tests/chat-composer.test.tsx +140 -0
  55. package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
  56. package/bizar-dash/tests/diagnostics-store.test.mjs +206 -0
  57. package/bizar-dash/tests/env-vars-extended.test.mjs +190 -0
  58. package/bizar-dash/tests/layout-spacing.test.mjs +101 -0
  59. package/bizar-dash/tests/memory-default-vault.test.mjs +98 -0
  60. package/bizar-dash/tests/minimax-bar.test.tsx +113 -0
  61. package/bizar-dash/tests/minimax-models.test.mjs +123 -0
  62. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
  63. package/bizar-dash/tests/routes-doctor.test.mjs +159 -0
  64. package/bizar-dash/tests/schedules-templates.test.mjs +144 -0
  65. package/bizar-dash/tests/settings-layout.test.tsx +129 -0
  66. package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
  67. package/bizar-dash/tests/settings-nav.test.tsx +126 -0
  68. package/cli/commands/dash.mjs +5 -1
  69. package/cli/commands/minimax.mjs +5 -2
  70. package/cli/tests/minimax-cli.test.mjs +79 -0
  71. package/config/agents/frigg.md +1 -1
  72. package/config/agents/heimdall.md +1 -1
  73. package/config/agents/mimir.md +1 -1
  74. package/config/agents/quick.md +1 -1
  75. package/config/agents/semble-search.md +1 -1
  76. package/config/agents/vor.md +1 -1
  77. package/config/opencode.json +28 -21
  78. package/config/opencode.json.template +8 -1
  79. package/package.json +1 -1
  80. package/plugins/bizar/index.ts +77 -0
  81. package/plugins/bizar/src/compaction.d.mts +48 -0
  82. package/plugins/bizar/src/compaction.mjs +192 -0
  83. package/plugins/bizar/tests/compaction.test.ts +264 -0
  84. package/templates/schedules/daily-backup.json +12 -0
  85. package/templates/schedules/daily-cleanup.json +12 -0
  86. package/templates/schedules/hourly-health-check.json +12 -0
  87. package/templates/schedules/webhook-on-push.json +13 -0
  88. package/templates/schedules/weekly-digest.json +13 -0
  89. package/bizar-dash/dist/assets/icons-CFqu2M-c.js.map +0 -1
  90. package/bizar-dash/dist/assets/main-Dl8yY5_H.js +0 -16
  91. package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +0 -1
  92. package/bizar-dash/dist/assets/main-ZAfGKENE.css +0 -1
  93. package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +0 -2
  94. package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +0 -1
  95. package/bizar-dash/src/web/views/settings/BackupSection.tsx +0 -16
  96. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +0 -16
  97. 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
- <h2 className="view-title">
308
- <Clock size={18} /> Schedules ({schedules.length})
309
- </h2>
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
- Recurring tasks for the active project: <strong>{snapshot.activeProject?.name || '(none)'}</strong>.
312
- Service daemon runs them at the right time.
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
- <Button variant="secondary" size="sm" onClick={reload}>
317
- <RefreshCw size={14} /> Refresh
318
- </Button>
319
- <Button variant="primary" size="sm" onClick={() => openEditor()}>
320
- <Plus size={14} /> New schedule
321
- </Button>
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
- {schedules.length === 0 ? (
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
- const [activeSection, setActiveSection] = useState<string | null>(() => {
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
- const onJumpSection = (id: string | null) => {
179
- setActiveSection(id);
180
- try {
181
- history.replaceState(null, '', id ? `#settings-${id}` : window.location.pathname);
182
- if (id) { const el = document.getElementById(`settings-${id}`); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
183
- else window.scrollTo({ top: 0, behavior: 'smooth' });
184
- } catch { /* ignore */ }
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
- <SettingsSearch sections={SETTINGS_SECTIONS} onJump={onJumpSection} />
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={() => onJumpSection(null)} title="Show all settings sections">All</button>
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={() => onJumpSection(s.id)}>{s.label}</button>
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>