@polderlabs/bizar 4.7.0 → 4.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/bizar-dash/dist/assets/main-DHZmbnxQ.js +361 -0
  2. package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +1 -0
  3. package/bizar-dash/dist/assets/main-DX_Jh8Wc.css +1 -0
  4. package/bizar-dash/dist/assets/{mobile-CWqPoGaT.js → mobile-BK8-ythT.js} +2 -2
  5. package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +1 -0
  6. package/bizar-dash/dist/assets/{mobile-i4Uv9eW8.js → mobile-Chvf9u_B.js} +1 -1
  7. package/bizar-dash/dist/assets/{mobile-i4Uv9eW8.js.map → mobile-Chvf9u_B.js.map} +1 -1
  8. package/bizar-dash/dist/index.html +3 -3
  9. package/bizar-dash/dist/mobile.html +2 -2
  10. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  11. package/bizar-dash/skills/publishing/SKILL.md +146 -0
  12. package/bizar-dash/src/server/api.mjs +8 -0
  13. package/bizar-dash/src/server/backup-store.mjs +525 -0
  14. package/bizar-dash/src/server/digest-store.mjs +558 -0
  15. package/bizar-dash/src/server/lib/rate-limit.mjs +122 -0
  16. package/bizar-dash/src/server/routes/backup.mjs +112 -0
  17. package/bizar-dash/src/server/routes/chat.mjs +14 -0
  18. package/bizar-dash/src/server/routes/digests.mjs +82 -0
  19. package/bizar-dash/src/server/routes-v2/events.mjs +14 -0
  20. package/bizar-dash/src/server/schedules-runner.mjs +126 -0
  21. package/bizar-dash/src/server/server.mjs +17 -0
  22. package/bizar-dash/src/web/App.tsx +8 -1
  23. package/bizar-dash/src/web/components/BackupRestore.tsx +330 -0
  24. package/bizar-dash/src/web/components/SearchModal.tsx +3 -1
  25. package/bizar-dash/src/web/components/chat/Composer.tsx +2 -0
  26. package/bizar-dash/src/web/styles/main.css +70 -8
  27. package/bizar-dash/src/web/views/Activity.tsx +11 -1
  28. package/bizar-dash/src/web/views/Agents.tsx +57 -42
  29. package/bizar-dash/src/web/views/Artifacts.tsx +38 -25
  30. package/bizar-dash/src/web/views/Chat.tsx +8 -0
  31. package/bizar-dash/src/web/views/History.tsx +4 -1
  32. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +21 -4
  33. package/bizar-dash/src/web/views/Mods.tsx +30 -17
  34. package/bizar-dash/src/web/views/Overview.tsx +8 -1
  35. package/bizar-dash/src/web/views/Providers.tsx +16 -16
  36. package/bizar-dash/src/web/views/Schedules.tsx +33 -15
  37. package/bizar-dash/src/web/views/Settings.tsx +97 -1751
  38. package/bizar-dash/src/web/views/Skills.tsx +4 -1
  39. package/bizar-dash/src/web/views/Tasks.tsx +11 -2
  40. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +8 -2
  41. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +3 -0
  42. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +12 -4
  43. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +3 -0
  44. package/bizar-dash/src/web/views/settings/ActivitySection.tsx +205 -0
  45. package/bizar-dash/src/web/views/settings/AgentSection.tsx +294 -0
  46. package/bizar-dash/src/web/views/settings/AuthSection.tsx +159 -0
  47. package/bizar-dash/src/web/views/settings/BackupSection.tsx +16 -0
  48. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +16 -0
  49. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +105 -0
  50. package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +39 -0
  51. package/bizar-dash/src/web/views/settings/MemorySection.tsx +16 -0
  52. package/bizar-dash/src/web/views/settings/NetworkSection.tsx +87 -0
  53. package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +34 -0
  54. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +16 -0
  55. package/bizar-dash/src/web/views/settings/SkillsSection.tsx +16 -0
  56. package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +81 -0
  57. package/bizar-dash/src/web/views/settings/ThemeSection.tsx +168 -0
  58. package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +256 -0
  59. package/bizar-dash/tests/a11y.test.tsx +206 -0
  60. package/bizar-dash/tests/backup-restore.test.mjs +217 -0
  61. package/bizar-dash/tests/backup-restore.test.tsx +123 -0
  62. package/bizar-dash/tests/backup-store.test.mjs +300 -0
  63. package/bizar-dash/tests/cli-error-visibility.test.mjs +153 -0
  64. package/bizar-dash/tests/digest-generation.test.mjs +191 -0
  65. package/bizar-dash/tests/digest-store.test.mjs +264 -0
  66. package/bizar-dash/tests/rate-limit.test.mjs +298 -0
  67. package/cli/bin.mjs +96 -2
  68. package/cli/commands/minimax.mjs +20 -0
  69. package/cli/commands/util.mjs +154 -1
  70. package/cli/digest.mjs +149 -0
  71. package/package.json +1 -1
  72. package/bizar-dash/dist/assets/main-DAlLdW8I.css +0 -1
  73. package/bizar-dash/dist/assets/main-DGGq-iZI.js +0 -361
  74. package/bizar-dash/dist/assets/main-DGGq-iZI.js.map +0 -1
  75. package/bizar-dash/dist/assets/mobile-CWqPoGaT.js.map +0 -1
@@ -144,16 +144,18 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
144
144
  width: 640,
145
145
  children: (
146
146
  <div className="agent-form">
147
- <label className="field-label">Name (a-z, 0-9, dashes)</label>
147
+ <label className="field-label" htmlFor="agent-new-name">Name (a-z, 0-9, dashes)</label>
148
148
  <input
149
+ id="agent-new-name"
149
150
  ref={(el) => (nameEl = el)}
150
151
  className="input"
151
152
  type="text"
152
153
  placeholder="my-agent"
153
154
  autoFocus
154
155
  />
155
- <label className="field-label">Description</label>
156
+ <label className="field-label" htmlFor="agent-new-desc">Description</label>
156
157
  <input
158
+ id="agent-new-desc"
157
159
  ref={(el) => { descEl = el; }}
158
160
  className="input"
159
161
  type="text"
@@ -161,8 +163,8 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
161
163
  />
162
164
  <div className="task-form-row">
163
165
  <div className="task-form-field">
164
- <label className="field-label">Model</label>
165
- <select ref={(el) => (modelEl = el)} className="select" defaultValue="">
166
+ <label className="field-label" htmlFor="agent-new-model">Model</label>
167
+ <select id="agent-new-model" ref={(el) => (modelEl = el)} className="select" defaultValue="">
166
168
  <option value="">(provider default)</option>
167
169
  {MODELS.map((m) => (
168
170
  <option key={m} value={m}>{m}</option>
@@ -170,22 +172,22 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
170
172
  </select>
171
173
  </div>
172
174
  <div className="task-form-field">
173
- <label className="field-label">Mode</label>
174
- <select ref={(el) => (modeEl = el)} className="select" defaultValue="subagent">
175
+ <label className="field-label" htmlFor="agent-new-mode">Mode</label>
176
+ <select id="agent-new-mode" ref={(el) => (modeEl = el)} className="select" defaultValue="subagent">
175
177
  <option value="primary">primary</option>
176
178
  <option value="subagent">subagent</option>
177
179
  <option value="all">all</option>
178
180
  </select>
179
181
  </div>
180
182
  <div className="task-form-field" style={{ flex: '0 0 80px' }}>
181
- <label className="field-label">Color</label>
182
- <input ref={(el) => (colorEl = el)} className="input" type="color" defaultValue="#8b5cf6" />
183
+ <label className="field-label" htmlFor="agent-new-color">Color</label>
184
+ <input id="agent-new-color" ref={(el) => (colorEl = el)} className="input" type="color" defaultValue="#8b5cf6" />
183
185
  </div>
184
186
  </div>
185
187
  <div className="task-form-row">
186
188
  <div className="task-form-field" style={{ flex: 1 }}>
187
- <label className="field-label">Category</label>
188
- <select ref={(el) => (categoryEl = el)} className="select" defaultValue="">
189
+ <label className="field-label" htmlFor="agent-new-category">Category</label>
190
+ <select id="agent-new-category" ref={(el) => (categoryEl = el)} className="select" defaultValue="">
189
191
  <option value="">(none)</option>
190
192
  {CATEGORIES.map((c) => (
191
193
  <option key={c.id} value={c.id}>{c.label}</option>
@@ -193,21 +195,24 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
193
195
  </select>
194
196
  </div>
195
197
  <div className="task-form-field" style={{ flex: 2 }}>
196
- <label className="field-label">Tags (comma-separated)</label>
197
- <input ref={(el) => (tagsEl = el)} className="input" type="text" placeholder="reasoning, code, planning" />
198
+ <label className="field-label" htmlFor="agent-new-tags">Tags (comma-separated)</label>
199
+ <input id="agent-new-tags" ref={(el) => (tagsEl = el)} className="input" type="text" placeholder="reasoning, code, planning" />
198
200
  </div>
199
201
  </div>
200
- <label className="field-label">Tools</label>
201
- <div ref={(el) => (toolsContainer = el)} className="agent-tools">
202
- {TOOL_OPTIONS.map((t) => (
203
- <label key={t} className="checkbox-row">
204
- <input type="checkbox" value={t} />
205
- <span>{t}</span>
206
- </label>
207
- ))}
208
- </div>
209
- <label className="field-label">System prompt</label>
202
+ <fieldset>
203
+ <legend className="field-label" style={{ padding: 0 }}>Tools</legend>
204
+ <div ref={(el) => (toolsContainer = el)} className="agent-tools">
205
+ {TOOL_OPTIONS.map((t) => (
206
+ <label key={t} className="checkbox-row">
207
+ <input type="checkbox" value={t} aria-label={t} />
208
+ <span>{t}</span>
209
+ </label>
210
+ ))}
211
+ </div>
212
+ </fieldset>
213
+ <label className="field-label" htmlFor="agent-new-prompt">System prompt</label>
210
214
  <textarea
215
+ id="agent-new-prompt"
211
216
  ref={(el) => (promptEl = el)}
212
217
  className="textarea"
213
218
  rows={6}
@@ -280,8 +285,9 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
280
285
  <div className="muted">
281
286
  File: <code>{full.path}</code>
282
287
  </div>
283
- <label className="field-label">Description</label>
288
+ <label className="field-label" htmlFor="agent-edit-desc">Description</label>
284
289
  <input
290
+ id="agent-edit-desc"
285
291
  ref={(el) => { descEl = el; }}
286
292
  className="input"
287
293
  type="text"
@@ -289,8 +295,8 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
289
295
  />
290
296
  <div className="task-form-row">
291
297
  <div className="task-form-field">
292
- <label className="field-label">Model</label>
293
- <select ref={(el) => (modelEl = el)} className="select" defaultValue={full.model}>
298
+ <label className="field-label" htmlFor="agent-edit-model">Model</label>
299
+ <select id="agent-edit-model" ref={(el) => (modelEl = el)} className="select" defaultValue={full.model}>
294
300
  <option value="">(provider default)</option>
295
301
  {MODELS.map((m) => (
296
302
  <option key={m} value={m}>{m}</option>
@@ -298,22 +304,22 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
298
304
  </select>
299
305
  </div>
300
306
  <div className="task-form-field">
301
- <label className="field-label">Mode</label>
302
- <select ref={(el) => (modeEl = el)} className="select" defaultValue={full.mode || 'subagent'}>
307
+ <label className="field-label" htmlFor="agent-edit-mode">Mode</label>
308
+ <select id="agent-edit-mode" ref={(el) => (modeEl = el)} className="select" defaultValue={full.mode || 'subagent'}>
303
309
  <option value="primary">primary</option>
304
310
  <option value="subagent">subagent</option>
305
311
  <option value="all">all</option>
306
312
  </select>
307
313
  </div>
308
314
  <div className="task-form-field" style={{ flex: '0 0 80px' }}>
309
- <label className="field-label">Color</label>
310
- <input ref={(el) => (colorEl = el)} className="input" type="color" defaultValue={full.color || '#8b5cf6'} />
315
+ <label className="field-label" htmlFor="agent-edit-color">Color</label>
316
+ <input id="agent-edit-color" ref={(el) => (colorEl = el)} className="input" type="color" defaultValue={full.color || '#8b5cf6'} />
311
317
  </div>
312
318
  </div>
313
319
  <div className="task-form-row">
314
320
  <div className="task-form-field" style={{ flex: 1 }}>
315
- <label className="field-label">Category</label>
316
- <select ref={(el) => (categoryEl = el)} className="select" defaultValue={full.category || ''}>
321
+ <label className="field-label" htmlFor="agent-edit-category">Category</label>
322
+ <select id="agent-edit-category" ref={(el) => (categoryEl = el)} className="select" defaultValue={full.category || ''}>
317
323
  <option value="">(none)</option>
318
324
  {CATEGORIES.map((c) => (
319
325
  <option key={c.id} value={c.id}>{c.label}</option>
@@ -321,8 +327,9 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
321
327
  </select>
322
328
  </div>
323
329
  <div className="task-form-field" style={{ flex: 2 }}>
324
- <label className="field-label">Tags (comma-separated)</label>
330
+ <label className="field-label" htmlFor="agent-edit-tags">Tags (comma-separated)</label>
325
331
  <input
332
+ id="agent-edit-tags"
326
333
  ref={(el) => (tagsEl = el)}
327
334
  className="input"
328
335
  type="text"
@@ -330,17 +337,20 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
330
337
  />
331
338
  </div>
332
339
  </div>
333
- <label className="field-label">Tools</label>
334
- <div ref={(el) => (toolsContainer = el)} className="agent-tools">
335
- {TOOL_OPTIONS.map((t) => (
336
- <label key={t} className="checkbox-row">
337
- <input type="checkbox" value={t} defaultChecked={full.tools?.includes(t)} />
338
- <span>{t}</span>
339
- </label>
340
- ))}
341
- </div>
342
- <label className="field-label">System prompt</label>
340
+ <fieldset>
341
+ <legend className="field-label" style={{ padding: 0 }}>Tools</legend>
342
+ <div ref={(el) => (toolsContainer = el)} className="agent-tools">
343
+ {TOOL_OPTIONS.map((t) => (
344
+ <label key={t} className="checkbox-row">
345
+ <input type="checkbox" value={t} aria-label={t} defaultChecked={full.tools?.includes(t)} />
346
+ <span>{t}</span>
347
+ </label>
348
+ ))}
349
+ </div>
350
+ </fieldset>
351
+ <label className="field-label" htmlFor="agent-edit-prompt">System prompt</label>
343
352
  <textarea
353
+ id="agent-edit-prompt"
344
354
  ref={(el) => (promptEl = el)}
345
355
  className="textarea"
346
356
  rows={8}
@@ -483,15 +493,20 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
483
493
  </div>
484
494
  <div className="view-actions">
485
495
  <div className="search-input">
496
+ <label htmlFor="agents-search" className="sr-only">Search agents</label>
486
497
  <input
498
+ id="agents-search"
487
499
  className="input"
488
500
  type="text"
489
501
  placeholder="Search…"
490
502
  value={search}
491
503
  onChange={(e) => setSearch(e.target.value)}
504
+ aria-label="Search agents"
492
505
  />
493
506
  </div>
507
+ <label htmlFor="agents-category-filter" className="sr-only">Filter by category</label>
494
508
  <select
509
+ id="agents-category-filter"
495
510
  className="select select-sm"
496
511
  value={categoryFilter}
497
512
  onChange={(e) => setCategoryFilter(e.target.value)}
@@ -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
 
@@ -234,12 +234,15 @@ export function History({ snapshot }: Props) {
234
234
  </div>
235
235
  <div className="view-actions">
236
236
  <div className="tasks-toolbar-group">
237
- <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>
238
239
  <select
240
+ id="history-range"
239
241
  className="select select-sm"
240
242
  value={rangeId}
241
243
  onChange={(e) => setRangeId(e.target.value)}
242
244
  title="Time range"
245
+ aria-label="Time range"
243
246
  >
244
247
  {TIME_RANGES.map((r) => (
245
248
  <option key={r.id} value={r.id}>{r.label}</option>
@@ -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
- <input type={showKey ? 'text' : 'password'} value={keyDraft} onChange={e => setKeyDraft(e.target.value)}
884
- placeholder="sk-cp-..." spellCheck={false} autoComplete="off" className="minimax-key-input" autoFocus />
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/&lt;id&gt;/</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
- <div className="mods-registry-head" onClick={() => {
359
- if (!registry && !registryLoading) loadRegistry();
360
- setRegistryOpen((v) => !v);
361
- }}>
362
- <Globe size={14} />
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
- </div>
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
- <div className="mod-instructions-head" onClick={() => {
590
- if (!instructions && !instructionsLoading) loadInstructions();
591
- setInstructionsOpen((v) => !v);
592
- }}>
593
- <FileCode size={14} />
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
- </div>
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">
@@ -288,11 +288,14 @@ function OverviewInner({
288
288
  }
289
289
  }}
290
290
  >
291
+ <label htmlFor="overview-hero-input" className="sr-only">Describe what you want Odin to do</label>
291
292
  <textarea
293
+ id="overview-hero-input"
292
294
  ref={inputRef}
293
295
  className="overview-input-hero"
294
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."
295
297
  disabled={submitting}
298
+ aria-label="Describe what you want Odin to do"
296
299
  />
297
300
  <div style={{ display: 'flex', gap: 'var(--space-2)', alignItems: 'center', flexWrap: 'wrap' }}>
298
301
  <Button
@@ -367,7 +370,11 @@ function OverviewInner({
367
370
  No activity yet. Use the chat above or invoke a Bizar command to start a feed.
368
371
  </div>
369
372
  ) : (
370
- <div className={cn('activity-feed-list-wrap', !activityExpanded && 'activity-feed-list-wrap-collapsed')}>
373
+ <div
374
+ className={cn('activity-feed-list-wrap', !activityExpanded && 'activity-feed-list-wrap-collapsed')}
375
+ aria-live="polite"
376
+ aria-relevant="additions"
377
+ >
371
378
  <VirtualList
372
379
  items={activityItems.slice(0, 30).filter((it, idx) => !hiddenKeys.has(itemKey(it, idx)))}
373
380
  itemHeight={60}
@@ -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&#10;gpt-4o-mini&#10;..." />
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&#10;gpt-4o-mini&#10;..." />
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: (