@polderlabs/bizar 4.5.2 → 4.7.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/main-DHZmbnxQ.js +361 -0
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +1 -0
- package/bizar-dash/dist/assets/main-DX_Jh8Wc.css +1 -0
- package/bizar-dash/dist/assets/{mobile-lbH6szyX.js → mobile-BK8-ythT.js} +18 -18
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js → mobile-Chvf9u_B.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js.map → mobile-Chvf9u_B.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/skills/publishing/SKILL.md +146 -0
- package/bizar-dash/src/server/api.mjs +8 -0
- package/bizar-dash/src/server/backup-store.mjs +525 -0
- package/bizar-dash/src/server/digest-store.mjs +558 -0
- package/bizar-dash/src/server/lib/rate-limit.mjs +122 -0
- package/bizar-dash/src/server/logger.mjs +71 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +45 -6
- package/bizar-dash/src/server/metrics.mjs +193 -0
- package/bizar-dash/src/server/routes/backup.mjs +112 -0
- package/bizar-dash/src/server/routes/chat.mjs +20 -3
- package/bizar-dash/src/server/routes/digests.mjs +82 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +3 -2
- package/bizar-dash/src/server/routes/memory.mjs +5 -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/routes-v2/events.mjs +14 -0
- package/bizar-dash/src/server/schedules-runner.mjs +126 -0
- package/bizar-dash/src/server/server.mjs +79 -0
- package/bizar-dash/src/web/App.tsx +8 -1
- package/bizar-dash/src/web/components/BackupRestore.tsx +330 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +6 -3
- 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/components/chat/Composer.tsx +2 -0
- package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
- 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/main.css +70 -8
- package/bizar-dash/src/web/views/Activity.tsx +35 -18
- package/bizar-dash/src/web/views/Agents.tsx +57 -42
- package/bizar-dash/src/web/views/Artifacts.tsx +38 -25
- package/bizar-dash/src/web/views/Chat.tsx +8 -0
- package/bizar-dash/src/web/views/History.tsx +94 -76
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +21 -4
- package/bizar-dash/src/web/views/Mods.tsx +30 -17
- package/bizar-dash/src/web/views/Overview.tsx +19 -9
- package/bizar-dash/src/web/views/Providers.tsx +16 -16
- package/bizar-dash/src/web/views/Schedules.tsx +33 -15
- package/bizar-dash/src/web/views/Settings.tsx +97 -1745
- package/bizar-dash/src/web/views/Skills.tsx +4 -1
- package/bizar-dash/src/web/views/Tasks.tsx +11 -2
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +8 -2
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +3 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +12 -4
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +3 -0
- package/bizar-dash/src/web/views/settings/ActivitySection.tsx +205 -0
- package/bizar-dash/src/web/views/settings/AgentSection.tsx +294 -0
- package/bizar-dash/src/web/views/settings/AuthSection.tsx +159 -0
- package/bizar-dash/src/web/views/settings/BackupSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/GeneralSection.tsx +105 -0
- package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +39 -0
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/NetworkSection.tsx +87 -0
- package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +34 -0
- package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/SkillsSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +81 -0
- package/bizar-dash/src/web/views/settings/ThemeSection.tsx +168 -0
- package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +256 -0
- package/bizar-dash/tests/a11y.test.tsx +206 -0
- package/bizar-dash/tests/backup-restore.test.mjs +217 -0
- package/bizar-dash/tests/backup-restore.test.tsx +123 -0
- package/bizar-dash/tests/backup-store.test.mjs +300 -0
- package/bizar-dash/tests/cli-bugfixes.test.mjs +4 -4
- package/bizar-dash/tests/cli-error-visibility.test.mjs +153 -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/digest-generation.test.mjs +191 -0
- package/bizar-dash/tests/digest-store.test.mjs +264 -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/rate-limit.test.mjs +298 -0
- package/bizar-dash/tests/server-bugfixes.test.mjs +2 -2
- 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 -2096
- package/cli/bg.mjs +5 -13
- package/cli/bin.mjs +221 -1350
- 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 +285 -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 +459 -0
- package/cli/digest.mjs +149 -0
- package/cli/doctor.mjs +1 -13
- package/cli/provision.mjs +2 -13
- package/cli/service-controller.mjs +1 -11
- package/cli/service.mjs +1 -11
- package/cli/utils.mjs +41 -1
- package/package.json +6 -1
- package/bizar-dash/dist/assets/main-B4OfGAwz.js +0 -361
- package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +0 -1
- package/bizar-dash/dist/assets/main-DAlLdW8I.css +0 -1
- package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +0 -1
|
@@ -200,13 +200,16 @@ export function Artifacts({ snapshot, refreshSnapshot }: Props) {
|
|
|
200
200
|
</div>
|
|
201
201
|
<div className="view-actions">
|
|
202
202
|
<div className="search-input">
|
|
203
|
-
<Search size={14} />
|
|
203
|
+
<Search size={14} aria-hidden />
|
|
204
|
+
<label htmlFor="artifacts-search" className="sr-only">Search artifacts</label>
|
|
204
205
|
<input
|
|
206
|
+
id="artifacts-search"
|
|
205
207
|
className="input"
|
|
206
208
|
type="text"
|
|
207
209
|
placeholder="Search…"
|
|
208
210
|
value={filter}
|
|
209
211
|
onChange={(e) => setFilter(e.target.value)}
|
|
212
|
+
aria-label="Search artifacts"
|
|
210
213
|
/>
|
|
211
214
|
</div>
|
|
212
215
|
<Button
|
|
@@ -275,7 +278,9 @@ function NewPlanCard({ onCreate }: { onCreate: (slug: string, title?: string) =>
|
|
|
275
278
|
setTitle('');
|
|
276
279
|
}}
|
|
277
280
|
>
|
|
281
|
+
<label htmlFor="new-artifact-slug" className="sr-only">Artifact slug</label>
|
|
278
282
|
<input
|
|
283
|
+
id="new-artifact-slug"
|
|
279
284
|
className="input"
|
|
280
285
|
type="text"
|
|
281
286
|
placeholder="slug (e.g. dashboard-v3.1)"
|
|
@@ -283,8 +288,11 @@ function NewPlanCard({ onCreate }: { onCreate: (slug: string, title?: string) =>
|
|
|
283
288
|
required
|
|
284
289
|
value={slug}
|
|
285
290
|
onChange={(e) => setSlug(e.target.value)}
|
|
291
|
+
aria-label="Artifact slug"
|
|
286
292
|
/>
|
|
293
|
+
<label htmlFor="new-artifact-title" className="sr-only">Artifact title</label>
|
|
287
294
|
<input
|
|
295
|
+
id="new-artifact-title"
|
|
288
296
|
className="input"
|
|
289
297
|
type="text"
|
|
290
298
|
placeholder="Title (optional)"
|
|
@@ -545,14 +553,14 @@ function PlanEditor({
|
|
|
545
553
|
width: 560,
|
|
546
554
|
children: (
|
|
547
555
|
<div className="artifact-config-form">
|
|
548
|
-
<label className="field-label">Title</label>
|
|
549
|
-
<input ref={(el) => { titleEl = el; }} className="input" type="text" defaultValue={meta.title} />
|
|
550
|
-
<label className="field-label">Description (markdown)</label>
|
|
551
|
-
<textarea ref={(el) => { descEl = el; }} className="textarea" rows={3} defaultValue={meta.description || ''} />
|
|
552
|
-
<label className="field-label">Tags (comma-separated)</label>
|
|
553
|
-
<input ref={(el) => { tagsEl = el; }} className="input" type="text" defaultValue={(meta.tags || []).join(', ')} />
|
|
554
|
-
<label className="field-label">Status</label>
|
|
555
|
-
<select ref={(el) => { statusEl = el; }} className="select" defaultValue={meta.status}>
|
|
556
|
+
<label className="field-label" htmlFor="artifact-cfg-title">Title</label>
|
|
557
|
+
<input id="artifact-cfg-title" ref={(el) => { titleEl = el; }} className="input" type="text" defaultValue={meta.title} />
|
|
558
|
+
<label className="field-label" htmlFor="artifact-cfg-desc">Description (markdown)</label>
|
|
559
|
+
<textarea id="artifact-cfg-desc" ref={(el) => { descEl = el; }} className="textarea" rows={3} defaultValue={meta.description || ''} />
|
|
560
|
+
<label className="field-label" htmlFor="artifact-cfg-tags">Tags (comma-separated)</label>
|
|
561
|
+
<input id="artifact-cfg-tags" ref={(el) => { tagsEl = el; }} className="input" type="text" defaultValue={(meta.tags || []).join(', ')} />
|
|
562
|
+
<label className="field-label" htmlFor="artifact-cfg-status">Status</label>
|
|
563
|
+
<select id="artifact-cfg-status" ref={(el) => { statusEl = el; }} className="select" defaultValue={meta.status}>
|
|
556
564
|
{PLAN_STATUSES.map((s) => (
|
|
557
565
|
<option key={s} value={s}>{s}</option>
|
|
558
566
|
))}
|
|
@@ -597,16 +605,16 @@ function PlanEditor({
|
|
|
597
605
|
width: 520,
|
|
598
606
|
children: (
|
|
599
607
|
<div className="artifact-element-form">
|
|
600
|
-
<label className="field-label">Type</label>
|
|
601
|
-
<select ref={(el) => { typeEl = el; }} className="select" defaultValue="task">
|
|
608
|
+
<label className="field-label" htmlFor="artifact-elem-type">Type</label>
|
|
609
|
+
<select id="artifact-elem-type" ref={(el) => { typeEl = el; }} className="select" defaultValue="task">
|
|
602
610
|
{ELEMENT_TYPES.map((t) => (
|
|
603
611
|
<option key={t.id} value={t.id}>{t.label}</option>
|
|
604
612
|
))}
|
|
605
613
|
</select>
|
|
606
|
-
<label className="field-label">Title</label>
|
|
607
|
-
<input ref={(el) => { titleEl = el; }} className="input" type="text" placeholder="Element title" autoFocus />
|
|
608
|
-
<label className="field-label">Content (markdown)</label>
|
|
609
|
-
<textarea ref={(el) => { contentEl = el; }} className="textarea" rows={4} placeholder="Description…" />
|
|
614
|
+
<label className="field-label" htmlFor="artifact-elem-title">Title</label>
|
|
615
|
+
<input id="artifact-elem-title" ref={(el) => { titleEl = el; }} className="input" type="text" placeholder="Element title" autoFocus />
|
|
616
|
+
<label className="field-label" htmlFor="artifact-elem-content">Content (markdown)</label>
|
|
617
|
+
<textarea id="artifact-elem-content" ref={(el) => { contentEl = el; }} className="textarea" rows={4} placeholder="Description…" />
|
|
610
618
|
</div>
|
|
611
619
|
),
|
|
612
620
|
footer: (
|
|
@@ -653,8 +661,8 @@ function PlanEditor({
|
|
|
653
661
|
width: 480,
|
|
654
662
|
children: (
|
|
655
663
|
<div>
|
|
656
|
-
<label className="field-label">Comment</label>
|
|
657
|
-
<textarea ref={(el) => { textEl = el; }} className="textarea" rows={4} placeholder="Comment for the whole artifact…" autoFocus />
|
|
664
|
+
<label className="field-label" htmlFor="canvas-comment">Comment</label>
|
|
665
|
+
<textarea id="canvas-comment" ref={(el) => { textEl = el; }} className="textarea" rows={4} placeholder="Comment for the whole artifact…" autoFocus />
|
|
658
666
|
</div>
|
|
659
667
|
),
|
|
660
668
|
footer: (
|
|
@@ -908,18 +916,18 @@ function editElementInline(
|
|
|
908
916
|
width: 520,
|
|
909
917
|
children: (
|
|
910
918
|
<div className="artifact-element-form">
|
|
911
|
-
<label className="field-label">Type</label>
|
|
912
|
-
<select ref={(el2) => { typeEl = el2; }} className="select" defaultValue={el.type}>
|
|
919
|
+
<label className="field-label" htmlFor="elem-edit-type">Type</label>
|
|
920
|
+
<select id="elem-edit-type" ref={(el2) => { typeEl = el2; }} className="select" defaultValue={el.type}>
|
|
913
921
|
{ELEMENT_TYPES.map((t) => (
|
|
914
922
|
<option key={t.id} value={t.id}>{t.label}</option>
|
|
915
923
|
))}
|
|
916
924
|
</select>
|
|
917
|
-
<label className="field-label">Title</label>
|
|
918
|
-
<input ref={(el2) => { titleEl = el2; }} className="input" type="text" defaultValue={el.title} />
|
|
919
|
-
<label className="field-label">Status</label>
|
|
920
|
-
<input ref={(el2) => { statusEl = el2; }} className="input" type="text" defaultValue={el.status} placeholder="open / done / blocked …" />
|
|
921
|
-
<label className="field-label">Content (markdown)</label>
|
|
922
|
-
<textarea ref={(el2) => { contentEl = el2; }} className="textarea" rows={5} defaultValue={el.content} />
|
|
925
|
+
<label className="field-label" htmlFor="elem-edit-title">Title</label>
|
|
926
|
+
<input id="elem-edit-title" ref={(el2) => { titleEl = el2; }} className="input" type="text" defaultValue={el.title} />
|
|
927
|
+
<label className="field-label" htmlFor="elem-edit-status">Status</label>
|
|
928
|
+
<input id="elem-edit-status" ref={(el2) => { statusEl = el2; }} className="input" type="text" defaultValue={el.status} placeholder="open / done / blocked …" />
|
|
929
|
+
<label className="field-label" htmlFor="elem-edit-content">Content (markdown)</label>
|
|
930
|
+
<textarea id="elem-edit-content" ref={(el2) => { contentEl = el2; }} className="textarea" rows={5} defaultValue={el.content} />
|
|
923
931
|
</div>
|
|
924
932
|
),
|
|
925
933
|
footer: (
|
|
@@ -1464,12 +1472,17 @@ function CommentsPanel({
|
|
|
1464
1472
|
setText('');
|
|
1465
1473
|
}}
|
|
1466
1474
|
>
|
|
1475
|
+
<label htmlFor="artifacts-comment-input" className="sr-only">
|
|
1476
|
+
{selectedElId ? 'Add a comment to this element' : 'Add a canvas comment'}
|
|
1477
|
+
</label>
|
|
1467
1478
|
<textarea
|
|
1479
|
+
id="artifacts-comment-input"
|
|
1468
1480
|
className="textarea"
|
|
1469
1481
|
rows={2}
|
|
1470
1482
|
placeholder={selectedElId ? 'Add a comment to this element…' : 'Add a canvas comment…'}
|
|
1471
1483
|
value={text}
|
|
1472
1484
|
onChange={(e) => setText(e.target.value)}
|
|
1485
|
+
aria-label={selectedElId ? 'Add a comment to this element' : 'Add a canvas comment'}
|
|
1473
1486
|
/>
|
|
1474
1487
|
<Button variant="primary" size="sm" type="submit" disabled={!text.trim()}>
|
|
1475
1488
|
<Send size={12} /> Post
|
|
@@ -141,8 +141,10 @@ export function Chat({
|
|
|
141
141
|
title: 'Rename session',
|
|
142
142
|
children: (
|
|
143
143
|
<input
|
|
144
|
+
id="rename-session-input"
|
|
144
145
|
autoFocus
|
|
145
146
|
defaultValue={currentTitle}
|
|
147
|
+
aria-label="Session title"
|
|
146
148
|
onChange={(e) => {
|
|
147
149
|
next = e.target.value;
|
|
148
150
|
}}
|
|
@@ -406,6 +408,10 @@ export function Chat({
|
|
|
406
408
|
className="chat-thread-scroll"
|
|
407
409
|
ref={chat.listRef}
|
|
408
410
|
onScroll={chat.handleScroll}
|
|
411
|
+
role="log"
|
|
412
|
+
aria-live="polite"
|
|
413
|
+
aria-relevant="additions"
|
|
414
|
+
aria-label="Chat message thread"
|
|
409
415
|
>
|
|
410
416
|
<ChatThread
|
|
411
417
|
messages={
|
|
@@ -463,6 +469,8 @@ export function Chat({
|
|
|
463
469
|
multiple
|
|
464
470
|
style={{ display: 'none' }}
|
|
465
471
|
onChange={onFiles}
|
|
472
|
+
aria-label="Attach files to message"
|
|
473
|
+
tabIndex={-1}
|
|
466
474
|
/>
|
|
467
475
|
</section>
|
|
468
476
|
|
|
@@ -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);
|
|
@@ -154,12 +234,15 @@ export function History({ snapshot }: Props) {
|
|
|
154
234
|
</div>
|
|
155
235
|
<div className="view-actions">
|
|
156
236
|
<div className="tasks-toolbar-group">
|
|
157
|
-
<Filter size={14} style={{ color: 'var(--text-dim)' }} />
|
|
237
|
+
<Filter size={14} style={{ color: 'var(--text-dim)' }} aria-hidden />
|
|
238
|
+
<label htmlFor="history-range" className="sr-only">Time range</label>
|
|
158
239
|
<select
|
|
240
|
+
id="history-range"
|
|
159
241
|
className="select select-sm"
|
|
160
242
|
value={rangeId}
|
|
161
243
|
onChange={(e) => setRangeId(e.target.value)}
|
|
162
244
|
title="Time range"
|
|
245
|
+
aria-label="Time range"
|
|
163
246
|
>
|
|
164
247
|
{TIME_RANGES.map((r) => (
|
|
165
248
|
<option key={r.id} value={r.id}>{r.label}</option>
|
|
@@ -203,80 +286,15 @@ export function History({ snapshot }: Props) {
|
|
|
203
286
|
</Card>
|
|
204
287
|
)}
|
|
205
288
|
|
|
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
|
-
})}
|
|
289
|
+
{data.projects.length > 0 && (
|
|
290
|
+
<VirtualList
|
|
291
|
+
items={data.projects}
|
|
292
|
+
itemHeight={70}
|
|
293
|
+
height={Math.min(data.projects.length * 70, 500)}
|
|
294
|
+
className="history-virtual-list"
|
|
295
|
+
renderItem={(p) => renderProjectCard(p, eventsByProject.get(p.id) || [], expanded, toggleProject)}
|
|
296
|
+
/>
|
|
297
|
+
)}
|
|
280
298
|
|
|
281
299
|
{/* Global / unassigned events */}
|
|
282
300
|
{eventsByProject.has('global') && (
|
|
@@ -585,18 +585,24 @@ function AnalyticsView({ toast }: { toast: ReturnType<typeof useToast> }) {
|
|
|
585
585
|
))}
|
|
586
586
|
{range === 'custom' && (
|
|
587
587
|
<div className="usage-custom-dates">
|
|
588
|
+
<label htmlFor="usage-from-date" className="sr-only">From date</label>
|
|
588
589
|
<input
|
|
590
|
+
id="usage-from-date"
|
|
589
591
|
type="date"
|
|
590
592
|
className="usage-date-input"
|
|
591
593
|
value={customFrom}
|
|
592
594
|
onChange={e => setCustomFrom(e.target.value)}
|
|
595
|
+
aria-label="From date"
|
|
593
596
|
/>
|
|
594
|
-
<span style={{ color: 'var(--text-muted)', fontSize: 12 }}>to</span>
|
|
597
|
+
<span style={{ color: 'var(--text-muted)', fontSize: 12 }} aria-hidden>to</span>
|
|
598
|
+
<label htmlFor="usage-to-date" className="sr-only">To date</label>
|
|
595
599
|
<input
|
|
600
|
+
id="usage-to-date"
|
|
596
601
|
type="date"
|
|
597
602
|
className="usage-date-input"
|
|
598
603
|
value={customTo}
|
|
599
604
|
onChange={e => setCustomTo(e.target.value)}
|
|
605
|
+
aria-label="To date"
|
|
600
606
|
/>
|
|
601
607
|
</div>
|
|
602
608
|
)}
|
|
@@ -879,9 +885,20 @@ function OnboardingWizard({ step, setStep, keyDraft, setKeyDraft, showKey, setSh
|
|
|
879
885
|
<p className="minimax-wizard-prose">Paste your key below. We'll test it against the real API before saving.</p>
|
|
880
886
|
<div className="minimax-key-row">
|
|
881
887
|
<div className="minimax-key-input-wrap">
|
|
882
|
-
<KeyRound size={12} className="minimax-key-icon" />
|
|
883
|
-
<
|
|
884
|
-
|
|
888
|
+
<KeyRound size={12} className="minimax-key-icon" aria-hidden />
|
|
889
|
+
<label htmlFor="minimax-key-input" className="sr-only">Subscription Key</label>
|
|
890
|
+
<input
|
|
891
|
+
id="minimax-key-input"
|
|
892
|
+
type={showKey ? 'text' : 'password'}
|
|
893
|
+
value={keyDraft}
|
|
894
|
+
onChange={e => setKeyDraft(e.target.value)}
|
|
895
|
+
placeholder="sk-cp-..."
|
|
896
|
+
spellCheck={false}
|
|
897
|
+
autoComplete="off"
|
|
898
|
+
className="minimax-key-input"
|
|
899
|
+
autoFocus
|
|
900
|
+
aria-label="Subscription Key"
|
|
901
|
+
/>
|
|
885
902
|
<button type="button" onClick={() => setShowKey(v => !v)} className="minimax-key-toggle" aria-label={showKey ? 'Hide key' : 'Show key'}>
|
|
886
903
|
{showKey ? <EyeOff size={13} /> : <Eye size={13} />}
|
|
887
904
|
</button>
|
|
@@ -204,8 +204,9 @@ export function Mods({ snapshot, refreshSnapshot }: Props) {
|
|
|
204
204
|
a <code>mod.json</code>). The folder will be copied to
|
|
205
205
|
{' '}<code>~/.config/bizar/mods/<id>/</code>.
|
|
206
206
|
</p>
|
|
207
|
-
<label className="field-label">Path</label>
|
|
207
|
+
<label className="field-label" htmlFor="install-mod-path">Path</label>
|
|
208
208
|
<input
|
|
209
|
+
id="install-mod-path"
|
|
209
210
|
ref={(el) => (pathEl = el)}
|
|
210
211
|
className="input"
|
|
211
212
|
type="text"
|
|
@@ -355,20 +356,26 @@ export function Mods({ snapshot, refreshSnapshot }: Props) {
|
|
|
355
356
|
|
|
356
357
|
{/* v3.16.0 — Registry browser */}
|
|
357
358
|
<Card className="mods-registry-card">
|
|
358
|
-
<
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
className="mods-registry-head"
|
|
362
|
+
aria-expanded={registryOpen}
|
|
363
|
+
aria-controls="mods-registry-body"
|
|
364
|
+
onClick={() => {
|
|
365
|
+
if (!registry && !registryLoading) loadRegistry();
|
|
366
|
+
setRegistryOpen((v) => !v);
|
|
367
|
+
}}
|
|
368
|
+
>
|
|
369
|
+
<Globe size={14} aria-hidden />
|
|
363
370
|
<span className="mods-registry-title">Mod registry</span>
|
|
364
371
|
<span className="muted" style={{ fontSize: 11 }}>
|
|
365
372
|
{registry?.mods ? `${registry.mods.length} available` : 'click to browse'}
|
|
366
373
|
</span>
|
|
367
374
|
<span className="mods-registry-spacer" />
|
|
368
|
-
{registryOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
369
|
-
</
|
|
375
|
+
{registryOpen ? <ChevronDown size={14} aria-hidden /> : <ChevronRight size={14} aria-hidden />}
|
|
376
|
+
</button>
|
|
370
377
|
{registryOpen && (
|
|
371
|
-
<div className="mods-registry-body">
|
|
378
|
+
<div id="mods-registry-body" className="mods-registry-body">
|
|
372
379
|
{registryLoading ? (
|
|
373
380
|
<div className="muted" style={{ padding: 12, fontSize: 12 }}>Loading registry…</div>
|
|
374
381
|
) : registry?.error ? (
|
|
@@ -586,20 +593,26 @@ function ModDetails({ mod }: { mod: Mod }) {
|
|
|
586
593
|
|
|
587
594
|
{/* v3.20 — Installed instructions (agents / commands / skills) */}
|
|
588
595
|
<div className="mod-instructions">
|
|
589
|
-
<
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
596
|
+
<button
|
|
597
|
+
type="button"
|
|
598
|
+
className="mod-instructions-head"
|
|
599
|
+
aria-expanded={instructionsOpen}
|
|
600
|
+
aria-controls={`mod-instructions-body-${mod.id}`}
|
|
601
|
+
onClick={() => {
|
|
602
|
+
if (!instructions && !instructionsLoading) loadInstructions();
|
|
603
|
+
setInstructionsOpen((v) => !v);
|
|
604
|
+
}}
|
|
605
|
+
>
|
|
606
|
+
<FileCode size={14} aria-hidden />
|
|
594
607
|
<span>Installed instructions</span>
|
|
595
608
|
<span className="muted" style={{ fontSize: 11 }}>
|
|
596
609
|
{instructions ? `${instructions.total} files` : 'click to view'}
|
|
597
610
|
</span>
|
|
598
611
|
<span className="mod-instructions-spacer" />
|
|
599
|
-
{instructionsOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
600
|
-
</
|
|
612
|
+
{instructionsOpen ? <ChevronDown size={14} aria-hidden /> : <ChevronRight size={14} aria-hidden />}
|
|
613
|
+
</button>
|
|
601
614
|
{instructionsOpen && instructions && (
|
|
602
|
-
<div className="mod-instructions-body">
|
|
615
|
+
<div id={`mod-instructions-body-${mod.id}`} className="mod-instructions-body">
|
|
603
616
|
{instructions.agents.length > 0 && (
|
|
604
617
|
<div className="mod-instructions-section">
|
|
605
618
|
<div className="mod-instructions-section-title">
|
|
@@ -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';
|
|
@@ -287,11 +288,14 @@ function OverviewInner({
|
|
|
287
288
|
}
|
|
288
289
|
}}
|
|
289
290
|
>
|
|
291
|
+
<label htmlFor="overview-hero-input" className="sr-only">Describe what you want Odin to do</label>
|
|
290
292
|
<textarea
|
|
293
|
+
id="overview-hero-input"
|
|
291
294
|
ref={inputRef}
|
|
292
295
|
className="overview-input-hero"
|
|
293
296
|
placeholder="e.g. Implement user authentication with email + password, including registration, login, password reset, and integration tests. Use Bcrypt, JWT tokens, and the existing API style."
|
|
294
297
|
disabled={submitting}
|
|
298
|
+
aria-label="Describe what you want Odin to do"
|
|
295
299
|
/>
|
|
296
300
|
<div style={{ display: 'flex', gap: 'var(--space-2)', alignItems: 'center', flexWrap: 'wrap' }}>
|
|
297
301
|
<Button
|
|
@@ -366,22 +370,28 @@ function OverviewInner({
|
|
|
366
370
|
No activity yet. Use the chat above or invoke a Bizar command to start a feed.
|
|
367
371
|
</div>
|
|
368
372
|
) : (
|
|
369
|
-
<div
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
373
|
+
<div
|
|
374
|
+
className={cn('activity-feed-list-wrap', !activityExpanded && 'activity-feed-list-wrap-collapsed')}
|
|
375
|
+
aria-live="polite"
|
|
376
|
+
aria-relevant="additions"
|
|
377
|
+
>
|
|
378
|
+
<VirtualList
|
|
379
|
+
items={activityItems.slice(0, 30).filter((it, idx) => !hiddenKeys.has(itemKey(it, idx)))}
|
|
380
|
+
itemHeight={60}
|
|
381
|
+
height={Math.min(activityItems.filter((it, idx) => !hiddenKeys.has(itemKey(it, idx))).length * 60, 480)}
|
|
382
|
+
className="activity-feed-list"
|
|
383
|
+
renderItem={(it, vlIdx) => {
|
|
384
|
+
const originalIdx = activityItems.slice(0, 30).findIndex((a) => a === it);
|
|
374
385
|
return (
|
|
375
386
|
<ActivityFeedItem
|
|
376
|
-
key={`${it.ts}-${idx}`}
|
|
377
387
|
item={it}
|
|
378
|
-
activityKey={
|
|
388
|
+
activityKey={itemKey(it, originalIdx)}
|
|
379
389
|
onNavigate={setActiveTab}
|
|
380
390
|
onHide={onHide}
|
|
381
391
|
/>
|
|
382
392
|
);
|
|
383
|
-
}
|
|
384
|
-
|
|
393
|
+
}}
|
|
394
|
+
/>
|
|
385
395
|
{!activityExpanded && activityItems.length > 8 && <div className="activity-feed-fade" aria-hidden="true" />}
|
|
386
396
|
</div>
|
|
387
397
|
)}
|
|
@@ -75,14 +75,14 @@ export function Providers({ snapshot, settings, refreshSnapshot }: Props) {
|
|
|
75
75
|
title: 'Add Provider',
|
|
76
76
|
children: (
|
|
77
77
|
<div className="provider-form">
|
|
78
|
-
<label className="field-label">Provider name</label>
|
|
79
|
-
<input ref={(el) => { nameEl = el; }} className="input" type="text" placeholder="e.g. Anthropic, OpenAI" autoFocus />
|
|
80
|
-
<label className="field-label">Base URL</label>
|
|
81
|
-
<input ref={(el) => { urlEl = el; }} className="input" type="text" placeholder="https://api.openai.com/v1" />
|
|
82
|
-
<label className="field-label">API Key</label>
|
|
83
|
-
<input ref={(el) => { keyEl = el; }} className="input" type="password" placeholder="sk-..." />
|
|
84
|
-
<label className="field-label">Models (one per line)</label>
|
|
85
|
-
<textarea ref={(el) => { modelsEl = el; }} className="input" rows={4} placeholder="gpt-4o gpt-4o-mini ..." />
|
|
78
|
+
<label className="field-label" htmlFor="provider-new-name">Provider name</label>
|
|
79
|
+
<input id="provider-new-name" ref={(el) => { nameEl = el; }} className="input" type="text" placeholder="e.g. Anthropic, OpenAI" autoFocus />
|
|
80
|
+
<label className="field-label" htmlFor="provider-new-url">Base URL</label>
|
|
81
|
+
<input id="provider-new-url" ref={(el) => { urlEl = el; }} className="input" type="text" placeholder="https://api.openai.com/v1" />
|
|
82
|
+
<label className="field-label" htmlFor="provider-new-key">API Key</label>
|
|
83
|
+
<input id="provider-new-key" ref={(el) => { keyEl = el; }} className="input" type="password" placeholder="sk-..." />
|
|
84
|
+
<label className="field-label" htmlFor="provider-new-models">Models (one per line)</label>
|
|
85
|
+
<textarea id="provider-new-models" ref={(el) => { modelsEl = el; }} className="input" rows={4} placeholder="gpt-4o gpt-4o-mini ..." />
|
|
86
86
|
</div>
|
|
87
87
|
),
|
|
88
88
|
footer: (
|
|
@@ -118,14 +118,14 @@ export function Providers({ snapshot, settings, refreshSnapshot }: Props) {
|
|
|
118
118
|
title: `Edit "${provider.name}"`,
|
|
119
119
|
children: (
|
|
120
120
|
<div className="provider-form">
|
|
121
|
-
<label className="field-label">Provider name</label>
|
|
122
|
-
<input ref={(el) => { nameEl = el; }} className="input" type="text" defaultValue={provider.name} />
|
|
123
|
-
<label className="field-label">Base URL</label>
|
|
124
|
-
<input ref={(el) => { urlEl = el; }} className="input" type="text" defaultValue={provider.baseURL} />
|
|
125
|
-
<label className="field-label">API Key (leave blank to keep current)</label>
|
|
126
|
-
<input ref={(el) => { keyEl = el; }} className="input" type="password" placeholder="sk-..." />
|
|
127
|
-
<label className="field-label">Models (one per line)</label>
|
|
128
|
-
<textarea ref={(el) => { modelsEl = el; }} className="input" rows={4} defaultValue={(provider.models || []).join('\n')} />
|
|
121
|
+
<label className="field-label" htmlFor="provider-edit-name">Provider name</label>
|
|
122
|
+
<input id="provider-edit-name" ref={(el) => { nameEl = el; }} className="input" type="text" defaultValue={provider.name} />
|
|
123
|
+
<label className="field-label" htmlFor="provider-edit-url">Base URL</label>
|
|
124
|
+
<input id="provider-edit-url" ref={(el) => { urlEl = el; }} className="input" type="text" defaultValue={provider.baseURL} />
|
|
125
|
+
<label className="field-label" htmlFor="provider-edit-key">API Key (leave blank to keep current)</label>
|
|
126
|
+
<input id="provider-edit-key" ref={(el) => { keyEl = el; }} className="input" type="password" placeholder="sk-..." />
|
|
127
|
+
<label className="field-label" htmlFor="provider-edit-models">Models (one per line)</label>
|
|
128
|
+
<textarea id="provider-edit-models" ref={(el) => { modelsEl = el; }} className="input" rows={4} defaultValue={(provider.models || []).join('\n')} />
|
|
129
129
|
</div>
|
|
130
130
|
),
|
|
131
131
|
footer: (
|