@polderlabs/bizar 4.5.1 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/bizar-dash/dist/assets/main-DAlLdW8I.css +1 -0
- package/bizar-dash/dist/assets/main-DGGq-iZI.js +361 -0
- package/bizar-dash/dist/assets/main-DGGq-iZI.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-O6ANdD4W.js → mobile-CWqPoGaT.js} +18 -19
- package/bizar-dash/dist/assets/mobile-CWqPoGaT.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js → mobile-i4Uv9eW8.js} +1 -2
- package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js.map → mobile-i4Uv9eW8.js.map} +1 -1
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/bizar-dash/src/server/headroom.mjs +37 -35
- package/bizar-dash/src/server/logger.mjs +71 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +84 -37
- package/bizar-dash/src/server/metrics.mjs +193 -0
- package/bizar-dash/src/server/mods-loader.mjs +13 -5
- package/bizar-dash/src/server/providers-store.mjs +73 -1
- package/bizar-dash/src/server/routes/chat.mjs +41 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +7 -2
- package/bizar-dash/src/server/routes/memory.mjs +13 -4
- package/bizar-dash/src/server/routes/misc.mjs +2 -1
- package/bizar-dash/src/server/routes/overview.mjs +2 -1
- package/bizar-dash/src/server/schedules-runner.mjs +4 -4
- package/bizar-dash/src/server/server.mjs +101 -37
- package/bizar-dash/src/server/watcher.mjs +2 -2
- package/bizar-dash/src/web/App.tsx +40 -6
- package/bizar-dash/src/web/components/SearchModal.tsx +3 -2
- package/bizar-dash/src/web/components/Toast.tsx +1 -1
- package/bizar-dash/src/web/components/Topbar.tsx +1 -1
- package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
- package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
- package/bizar-dash/src/web/lib/api.ts +12 -11
- package/bizar-dash/src/web/lib/i18n.ts +25 -0
- package/bizar-dash/src/web/locales/en.json +52 -0
- package/bizar-dash/src/web/main.tsx +5 -0
- package/bizar-dash/src/web/styles/chat.css +2 -0
- package/bizar-dash/src/web/styles/main.css +52 -23
- package/bizar-dash/src/web/styles/tasks.css +2 -0
- package/bizar-dash/src/web/views/Activity.tsx +24 -17
- package/bizar-dash/src/web/views/History.tsx +90 -75
- package/bizar-dash/src/web/views/Memory.tsx +4 -3
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +3 -2
- package/bizar-dash/src/web/views/Overview.tsx +15 -10
- package/bizar-dash/src/web/views/Settings.tsx +9 -2
- package/bizar-dash/src/web/views/Skills.tsx +3 -2
- package/bizar-dash/src/web/views/Tasks.tsx +4 -3
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +8 -6
- package/bizar-dash/tests/cli-bugfixes.test.mjs +151 -0
- package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
- package/bizar-dash/tests/components/Button.test.tsx +41 -0
- package/bizar-dash/tests/components/Card.test.tsx +42 -0
- package/bizar-dash/tests/components/Modal.test.tsx +104 -0
- package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
- package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
- package/bizar-dash/tests/components/Toast.test.tsx +108 -0
- package/bizar-dash/tests/frontend-bugfixes.test.mjs +151 -0
- package/bizar-dash/tests/hooks/useModal.test.tsx +84 -0
- package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
- package/bizar-dash/tests/lib/i18n.test.ts +46 -0
- package/bizar-dash/tests/lib/utils.test.ts +194 -0
- package/bizar-dash/tests/logger.test.mjs +207 -0
- package/bizar-dash/tests/metrics.test.mjs +183 -0
- package/bizar-dash/tests/server-bugfixes.test.mjs +318 -0
- package/bizar-dash/tests/setup.ts +7 -0
- package/bizar-dash/vitest.config.ts +13 -0
- package/cli/artifact-cli.mjs +605 -0
- package/cli/artifact-render.mjs +621 -0
- package/cli/artifact-server.mjs +847 -0
- package/cli/artifact.mjs +38 -2089
- package/cli/bg.mjs +5 -13
- package/cli/bin.mjs +170 -1348
- package/cli/commands/artifact.mjs +20 -0
- package/cli/commands/dash.mjs +160 -0
- package/cli/commands/headroom.mjs +204 -0
- package/cli/commands/install.mjs +169 -0
- package/cli/commands/memory.mjs +25 -0
- package/cli/commands/minimax.mjs +265 -0
- package/cli/commands/mod.mjs +185 -0
- package/cli/commands/service.mjs +65 -0
- package/cli/commands/usage.mjs +109 -0
- package/cli/commands/util.mjs +306 -0
- package/cli/doctor.mjs +6 -16
- package/cli/memory.mjs +65 -21
- package/cli/provision.mjs +10 -15
- package/cli/service-controller.mjs +1 -11
- package/cli/service.mjs +1 -11
- package/cli/utils.mjs +41 -1
- package/install.sh +1 -2
- package/package.json +8 -3
- package/bizar-dash/dist/assets/main-eWZ4NlCL.css +0 -1
- package/bizar-dash/dist/assets/main-usWhlPWa.js +0 -362
- package/bizar-dash/dist/assets/main-usWhlPWa.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +0 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Shows per-project timelines (task events, plan events, project lifecycle)
|
|
3
3
|
// aggregated from the activity log.
|
|
4
4
|
|
|
5
|
-
import { useEffect, useState } from 'react';
|
|
5
|
+
import React, { useEffect, useState } from 'react';
|
|
6
6
|
import {
|
|
7
7
|
History as HistoryIcon,
|
|
8
8
|
RefreshCw,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { Card, CardTitle, CardMeta } from '../components/Card';
|
|
18
18
|
import { Button } from '../components/Button';
|
|
19
19
|
import { Spinner } from '../components/Spinner';
|
|
20
|
+
import { VirtualList } from '../components/VirtualList';
|
|
20
21
|
import { useToast } from '../components/Toast';
|
|
21
22
|
import { api } from '../lib/api';
|
|
22
23
|
import { cn, formatRelative } from '../lib/utils';
|
|
@@ -65,6 +66,85 @@ const TIME_RANGES = [
|
|
|
65
66
|
{ id: 'all', label: 'All time', ms: 0 },
|
|
66
67
|
];
|
|
67
68
|
|
|
69
|
+
function renderProjectCard(
|
|
70
|
+
p: ProjectHistory,
|
|
71
|
+
events: HistoryEvent[],
|
|
72
|
+
expanded: Set<string>,
|
|
73
|
+
toggleProject: (id: string) => void,
|
|
74
|
+
): React.ReactNode {
|
|
75
|
+
const isOpen = expanded.has(p.id);
|
|
76
|
+
return (
|
|
77
|
+
<Card key={p.id} className="history-project" style={{ marginBottom: 4 }}>
|
|
78
|
+
<div className="history-project-head">
|
|
79
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0, flex: 1 }}>
|
|
80
|
+
<div className="history-project-name">
|
|
81
|
+
<Folder size={16} /> {p.name}
|
|
82
|
+
</div>
|
|
83
|
+
<div className="history-project-meta">{p.path}</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="history-project-stats">
|
|
86
|
+
<span className="history-project-stat">
|
|
87
|
+
<span className="history-project-stat-num">{p.tasks.done}</span>
|
|
88
|
+
<span className="muted">/ {p.tasks.total} done</span>
|
|
89
|
+
</span>
|
|
90
|
+
{p.tasks.doing > 0 && (
|
|
91
|
+
<span className="history-project-stat">
|
|
92
|
+
<span className="history-project-stat-num">{p.tasks.doing}</span>
|
|
93
|
+
<span className="muted">doing</span>
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
{p.tasks.blocked > 0 && (
|
|
97
|
+
<span className="history-project-stat">
|
|
98
|
+
<span className="history-project-stat-num">{p.tasks.blocked}</span>
|
|
99
|
+
<span className="muted">blocked</span>
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
102
|
+
<span className="history-project-stat">
|
|
103
|
+
<FileText size={11} /> <span className="history-project-stat-num">{p.plans}</span>
|
|
104
|
+
<span className="muted"> plan{p.plans === 1 ? '' : 's'}</span>
|
|
105
|
+
</span>
|
|
106
|
+
{p.lastAccessed && (
|
|
107
|
+
<span className="history-project-stat">
|
|
108
|
+
<span className="muted">last opened {formatRelative(p.lastAccessed)}</span>
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
className="icon-btn"
|
|
115
|
+
onClick={() => toggleProject(p.id)}
|
|
116
|
+
aria-label={isOpen ? 'Collapse' : 'Expand'}
|
|
117
|
+
>
|
|
118
|
+
{isOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{isOpen && (
|
|
123
|
+
<div className="history-timeline-mini">
|
|
124
|
+
{events.length === 0 ? (
|
|
125
|
+
<div className="muted" style={{ padding: '12px 8px', fontSize: 12 }}>
|
|
126
|
+
No events in this time range.
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
events.slice(-100).reverse().map((ev, i) => (
|
|
130
|
+
<div key={i} className="history-timeline-row">
|
|
131
|
+
<span className="history-timeline-ts">
|
|
132
|
+
{new Date(ev.ts).toLocaleTimeString()}
|
|
133
|
+
</span>
|
|
134
|
+
<span className="history-timeline-kind">{ev.kind || 'event'}</span>
|
|
135
|
+
<span className="history-timeline-msg" title={String(ev.text || ev.title || '')}>
|
|
136
|
+
{ev.author ? `@${ev.author} ` : ''}
|
|
137
|
+
{String(ev.text ?? ev.title ?? ev.name ?? '')}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
))
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</Card>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
68
148
|
export function History({ snapshot }: Props) {
|
|
69
149
|
const toast = useToast();
|
|
70
150
|
const [data, setData] = useState<HistoryResponse | null>(null);
|
|
@@ -203,80 +283,15 @@ export function History({ snapshot }: Props) {
|
|
|
203
283
|
</Card>
|
|
204
284
|
)}
|
|
205
285
|
|
|
206
|
-
{data.projects.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
</div>
|
|
216
|
-
<div className="history-project-meta">{p.path}</div>
|
|
217
|
-
</div>
|
|
218
|
-
<div className="history-project-stats">
|
|
219
|
-
<span className="history-project-stat">
|
|
220
|
-
<span className="history-project-stat-num">{p.tasks.done}</span>
|
|
221
|
-
<span className="muted">/ {p.tasks.total} done</span>
|
|
222
|
-
</span>
|
|
223
|
-
{p.tasks.doing > 0 && (
|
|
224
|
-
<span className="history-project-stat">
|
|
225
|
-
<span className="history-project-stat-num">{p.tasks.doing}</span>
|
|
226
|
-
<span className="muted">doing</span>
|
|
227
|
-
</span>
|
|
228
|
-
)}
|
|
229
|
-
{p.tasks.blocked > 0 && (
|
|
230
|
-
<span className="history-project-stat">
|
|
231
|
-
<span className="history-project-stat-num">{p.tasks.blocked}</span>
|
|
232
|
-
<span className="muted">blocked</span>
|
|
233
|
-
</span>
|
|
234
|
-
)}
|
|
235
|
-
<span className="history-project-stat">
|
|
236
|
-
<FileText size={11} /> <span className="history-project-stat-num">{p.plans}</span>
|
|
237
|
-
<span className="muted"> plan{p.plans === 1 ? '' : 's'}</span>
|
|
238
|
-
</span>
|
|
239
|
-
{p.lastAccessed && (
|
|
240
|
-
<span className="history-project-stat">
|
|
241
|
-
<span className="muted">last opened {formatRelative(p.lastAccessed)}</span>
|
|
242
|
-
</span>
|
|
243
|
-
)}
|
|
244
|
-
</div>
|
|
245
|
-
<button
|
|
246
|
-
type="button"
|
|
247
|
-
className="icon-btn"
|
|
248
|
-
onClick={() => toggleProject(p.id)}
|
|
249
|
-
aria-label={isOpen ? 'Collapse' : 'Expand'}
|
|
250
|
-
>
|
|
251
|
-
{isOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
252
|
-
</button>
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
{isOpen && (
|
|
256
|
-
<div className="history-timeline-mini">
|
|
257
|
-
{events.length === 0 ? (
|
|
258
|
-
<div className="muted" style={{ padding: '12px 8px', fontSize: 12 }}>
|
|
259
|
-
No events in this time range.
|
|
260
|
-
</div>
|
|
261
|
-
) : (
|
|
262
|
-
events.slice(-100).reverse().map((ev, i) => (
|
|
263
|
-
<div key={i} className="history-timeline-row">
|
|
264
|
-
<span className="history-timeline-ts">
|
|
265
|
-
{new Date(ev.ts).toLocaleTimeString()}
|
|
266
|
-
</span>
|
|
267
|
-
<span className="history-timeline-kind">{ev.kind || 'event'}</span>
|
|
268
|
-
<span className="history-timeline-msg" title={String(ev.text || ev.title || '')}>
|
|
269
|
-
{ev.author ? `@${ev.author} ` : ''}
|
|
270
|
-
{String(ev.text ?? ev.title ?? ev.name ?? '')}
|
|
271
|
-
</span>
|
|
272
|
-
</div>
|
|
273
|
-
))
|
|
274
|
-
)}
|
|
275
|
-
</div>
|
|
276
|
-
)}
|
|
277
|
-
</Card>
|
|
278
|
-
);
|
|
279
|
-
})}
|
|
286
|
+
{data.projects.length > 0 && (
|
|
287
|
+
<VirtualList
|
|
288
|
+
items={data.projects}
|
|
289
|
+
itemHeight={70}
|
|
290
|
+
height={Math.min(data.projects.length * 70, 500)}
|
|
291
|
+
className="history-virtual-list"
|
|
292
|
+
renderItem={(p) => renderProjectCard(p, eventsByProject.get(p.id) || [], expanded, toggleProject)}
|
|
293
|
+
/>
|
|
294
|
+
)}
|
|
280
295
|
|
|
281
296
|
{/* Global / unassigned events */}
|
|
282
297
|
{eventsByProject.has('global') && (
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// semantic search) plus a config panel and a high-level overview. Each is a
|
|
8
8
|
// standalone component under views/memory/.
|
|
9
9
|
|
|
10
|
-
import { useCallback, useState } from 'react';
|
|
10
|
+
import React, { useCallback, useState } from 'react';
|
|
11
11
|
import {
|
|
12
12
|
Brain,
|
|
13
13
|
FileText,
|
|
@@ -50,7 +50,7 @@ const SOURCES: Array<{
|
|
|
50
50
|
{ id: 'config', label: 'Config', icon: Sliders },
|
|
51
51
|
];
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
function MemoryInner(_props: Props) {
|
|
54
54
|
const toast = useToast();
|
|
55
55
|
const [active, setActive] = useState<SubPanel>('overview');
|
|
56
56
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
@@ -137,4 +137,5 @@ export function Memory(_props: Props) {
|
|
|
137
137
|
</div>
|
|
138
138
|
</div>
|
|
139
139
|
);
|
|
140
|
-
}
|
|
140
|
+
}
|
|
141
|
+
export const Memory = React.memo(MemoryInner);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// (written by minimax.mjs after every chatCompletion / fetchRemains call).
|
|
9
9
|
// The Usage Analytics tab is only shown when the key is configured.
|
|
10
10
|
|
|
11
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
11
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
12
12
|
import {
|
|
13
13
|
Coins,
|
|
14
14
|
RefreshCw,
|
|
@@ -196,7 +196,7 @@ type TimeRange = '24h' | '7d' | '30d' | 'custom';
|
|
|
196
196
|
|
|
197
197
|
// ─── Main component ────────────────────────────────────────────────────
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
function MiniMaxUsageInner({ activeTab, setActiveTab }: Props) {
|
|
200
200
|
// Share state between tabs.
|
|
201
201
|
const [view, setView] = useState<'quota' | 'analytics'>('quota');
|
|
202
202
|
const toast = useToast();
|
|
@@ -921,3 +921,4 @@ function OnboardingWizard({ step, setStep, keyDraft, setKeyDraft, showKey, setSh
|
|
|
921
921
|
</div>
|
|
922
922
|
);
|
|
923
923
|
}
|
|
924
|
+
export const MiniMaxUsage = React.memo(MiniMaxUsageInner);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/views/Overview.tsx — v3.7.0 activity cards + SSE stream.
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
Bot,
|
|
5
5
|
CheckSquare,
|
|
@@ -33,6 +33,7 @@ import { Button } from '../components/Button';
|
|
|
33
33
|
import { MemoryStatusCard } from './memory/MemoryStatusCard';
|
|
34
34
|
import { EmptyState } from '../components/EmptyState';
|
|
35
35
|
import { Spinner } from '../components/Spinner';
|
|
36
|
+
import { VirtualList } from '../components/VirtualList';
|
|
36
37
|
import { useToast } from '../components/Toast';
|
|
37
38
|
import { useModal } from '../components/Modal';
|
|
38
39
|
import { FileBrowser } from '../components/FileBrowser';
|
|
@@ -49,7 +50,7 @@ type Props = {
|
|
|
49
50
|
refreshSnapshot: () => Promise<void>;
|
|
50
51
|
};
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
function OverviewInner({
|
|
53
54
|
snapshot,
|
|
54
55
|
settings,
|
|
55
56
|
setActiveTab,
|
|
@@ -367,21 +368,23 @@ export function Overview({
|
|
|
367
368
|
</div>
|
|
368
369
|
) : (
|
|
369
370
|
<div className={cn('activity-feed-list-wrap', !activityExpanded && 'activity-feed-list-wrap-collapsed')}>
|
|
370
|
-
<
|
|
371
|
-
{activityItems.slice(0, 30).
|
|
372
|
-
|
|
373
|
-
|
|
371
|
+
<VirtualList
|
|
372
|
+
items={activityItems.slice(0, 30).filter((it, idx) => !hiddenKeys.has(itemKey(it, idx)))}
|
|
373
|
+
itemHeight={60}
|
|
374
|
+
height={Math.min(activityItems.filter((it, idx) => !hiddenKeys.has(itemKey(it, idx))).length * 60, 480)}
|
|
375
|
+
className="activity-feed-list"
|
|
376
|
+
renderItem={(it, vlIdx) => {
|
|
377
|
+
const originalIdx = activityItems.slice(0, 30).findIndex((a) => a === it);
|
|
374
378
|
return (
|
|
375
379
|
<ActivityFeedItem
|
|
376
|
-
key={`${it.ts}-${idx}`}
|
|
377
380
|
item={it}
|
|
378
|
-
activityKey={
|
|
381
|
+
activityKey={itemKey(it, originalIdx)}
|
|
379
382
|
onNavigate={setActiveTab}
|
|
380
383
|
onHide={onHide}
|
|
381
384
|
/>
|
|
382
385
|
);
|
|
383
|
-
}
|
|
384
|
-
|
|
386
|
+
}}
|
|
387
|
+
/>
|
|
385
388
|
{!activityExpanded && activityItems.length > 8 && <div className="activity-feed-fade" aria-hidden="true" />}
|
|
386
389
|
</div>
|
|
387
390
|
)}
|
|
@@ -813,3 +816,5 @@ function ActivityFeedItem({
|
|
|
813
816
|
</div>
|
|
814
817
|
);
|
|
815
818
|
}
|
|
819
|
+
const OverviewMemo = React.memo(OverviewInner);
|
|
820
|
+
export { OverviewMemo as Overview };
|
|
@@ -485,7 +485,7 @@ function UpdatesCard() {
|
|
|
485
485
|
);
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
-
|
|
488
|
+
function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
|
|
489
489
|
const toast = useToast();
|
|
490
490
|
const [settings, setSettings] = useState<Settings>(initial);
|
|
491
491
|
const [dirty, setDirty] = useState(false);
|
|
@@ -888,6 +888,7 @@ export function SettingsView({ settings: initial, refreshSnapshot }: Props) {
|
|
|
888
888
|
className="input"
|
|
889
889
|
value={settings.theme.accent}
|
|
890
890
|
onChange={(e) => patchTheme({ accent: e.target.value })}
|
|
891
|
+
aria-label="Accent color hex"
|
|
891
892
|
/>
|
|
892
893
|
</div>
|
|
893
894
|
</div>
|
|
@@ -906,6 +907,7 @@ export function SettingsView({ settings: initial, refreshSnapshot }: Props) {
|
|
|
906
907
|
className="input"
|
|
907
908
|
value={settings.theme.success}
|
|
908
909
|
onChange={(e) => patchTheme({ success: e.target.value })}
|
|
910
|
+
aria-label="Success color hex"
|
|
909
911
|
/>
|
|
910
912
|
</div>
|
|
911
913
|
</div>
|
|
@@ -924,6 +926,7 @@ export function SettingsView({ settings: initial, refreshSnapshot }: Props) {
|
|
|
924
926
|
className="input"
|
|
925
927
|
value={settings.theme.warning}
|
|
926
928
|
onChange={(e) => patchTheme({ warning: e.target.value })}
|
|
929
|
+
aria-label="Warning color hex"
|
|
927
930
|
/>
|
|
928
931
|
</div>
|
|
929
932
|
</div>
|
|
@@ -942,6 +945,7 @@ export function SettingsView({ settings: initial, refreshSnapshot }: Props) {
|
|
|
942
945
|
className="input"
|
|
943
946
|
value={settings.theme.error}
|
|
944
947
|
onChange={(e) => patchTheme({ error: e.target.value })}
|
|
948
|
+
aria-label="Error color hex"
|
|
945
949
|
/>
|
|
946
950
|
</div>
|
|
947
951
|
</div>
|
|
@@ -960,6 +964,7 @@ export function SettingsView({ settings: initial, refreshSnapshot }: Props) {
|
|
|
960
964
|
className="input"
|
|
961
965
|
value={settings.theme.info}
|
|
962
966
|
onChange={(e) => patchTheme({ info: e.target.value })}
|
|
967
|
+
aria-label="Info color hex"
|
|
963
968
|
/>
|
|
964
969
|
</div>
|
|
965
970
|
</div>
|
|
@@ -1144,8 +1149,9 @@ export function SettingsView({ settings: initial, refreshSnapshot }: Props) {
|
|
|
1144
1149
|
/>
|
|
1145
1150
|
</div>
|
|
1146
1151
|
<div className="task-form-field">
|
|
1147
|
-
<label className="field-label">Use HTTPS</label>
|
|
1152
|
+
<label className="field-label" htmlFor="tailscale-https">Use HTTPS</label>
|
|
1148
1153
|
<input
|
|
1154
|
+
id="tailscale-https"
|
|
1149
1155
|
type="checkbox"
|
|
1150
1156
|
checked={tailscaleDraft.https}
|
|
1151
1157
|
onChange={(e) => setTailscaleDraft((cur) => ({ ...cur, https: e.target.checked }))}
|
|
@@ -1821,3 +1827,4 @@ function ActivityLogCard() {
|
|
|
1821
1827
|
</Card>
|
|
1822
1828
|
);
|
|
1823
1829
|
}
|
|
1830
|
+
export const SettingsView = React.memo(SettingsViewInner);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/views/Skills.tsx — v4.0.0 skills browser: scans all local SKILL.md sources.
|
|
2
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
Sparkles,
|
|
5
5
|
Search,
|
|
@@ -58,7 +58,7 @@ const SOURCE_ICON: Record<string, typeof Package> = {
|
|
|
58
58
|
project: Folder,
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
function SkillsInner({ snapshot, refreshSnapshot }: Props) {
|
|
62
62
|
const toast = useToast();
|
|
63
63
|
const modal = useModal();
|
|
64
64
|
const [skills, setSkills] = useState<Skill[]>([]);
|
|
@@ -377,3 +377,4 @@ function highlightMatch(text: string, q: string): React.ReactNode {
|
|
|
377
377
|
</>
|
|
378
378
|
);
|
|
379
379
|
}
|
|
380
|
+
export const Skills = React.memo(SkillsInner);
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// We keep the API contract (statuses: queued/doing/done/blocked/backlog)
|
|
21
21
|
// so the dashboard doesn't break the existing server, tasks store, and
|
|
22
22
|
// tests. The UI maps API statuses to friendlier labels.
|
|
23
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
23
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
24
24
|
import {
|
|
25
25
|
CheckSquare,
|
|
26
26
|
Plus,
|
|
@@ -76,7 +76,7 @@ const COLUMNS: { id: Task['status']; label: string; kind: StatusKind }[] = [
|
|
|
76
76
|
const PRIORITIES = ['low', 'normal', 'high'] as const;
|
|
77
77
|
type Priority = (typeof PRIORITIES)[number];
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
function TasksInner({ snapshot, refreshSnapshot, setActiveTab }: Props) {
|
|
80
80
|
const toast = useToast();
|
|
81
81
|
const modal = useModal();
|
|
82
82
|
const [tasks, setTasks] = useState<Task[]>(snapshot.tasks || []);
|
|
@@ -771,4 +771,5 @@ function openEditTaskModal(
|
|
|
771
771
|
</div>
|
|
772
772
|
),
|
|
773
773
|
});
|
|
774
|
-
}
|
|
774
|
+
}
|
|
775
|
+
export const Tasks = React.memo(TasksInner);
|
|
@@ -98,14 +98,14 @@ export function MemoryOverview({ refreshKey, onRefresh, setActiveSubPanel }: Pro
|
|
|
98
98
|
const [data, setData] = useState<MemoryOverviewData | null>(null);
|
|
99
99
|
const [loading, setLoading] = useState(true);
|
|
100
100
|
|
|
101
|
-
const reload = async () => {
|
|
101
|
+
const reload = async (signal?: AbortSignal) => {
|
|
102
102
|
setLoading(true);
|
|
103
103
|
try {
|
|
104
104
|
const [health, status, lightrag, storage] = await Promise.all([
|
|
105
|
-
api.get<HealthResponse>('/memory/health').catch(() => null),
|
|
106
|
-
api.get<MemoryStatus>('/memory/status').catch(() => null),
|
|
107
|
-
api.get<LightragStats>('/memory/lightrag/stats').catch(() => null),
|
|
108
|
-
api.get<StorageStats>('/memory/storage').catch(() => null),
|
|
105
|
+
api.get<HealthResponse>('/memory/health', signal).catch(() => null),
|
|
106
|
+
api.get<MemoryStatus>('/memory/status', signal).catch(() => null),
|
|
107
|
+
api.get<LightragStats>('/memory/lightrag/stats', signal).catch(() => null),
|
|
108
|
+
api.get<StorageStats>('/memory/storage', signal).catch(() => null),
|
|
109
109
|
]);
|
|
110
110
|
setData({
|
|
111
111
|
health: health || {
|
|
@@ -145,7 +145,9 @@ export function MemoryOverview({ refreshKey, onRefresh, setActiveSubPanel }: Pro
|
|
|
145
145
|
};
|
|
146
146
|
|
|
147
147
|
useEffect(() => {
|
|
148
|
-
|
|
148
|
+
const ctrl = new AbortController();
|
|
149
|
+
void reload(ctrl.signal);
|
|
150
|
+
return () => ctrl.abort();
|
|
149
151
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
150
152
|
}, [refreshKey]);
|
|
151
153
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/cli-bugfixes.test.mjs — regression tests for CLI + installer bug fixes.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* B1 — parseWithModsFlag exits with code 2 when no value follows --with-mods
|
|
6
|
+
* B7 — check-deps.mjs uses path.join() (Windows fix) + covers all deps
|
|
7
|
+
* B9 — --json flag works for `bizar doctor --json`
|
|
8
|
+
* B10 — --debug flag sets DEBUG env var
|
|
9
|
+
*
|
|
10
|
+
* Run with: node --test bizar-dash/tests/cli-bugfixes.test.mjs
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
const REPO = join(process.cwd());
|
|
17
|
+
|
|
18
|
+
// ─── B1: parseWithModsFlag exits with code 2 on missing value ──────────────────
|
|
19
|
+
|
|
20
|
+
describe('B1 — parseWithModsFlag', () => {
|
|
21
|
+
it('exits with code 2 when --with-mods is the last arg', async () => {
|
|
22
|
+
// Import fresh each time to avoid module-level side effects
|
|
23
|
+
const { parseWithModsFlag } = await import('../../cli/commands/install.mjs');
|
|
24
|
+
let exitCode = null;
|
|
25
|
+
const origExit = process.exit;
|
|
26
|
+
// Mock process.exit to capture the code without terminating the test
|
|
27
|
+
try {
|
|
28
|
+
process.exit = (code) => { exitCode = code; throw new Error('exit:' + code); };
|
|
29
|
+
parseWithModsFlag(['install', '--with-mods']);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err.message.startsWith('exit:')) exitCode = parseInt(err.message.split(':')[1]);
|
|
32
|
+
} finally {
|
|
33
|
+
process.exit = origExit;
|
|
34
|
+
}
|
|
35
|
+
assert.strictEqual(exitCode, 2, 'should have called process.exit(2)');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('exits with code 2 when next arg is another flag', async () => {
|
|
39
|
+
const { parseWithModsFlag } = await import('../../cli/commands/install.mjs');
|
|
40
|
+
let exitCode = null;
|
|
41
|
+
const origExit = process.exit;
|
|
42
|
+
try {
|
|
43
|
+
process.exit = (code) => { exitCode = code; throw new Error('exit:' + code); };
|
|
44
|
+
parseWithModsFlag(['install', '--with-mods', '--force']);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.message.startsWith('exit:')) exitCode = parseInt(err.message.split(':')[1]);
|
|
47
|
+
} finally {
|
|
48
|
+
process.exit = origExit;
|
|
49
|
+
}
|
|
50
|
+
assert.strictEqual(exitCode, 2);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns null when --with-mods is not present', async () => {
|
|
54
|
+
const { parseWithModsFlag } = await import('../../cli/commands/install.mjs');
|
|
55
|
+
let exitCode = null;
|
|
56
|
+
const origExit = process.exit;
|
|
57
|
+
try {
|
|
58
|
+
process.exit = (code) => { exitCode = code; throw new Error('exit:' + code); };
|
|
59
|
+
const result = parseWithModsFlag(['install', '--force']);
|
|
60
|
+
assert.strictEqual(result, null);
|
|
61
|
+
assert.strictEqual(exitCode, null);
|
|
62
|
+
} finally {
|
|
63
|
+
process.exit = origExit;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns array of mod ids when value is provided', async () => {
|
|
68
|
+
const { parseWithModsFlag } = await import('../../cli/commands/install.mjs');
|
|
69
|
+
let exitCode = null;
|
|
70
|
+
const origExit = process.exit;
|
|
71
|
+
try {
|
|
72
|
+
process.exit = (code) => { exitCode = code; throw new Error('exit:' + code); };
|
|
73
|
+
const result = parseWithModsFlag(['install', '--with-mods', 'a,b,c']);
|
|
74
|
+
assert.deepStrictEqual(result, ['a', 'b', 'c']);
|
|
75
|
+
assert.strictEqual(exitCode, null);
|
|
76
|
+
} finally {
|
|
77
|
+
process.exit = origExit;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ─── B7: check-deps.mjs cross-platform path joining ───────────────────────────
|
|
83
|
+
|
|
84
|
+
describe('B7 — check-deps.mjs which() uses path.join()', () => {
|
|
85
|
+
it('covers all required dependencies', async () => {
|
|
86
|
+
const { checkDeps } = await import('../../scripts/check-deps.mjs');
|
|
87
|
+
const result = await checkDeps({ strict: false });
|
|
88
|
+
const names = [...result.present, ...result.missing].map(d => d.name);
|
|
89
|
+
const required = ['node', 'bun', 'opencode', 'tmux', 'git', 'python3', 'pip', 'jq', 'gh', 'headroom', 'semble', 'skills'];
|
|
90
|
+
for (const dep of required) {
|
|
91
|
+
assert.ok(names.includes(dep), `expected ${dep} to be checked`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('which() uses join() from node:path for path construction', async () => {
|
|
96
|
+
const { readFileSync } = await import('node:fs');
|
|
97
|
+
const src = readFileSync(join(process.cwd(), 'scripts', 'check-deps.mjs'), 'utf8');
|
|
98
|
+
assert.ok(src.includes("import { join } from 'node:path'"), 'should import join from node:path');
|
|
99
|
+
assert.ok(src.includes('join(dir, cmd + ext)'), 'should use join() for path construction');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ─── B9: --json flag for doctor ───────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
describe('B9 — --json flag', () => {
|
|
106
|
+
it('doctor --json outputs valid JSON', async () => {
|
|
107
|
+
const { spawnSync } = await import('node:child_process');
|
|
108
|
+
const bin = join(REPO, 'cli', 'bin.mjs');
|
|
109
|
+
const result = spawnSync('node', [bin, 'doctor', '--json'], {
|
|
110
|
+
encoding: 'utf8',
|
|
111
|
+
timeout: 30000,
|
|
112
|
+
});
|
|
113
|
+
const out = result.stdout.trim();
|
|
114
|
+
// Should be parseable as JSON
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(out);
|
|
118
|
+
} catch {
|
|
119
|
+
assert.fail(`expected JSON output, got: ${out.slice(0, 200)}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
assert.ok('passed' in parsed, 'JSON should have passed field');
|
|
123
|
+
assert.ok('failed' in parsed, 'JSON should have failed field');
|
|
124
|
+
assert.ok('results' in parsed, 'JSON should have results field');
|
|
125
|
+
assert.ok(Array.isArray(parsed.results), 'results should be an array');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ─── B10: --debug flag sets DEBUG env var ──────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
describe('B10 — --debug flag', () => {
|
|
132
|
+
it('dbg() helper is defined in bin.mjs', async () => {
|
|
133
|
+
const { readFileSync } = await import('node:fs');
|
|
134
|
+
const src = readFileSync(join(REPO, 'cli', 'bin.mjs'), 'utf8');
|
|
135
|
+
assert.ok(src.includes('function dbg('), 'dbg() helper should be defined');
|
|
136
|
+
assert.ok(src.includes("process.env.DEBUG = 'bizar:*'"), '--debug should set DEBUG=bizar:*');
|
|
137
|
+
assert.ok(src.includes("process.env.BIZAR_DEBUG = '1'"), '--debug should set BIZAR_DEBUG=1');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('--debug flag is accepted without error', async () => {
|
|
141
|
+
const { spawnSync } = await import('node:child_process');
|
|
142
|
+
const bin = join(REPO, 'cli', 'bin.mjs');
|
|
143
|
+
// Just verify --debug doesn't cause a parse error or unknown flag
|
|
144
|
+
const result = spawnSync('node', [bin, '--help', '--debug'], {
|
|
145
|
+
encoding: 'utf8',
|
|
146
|
+
timeout: 10000,
|
|
147
|
+
});
|
|
148
|
+
// --debug should not cause an error (help should print)
|
|
149
|
+
assert.ok(result.status === 0 || result.stdout.includes('bizar'), '--debug should not error');
|
|
150
|
+
});
|
|
151
|
+
});
|