@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.
Files changed (183) hide show
  1. package/bizar-dash/dist/assets/icons-Bo0iH9EC.js +701 -0
  2. package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
  3. package/bizar-dash/dist/assets/{index-DU61awG3.js → index-DmpSFPJY.js} +1 -1
  4. package/bizar-dash/dist/assets/{index-DU61awG3.js.map → index-DmpSFPJY.js.map} +1 -1
  5. package/bizar-dash/dist/assets/main-C1cpttnv.js +19 -0
  6. package/bizar-dash/dist/assets/main-C1cpttnv.js.map +1 -0
  7. package/bizar-dash/dist/assets/main-DTkNlLrw.css +1 -0
  8. package/bizar-dash/dist/assets/markdown-DIquRulQ.js +29 -0
  9. package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +1 -0
  10. package/bizar-dash/dist/assets/mobile-BtxQJftK.js +2 -0
  11. package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
  12. package/bizar-dash/dist/assets/mobile-DD-FZrTC.js +1 -0
  13. package/bizar-dash/dist/assets/{mobile-CL5uUQEC.js.map → mobile-DD-FZrTC.js.map} +1 -1
  14. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
  15. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
  16. package/bizar-dash/dist/index.html +6 -3
  17. package/bizar-dash/dist/mobile.html +5 -2
  18. package/bizar-dash/node_modules/.package-lock.json +6 -0
  19. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  20. package/bizar-dash/package-lock.json +6 -0
  21. package/bizar-dash/skills/eval/SKILL.md +237 -0
  22. package/bizar-dash/src/server/api.mjs +35 -0
  23. package/bizar-dash/src/server/auth.mjs +155 -1
  24. package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
  25. package/bizar-dash/src/server/eval-store.mjs +226 -0
  26. package/bizar-dash/src/server/eval.mjs +347 -0
  27. package/bizar-dash/src/server/memory-store.mjs +46 -0
  28. package/bizar-dash/src/server/ocr.mjs +55 -0
  29. package/bizar-dash/src/server/plugins/registry.mjs +363 -0
  30. package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
  31. package/bizar-dash/src/server/plugins/store.mjs +659 -0
  32. package/bizar-dash/src/server/providers-store.mjs +11 -4
  33. package/bizar-dash/src/server/routes/_shared.mjs +2 -2
  34. package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
  35. package/bizar-dash/src/server/routes/doctor.mjs +71 -0
  36. package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
  37. package/bizar-dash/src/server/routes/eval.mjs +147 -0
  38. package/bizar-dash/src/server/routes/memory.mjs +3 -0
  39. package/bizar-dash/src/server/routes/ocr.mjs +182 -0
  40. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
  41. package/bizar-dash/src/server/routes/plugins.mjs +220 -0
  42. package/bizar-dash/src/server/routes/schedules.mjs +55 -0
  43. package/bizar-dash/src/server/routes/users.mjs +84 -0
  44. package/bizar-dash/src/server/routes/voice.mjs +131 -0
  45. package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
  46. package/bizar-dash/src/server/serve-info.mjs +172 -0
  47. package/bizar-dash/src/server/voice-store.mjs +202 -0
  48. package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
  49. package/bizar-dash/src/server/workspaces.mjs +626 -0
  50. package/bizar-dash/src/web/App.tsx +48 -11
  51. package/bizar-dash/src/web/components/AutosaveField.tsx +50 -0
  52. package/bizar-dash/src/web/components/DoctorPanel.tsx +160 -0
  53. package/bizar-dash/src/web/components/EnvVarManager.tsx +245 -62
  54. package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
  55. package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
  56. package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
  57. package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
  58. package/bizar-dash/src/web/components/SettingsNav.tsx +101 -0
  59. package/bizar-dash/src/web/components/Sidebar.tsx +25 -1
  60. package/bizar-dash/src/web/components/StatusBadge.tsx +33 -2
  61. package/bizar-dash/src/web/components/Topbar.tsx +11 -0
  62. package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
  63. package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
  64. package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
  65. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +64 -0
  66. package/bizar-dash/src/web/components/chat/Composer.tsx +1 -1
  67. package/bizar-dash/src/web/components/chat/useChat.ts +118 -1
  68. package/bizar-dash/src/web/hooks/useAutosave.ts +107 -0
  69. package/bizar-dash/src/web/lib/types.ts +134 -0
  70. package/bizar-dash/src/web/styles/chat.css +8 -5
  71. package/bizar-dash/src/web/styles/main.css +77 -2
  72. package/bizar-dash/src/web/styles/memory.css +82 -0
  73. package/bizar-dash/src/web/styles/settings.css +265 -0
  74. package/bizar-dash/src/web/views/Chat.tsx +15 -1
  75. package/bizar-dash/src/web/views/Doctor.tsx +317 -0
  76. package/bizar-dash/src/web/views/Memory.tsx +17 -2
  77. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +1 -0
  78. package/bizar-dash/src/web/views/Schedules.tsx +94 -12
  79. package/bizar-dash/src/web/views/Settings.tsx +78 -45
  80. package/bizar-dash/src/web/views/Workspace.tsx +294 -0
  81. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
  82. package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
  83. package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
  84. package/bizar-dash/src/web/views/settings/AgentSection.tsx +23 -12
  85. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +5 -6
  86. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +38 -15
  87. package/bizar-dash/src/web/views/settings/MemorySection.tsx +92 -8
  88. package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
  89. package/bizar-dash/tests/autosave.test.tsx +276 -0
  90. package/bizar-dash/tests/bundle-analysis.test.mjs +5 -2
  91. package/bizar-dash/tests/chat-composer.test.tsx +140 -0
  92. package/bizar-dash/tests/clipboard.test.mjs +147 -0
  93. package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
  94. package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
  95. package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
  96. package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
  97. package/bizar-dash/tests/diagnostics-store.test.mjs +206 -0
  98. package/bizar-dash/tests/env-vars-extended.test.mjs +190 -0
  99. package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
  100. package/bizar-dash/tests/eval/report.test.mjs +284 -0
  101. package/bizar-dash/tests/eval/runner.test.mjs +471 -0
  102. package/bizar-dash/tests/layout-spacing.test.mjs +101 -0
  103. package/bizar-dash/tests/memory-default-vault.test.mjs +98 -0
  104. package/bizar-dash/tests/minimax-bar.test.tsx +113 -0
  105. package/bizar-dash/tests/minimax-models.test.mjs +123 -0
  106. package/bizar-dash/tests/ocr.test.mjs +87 -0
  107. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
  108. package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
  109. package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
  110. package/bizar-dash/tests/plugins-store.test.mjs +455 -0
  111. package/bizar-dash/tests/routes-doctor.test.mjs +159 -0
  112. package/bizar-dash/tests/schedules-templates.test.mjs +144 -0
  113. package/bizar-dash/tests/settings-layout.test.tsx +129 -0
  114. package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
  115. package/bizar-dash/tests/settings-nav.test.tsx +126 -0
  116. package/bizar-dash/tests/users.test.mjs +108 -0
  117. package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
  118. package/bizar-dash/tests/voice-store.test.mjs +148 -0
  119. package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
  120. package/bizar-dash/tests/workspaces.test.mjs +527 -0
  121. package/cli/bin.mjs +72 -2
  122. package/cli/commands/clip.mjs +146 -0
  123. package/cli/commands/deploy/cloudflare.mjs +250 -0
  124. package/cli/commands/deploy/docker.mjs +221 -0
  125. package/cli/commands/deploy/fly.mjs +161 -0
  126. package/cli/commands/deploy/vercel.mjs +225 -0
  127. package/cli/commands/deploy.mjs +240 -0
  128. package/cli/commands/eval.mjs +378 -0
  129. package/cli/commands/marketplace.mjs +64 -0
  130. package/cli/commands/minimax.mjs +5 -2
  131. package/cli/commands/ocr.mjs +165 -0
  132. package/cli/commands/plugin.mjs +358 -0
  133. package/cli/commands/voice.mjs +211 -0
  134. package/cli/commands/workspace.mjs +247 -0
  135. package/cli/tests/minimax-cli.test.mjs +79 -0
  136. package/config/agents/frigg.md +1 -1
  137. package/config/agents/heimdall.md +1 -1
  138. package/config/agents/mimir.md +1 -1
  139. package/config/agents/quick.md +1 -1
  140. package/config/agents/semble-search.md +1 -1
  141. package/config/agents/vor.md +1 -1
  142. package/config/opencode.json +28 -21
  143. package/config/opencode.json.template +8 -1
  144. package/package.json +12 -8
  145. package/plugins/bizar/index.ts +77 -0
  146. package/plugins/bizar/src/compaction.d.mts +48 -0
  147. package/plugins/bizar/src/compaction.mjs +192 -0
  148. package/plugins/bizar/tests/compaction.test.ts +264 -0
  149. package/templates/deploy/cloudflare/README.md +32 -0
  150. package/templates/deploy/cloudflare/functions-index.template.js +15 -0
  151. package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
  152. package/templates/deploy/docker/.env.template +16 -0
  153. package/templates/deploy/docker/README.md +58 -0
  154. package/templates/deploy/docker/docker-compose.template.yml +23 -0
  155. package/templates/deploy/fly/README.md +35 -0
  156. package/templates/deploy/fly/fly.toml.template +28 -0
  157. package/templates/deploy/vercel/README.md +29 -0
  158. package/templates/deploy/vercel/api-index.template.js +18 -0
  159. package/templates/deploy/vercel/vercel.json.template +16 -0
  160. package/templates/eval-fixtures/README.md +58 -0
  161. package/templates/eval-fixtures/code-search-basic.json +28 -0
  162. package/templates/eval-fixtures/latency-bounds.json +16 -0
  163. package/templates/eval-fixtures/regression-suite.json +79 -0
  164. package/templates/eval-fixtures/response-format.json +30 -0
  165. package/templates/eval-fixtures/tool-call-correctness.json +24 -0
  166. package/templates/plugin-template/README.md +121 -0
  167. package/templates/plugin-template/index.js +66 -0
  168. package/templates/plugin-template/plugin.json +42 -0
  169. package/templates/plugin-template/tests/plugin.test.js +83 -0
  170. package/templates/schedules/daily-backup.json +12 -0
  171. package/templates/schedules/daily-cleanup.json +12 -0
  172. package/templates/schedules/hourly-health-check.json +12 -0
  173. package/templates/schedules/webhook-on-push.json +13 -0
  174. package/templates/schedules/weekly-digest.json +13 -0
  175. package/bizar-dash/dist/assets/main-DaC1Lc6q.js +0 -366
  176. package/bizar-dash/dist/assets/main-DaC1Lc6q.js.map +0 -1
  177. package/bizar-dash/dist/assets/main-DfmIfOUS.css +0 -1
  178. package/bizar-dash/dist/assets/mobile-CL5uUQEC.js +0 -1
  179. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js +0 -338
  180. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js.map +0 -1
  181. package/bizar-dash/src/web/views/settings/BackupSection.tsx +0 -16
  182. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +0 -16
  183. package/bizar-dash/src/web/views/settings/SkillsSection.tsx +0 -16
@@ -1,14 +1,13 @@
1
- // src/web/views/memory/ConfigPanel.tsx — global memory config + per-system toggles.
1
+ // src/web/views/memory/ConfigPanel.tsx — simplified memory config.
2
2
  import { useEffect, useState } from 'react';
3
3
  import {
4
- Brain,
5
- Database,
6
- FileText,
7
4
  GitBranch,
5
+ GitCommitHorizontal,
6
+ Download,
7
+ Upload,
8
8
  Loader2,
9
9
  Plug,
10
10
  Save,
11
- Sparkles,
12
11
  } from 'lucide-react';
13
12
  import { Button } from '../../components/Button';
14
13
  import { Card, CardMeta, CardTitle } from '../../components/Card';
@@ -17,10 +16,19 @@ import { useToast } from '../../components/Toast';
17
16
  import { api } from '../../lib/api';
18
17
  import { cn } from '../../lib/utils';
19
18
 
19
+ type GitConfig = {
20
+ remoteUrl?: string;
21
+ };
22
+
20
23
  type GlobalConfig = {
21
- lightrag?: { enabled?: boolean; url?: string; llm?: string; embedding?: string };
22
- obsidian?: { vaultPath?: string; syncInterval?: number };
23
- git?: { repoPath?: string; remoteUrl?: string; branch?: string; autoSync?: boolean };
24
+ git?: GitConfig;
25
+ };
26
+
27
+ type SyncStatus = {
28
+ clean?: boolean;
29
+ branch?: string;
30
+ modified?: number;
31
+ untracked?: number;
24
32
  };
25
33
 
26
34
  type Props = { refreshKey: number };
@@ -30,6 +38,10 @@ export function ConfigPanel({ refreshKey }: Props) {
30
38
  const [cfg, setCfg] = useState<GlobalConfig | null>(null);
31
39
  const [loading, setLoading] = useState(true);
32
40
  const [saving, setSaving] = useState(false);
41
+ const [remoteUrl, setRemoteUrl] = useState('');
42
+ const [vaultPath] = useState<string>('');
43
+ const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
44
+ const [actionLoading, setActionLoading] = useState<string | null>(null);
33
45
  const [tests, setTests] = useState<Array<{ name: string; pass: boolean; detail: string }> | null>(null);
34
46
  const [testing, setTesting] = useState(false);
35
47
 
@@ -38,6 +50,9 @@ export function ConfigPanel({ refreshKey }: Props) {
38
50
  try {
39
51
  const r = await api.get<{ config: GlobalConfig }>('/memory/config/global');
40
52
  setCfg(r.config);
53
+ setRemoteUrl(r.config?.git?.remoteUrl || '');
54
+ // Also fetch git status from the per-project vault
55
+ await loadSyncStatus();
41
56
  } catch (err) {
42
57
  toast.error(`Config load failed: ${(err as Error).message}`);
43
58
  } finally {
@@ -45,20 +60,32 @@ export function ConfigPanel({ refreshKey }: Props) {
45
60
  }
46
61
  };
47
62
 
63
+ const loadSyncStatus = async () => {
64
+ try {
65
+ const r = await api.get<{ ok: boolean; clean?: boolean; branch?: string; modified?: number; untracked?: number }>('/memory/git/status');
66
+ if (r.ok) {
67
+ setSyncStatus({
68
+ clean: r.clean,
69
+ branch: r.branch,
70
+ modified: r.modified,
71
+ untracked: r.untracked,
72
+ });
73
+ }
74
+ } catch {
75
+ // git status is best-effort
76
+ }
77
+ };
78
+
48
79
  useEffect(() => {
49
80
  reload();
50
81
  // eslint-disable-next-line react-hooks/exhaustive-deps
51
82
  }, [refreshKey]);
52
83
 
53
- const update = (block: keyof GlobalConfig, key: string, value: unknown) => {
54
- setCfg((cur) => cur ? { ...cur, [block]: { ...(cur[block] || {}), [key]: value } } : cur);
55
- };
56
-
57
84
  const onSave = async () => {
58
85
  if (!cfg) return;
59
86
  setSaving(true);
60
87
  try {
61
- await api.put('/memory/config/global', cfg);
88
+ await api.put('/memory/config/global', { git: { remoteUrl } });
62
89
  toast.success('Config saved.');
63
90
  } catch (err) {
64
91
  toast.error(`Save failed: ${(err as Error).message}`);
@@ -67,6 +94,30 @@ export function ConfigPanel({ refreshKey }: Props) {
67
94
  }
68
95
  };
69
96
 
97
+ const runAction = async (action: 'pull' | 'push' | 'commit') => {
98
+ setActionLoading(action);
99
+ try {
100
+ if (action === 'pull') {
101
+ const r = await api.post<{ ok: boolean; output?: string }>('/memory/git/pull', {});
102
+ if (r.ok) toast.success('Pull successful.');
103
+ else toast.error(`Pull failed: ${r.output || 'unknown error'}`);
104
+ } else if (action === 'push') {
105
+ const r = await api.post<{ ok: boolean }>('/memory/git/push', {});
106
+ if (r.ok) toast.success('Push successful.');
107
+ else toast.error('Push failed.');
108
+ } else if (action === 'commit') {
109
+ const r = await api.post<{ ok: boolean; message?: string }>('/memory/git/commit', {});
110
+ if (r.ok) toast.success(`Committed: ${r.message}`);
111
+ else toast.error('Commit failed.');
112
+ }
113
+ await loadSyncStatus();
114
+ } catch (err) {
115
+ toast.error(`${action} failed: ${(err as Error).message}`);
116
+ } finally {
117
+ setActionLoading(null);
118
+ }
119
+ };
120
+
70
121
  const onTest = async () => {
71
122
  setTesting(true);
72
123
  try {
@@ -94,144 +145,75 @@ export function ConfigPanel({ refreshKey }: Props) {
94
145
 
95
146
  return (
96
147
  <div className="memory-panel-content">
97
- {/* ── LightRAG ──────────────────────────────────────────────── */}
98
- <Card>
99
- <CardTitle><Brain size={14} /> LightRAG</CardTitle>
100
- <CardMeta>URL + model overrides. Server-side defaults from <code>opencode Zen free tier</code>.</CardMeta>
101
- <div className="memory-config-form">
102
- <Row label="Enabled" inline>
103
- <label className="memory-switch">
104
- <input
105
- type="checkbox"
106
- checked={!!cfg.lightrag?.enabled}
107
- onChange={(e) => update('lightrag', 'enabled', e.target.checked)}
108
- />
109
- <span>{cfg.lightrag?.enabled ? 'on' : 'off'}</span>
110
- </label>
111
- </Row>
112
- <Row label="URL">
113
- <input
114
- type="text"
115
- className="input mono"
116
- value={cfg.lightrag?.url || ''}
117
- onChange={(e) => update('lightrag', 'url', e.target.value)}
118
- placeholder="http://127.0.0.1:9621"
119
- />
120
- </Row>
121
- <Row label="LLM model">
122
- <input
123
- type="text"
124
- className="input mono"
125
- value={cfg.lightrag?.llm || ''}
126
- onChange={(e) => update('lightrag', 'llm', e.target.value)}
127
- placeholder="opencode/gpt-5-nano"
128
- />
129
- </Row>
130
- <Row label="Embedding">
131
- <input
132
- type="text"
133
- className="input mono"
134
- value={cfg.lightrag?.embedding || ''}
135
- onChange={(e) => update('lightrag', 'embedding', e.target.value)}
136
- placeholder="opencode/text-embedding-3-small"
137
- />
138
- </Row>
139
- </div>
140
- </Card>
141
-
142
- {/* ── Obsidian ──────────────────────────────────────────────── */}
143
- <Card>
144
- <CardTitle><FileText size={14} /> Obsidian vault</CardTitle>
145
- <CardMeta>Path + sync cadence for the markdown vault.</CardMeta>
146
- <div className="memory-config-form">
147
- <Row label="Vault path">
148
- <input
149
- type="text"
150
- className="input mono"
151
- value={cfg.obsidian?.vaultPath || ''}
152
- onChange={(e) => update('obsidian', 'vaultPath', e.target.value)}
153
- placeholder="/home/me/vault"
154
- />
155
- </Row>
156
- <Row label="Sync interval (seconds)">
157
- <input
158
- type="number"
159
- min={0}
160
- max={2592000}
161
- className="input mono"
162
- value={cfg.obsidian?.syncInterval ?? 300}
163
- onChange={(e) => update('obsidian', 'syncInterval', Number(e.target.value))}
164
- />
165
- </Row>
166
- </div>
167
- </Card>
168
-
169
- {/* ── Git ───────────────────────────────────────────────────── */}
148
+ {/* ── Git sync ───────────────────────────────────────────────────── */}
170
149
  <Card>
171
150
  <CardTitle><GitBranch size={14} /> Git sync</CardTitle>
172
- <CardMeta>Repo path, remote, branch, and auto-sync toggle.</CardMeta>
151
+ <CardMeta>Configure the git remote and sync your memory vault.</CardMeta>
173
152
  <div className="memory-config-form">
174
- <Row label="Repo path">
175
- <input
176
- type="text"
177
- className="input mono"
178
- value={cfg.git?.repoPath || ''}
179
- onChange={(e) => update('git', 'repoPath', e.target.value)}
180
- placeholder="/path/to/repo"
181
- />
153
+ <Row label="Vault path" inline>
154
+ <span className="memory-vault-path mono muted">{vaultPath || '(loading…)'}</span>
182
155
  </Row>
183
156
  <Row label="Remote URL">
184
157
  <input
185
158
  type="text"
186
159
  className="input mono"
187
- value={cfg.git?.remoteUrl || ''}
188
- onChange={(e) => update('git', 'remoteUrl', e.target.value)}
160
+ value={remoteUrl}
161
+ onChange={(e) => setRemoteUrl(e.target.value)}
189
162
  placeholder="git@github.com:org/repo.git"
190
163
  />
191
164
  </Row>
192
- <Row label="Branch">
193
- <input
194
- type="text"
195
- className="input mono"
196
- value={cfg.git?.branch || 'main'}
197
- onChange={(e) => update('git', 'branch', e.target.value)}
198
- placeholder="main"
199
- />
200
- </Row>
201
- <Row label="Auto-sync" inline>
202
- <label className="memory-switch">
203
- <input
204
- type="checkbox"
205
- checked={!!cfg.git?.autoSync}
206
- onChange={(e) => update('git', 'autoSync', e.target.checked)}
207
- />
208
- <span>{cfg.git?.autoSync ? 'on' : 'off'}</span>
209
- </label>
210
- </Row>
165
+ {syncStatus && (
166
+ <Row label="Status" inline>
167
+ <span className={cn('memory-pill', syncStatus.clean ? 'memory-pill-ok' : 'memory-pill-warn')}>
168
+ {syncStatus.clean ? 'clean' : 'dirty'}
169
+ </span>
170
+ {syncStatus.branch && (
171
+ <span className="muted" style={{ marginLeft: 8 }}>
172
+ <GitBranch size={12} style={{ display: 'inline' }} /> {syncStatus.branch}
173
+ </span>
174
+ )}
175
+ {!syncStatus.clean && syncStatus.modified !== undefined && (
176
+ <span className="muted" style={{ marginLeft: 8 }}>
177
+ {syncStatus.modified} modified, {syncStatus.untracked} untracked
178
+ </span>
179
+ )}
180
+ </Row>
181
+ )}
211
182
  </div>
212
- </Card>
213
-
214
- {/* ── CCR (placeholder when Headroom is installed) ─────────── */}
215
- <Card>
216
- <CardTitle>
217
- <Sparkles size={14} /> CCR (Compress-Cache-Retrieve)
218
- </CardTitle>
219
- <CardMeta>
220
- Reversible compression for very long conversations. Enabled automatically
221
- when the Headroom mod is installed.
222
- </CardMeta>
223
- <div className="muted text-sm">
224
- See Settings → Headroom for CCR configuration. This panel will surface
225
- retention + compression-ratio controls when the mod is active.
183
+ <div className="memory-action-row" style={{ marginTop: 12 }}>
184
+ <Button
185
+ variant="secondary"
186
+ onClick={() => runAction('pull')}
187
+ disabled={actionLoading !== null}
188
+ >
189
+ {actionLoading === 'pull' ? <Loader2 size={12} className="memory-spin" /> : <Download size={12} />}
190
+ Pull
191
+ </Button>
192
+ <Button
193
+ variant="secondary"
194
+ onClick={() => runAction('commit')}
195
+ disabled={actionLoading !== null}
196
+ >
197
+ {actionLoading === 'commit' ? <Loader2 size={12} className="memory-spin" /> : <GitCommitHorizontal size={12} />}
198
+ Commit
199
+ </Button>
200
+ <Button
201
+ variant="secondary"
202
+ onClick={() => runAction('push')}
203
+ disabled={actionLoading !== null}
204
+ >
205
+ {actionLoading === 'push' ? <Loader2 size={12} className="memory-spin" /> : <Upload size={12} />}
206
+ Push
207
+ </Button>
226
208
  </div>
227
209
  </Card>
228
210
 
229
- {/* ── Test all connections ─────────────────────────────────── */}
211
+ {/* ── Connection tests ─────────────────────────────────────────── */}
230
212
  <Card>
231
213
  <CardTitle>
232
214
  <Plug size={14} /> Connection tests
233
215
  </CardTitle>
234
- <CardMeta>Verify the configured git repo, remote, and path.</CardMeta>
216
+ <CardMeta>Verify the configured git remote is reachable.</CardMeta>
235
217
  <div className="memory-action-row">
236
218
  <Button variant="secondary" onClick={onTest} disabled={testing}>
237
219
  {testing ? <Loader2 size={12} className="memory-spin" /> : <Plug size={12} />}
@@ -292,4 +274,3 @@ function Row({
292
274
  </label>
293
275
  );
294
276
  }
295
-
@@ -0,0 +1,23 @@
1
+ // src/web/views/memory/FromScreenshotPanel.tsx — v5.0.0
2
+ //
3
+ // Memory tab panel that wraps ScreenshotOCR for the three-column Memory layout.
4
+ // Provides a self-contained screenshot → OCR → save workflow.
5
+
6
+ import { ScreenshotOCR } from '../../components/ScreenshotOCR';
7
+ import { Card } from '../../components/Card';
8
+
9
+ type Props = {
10
+ refreshKey: number;
11
+ };
12
+
13
+ export function FromScreenshotPanel({ refreshKey: _refreshKey }: Props) {
14
+ return (
15
+ <div className="memory-panel-content">
16
+ <Card variant="outlined" className="memory-panel-card">
17
+ <div className="memory-panel-body">
18
+ <ScreenshotOCR />
19
+ </div>
20
+ </Card>
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,101 @@
1
+ // src/web/views/memory/VaultFromClipboardPanel.tsx — v5.0.0
2
+ //
3
+ // Memory tab panel for pasting content directly into the vault.
4
+ // Accepts URL + title + content text, saves via /api/clipboard/save.
5
+
6
+ import { useState } from 'react';
7
+ import { Clipboard, Loader2 } from 'lucide-react';
8
+ import { Button } from '../../components/Button';
9
+ import { Card, CardTitle } from '../../components/Card';
10
+ import { useToast } from '../../components/Toast';
11
+ import { api } from '../../lib/api';
12
+
13
+ type Props = {
14
+ refreshKey: number;
15
+ };
16
+
17
+ export function VaultFromClipboardPanel({ refreshKey: _refreshKey }: Props) {
18
+ const toast = useToast();
19
+ const [url, setUrl] = useState('');
20
+ const [title, setTitle] = useState('');
21
+ const [content, setContent] = useState('');
22
+ const [saving, setSaving] = useState(false);
23
+
24
+ const handleSave = async () => {
25
+ if (!content.trim()) {
26
+ toast.error('Content is required');
27
+ return;
28
+ }
29
+ setSaving(true);
30
+ try {
31
+ const r = await api.post<{ ok: boolean; notePath: string }>('/clipboard/save', {
32
+ url: url.trim() || undefined,
33
+ title: title.trim() || undefined,
34
+ content: content,
35
+ savedAt: new Date().toISOString(),
36
+ });
37
+ if (r.ok) {
38
+ toast.success(`Saved to ${r.notePath}`);
39
+ setUrl('');
40
+ setTitle('');
41
+ setContent('');
42
+ }
43
+ } catch (err) {
44
+ toast.error(`Save failed: ${(err as Error).message}`);
45
+ } finally {
46
+ setSaving(false);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <div className="memory-panel-content">
52
+ <Card variant="outlined" className="memory-panel-card">
53
+ <div className="memory-panel-body">
54
+ <div className="vault-clipboard-panel">
55
+ <div className="vault-clipboard-header">
56
+ <Clipboard size={16} />
57
+ <span>Paste Content</span>
58
+ </div>
59
+
60
+ <label className="field-label" htmlFor="clip-url">URL (optional)</label>
61
+ <input
62
+ id="clip-url"
63
+ type="text"
64
+ className="input"
65
+ placeholder="https://example.com"
66
+ value={url}
67
+ onChange={(e) => setUrl(e.target.value)}
68
+ />
69
+
70
+ <label className="field-label" htmlFor="clip-title" style={{ marginTop: 10 }}>Title (optional)</label>
71
+ <input
72
+ id="clip-title"
73
+ type="text"
74
+ className="input"
75
+ placeholder="Page title"
76
+ value={title}
77
+ onChange={(e) => setTitle(e.target.value)}
78
+ />
79
+
80
+ <label className="field-label" htmlFor="clip-content" style={{ marginTop: 10 }}>Content *</label>
81
+ <textarea
82
+ id="clip-content"
83
+ className="input vault-clipboard-textarea mono text-sm"
84
+ rows={12}
85
+ placeholder="Paste or type content to save to your vault…"
86
+ value={content}
87
+ onChange={(e) => setContent(e.target.value)}
88
+ />
89
+
90
+ <div className="vault-clipboard-actions" style={{ marginTop: 12 }}>
91
+ <Button variant="primary" size="sm" onClick={handleSave} disabled={saving || !content.trim()}>
92
+ {saving && <Loader2 size={12} className="spinner" />}
93
+ Save to Vault
94
+ </Button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </Card>
99
+ </div>
100
+ );
101
+ }
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
3
3
  import { Server as ServerIcon, Globe, Save } from 'lucide-react';
4
4
  import { Card, CardTitle, CardMeta } from '../../components/Card';
5
5
  import { Button } from '../../components/Button';
6
+ import { AutosaveField } from '../../components/AutosaveField';
6
7
  import { useToast } from '../../components/Toast';
7
8
  import { api } from '../../lib/api';
8
9
  import type { Settings } from '../../lib/types';
@@ -11,9 +12,11 @@ type Props = {
11
12
  settings: Settings;
12
13
  patchAgents: (patch: Partial<Settings['agents']>) => void;
13
14
  patchDashboard: (patch: Partial<Settings['dashboard']>) => void;
15
+ /** Called after state is patched; parent debounces the API save */
16
+ autoSave?: (key: keyof Settings, value: Settings[keyof Settings]) => void;
14
17
  };
15
18
 
16
- export function AgentSection({ settings, patchAgents, patchDashboard }: Props) {
19
+ export function AgentSection({ settings, patchAgents, patchDashboard, autoSave }: Props) {
17
20
  const toast = useToast();
18
21
  const [pluginOptions, setPluginOptions] = useState<Record<string, number>>({});
19
22
 
@@ -139,19 +142,27 @@ export function AgentSection({ settings, patchAgents, patchDashboard }: Props) {
139
142
  <label className="field-label" htmlFor="set-allowed-roots">
140
143
  Additional allowed roots <span className="muted">(advanced)</span>
141
144
  </label>
142
- <textarea
143
- id="set-allowed-roots"
144
- className="textarea"
145
- rows={4}
146
- placeholder="/workspace&#10;/srv/projects"
147
- value={(settings.dashboard.allowedRoots ?? []).join('\n')}
148
- onChange={(e) => {
149
- const lines = e.target.value
150
- .split('\n')
151
- .map((l) => l.trim())
152
- .filter(Boolean);
145
+ {/* key= forces re-mount when allowedRoots changes externally (e.g. reset/reload) */}
146
+ <AutosaveField
147
+ key={settings.dashboard.allowedRoots?.join('\n')}
148
+ initialValue={(settings.dashboard.allowedRoots ?? []).join('\n')}
149
+ delay={1500}
150
+ saveFn={async (v) => {
151
+ const lines = v.split('\n').map((l) => l.trim()).filter(Boolean);
153
152
  patchDashboard({ allowedRoots: lines });
153
+ autoSave?.('dashboard', { allowedRoots: lines } as Settings['dashboard']);
154
154
  }}
155
+ render={({ value, onChange, onBlur }) => (
156
+ <textarea
157
+ id="set-allowed-roots"
158
+ className="textarea"
159
+ rows={4}
160
+ placeholder="/workspace&#10;/srv/projects"
161
+ value={value}
162
+ onChange={e => onChange(e.target.value)}
163
+ onBlur={onBlur}
164
+ />
165
+ )}
155
166
  />
156
167
  <p className="field-help">
157
168
  Optional. Add filesystem roots beyond your home directory that the file
@@ -1,16 +1,15 @@
1
1
  // src/web/views/settings/EnvVarsSection.tsx
2
- // Placeholder — EnvVarManager integration to be added in a future PR.
3
2
  import React from 'react';
3
+ import { KeyRound } from 'lucide-react';
4
4
  import { Card, CardTitle, CardMeta } from '../../components/Card';
5
+ import { EnvVarManager } from '../../components/EnvVarManager';
5
6
 
6
7
  export function EnvVarsSection() {
7
8
  return (
8
9
  <Card id="settings-env-vars" data-section="env-vars">
9
- <CardTitle>Environment Variables</CardTitle>
10
- <CardMeta>Configure environment variables for the Bizar runtime.</CardMeta>
11
- <p className="muted" style={{ fontSize: 12 }}>
12
- Environment variable management is coming soon.
13
- </p>
10
+ <CardTitle><KeyRound size={14} /> Environment Variables</CardTitle>
11
+ <CardMeta>Manage BIZAR_* env vars (API keys, secrets). Values are stored in <code>~/.config/bizar/env.json</code> (mode 0600).</CardMeta>
12
+ <EnvVarManager />
14
13
  </Card>
15
14
  );
16
15
  }
@@ -2,6 +2,7 @@
2
2
  import React from 'react';
3
3
  import { Layout as LayoutIcon } from 'lucide-react';
4
4
  import { Card, CardTitle, CardMeta } from '../../components/Card';
5
+ import { AutosaveField } from '../../components/AutosaveField';
5
6
  import { cn } from '../../lib/utils';
6
7
  import type { Settings } from '../../lib/types';
7
8
 
@@ -9,6 +10,8 @@ type Props = {
9
10
  settings: Settings;
10
11
  patchUi: (patch: Partial<Settings['ui']>) => void;
11
12
  patchTop: <K extends keyof Settings>(key: K, value: Settings[K]) => void;
13
+ /** Called by AutosaveField after state is patched; the parent debounces the API save */
14
+ autoSave?: (key: keyof Settings, value: Settings[keyof Settings]) => void;
12
15
  };
13
16
 
14
17
  const LAYOUTS = [
@@ -17,7 +20,7 @@ const LAYOUTS = [
17
20
  { id: 'both', label: 'Both' },
18
21
  ] as const;
19
22
 
20
- export function GeneralSection({ settings, patchUi, patchTop }: Props) {
23
+ export function GeneralSection({ settings, patchUi, patchTop, autoSave }: Props) {
21
24
  return (
22
25
  <>
23
26
  {/* UI Layout */}
@@ -79,24 +82,44 @@ export function GeneralSection({ settings, patchUi, patchTop }: Props) {
79
82
  <CardMeta>Default agent + model override.</CardMeta>
80
83
  <div className="field" data-setting-id="defaultAgent">
81
84
  <label className="field-label" htmlFor="set-default-agent">Default agent</label>
82
- <input
83
- id="set-default-agent"
84
- className="input"
85
- type="text"
86
- placeholder="e.g. odin"
87
- value={settings.defaultAgent || ''}
88
- onChange={(e) => patchTop('defaultAgent', e.target.value)}
85
+ <AutosaveField
86
+ initialValue={settings.defaultAgent || ''}
87
+ saveFn={async (v) => {
88
+ patchTop('defaultAgent', v);
89
+ autoSave?.('defaultAgent', v);
90
+ }}
91
+ render={({ value, onChange, onBlur }) => (
92
+ <input
93
+ id="set-default-agent"
94
+ className="input"
95
+ type="text"
96
+ placeholder="e.g. odin"
97
+ value={value}
98
+ onChange={e => onChange(e.target.value)}
99
+ onBlur={onBlur}
100
+ />
101
+ )}
89
102
  />
90
103
  </div>
91
104
  <div className="field" data-setting-id="defaultModel">
92
105
  <label className="field-label" htmlFor="set-default-model">Model override</label>
93
- <input
94
- id="set-default-model"
95
- className="input"
96
- type="text"
97
- placeholder="(leave empty for provider default)"
98
- value={settings.defaultModel || ''}
99
- onChange={(e) => patchTop('defaultModel', e.target.value)}
106
+ <AutosaveField
107
+ initialValue={settings.defaultModel || ''}
108
+ saveFn={async (v) => {
109
+ patchTop('defaultModel', v);
110
+ autoSave?.('defaultModel', v);
111
+ }}
112
+ render={({ value, onChange, onBlur }) => (
113
+ <input
114
+ id="set-default-model"
115
+ className="input"
116
+ type="text"
117
+ placeholder="(leave empty for provider default)"
118
+ value={value}
119
+ onChange={e => onChange(e.target.value)}
120
+ onBlur={onBlur}
121
+ />
122
+ )}
100
123
  />
101
124
  </div>
102
125
  </Card>