@polderlabs/bizar 4.9.0 → 5.0.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.
- package/bizar-dash/dist/assets/icons-Bo0iH9EC.js +701 -0
- package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
- package/bizar-dash/dist/assets/{index-DU61awG3.js → index-DmpSFPJY.js} +1 -1
- package/bizar-dash/dist/assets/{index-DU61awG3.js.map → index-DmpSFPJY.js.map} +1 -1
- 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/markdown-DIquRulQ.js +29 -0
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +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-DD-FZrTC.js +1 -0
- package/bizar-dash/dist/assets/{mobile-CL5uUQEC.js.map → mobile-DD-FZrTC.js.map} +1 -1
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
- package/bizar-dash/dist/index.html +6 -3
- package/bizar-dash/dist/mobile.html +5 -2
- 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/skills/eval/SKILL.md +237 -0
- package/bizar-dash/src/server/api.mjs +35 -0
- package/bizar-dash/src/server/auth.mjs +155 -1
- package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
- package/bizar-dash/src/server/eval-store.mjs +226 -0
- package/bizar-dash/src/server/eval.mjs +347 -0
- package/bizar-dash/src/server/memory-store.mjs +46 -0
- package/bizar-dash/src/server/ocr.mjs +55 -0
- package/bizar-dash/src/server/plugins/registry.mjs +363 -0
- package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
- package/bizar-dash/src/server/plugins/store.mjs +659 -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/clipboard.mjs +173 -0
- 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/eval.mjs +147 -0
- package/bizar-dash/src/server/routes/memory.mjs +3 -0
- package/bizar-dash/src/server/routes/ocr.mjs +182 -0
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
- package/bizar-dash/src/server/routes/plugins.mjs +220 -0
- package/bizar-dash/src/server/routes/schedules.mjs +55 -0
- package/bizar-dash/src/server/routes/users.mjs +84 -0
- package/bizar-dash/src/server/routes/voice.mjs +131 -0
- package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
- package/bizar-dash/src/server/serve-info.mjs +172 -0
- package/bizar-dash/src/server/voice-store.mjs +202 -0
- package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
- package/bizar-dash/src/server/workspaces.mjs +626 -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/InviteDialog.tsx +205 -0
- package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
- package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
- package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -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/VoiceNotesPanel.tsx +182 -0
- package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
- package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -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/memory.css +82 -0
- 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/Memory.tsx +17 -2
- 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 +78 -45
- package/bizar-dash/src/web/views/Workspace.tsx +294 -0
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
- package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
- package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
- 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/src/web/views/settings/WorkspacesSection.tsx +189 -0
- package/bizar-dash/tests/autosave.test.tsx +276 -0
- package/bizar-dash/tests/bundle-analysis.test.mjs +5 -2
- package/bizar-dash/tests/chat-composer.test.tsx +140 -0
- package/bizar-dash/tests/clipboard.test.mjs +147 -0
- package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
- package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
- package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
- package/bizar-dash/tests/deploy-templates.test.mjs +100 -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/eval/fixtures.test.mjs +141 -0
- package/bizar-dash/tests/eval/report.test.mjs +284 -0
- package/bizar-dash/tests/eval/runner.test.mjs +471 -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/ocr.test.mjs +87 -0
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
- package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
- package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
- package/bizar-dash/tests/plugins-store.test.mjs +455 -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/bizar-dash/tests/users.test.mjs +108 -0
- package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
- package/bizar-dash/tests/voice-store.test.mjs +148 -0
- package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
- package/bizar-dash/tests/workspaces.test.mjs +527 -0
- package/cli/bin.mjs +72 -2
- package/cli/commands/clip.mjs +146 -0
- package/cli/commands/deploy/cloudflare.mjs +250 -0
- package/cli/commands/deploy/docker.mjs +221 -0
- package/cli/commands/deploy/fly.mjs +161 -0
- package/cli/commands/deploy/vercel.mjs +225 -0
- package/cli/commands/deploy.mjs +240 -0
- package/cli/commands/eval.mjs +378 -0
- package/cli/commands/marketplace.mjs +64 -0
- package/cli/commands/minimax.mjs +5 -2
- package/cli/commands/ocr.mjs +165 -0
- package/cli/commands/plugin.mjs +358 -0
- package/cli/commands/voice.mjs +211 -0
- package/cli/commands/workspace.mjs +247 -0
- 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 +12 -8
- 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/deploy/cloudflare/README.md +32 -0
- package/templates/deploy/cloudflare/functions-index.template.js +15 -0
- package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
- package/templates/deploy/docker/.env.template +16 -0
- package/templates/deploy/docker/README.md +58 -0
- package/templates/deploy/docker/docker-compose.template.yml +23 -0
- package/templates/deploy/fly/README.md +35 -0
- package/templates/deploy/fly/fly.toml.template +28 -0
- package/templates/deploy/vercel/README.md +29 -0
- package/templates/deploy/vercel/api-index.template.js +18 -0
- package/templates/deploy/vercel/vercel.json.template +16 -0
- package/templates/eval-fixtures/README.md +58 -0
- package/templates/eval-fixtures/code-search-basic.json +28 -0
- package/templates/eval-fixtures/latency-bounds.json +16 -0
- package/templates/eval-fixtures/regression-suite.json +79 -0
- package/templates/eval-fixtures/response-format.json +30 -0
- package/templates/eval-fixtures/tool-call-correctness.json +24 -0
- package/templates/plugin-template/README.md +121 -0
- package/templates/plugin-template/index.js +66 -0
- package/templates/plugin-template/plugin.json +42 -0
- package/templates/plugin-template/tests/plugin.test.js +83 -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/main-DaC1Lc6q.js +0 -366
- package/bizar-dash/dist/assets/main-DaC1Lc6q.js.map +0 -1
- package/bizar-dash/dist/assets/main-DfmIfOUS.css +0 -1
- package/bizar-dash/dist/assets/mobile-CL5uUQEC.js +0 -1
- package/bizar-dash/dist/assets/mobile-D5WTWvuh.js +0 -338
- package/bizar-dash/dist/assets/mobile-D5WTWvuh.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
|
@@ -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,11 +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 { WorkspacesSection } from './settings/WorkspacesSection';
|
|
21
22
|
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
23
|
|
|
27
24
|
type Props = {
|
|
28
25
|
snapshot: Snapshot;
|
|
@@ -30,6 +27,12 @@ type Props = {
|
|
|
30
27
|
activeTab: string;
|
|
31
28
|
setActiveTab: (id: string) => void;
|
|
32
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;
|
|
33
36
|
};
|
|
34
37
|
|
|
35
38
|
const SECTION_LINKS = [
|
|
@@ -37,6 +40,7 @@ const SECTION_LINKS = [
|
|
|
37
40
|
{ id: 'updates', label: 'Updates' },
|
|
38
41
|
{ id: 'layout', label: 'Layout' },
|
|
39
42
|
{ id: 'general', label: 'General' },
|
|
43
|
+
{ id: 'env-vars', label: 'Env Vars' },
|
|
40
44
|
{ id: 'network', label: 'Network' },
|
|
41
45
|
{ id: 'notifications', label: 'Notifications' },
|
|
42
46
|
{ id: 'auth', label: 'Auth' },
|
|
@@ -47,11 +51,7 @@ const SECTION_LINKS = [
|
|
|
47
51
|
{ id: 'headroom', label: 'Headroom' },
|
|
48
52
|
{ id: 'activity-log', label: 'Activity' },
|
|
49
53
|
{ id: 'about', label: 'About' },
|
|
50
|
-
{ id: '
|
|
51
|
-
{ id: 'providers', label: 'Providers' },
|
|
52
|
-
{ id: 'memory', label: 'Memory' },
|
|
53
|
-
{ id: 'skills', label: 'Skills' },
|
|
54
|
-
{ id: 'backup', label: 'Backup' },
|
|
54
|
+
{ id: 'workspaces', label: 'Workspaces' },
|
|
55
55
|
] as const;
|
|
56
56
|
|
|
57
57
|
/* ─── Settings search sections metadata ─── */
|
|
@@ -81,6 +81,9 @@ const SETTINGS_SECTIONS: SettingsSection[] = [
|
|
|
81
81
|
{ key: 'defaultAgent', label: 'Default agent', section: 'general' },
|
|
82
82
|
{ key: 'defaultModel', label: 'Model override', section: 'general' },
|
|
83
83
|
] },
|
|
84
|
+
{ id: 'env-vars', label: 'Environment Variables', fields: [
|
|
85
|
+
{ key: 'env-vars.count', label: 'Env var count', section: 'env-vars' },
|
|
86
|
+
] },
|
|
84
87
|
{ id: 'network', label: 'Network', fields: [
|
|
85
88
|
{ key: 'tailscale.enabled', label: 'Tailscale Serve', section: 'network' },
|
|
86
89
|
{ key: 'tailscale.port', label: 'Tailscale port', section: 'network' },
|
|
@@ -129,25 +132,9 @@ const SETTINGS_SECTIONS: SettingsSection[] = [
|
|
|
129
132
|
{ key: 'about.homepage', label: 'Homepage', section: 'about' },
|
|
130
133
|
{ key: 'about.license', label: 'License', section: 'about' },
|
|
131
134
|
] },
|
|
132
|
-
{ id: 'env-vars', label: 'Env Vars', fields: [
|
|
133
|
-
{ key: 'env.add', label: 'Add variable', section: 'env-vars' },
|
|
134
|
-
] },
|
|
135
|
-
{ id: 'providers', label: 'Providers', fields: [
|
|
136
|
-
{ key: 'providers.list', label: 'Provider list', section: 'providers' },
|
|
137
|
-
] },
|
|
138
|
-
{ id: 'memory', label: 'Memory', fields: [
|
|
139
|
-
{ key: 'memory.config', label: 'Memory config', section: 'memory' },
|
|
140
|
-
] },
|
|
141
|
-
{ id: 'skills', label: 'Skills', fields: [
|
|
142
|
-
{ key: 'skills.paths', label: 'Skill paths', section: 'skills' },
|
|
143
|
-
] },
|
|
144
|
-
{ id: 'backup', label: 'Backup', fields: [
|
|
145
|
-
{ key: 'backup.create', label: 'Create backup', section: 'backup' },
|
|
146
|
-
{ key: 'backup.restore', label: 'Restore backup', section: 'backup' },
|
|
147
|
-
] },
|
|
148
135
|
];
|
|
149
136
|
|
|
150
|
-
function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
|
|
137
|
+
function SettingsViewInner({ settings: initial, refreshSnapshot, settingsMode, settingsActiveSection, setSettingsActiveSection, setActiveTab }: Props) {
|
|
151
138
|
const toast = useToast();
|
|
152
139
|
const [settings, setSettings] = useState<Settings>(initial);
|
|
153
140
|
const [dirty, setDirty] = useState(false);
|
|
@@ -156,10 +143,31 @@ function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
|
|
|
156
143
|
const [tailscaleDraft, setTailscaleDraft] = useState({ port: 4321, https: true, hostname: '' });
|
|
157
144
|
const [authStatus, setAuthStatus] = useState<{ required: boolean; loopback: boolean; peer: string } | null>(null);
|
|
158
145
|
|
|
146
|
+
// v4.9.0 — Exit settings mode by switching back to overview tab.
|
|
147
|
+
const handleExitSettings = () => { setActiveTab('overview'); };
|
|
148
|
+
|
|
159
149
|
useEffect(() => { setSettings(initial); setDirty(false); if (initial.theme) applyThemeTokens(initial.theme); }, [initial]);
|
|
160
150
|
useEffect(() => { if (!tailscale) return; setTailscaleDraft({ port: tailscale.settings.port, https: tailscale.settings.https !== false, hostname: tailscale.settings.hostname || '' }); }, [tailscale]);
|
|
161
151
|
useEffect(() => { api.get<TailscaleStatus>('/tailscale/status').then(setTailscale).catch(() => undefined); }, []);
|
|
162
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
|
+
|
|
163
171
|
const patchTheme = (patch: Partial<Settings['theme']>) => { setSettings((cur) => { const next = { ...cur, theme: { ...cur.theme, ...patch } }; applyThemeTokens(next.theme); return next; }); setDirty(true); };
|
|
164
172
|
const patchUi = (patch: Partial<Settings['ui']>) => { setSettings((cur) => ({ ...cur, ui: { ...cur.ui, ...patch } })); setDirty(true); };
|
|
165
173
|
const patchTop = <K extends keyof Settings>(key: K, value: Settings[K]) => { setSettings((cur) => ({ ...cur, [key]: value })); setDirty(true); };
|
|
@@ -167,19 +175,36 @@ function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
|
|
|
167
175
|
const patchAgents = (patch: Partial<Settings['agents']>) => { setSettings((cur) => ({ ...cur, agents: { ...cur.agents, ...patch } })); setDirty(true); };
|
|
168
176
|
const patchDashboard = (patch: Partial<Settings['dashboard']>) => { setSettings((cur) => ({ ...cur, dashboard: { ...cur.dashboard, ...patch } })); setDirty(true); };
|
|
169
177
|
|
|
170
|
-
|
|
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>(() => {
|
|
171
182
|
if (typeof window === 'undefined') return null;
|
|
172
183
|
const hash = window.location.hash.replace(/^#settings-/, '');
|
|
173
184
|
return SECTION_LINKS.some((s) => s.id === hash) ? hash : null;
|
|
174
185
|
});
|
|
175
186
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
}
|
|
183
208
|
};
|
|
184
209
|
|
|
185
210
|
const onTailscaleToggle = async () => {
|
|
@@ -220,6 +245,7 @@ function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
|
|
|
220
245
|
const sp = { settings, patchTheme, patchUi, patchTop, patchNotifications, patchAgents, patchDashboard };
|
|
221
246
|
const about = settings.about || { version: '3.0.4', homepage: 'https://github.com/DrB0rk/BizarHarness', license: 'MIT' };
|
|
222
247
|
|
|
248
|
+
// v4.9.0 — showAll means no section is filtered (activeSection is null).
|
|
223
249
|
const showAll = activeSection === null;
|
|
224
250
|
const sectionOf = (id: string) => activeSection === id;
|
|
225
251
|
|
|
@@ -239,31 +265,38 @@ function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
|
|
|
239
265
|
</div>
|
|
240
266
|
</header>
|
|
241
267
|
|
|
242
|
-
|
|
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} />
|
|
243
276
|
|
|
277
|
+
{/* v4.9.0 — Subnav removed; section navigation is now driven by the sidebar */}
|
|
278
|
+
{/*
|
|
244
279
|
<nav className="settings-subnav" aria-label="Settings sections">
|
|
245
|
-
<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>
|
|
246
281
|
{SECTION_LINKS.map((s) => (
|
|
247
|
-
<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>
|
|
248
283
|
))}
|
|
249
284
|
</nav>
|
|
285
|
+
*/}
|
|
250
286
|
|
|
251
287
|
<div className={cn('settings-grid', activeSection && 'settings-grid-filtered')} data-active-section={activeSection || undefined}>
|
|
252
288
|
{(showAll || sectionOf('theme')) && <ThemeSection {...sp} />}
|
|
253
289
|
{(showAll || sectionOf('updates')) && <UpdatesSection />}
|
|
254
|
-
{(showAll || sectionOf('layout') || sectionOf('general')) && <GeneralSection {...sp} />}
|
|
290
|
+
{(showAll || sectionOf('layout') || sectionOf('general')) && <GeneralSection {...sp} autoSave={autoSave} />}
|
|
291
|
+
{(showAll || sectionOf('env-vars')) && <EnvVarsSection />}
|
|
255
292
|
{(showAll || sectionOf('network') || sectionOf('service') || sectionOf('tailscale')) && <NetworkSection tailscale={tailscale} tailscaleDraft={tailscaleDraft} setTailscaleDraft={setTailscaleDraft} onTailscaleToggle={onTailscaleToggle} />}
|
|
256
293
|
{(showAll || sectionOf('notifications')) && <NotificationsSection {...sp} />}
|
|
257
294
|
{(showAll || sectionOf('auth')) && <AuthSection settings={settings} authStatus={authStatus} setAuthStatus={setAuthStatus} />}
|
|
258
|
-
{(showAll || sectionOf('agents') || sectionOf('dashboard') || sectionOf('background')) && <AgentSection {...sp} />}
|
|
295
|
+
{(showAll || sectionOf('agents') || sectionOf('dashboard') || sectionOf('background')) && <AgentSection {...sp} autoSave={autoSave} />}
|
|
259
296
|
{(showAll || sectionOf('system-llm')) && <SystemLlmSection {...sp} />}
|
|
260
297
|
{(showAll || sectionOf('headroom')) && <HeadroomSection settings={settings} setSettings={setSettings} setDirty={setDirty} />}
|
|
261
298
|
{(showAll || sectionOf('activity-log') || sectionOf('about')) && <ActivitySection about={about} />}
|
|
262
|
-
{(showAll || sectionOf('
|
|
263
|
-
{(showAll || sectionOf('providers')) && <ProvidersSection />}
|
|
264
|
-
{(showAll || sectionOf('memory')) && <MemorySection />}
|
|
265
|
-
{(showAll || sectionOf('skills')) && <SkillsSection />}
|
|
266
|
-
{(showAll || sectionOf('backup')) && <BackupSection />}
|
|
299
|
+
{(showAll || sectionOf('workspaces')) && <WorkspacesSection />}
|
|
267
300
|
</div>
|
|
268
301
|
</div>
|
|
269
302
|
);
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// src/web/views/Workspace.tsx — v5.0.0 — workspace management view.
|
|
2
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
3
|
+
import { Users, Plus, Trash2, Shield, Crown, Eye } from 'lucide-react';
|
|
4
|
+
import { api } from '../lib/api';
|
|
5
|
+
import { useModal } from '../components/Modal';
|
|
6
|
+
import { Button } from '../components/Button';
|
|
7
|
+
import { InviteDialog, type InviteInfo } from '../components/InviteDialog';
|
|
8
|
+
import { cn } from '../lib/utils';
|
|
9
|
+
|
|
10
|
+
export type WorkspaceMember = {
|
|
11
|
+
userId: string;
|
|
12
|
+
email: string;
|
|
13
|
+
name: string;
|
|
14
|
+
role: 'admin' | 'editor' | 'viewer';
|
|
15
|
+
joinedAt: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type WorkspaceInfo = {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
ownerId: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type Props = {
|
|
26
|
+
workspaceId: string;
|
|
27
|
+
onWorkspaceDeleted?: () => void;
|
|
28
|
+
onWorkspaceUpdated?: (name: string) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const ROLE_ICONS = {
|
|
32
|
+
admin: Crown,
|
|
33
|
+
editor: Shield,
|
|
34
|
+
viewer: Eye,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ROLE_COLORS = {
|
|
38
|
+
admin: 'role-admin',
|
|
39
|
+
editor: 'role-editor',
|
|
40
|
+
viewer: 'role-viewer',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function WorkspaceView({ workspaceId, onWorkspaceDeleted, onWorkspaceUpdated }: Props) {
|
|
44
|
+
const modal = useModal();
|
|
45
|
+
const [workspace, setWorkspace] = useState<WorkspaceInfo | null>(null);
|
|
46
|
+
const [members, setMembers] = useState<WorkspaceMember[]>([]);
|
|
47
|
+
const [invites, setInvites] = useState<InviteInfo[]>([]);
|
|
48
|
+
const [loading, setLoading] = useState(true);
|
|
49
|
+
const [error, setError] = useState<string | null>(null);
|
|
50
|
+
const [nameDraft, setNameDraft] = useState('');
|
|
51
|
+
const [nameEditing, setNameEditing] = useState(false);
|
|
52
|
+
|
|
53
|
+
const loadWorkspace = useCallback(async () => {
|
|
54
|
+
setLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
try {
|
|
57
|
+
const r = await api.get<{ workspace: WorkspaceInfo; members: WorkspaceMember[] }>(
|
|
58
|
+
`/workspaces/${workspaceId}`,
|
|
59
|
+
);
|
|
60
|
+
setWorkspace(r.workspace);
|
|
61
|
+
setMembers(r.members || []);
|
|
62
|
+
setNameDraft(r.workspace.name);
|
|
63
|
+
} catch (err: unknown) {
|
|
64
|
+
setError((err as Error).message || 'Failed to load workspace');
|
|
65
|
+
} finally {
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}
|
|
68
|
+
}, [workspaceId]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
loadWorkspace();
|
|
72
|
+
}, [loadWorkspace]);
|
|
73
|
+
|
|
74
|
+
const loadInvites = async () => {
|
|
75
|
+
try {
|
|
76
|
+
const r = await api.get<{ invites: InviteInfo[] }>(`/workspaces/${workspaceId}/invites`);
|
|
77
|
+
setInvites(r.invites || []);
|
|
78
|
+
} catch {
|
|
79
|
+
// Ignore
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
loadInvites();
|
|
85
|
+
}, [workspaceId]);
|
|
86
|
+
|
|
87
|
+
const handleSaveName = async () => {
|
|
88
|
+
if (!nameDraft.trim() || !workspace) return;
|
|
89
|
+
try {
|
|
90
|
+
// Workspace rename would be a separate endpoint; for now just update locally
|
|
91
|
+
setWorkspace((w) => (w ? { ...w, name: nameDraft.trim() } : w));
|
|
92
|
+
onWorkspaceUpdated?.(nameDraft.trim());
|
|
93
|
+
setNameEditing(false);
|
|
94
|
+
} catch {
|
|
95
|
+
setError('Failed to update name');
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleDeleteWorkspace = async () => {
|
|
100
|
+
if (!confirm(`Delete workspace "${workspace?.name}"? This cannot be undone.`)) return;
|
|
101
|
+
if (!confirm('Are you absolutely sure? All workspace data will be permanently deleted.')) return;
|
|
102
|
+
try {
|
|
103
|
+
await api.del(`/workspaces/${workspaceId}`);
|
|
104
|
+
onWorkspaceDeleted?.();
|
|
105
|
+
} catch (err: unknown) {
|
|
106
|
+
setError((err as Error).message || 'Failed to delete workspace');
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleRemoveMember = async (userId: string) => {
|
|
111
|
+
if (!confirm('Remove this member from the workspace?')) return;
|
|
112
|
+
try {
|
|
113
|
+
await api.del(`/workspaces/${workspaceId}/members/${userId}`);
|
|
114
|
+
setMembers((prev) => prev.filter((m) => m.userId !== userId));
|
|
115
|
+
} catch (err: unknown) {
|
|
116
|
+
setError((err as Error).message || 'Failed to remove member');
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const handleUpdateRole = async (userId: string, newRole: string) => {
|
|
121
|
+
try {
|
|
122
|
+
await api.post(`/workspaces/${workspaceId}/members/${userId}`, { role: newRole });
|
|
123
|
+
setMembers((prev) =>
|
|
124
|
+
prev.map((m) => (m.userId === userId ? { ...m, role: newRole as WorkspaceMember['role'] } : m)),
|
|
125
|
+
);
|
|
126
|
+
} catch (err: unknown) {
|
|
127
|
+
setError((err as Error).message || 'Failed to update role');
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Note: In a real implementation, we'd get the current user ID from the session/JWT.
|
|
132
|
+
// For now, we trust the server to only render this view for authorized users.
|
|
133
|
+
const currentUserIsAdmin = true;
|
|
134
|
+
|
|
135
|
+
if (loading) {
|
|
136
|
+
return (
|
|
137
|
+
<div className="view view-workspace">
|
|
138
|
+
<div className="workspace-loading">Loading workspace...</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (error && !workspace) {
|
|
144
|
+
return (
|
|
145
|
+
<div className="view view-workspace">
|
|
146
|
+
<div className="workspace-error">
|
|
147
|
+
<p>{error}</p>
|
|
148
|
+
<Button variant="secondary" size="sm" onClick={loadWorkspace}>
|
|
149
|
+
Retry
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className="view view-workspace">
|
|
158
|
+
<header className="view-header">
|
|
159
|
+
<div className="view-header-text">
|
|
160
|
+
<h2 className="view-title">
|
|
161
|
+
<Users size={18} />
|
|
162
|
+
{nameEditing ? (
|
|
163
|
+
<input
|
|
164
|
+
type="text"
|
|
165
|
+
className="workspace-name-input"
|
|
166
|
+
value={nameDraft}
|
|
167
|
+
onChange={(e) => setNameDraft(e.target.value)}
|
|
168
|
+
onBlur={handleSaveName}
|
|
169
|
+
onKeyDown={(e) => {
|
|
170
|
+
if (e.key === 'Enter') handleSaveName();
|
|
171
|
+
if (e.key === 'Escape') setNameEditing(false);
|
|
172
|
+
}}
|
|
173
|
+
autoFocus
|
|
174
|
+
/>
|
|
175
|
+
) : (
|
|
176
|
+
<span onClick={() => currentUserIsAdmin && setNameEditing(true)} className={cn('workspace-name', currentUserIsAdmin && 'workspace-name-editable')}>
|
|
177
|
+
{workspace?.name}
|
|
178
|
+
</span>
|
|
179
|
+
)}
|
|
180
|
+
</h2>
|
|
181
|
+
<p className="view-subtitle">
|
|
182
|
+
Created {workspace?.createdAt ? new Date(workspace.createdAt).toLocaleDateString() : '—'}
|
|
183
|
+
</p>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="view-actions">
|
|
186
|
+
{currentUserIsAdmin && (
|
|
187
|
+
<>
|
|
188
|
+
<Button
|
|
189
|
+
variant="secondary"
|
|
190
|
+
size="sm"
|
|
191
|
+
onClick={() => {
|
|
192
|
+
modal.open({
|
|
193
|
+
title: 'Invite Members',
|
|
194
|
+
children: <InviteDialog workspaceId={workspaceId} onInviteCreated={loadInvites} />,
|
|
195
|
+
width: 480,
|
|
196
|
+
});
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
<Plus size={14} />
|
|
200
|
+
Invite
|
|
201
|
+
</Button>
|
|
202
|
+
<Button variant="danger" size="sm" onClick={handleDeleteWorkspace}>
|
|
203
|
+
<Trash2 size={14} />
|
|
204
|
+
Delete
|
|
205
|
+
</Button>
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
</header>
|
|
210
|
+
|
|
211
|
+
{error && (
|
|
212
|
+
<div className="workspace-error-banner">
|
|
213
|
+
<span>{error}</span>
|
|
214
|
+
<button onClick={() => setError(null)}>×</button>
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
<div className="workspace-sections">
|
|
219
|
+
{/* Members section */}
|
|
220
|
+
<section className="workspace-section">
|
|
221
|
+
<h3 className="workspace-section-title">Members ({members.length})</h3>
|
|
222
|
+
<div className="workspace-members-list">
|
|
223
|
+
{members.map((member) => {
|
|
224
|
+
const RoleIcon = ROLE_ICONS[member.role] || Eye;
|
|
225
|
+
return (
|
|
226
|
+
<div key={member.userId} className="workspace-member">
|
|
227
|
+
<div className="workspace-member-info">
|
|
228
|
+
<div className="workspace-member-avatar">
|
|
229
|
+
{member.name?.[0]?.toUpperCase() || member.email[0].toUpperCase()}
|
|
230
|
+
</div>
|
|
231
|
+
<div className="workspace-member-details">
|
|
232
|
+
<span className="workspace-member-name">{member.name || 'Unknown'}</span>
|
|
233
|
+
<span className="workspace-member-email">{member.email}</span>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
<div className="workspace-member-actions">
|
|
237
|
+
<span className={cn('workspace-member-role', ROLE_COLORS[member.role])}>
|
|
238
|
+
<RoleIcon size={12} />
|
|
239
|
+
{member.role}
|
|
240
|
+
</span>
|
|
241
|
+
{currentUserIsAdmin && member.role !== 'admin' && (
|
|
242
|
+
<>
|
|
243
|
+
<select
|
|
244
|
+
className="workspace-role-select"
|
|
245
|
+
value={member.role}
|
|
246
|
+
onChange={(e) => handleUpdateRole(member.userId, e.target.value)}
|
|
247
|
+
>
|
|
248
|
+
<option value="viewer">Viewer</option>
|
|
249
|
+
<option value="editor">Editor</option>
|
|
250
|
+
<option value="admin">Admin</option>
|
|
251
|
+
</select>
|
|
252
|
+
<Button
|
|
253
|
+
variant="ghost"
|
|
254
|
+
size="sm"
|
|
255
|
+
iconOnly
|
|
256
|
+
onClick={() => handleRemoveMember(member.userId)}
|
|
257
|
+
title="Remove member"
|
|
258
|
+
>
|
|
259
|
+
<Trash2 size={13} />
|
|
260
|
+
</Button>
|
|
261
|
+
</>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
})}
|
|
267
|
+
</div>
|
|
268
|
+
</section>
|
|
269
|
+
|
|
270
|
+
{/* Pending invites section */}
|
|
271
|
+
{currentUserIsAdmin && (
|
|
272
|
+
<section className="workspace-section">
|
|
273
|
+
<h3 className="workspace-section-title">Pending Invites ({invites.length})</h3>
|
|
274
|
+
{invites.length === 0 ? (
|
|
275
|
+
<p className="workspace-empty">No pending invites</p>
|
|
276
|
+
) : (
|
|
277
|
+
<div className="workspace-invites-list">
|
|
278
|
+
{invites.map((inv) => (
|
|
279
|
+
<div key={inv.token} className="workspace-invite">
|
|
280
|
+
<span className="workspace-invite-email">{inv.email}</span>
|
|
281
|
+
<span className={cn('workspace-invite-role', ROLE_COLORS[inv.role])}>{inv.role}</span>
|
|
282
|
+
<span className="workspace-invite-expiry">
|
|
283
|
+
Expires {new Date(inv.expiresAt).toLocaleDateString()}
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
))}
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
</section>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
}
|