@polderlabs/bizar 3.23.1 → 4.2.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/README.md +83 -16
- package/bizar-dash/.bizar/activity.log +3 -0
- package/bizar-dash/.bizar/graph/.graphify_analysis.json +1540 -0
- package/bizar-dash/.bizar/graph/.graphify_labels.json +1 -0
- package/bizar-dash/.bizar/graph/GRAPH_REPORT.md +404 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/0661b816358db84dfdb7f10443db8a61152fd20b3fee950caf8fbf32920a1790.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/16da06d774142a6e5d74829ad982531286a6193ef798de8f593db029e123ec32.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/27e5697c01254c69c64c5a33896669073b40afb5ba88375102ae77fa5404136d.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/532997898027254aa4c0932243dc71d2f55888181232b65974961934997dc85b.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/84e3451a64c7f99f0cd6a88b0748d7426780486f53ab4935b7476b28c08f8293.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/88e5ea5f3e78fa69353fe9e6a8d1a05391d54605e25600cb125531eb5a6432ff.json +1 -0
- package/bizar-dash/.bizar/graph/cache/stat-index.json +1 -0
- package/bizar-dash/.bizar/graph/graph.html +307 -0
- package/bizar-dash/.bizar/graph/graph.json +40267 -0
- package/bizar-dash/.bizar/graph/manifest.json +762 -0
- package/bizar-dash/.graphifyignore +40 -0
- package/bizar-dash/.obsidian/README.md +14 -0
- package/bizar-dash/.obsidian/daily/2026-06-25.md +9 -0
- package/bizar-dash/.obsidian/vault.json +7 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/SKILL.md +103 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/assets/template.html +338 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/example.html +81 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/open-design.json +172 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/checklist.md +44 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/layouts.md +247 -0
- package/bizar-dash/CHANGELOG.md +474 -0
- package/bizar-dash/DESIGN.md +136 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js +331 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js.map +1 -0
- package/bizar-dash/dist/assets/main-DlJ8wgLR.css +1 -0
- package/bizar-dash/dist/assets/mobile-CsZQAswA.css +1 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js +2 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js +354 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js.map +1 -0
- package/bizar-dash/dist/index.html +40 -0
- package/bizar-dash/dist/mobile.html +22 -0
- package/bizar-dash/plans/debug-plan/comments.json +1 -0
- package/bizar-dash/plans/debug-plan/meta.json +10 -0
- package/bizar-dash/plans/debug-plan/plan.json +174 -0
- package/bizar-dash/scripts/smoke-bg-retry.mjs +246 -0
- package/bizar-dash/src/cli/dashboard-ports.mjs +488 -0
- package/bizar-dash/src/cli.mjs +522 -0
- package/bizar-dash/src/server/activity-log.mjs +174 -0
- package/bizar-dash/src/server/agents-store.mjs +465 -0
- package/bizar-dash/src/server/api.mjs +138 -0
- package/bizar-dash/src/server/artifact-template.html +810 -0
- package/bizar-dash/src/server/artifacts-store.mjs +733 -0
- package/bizar-dash/src/server/auth.mjs +381 -0
- package/bizar-dash/src/server/background-store.mjs +447 -0
- package/bizar-dash/src/server/bg-poller.mjs +305 -0
- package/bizar-dash/src/server/bg-retry.mjs +574 -0
- package/bizar-dash/src/server/browser.mjs +40 -0
- package/bizar-dash/src/server/diagnostics-store.mjs +148 -0
- package/bizar-dash/src/server/dialog-poller.mjs +105 -0
- package/bizar-dash/src/server/dialog-store.mjs +177 -0
- package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +583 -0
- package/bizar-dash/src/server/lib/path-safe.mjs +283 -0
- package/bizar-dash/src/server/memory-git.mjs +329 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
- package/bizar-dash/src/server/memory-schema.mjs +145 -0
- package/bizar-dash/src/server/memory-secrets.mjs +84 -0
- package/bizar-dash/src/server/memory-store.mjs +471 -0
- package/bizar-dash/src/server/mod-security.mjs +398 -0
- package/bizar-dash/src/server/mods-loader.mjs +1067 -0
- package/bizar-dash/src/server/notifications-store.mjs +243 -0
- package/bizar-dash/src/server/obsidian-store.mjs +318 -0
- package/bizar-dash/src/server/opencode-runner.mjs +279 -0
- package/bizar-dash/src/server/pair-store.mjs +138 -0
- package/bizar-dash/src/server/projects-store.mjs +364 -0
- package/bizar-dash/src/server/providers-store.mjs +789 -0
- package/bizar-dash/src/server/routes/_shared.mjs +415 -0
- package/bizar-dash/src/server/routes/activity.mjs +110 -0
- package/bizar-dash/src/server/routes/agents.mjs +134 -0
- package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
- package/bizar-dash/src/server/routes/auth.mjs +69 -0
- package/bizar-dash/src/server/routes/background.mjs +165 -0
- package/bizar-dash/src/server/routes/chat.mjs +485 -0
- package/bizar-dash/src/server/routes/config.mjs +129 -0
- package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
- package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
- package/bizar-dash/src/server/routes/fs.mjs +429 -0
- package/bizar-dash/src/server/routes/history.mjs +78 -0
- package/bizar-dash/src/server/routes/memory.mjs +658 -0
- package/bizar-dash/src/server/routes/misc.mjs +89 -0
- package/bizar-dash/src/server/routes/mods.mjs +359 -0
- package/bizar-dash/src/server/routes/notifications.mjs +54 -0
- package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
- package/bizar-dash/src/server/routes/overview.mjs +91 -0
- package/bizar-dash/src/server/routes/pair.mjs +67 -0
- package/bizar-dash/src/server/routes/projects.mjs +175 -0
- package/bizar-dash/src/server/routes/providers.mjs +47 -0
- package/bizar-dash/src/server/routes/schedules.mjs +104 -0
- package/bizar-dash/src/server/routes/settings.mjs +51 -0
- package/bizar-dash/src/server/routes/skills.mjs +76 -0
- package/bizar-dash/src/server/routes/tasks.mjs +468 -0
- package/bizar-dash/src/server/routes/themes.mjs +51 -0
- package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
- package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
- package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
- package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
- package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
- package/bizar-dash/src/server/schedules-runner.mjs +369 -0
- package/bizar-dash/src/server/schedules-store.mjs +268 -0
- package/bizar-dash/src/server/search-store.mjs +220 -0
- package/bizar-dash/src/server/serve-info.mjs +684 -0
- package/bizar-dash/src/server/server.mjs +796 -0
- package/bizar-dash/src/server/settings-store.mjs +45 -0
- package/bizar-dash/src/server/skills-store.mjs +300 -0
- package/bizar-dash/src/server/state.mjs +437 -0
- package/bizar-dash/src/server/tailscale-store.mjs +113 -0
- package/bizar-dash/src/server/task-delegator.mjs +824 -0
- package/bizar-dash/src/server/tasks-store.mjs +508 -0
- package/bizar-dash/src/server/tui.mjs +923 -0
- package/bizar-dash/src/server/update-store.mjs +168 -0
- package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
- package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
- package/bizar-dash/src/server/watcher.mjs +81 -0
- package/bizar-dash/src/server/yaml.mjs +238 -0
- package/bizar-dash/src/web/App.tsx +718 -0
- package/bizar-dash/src/web/MobileApp.tsx +408 -0
- package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
- package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
- package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
- package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
- package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
- package/bizar-dash/src/web/components/Button.tsx +56 -0
- package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
- package/bizar-dash/src/web/components/Card.tsx +40 -0
- package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
- package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
- package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
- package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
- package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
- package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
- package/bizar-dash/src/web/components/Modal.tsx +215 -0
- package/bizar-dash/src/web/components/Notifications.tsx +241 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
- package/bizar-dash/src/web/components/Spinner.tsx +19 -0
- package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
- package/bizar-dash/src/web/components/Tag.tsx +28 -0
- package/bizar-dash/src/web/components/Toast.tsx +150 -0
- package/bizar-dash/src/web/components/Topbar.tsx +368 -0
- package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
- package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
- package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
- package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
- package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
- package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
- package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
- package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
- package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
- package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
- package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
- package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
- package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
- package/bizar-dash/src/web/components/chat/index.ts +20 -0
- package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
- package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
- package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
- package/bizar-dash/src/web/index.html +37 -0
- package/bizar-dash/src/web/lib/api.ts +193 -0
- package/bizar-dash/src/web/lib/markdown.tsx +59 -0
- package/bizar-dash/src/web/lib/types.ts +668 -0
- package/bizar-dash/src/web/lib/utils.ts +114 -0
- package/bizar-dash/src/web/lib/ws.ts +176 -0
- package/bizar-dash/src/web/main.tsx +34 -0
- package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
- package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
- package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
- package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
- package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
- package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
- package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
- package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
- package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
- package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
- package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
- package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
- package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
- package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
- package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
- package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
- package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
- package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
- package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
- package/bizar-dash/src/web/mobile.html +20 -0
- package/bizar-dash/src/web/mobile.tsx +13 -0
- package/bizar-dash/src/web/styles/chat.css +198 -0
- package/bizar-dash/src/web/styles/glyphs.css +960 -0
- package/bizar-dash/src/web/styles/main.css +8855 -0
- package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
- package/bizar-dash/src/web/styles/mobile.css +1960 -0
- package/bizar-dash/src/web/views/Activity.tsx +1203 -0
- package/bizar-dash/src/web/views/Agents.tsx +701 -0
- package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
- package/bizar-dash/src/web/views/Chat.tsx +190 -0
- package/bizar-dash/src/web/views/Config.tsx +2063 -0
- package/bizar-dash/src/web/views/History.tsx +335 -0
- package/bizar-dash/src/web/views/ModView.tsx +207 -0
- package/bizar-dash/src/web/views/Mods.tsx +693 -0
- package/bizar-dash/src/web/views/Overview.tsx +812 -0
- package/bizar-dash/src/web/views/Providers.tsx +302 -0
- package/bizar-dash/src/web/views/Schedules.tsx +802 -0
- package/bizar-dash/src/web/views/Settings.tsx +1787 -0
- package/bizar-dash/src/web/views/Skills.tsx +431 -0
- package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
- package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
- package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
- package/bizar-dash/templates/mod/FORMAT.md +76 -0
- package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
- package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
- package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
- package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
- package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
- package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
- package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
- package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
- package/bizar-dash/tests/memory-cli.test.mjs +542 -0
- package/bizar-dash/tests/memory-config.test.mjs +422 -0
- package/bizar-dash/tests/memory-git.test.mjs +246 -0
- package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
- package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
- package/bizar-dash/tests/memory-schema.test.mjs +198 -0
- package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
- package/bizar-dash/tests/memory-store.test.mjs +390 -0
- package/bizar-dash/tests/memory-sync.test.mjs +141 -0
- package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
- package/bizar-dash/tests/mod-security.test.mjs +273 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
- package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
- package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
- package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
- package/bizar-dash/tests/path-safe.test.mjs +108 -0
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
- package/bizar-dash/tests/smoke-v2.mjs +212 -0
- package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
- package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
- package/bizar-dash/tests/yaml.test.mjs +128 -0
- package/bizar-dash/tsconfig.json +23 -0
- package/bizar-dash/vite.config.ts +27 -0
- package/cli/atomic.mjs +73 -0
- package/cli/banner.mjs +1 -1
- package/cli/bin.mjs +44 -47
- package/cli/bootstrap.mjs +1 -1
- package/cli/copy.mjs +25 -16
- package/cli/dev-link.mjs +17 -1
- package/cli/doctor.mjs +4 -4
- package/cli/doctor.test.mjs +2 -2
- package/cli/init.mjs +135 -1
- package/cli/install.mjs +31 -39
- package/cli/memory-constants.mjs +59 -0
- package/cli/memory.mjs +1436 -0
- package/cli/service.mjs +5 -6
- package/cli/utils.mjs +6 -3
- package/config/AGENTS.md +7 -7
- package/config/agents/_shared/AGENT_BASELINE.md +59 -61
- package/config/opencode.json +13 -38
- package/config/skills/memory-protocol/SKILL.md +105 -0
- package/config/skills/obsidian/SKILL.md +270 -65
- package/install.sh +59 -19
- package/package.json +49 -14
- package/packages/sdk/LICENSE +21 -0
- package/packages/sdk/README.md +80 -0
- package/packages/sdk/src/client.ts +183 -0
- package/packages/sdk/src/errors.ts +120 -0
- package/packages/sdk/src/events.ts +153 -0
- package/packages/sdk/src/index.ts +63 -0
- package/packages/sdk/src/types.ts +176 -0
- package/packages/sdk/src/version.ts +4 -0
- package/packages/sdk/tests/client.test.ts +217 -0
- package/packages/sdk/tests/errors.test.ts +108 -0
- package/packages/sdk/tests/events.test.ts +139 -0
- package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
- package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
- package/packages/sdk/tsconfig.json +25 -0
- package/packages/sdk/vitest.config.ts +9 -0
- package/plugins/bizar/LICENSE +21 -0
- package/plugins/bizar/README.md +448 -0
- package/plugins/bizar/dist/index.js +29901 -0
- package/plugins/bizar/index.ts +1491 -0
- package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
- package/plugins/bizar/src/background-state.ts +551 -0
- package/plugins/bizar/src/background.ts +1250 -0
- package/plugins/bizar/src/commands-impl.ts +464 -0
- package/plugins/bizar/src/commands.ts +1168 -0
- package/plugins/bizar/src/dashboard-client.ts +233 -0
- package/plugins/bizar/src/event-stream.ts +606 -0
- package/plugins/bizar/src/fingerprint.ts +120 -0
- package/plugins/bizar/src/handoff.ts +79 -0
- package/plugins/bizar/src/http-client.ts +467 -0
- package/plugins/bizar/src/key-rotation.ts +218 -0
- package/plugins/bizar/src/logger.ts +144 -0
- package/plugins/bizar/src/loop.ts +176 -0
- package/plugins/bizar/src/opencode-runner.ts +390 -0
- package/plugins/bizar/src/options.ts +421 -0
- package/plugins/bizar/src/plan-fs.ts +323 -0
- package/plugins/bizar/src/reasoning-clean.ts +454 -0
- package/plugins/bizar/src/report.ts +178 -0
- package/plugins/bizar/src/research-prompt.ts +35 -0
- package/plugins/bizar/src/serve-info.ts +228 -0
- package/plugins/bizar/src/serve.ts +496 -0
- package/plugins/bizar/src/settings.ts +349 -0
- package/plugins/bizar/src/state.ts +298 -0
- package/plugins/bizar/src/tools/bg-collect.ts +104 -0
- package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
- package/plugins/bizar/src/tools/bg-kill.ts +87 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
- package/plugins/bizar/src/tools/bg-status.ts +99 -0
- package/plugins/bizar/src/tools/open-kb.ts +191 -0
- package/plugins/bizar/src/tools/plan-action.ts +767 -0
- package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
- package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
- package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
- package/plugins/bizar/tests/background-state.test.ts +277 -0
- package/plugins/bizar/tests/background.test.ts +402 -0
- package/plugins/bizar/tests/block.test.ts +195 -0
- package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
- package/plugins/bizar/tests/commands-impl.test.ts +442 -0
- package/plugins/bizar/tests/commands.test.ts +584 -0
- package/plugins/bizar/tests/config.test.ts +122 -0
- package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
- package/plugins/bizar/tests/dispose.test.ts +336 -0
- package/plugins/bizar/tests/event-stream.test.ts +409 -0
- package/plugins/bizar/tests/event.test.ts +262 -0
- package/plugins/bizar/tests/fingerprint.test.ts +162 -0
- package/plugins/bizar/tests/http-client.test.ts +404 -0
- package/plugins/bizar/tests/init-helpers.test.ts +203 -0
- package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
- package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
- package/plugins/bizar/tests/key-rotation.test.ts +396 -0
- package/plugins/bizar/tests/loop.test.ts +397 -0
- package/plugins/bizar/tests/options.test.ts +276 -0
- package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
- package/plugins/bizar/tests/serve.test.ts +339 -0
- package/plugins/bizar/tests/settings.test.ts +351 -0
- package/plugins/bizar/tests/stall-think.test.ts +750 -0
- package/plugins/bizar/tests/state.test.ts +276 -0
- package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
- package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
- package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
- package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
- package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
- package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
- package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
- package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
- package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
- package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
- package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
- package/plugins/bizar/tsconfig.json +29 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
// src/App.tsx — root shell. Wires data + contexts + tab routing.
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Topbar, TABS } from './components/Topbar';
|
|
5
|
+
import { Sidebar } from './components/Sidebar';
|
|
6
|
+
import { ModalProvider, useModal } from './components/Modal';
|
|
7
|
+
import { ToastProvider, useToast } from './components/Toast';
|
|
8
|
+
import { SearchModal } from './components/SearchModal';
|
|
9
|
+
import { Notifications } from './components/Notifications';
|
|
10
|
+
import { CommandDialog, type DialogDescriptor } from './components/CommandDialog';
|
|
11
|
+
import { api } from './lib/api';
|
|
12
|
+
import { Ws } from './lib/ws';
|
|
13
|
+
import {
|
|
14
|
+
applyTheme,
|
|
15
|
+
applyThemeTokens,
|
|
16
|
+
type Artifact,
|
|
17
|
+
type Settings,
|
|
18
|
+
type SettingsResponse,
|
|
19
|
+
type Snapshot,
|
|
20
|
+
type WsMessage,
|
|
21
|
+
type WsStatus,
|
|
22
|
+
type ProjectRecord,
|
|
23
|
+
type SearchResult,
|
|
24
|
+
} from './lib/types';
|
|
25
|
+
import { Overview } from './views/Overview';
|
|
26
|
+
import { Chat } from './views/Chat';
|
|
27
|
+
import { Agents } from './views/Agents';
|
|
28
|
+
import { Artifacts } from './views/Artifacts';
|
|
29
|
+
import { Tasks } from './views/Tasks';
|
|
30
|
+
import { Activity } from './views/Activity';
|
|
31
|
+
import { Config } from './views/Config';
|
|
32
|
+
import { SettingsView } from './views/Settings';
|
|
33
|
+
import { Mods } from './views/Mods';
|
|
34
|
+
import { ModView, type ModView as ModViewType } from './views/ModView';
|
|
35
|
+
import { Schedules } from './views/Schedules';
|
|
36
|
+
import { Skills } from './views/Skills';
|
|
37
|
+
import { History } from './views/History';
|
|
38
|
+
import { BackgroundAgents } from './views/BackgroundAgents';
|
|
39
|
+
import { Spinner } from './components/Spinner';
|
|
40
|
+
import { Button } from './components/Button';
|
|
41
|
+
import { AlertTriangle, Globe, LayoutTemplate, X } from 'lucide-react';
|
|
42
|
+
import './styles/main.css';
|
|
43
|
+
|
|
44
|
+
type ViewProps = {
|
|
45
|
+
snapshot: Snapshot;
|
|
46
|
+
settings: Settings;
|
|
47
|
+
activeTab: string;
|
|
48
|
+
setActiveTab: (id: string) => void;
|
|
49
|
+
refreshSnapshot: () => Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* v3.3.0 — Cross-view scratch state. The "Open in chat" button
|
|
52
|
+
* on the Agents tab sets `pendingInitialAgent`, which the Chat
|
|
53
|
+
* view reads on mount (and clears). Keeps the chat a true
|
|
54
|
+
* one-click handoff without needing URL state.
|
|
55
|
+
*/
|
|
56
|
+
crossState?: CrossViewState;
|
|
57
|
+
setCrossState?: (patch: Partial<CrossViewState>) => void;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type CrossViewState = {
|
|
61
|
+
initialAgent?: string | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const VIEW_MAP: Record<string, (p: ViewProps) => React.ReactNode> = {
|
|
65
|
+
overview: Overview,
|
|
66
|
+
chat: Chat,
|
|
67
|
+
agents: Agents,
|
|
68
|
+
artifacts: Artifacts,
|
|
69
|
+
tasks: Tasks,
|
|
70
|
+
activity: Activity,
|
|
71
|
+
background: BackgroundAgents,
|
|
72
|
+
config: Config,
|
|
73
|
+
settings: SettingsView,
|
|
74
|
+
mods: Mods,
|
|
75
|
+
schedules: Schedules,
|
|
76
|
+
skills: Skills,
|
|
77
|
+
history: History,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const VERSION = 'v3.21.0';
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Render the active view. If `activeTab` matches a built-in tab id,
|
|
84
|
+
* use VIEW_MAP. If it matches a mod view id (any id not in VIEW_MAP),
|
|
85
|
+
* render the ModView component with that view id.
|
|
86
|
+
*/
|
|
87
|
+
function renderActiveView(
|
|
88
|
+
activeTab: string,
|
|
89
|
+
viewProps: ViewProps,
|
|
90
|
+
modViews: ModViewType[],
|
|
91
|
+
reloadKey: number,
|
|
92
|
+
): React.ReactNode {
|
|
93
|
+
if (VIEW_MAP[activeTab]) {
|
|
94
|
+
const V = VIEW_MAP[activeTab];
|
|
95
|
+
return <V {...viewProps} />;
|
|
96
|
+
}
|
|
97
|
+
// Mod view — find by id
|
|
98
|
+
const mv = modViews.find((v) => v.id === activeTab);
|
|
99
|
+
if (mv) {
|
|
100
|
+
return (
|
|
101
|
+
<ModView
|
|
102
|
+
viewId={mv.id}
|
|
103
|
+
reloadKey={reloadKey}
|
|
104
|
+
activeTab={viewProps.activeTab}
|
|
105
|
+
setActiveTab={viewProps.setActiveTab}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
// Fallback to Overview
|
|
110
|
+
const V = VIEW_MAP.overview;
|
|
111
|
+
return <V {...viewProps} />;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function App() {
|
|
115
|
+
return (
|
|
116
|
+
<ToastProvider>
|
|
117
|
+
<ModalProvider>
|
|
118
|
+
<Shell />
|
|
119
|
+
</ModalProvider>
|
|
120
|
+
</ToastProvider>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function Shell() {
|
|
125
|
+
const toast = useToast();
|
|
126
|
+
const modalApi = useModal();
|
|
127
|
+
const { isModalOpen } = modalApi;
|
|
128
|
+
const isModalOpenRef = useRef(false);
|
|
129
|
+
const [activeTab, setActiveTab] = useState<string>('overview');
|
|
130
|
+
const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
|
|
131
|
+
const [settings, setSettings] = useState<Settings | null>(null);
|
|
132
|
+
const [wsStatus, setWsStatus] = useState<WsStatus>('connecting');
|
|
133
|
+
const [bootError, setBootError] = useState<string | null>(null);
|
|
134
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
135
|
+
const [stuckAgents, setStuckAgents] = useState<{ name: string }[]>([]);
|
|
136
|
+
const [stuckBannerDismissed, setStuckBannerDismissed] = useState(false);
|
|
137
|
+
// v3.20.3 — Mod views become first-class nav entries. Fetched on mount
|
|
138
|
+
// and after every mod install/uninstall/enable toggle.
|
|
139
|
+
const [modViews, setModViews] = useState<ModViewType[]>([]);
|
|
140
|
+
const [modViewsReloadKey, setModViewsReloadKey] = useState(0);
|
|
141
|
+
const wsRef = useRef<Ws | null>(null);
|
|
142
|
+
const [ws, setWs] = useState<Ws | null>(null);
|
|
143
|
+
|
|
144
|
+
// Fetch installed mod views (any enabled mod with a web/index.html or
|
|
145
|
+
// a views/registry.json entry). These become top-level nav tabs.
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
let cancelled = false;
|
|
148
|
+
(async () => {
|
|
149
|
+
try {
|
|
150
|
+
const r = await api.get<{ views: ModViewType[] }>('/mods/views');
|
|
151
|
+
if (cancelled) return;
|
|
152
|
+
setModViews(r.views || []);
|
|
153
|
+
} catch {
|
|
154
|
+
if (!cancelled) setModViews([]);
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
157
|
+
return () => { cancelled = true; };
|
|
158
|
+
}, [modViewsReloadKey, snapshot?.mods]);
|
|
159
|
+
|
|
160
|
+
// Re-fetch mod views when ws events arrive about mod changes.
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (!ws) return;
|
|
163
|
+
const off = ws.on((msg: { type?: string }) => {
|
|
164
|
+
if (msg?.type === 'mod:change' || msg?.type === 'mod:enabled' || msg?.type === 'mod:installed') {
|
|
165
|
+
setModViewsReloadKey((n) => n + 1);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return () => off();
|
|
169
|
+
}, [ws]);
|
|
170
|
+
|
|
171
|
+
// Apply theme tokens
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (settings?.theme) {
|
|
174
|
+
applyTheme(settings.theme);
|
|
175
|
+
applyThemeTokens(settings.theme);
|
|
176
|
+
}
|
|
177
|
+
}, [settings?.theme]);
|
|
178
|
+
|
|
179
|
+
// v3.6.1 — Keep a ref in sync with isModalOpen so the keyboard handler
|
|
180
|
+
// always reads the current value even between state updates and effect
|
|
181
|
+
// re-runs. This closes the race window where modal.close() has cleared
|
|
182
|
+
// the state but the old keyboard handler (with isModalOpen=false in
|
|
183
|
+
// its closure) hasn't been replaced yet.
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
isModalOpenRef.current = isModalOpen;
|
|
186
|
+
}, [isModalOpen]);
|
|
187
|
+
|
|
188
|
+
// System-mode listener
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (settings?.theme?.mode !== 'system') return;
|
|
191
|
+
const mql = window.matchMedia('(prefers-color-scheme: light)');
|
|
192
|
+
const handler = () => applyTheme(settings.theme.mode);
|
|
193
|
+
mql.addEventListener('change', handler);
|
|
194
|
+
return () => mql.removeEventListener('change', handler);
|
|
195
|
+
}, [settings?.theme?.mode]);
|
|
196
|
+
|
|
197
|
+
// Initial fetch — runs once on mount
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
let cancelled = false;
|
|
200
|
+
const loadBootData = () =>
|
|
201
|
+
Promise.all([
|
|
202
|
+
api.get<Snapshot>('/snapshot').catch(() => null),
|
|
203
|
+
api.get<SettingsResponse>('/settings').catch(() => null),
|
|
204
|
+
api.get<{ stuck: { name: string }[] }>('/agents/stuck').catch(() => null),
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
const applyBootData = (
|
|
208
|
+
snap: Snapshot | null,
|
|
209
|
+
set: SettingsResponse | null,
|
|
210
|
+
stuck: { stuck: { name: string }[] } | null,
|
|
211
|
+
) => {
|
|
212
|
+
if (snap) setSnapshot(snap);
|
|
213
|
+
if (set?.data) setSettings(set.data);
|
|
214
|
+
if (stuck?.stuck) setStuckAgents(stuck.stuck);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
(async () => {
|
|
218
|
+
try {
|
|
219
|
+
const auth = await api.probeAuthStatus();
|
|
220
|
+
const [snap, set, stuck] = await loadBootData();
|
|
221
|
+
if (cancelled) return;
|
|
222
|
+
|
|
223
|
+
applyBootData(snap, set, stuck);
|
|
224
|
+
if (snap || set) return;
|
|
225
|
+
|
|
226
|
+
if (!auth.loopback) {
|
|
227
|
+
setBootError('Dashboard server unreachable.');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
232
|
+
if (cancelled) return;
|
|
233
|
+
|
|
234
|
+
const [retrySnap, retrySet, retryStuck] = await loadBootData();
|
|
235
|
+
if (cancelled) return;
|
|
236
|
+
|
|
237
|
+
applyBootData(retrySnap, retrySet, retryStuck);
|
|
238
|
+
if (!retrySnap && !retrySet) {
|
|
239
|
+
setBootError('Dashboard server unreachable.');
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (cancelled) return;
|
|
243
|
+
const msg = (err as Error)?.message ?? 'unknown error';
|
|
244
|
+
setBootError(msg);
|
|
245
|
+
toast.error(`Failed to load: ${msg}`);
|
|
246
|
+
}
|
|
247
|
+
})();
|
|
248
|
+
|
|
249
|
+
return () => {
|
|
250
|
+
cancelled = true;
|
|
251
|
+
};
|
|
252
|
+
}, [toast]);
|
|
253
|
+
|
|
254
|
+
// Apply defaultTab ONCE on mount only — must NOT re-fire on toast changes
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
let cancelled = false;
|
|
257
|
+
api.get<SettingsResponse>('/settings')
|
|
258
|
+
.then((set) => {
|
|
259
|
+
if (cancelled) return;
|
|
260
|
+
const defaultTab = set?.data?.ui?.defaultTab;
|
|
261
|
+
if (defaultTab) setActiveTab(defaultTab);
|
|
262
|
+
})
|
|
263
|
+
.catch(() => undefined);
|
|
264
|
+
return () => {
|
|
265
|
+
cancelled = true;
|
|
266
|
+
};
|
|
267
|
+
}, []); // empty deps — runs once on mount
|
|
268
|
+
|
|
269
|
+
// v3.1.0 — Periodic stuck-agent poll. Light-weight; the server does
|
|
270
|
+
// the threshold math and only returns the list.
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
let cancelled = false;
|
|
273
|
+
const tick = async () => {
|
|
274
|
+
try {
|
|
275
|
+
const r = await api.get<{ stuck: { name: string }[] }>('/agents/stuck');
|
|
276
|
+
if (!cancelled) {
|
|
277
|
+
setStuckAgents((cur) => {
|
|
278
|
+
const next = r.stuck || [];
|
|
279
|
+
const wasEmpty = cur.length === 0;
|
|
280
|
+
if (wasEmpty && next.length > 0) {
|
|
281
|
+
toast.warning(`${next.length} agent${next.length === 1 ? '' : 's'} stuck`, 5000);
|
|
282
|
+
}
|
|
283
|
+
return next;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
/* best-effort */
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const id = setInterval(tick, 30_000);
|
|
291
|
+
tick();
|
|
292
|
+
return () => {
|
|
293
|
+
cancelled = true;
|
|
294
|
+
clearInterval(id);
|
|
295
|
+
};
|
|
296
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
297
|
+
}, []);
|
|
298
|
+
|
|
299
|
+
// WebSocket lifecycle
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
const ws = new Ws();
|
|
302
|
+
wsRef.current = ws;
|
|
303
|
+
setWs(ws);
|
|
304
|
+
const offStatus = ws.onStatus((s) => setWsStatus(s));
|
|
305
|
+
const offMsg = ws.on((msg: WsMessage) => {
|
|
306
|
+
if (msg.type === 'snapshot' && 'data' in msg && msg.data) {
|
|
307
|
+
setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...msg.data }));
|
|
308
|
+
} else if (msg.type === 'change') {
|
|
309
|
+
const m = msg;
|
|
310
|
+
const file = m.path?.split('/').pop() || m.path || '';
|
|
311
|
+
toast.info(`File changed: ${file}`, 2500);
|
|
312
|
+
api
|
|
313
|
+
.get<Snapshot>('/snapshot')
|
|
314
|
+
.then((s) => setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s })))
|
|
315
|
+
.catch(() => undefined);
|
|
316
|
+
} else if (msg.type === 'tasks:change') {
|
|
317
|
+
const m = msg;
|
|
318
|
+
setSnapshot((cur) => {
|
|
319
|
+
if (!cur) return cur;
|
|
320
|
+
const tasks = (cur.tasks || []).map((t) =>
|
|
321
|
+
t.id === m.task.id ? m.task : t,
|
|
322
|
+
);
|
|
323
|
+
const exists = tasks.some((t) => t.id === m.task.id);
|
|
324
|
+
return { ...cur, tasks: exists ? tasks : [m.task, ...tasks] };
|
|
325
|
+
});
|
|
326
|
+
} else if (msg.type === 'tasks:delete') {
|
|
327
|
+
const m = msg;
|
|
328
|
+
setSnapshot((cur) =>
|
|
329
|
+
cur
|
|
330
|
+
? { ...cur, tasks: (cur.tasks || []).filter((t) => t.id !== m.id) }
|
|
331
|
+
: cur,
|
|
332
|
+
);
|
|
333
|
+
} else if (msg.type === 'settings:change') {
|
|
334
|
+
const m = msg;
|
|
335
|
+
if (m.settings) setSettings(m.settings);
|
|
336
|
+
} else if (msg.type === 'project:change') {
|
|
337
|
+
// Refresh the whole snapshot to pick up the new active project
|
|
338
|
+
api
|
|
339
|
+
.get<Snapshot>('/snapshot')
|
|
340
|
+
.then((s) => setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s })))
|
|
341
|
+
.catch(() => undefined);
|
|
342
|
+
} else if (msg.type === 'agents:change' || msg.type === 'schedules:change') {
|
|
343
|
+
api
|
|
344
|
+
.get<Snapshot>('/snapshot')
|
|
345
|
+
.then((s) => setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s })))
|
|
346
|
+
.catch(() => undefined);
|
|
347
|
+
} else if (msg.type === 'agent:status' || msg.type === 'agent:restarted') {
|
|
348
|
+
const m = msg;
|
|
349
|
+
setSnapshot((cur) => {
|
|
350
|
+
if (!cur) return cur;
|
|
351
|
+
const agents = (cur.agents || []).map((a) => (a.name === m.agent.name ? m.agent : a));
|
|
352
|
+
return { ...cur, agents };
|
|
353
|
+
});
|
|
354
|
+
} else if (msg.type === 'artifact:change') {
|
|
355
|
+
// Artifacts list refreshes after any artifact mutation.
|
|
356
|
+
api
|
|
357
|
+
.get<{ artifacts: Artifact[] }>('/artifacts')
|
|
358
|
+
.then((d) => {
|
|
359
|
+
setSnapshot((cur) => (cur ? { ...cur, artifacts: d.artifacts || [] } : cur));
|
|
360
|
+
})
|
|
361
|
+
.catch(() => undefined);
|
|
362
|
+
} else if (msg.type === 'agent:stuck') {
|
|
363
|
+
const m = msg;
|
|
364
|
+
setStuckAgents(m.agents || []);
|
|
365
|
+
} else if (msg.type === 'dialog:show') {
|
|
366
|
+
// v0.5.1: Open a command dialog received from the plugin via WS broadcast.
|
|
367
|
+
const m = msg as { type: 'dialog:show'; dialog: DialogDescriptor };
|
|
368
|
+
if (m.dialog) {
|
|
369
|
+
modalApi.open({
|
|
370
|
+
title: m.dialog.title,
|
|
371
|
+
width: 520,
|
|
372
|
+
children: <CommandDialog dialog={m.dialog} onClose={() => modalApi.close()} />,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
return () => {
|
|
378
|
+
offStatus();
|
|
379
|
+
offMsg();
|
|
380
|
+
ws.close();
|
|
381
|
+
setWs(null);
|
|
382
|
+
};
|
|
383
|
+
}, [toast]);
|
|
384
|
+
|
|
385
|
+
const subscribeToWs = useCallback((cb: (msg: WsMessage) => void) => {
|
|
386
|
+
if (!ws) return () => undefined;
|
|
387
|
+
return ws.on(cb);
|
|
388
|
+
}, [ws]);
|
|
389
|
+
|
|
390
|
+
// v3.3.1 — Track recent user interactions. After any mousedown/click/
|
|
391
|
+
// focusin/keydown we set a 1500ms "safe" window during which digit-key
|
|
392
|
+
// shortcuts are suppressed. This protects against the case where a modal
|
|
393
|
+
// closes on a click and the user's next key event (often a key-repeat)
|
|
394
|
+
// would otherwise trigger setActiveTab("overview").
|
|
395
|
+
// v3.3.0 used 250ms which was too short (keyboard repeat can fire later).
|
|
396
|
+
//
|
|
397
|
+
// v3.5.4 (bug #4) — The safe window alone was not enough. Users were
|
|
398
|
+
// still being bounced to overview after action grace windows expired
|
|
399
|
+
// because the transientFocus check (activeElement === body) was nested
|
|
400
|
+
// inside the safe-window branch. See the handler below for the new
|
|
401
|
+
// two-gate model: (1) body/null focus ALWAYS blocks digit shortcuts;
|
|
402
|
+
// (2) the safe window relaxes that block only for the first 1500ms
|
|
403
|
+
// after an interaction. After the window passes, the user must
|
|
404
|
+
// explicitly focus a real element before digit-key tab switches work.
|
|
405
|
+
const safeUntilRef = useRef(0);
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
const bump = () => {
|
|
408
|
+
safeUntilRef.current = Date.now() + 1500;
|
|
409
|
+
};
|
|
410
|
+
document.addEventListener('mousedown', bump, true);
|
|
411
|
+
document.addEventListener('click', bump, true);
|
|
412
|
+
document.addEventListener('focusin', bump, true);
|
|
413
|
+
document.addEventListener('keydown', bump, true);
|
|
414
|
+
return () => {
|
|
415
|
+
document.removeEventListener('mousedown', bump, true);
|
|
416
|
+
document.removeEventListener('click', bump, true);
|
|
417
|
+
document.removeEventListener('focusin', bump, true);
|
|
418
|
+
document.removeEventListener('keydown', bump, true);
|
|
419
|
+
};
|
|
420
|
+
}, []);
|
|
421
|
+
|
|
422
|
+
// Keyboard shortcuts
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
const map: Record<string, string> = {};
|
|
425
|
+
TABS.forEach((t, i) => {
|
|
426
|
+
map[String(i + 1)] = t.id;
|
|
427
|
+
});
|
|
428
|
+
const handler = (e: KeyboardEvent) => {
|
|
429
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
430
|
+
e.preventDefault();
|
|
431
|
+
setSearchOpen(true);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (e.key === '/' && !e.metaKey && !e.ctrlKey) {
|
|
435
|
+
const t = (e.target as HTMLElement | null)?.tagName?.toLowerCase();
|
|
436
|
+
if (t !== 'input' && t !== 'textarea' && !((e.target as HTMLElement)?.isContentEditable)) {
|
|
437
|
+
e.preventDefault();
|
|
438
|
+
setSearchOpen(true);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// v3.2.0 — broader guard. Digit-key shortcuts must not fire while
|
|
443
|
+
// the user is interacting with ANY focusable form control or a
|
|
444
|
+
// modal/contentEditable region. Previously this only excluded
|
|
445
|
+
// <input> and <textarea>, which let digit presses through when
|
|
446
|
+
// focus was on a <select>, <button>, <label>, or contenteditable
|
|
447
|
+
// node — causing mysterious tab switches (most often back to
|
|
448
|
+
// "overview" via the `1` shortcut).
|
|
449
|
+
//
|
|
450
|
+
// v3.3.1 — "click after modal close" guard (extended). When a modal
|
|
451
|
+
// closes (e.g. the user clicks "Submit to Odin" in the Tasks tab),
|
|
452
|
+
// React portals the modal out of the DOM and the previously-focused
|
|
453
|
+
// button is unmounted. Focus falls back to <body>. If the user then
|
|
454
|
+
// accidentally taps a digit key (1/2/3/...) — or a key-repeat fires
|
|
455
|
+
// while they're still mid-click — the handler would switch tabs.
|
|
456
|
+
// The safe window is now 1500ms (up from 250ms) and covers all
|
|
457
|
+
// interaction types (mousedown/click/focusin/keydown).
|
|
458
|
+
//
|
|
459
|
+
// v3.3.1 — transient-focus check. If active element is body/null,
|
|
460
|
+
// treat it as a transient focus state (modal close → body fallback)
|
|
461
|
+
// and bail. v3.5.4 (bug #4): this check is now independent of the
|
|
462
|
+
// safe window — it ALWAYS applies. Previously it was only effective
|
|
463
|
+
// inside the 1500ms grace window, which let digit-key presses through
|
|
464
|
+
// once the window expired. The user would see themselves bounced back
|
|
465
|
+
// to overview (digit `1`) after every action whose grace window had
|
|
466
|
+
// already elapsed. The safe window now only relaxes the rule
|
|
467
|
+
// temporarily for the action that JUST happened; it must be combined
|
|
468
|
+
// with a real focus element for digit-key shortcuts to fire.
|
|
469
|
+
const activeEl = document.activeElement;
|
|
470
|
+
const transientFocus = !activeEl || activeEl === document.body || activeEl === document.documentElement;
|
|
471
|
+
if (transientFocus) {
|
|
472
|
+
// Even within the grace window, body/null focus means there is
|
|
473
|
+
// no real input target — refuse to switch tabs. The user must
|
|
474
|
+
// explicitly click into a focusable element (input, button, etc.)
|
|
475
|
+
// to enable digit-key tab switching again.
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const target = e.target as HTMLElement | null;
|
|
479
|
+
const tag = target?.tagName?.toLowerCase();
|
|
480
|
+
const isFormControl =
|
|
481
|
+
tag === 'input' ||
|
|
482
|
+
tag === 'textarea' ||
|
|
483
|
+
tag === 'select' ||
|
|
484
|
+
tag === 'button' ||
|
|
485
|
+
tag === 'option' ||
|
|
486
|
+
tag === 'label' ||
|
|
487
|
+
!!target?.isContentEditable;
|
|
488
|
+
// Inside a form? Even safer: walk up to check.
|
|
489
|
+
let inForm = false;
|
|
490
|
+
if (target && typeof target.closest === 'function') {
|
|
491
|
+
inForm = !!target.closest('form, [role="dialog"], [contenteditable], [data-no-key]');
|
|
492
|
+
}
|
|
493
|
+
// v3.5.2 — reject key-repeat events. When a user presses and holds
|
|
494
|
+
// a digit key the browser fires repeated keydown events; we must only
|
|
495
|
+
// act on the first (e.repeat === false). This prevents the modal-close
|
|
496
|
+
// + key-repeat scenario from switching tabs after the safe window closes.
|
|
497
|
+
if (e.repeat) return;
|
|
498
|
+
// v3.6.1 — Check isModalOpenRef (not the closure variable) so we always
|
|
499
|
+
// see the current value even between state update and effect re-run.
|
|
500
|
+
if (isModalOpenRef.current) return;
|
|
501
|
+
if (
|
|
502
|
+
isFormControl ||
|
|
503
|
+
inForm ||
|
|
504
|
+
e.metaKey ||
|
|
505
|
+
e.ctrlKey ||
|
|
506
|
+
e.altKey ||
|
|
507
|
+
e.shiftKey ||
|
|
508
|
+
Date.now() < safeUntilRef.current
|
|
509
|
+
)
|
|
510
|
+
return;
|
|
511
|
+
const id = map[e.key];
|
|
512
|
+
if (id) {
|
|
513
|
+
e.preventDefault();
|
|
514
|
+
setActiveTab(id);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
document.addEventListener('keydown', handler);
|
|
518
|
+
return () => document.removeEventListener('keydown', handler);
|
|
519
|
+
}, []);
|
|
520
|
+
|
|
521
|
+
const refreshSnapshot = useMemo(
|
|
522
|
+
() => async () => {
|
|
523
|
+
try {
|
|
524
|
+
const s = await api.get<Snapshot>('/snapshot');
|
|
525
|
+
setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s }));
|
|
526
|
+
} catch (err) {
|
|
527
|
+
toast.error(`Refresh failed: ${(err as Error).message}`);
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
[toast],
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
// v3.20.3 — Merge built-in tabs with installed mod views. Mod views
|
|
534
|
+
// are appended after the built-in tabs and use the same rendering
|
|
535
|
+
// surface; their tab ids are mod view ids (e.g. 'graphify:web').
|
|
536
|
+
// Declared AFTER refreshSnapshot so the deps list has the right value.
|
|
537
|
+
const mergedTabs = useMemo(() => {
|
|
538
|
+
const modTabs = modViews.map((v) => ({
|
|
539
|
+
id: v.id,
|
|
540
|
+
label: v.label,
|
|
541
|
+
icon: v.kind === 'iframe' ? Globe : LayoutTemplate,
|
|
542
|
+
isMod: true,
|
|
543
|
+
modId: v.modId,
|
|
544
|
+
}));
|
|
545
|
+
return [...TABS, ...modTabs];
|
|
546
|
+
}, [modViews]);
|
|
547
|
+
|
|
548
|
+
// Surface the underlying view for the active tab (built-in or mod).
|
|
549
|
+
// Declared AFTER refreshSnapshot so its useMemo factory can capture it.
|
|
550
|
+
const renderedView = useMemo(() => {
|
|
551
|
+
if (snapshot && settings) {
|
|
552
|
+
const viewProps: ViewProps = {
|
|
553
|
+
snapshot,
|
|
554
|
+
settings,
|
|
555
|
+
activeTab,
|
|
556
|
+
setActiveTab: setActiveTab,
|
|
557
|
+
refreshSnapshot,
|
|
558
|
+
};
|
|
559
|
+
return renderActiveView(activeTab, viewProps, modViews, modViewsReloadKey);
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
}, [activeTab, snapshot, settings, modViews, modViewsReloadKey, refreshSnapshot]);
|
|
563
|
+
|
|
564
|
+
const refreshProjects = async () => {
|
|
565
|
+
try {
|
|
566
|
+
const r = await api.get<{ projects: ProjectRecord[]; active: string | null }>('/projects');
|
|
567
|
+
setSnapshot((cur) =>
|
|
568
|
+
cur ? { ...cur, projects: r.projects || [], activeProject: r.projects?.find((p) => p.id === r.active) || null } : cur,
|
|
569
|
+
);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
toast.error(`Projects refresh failed: ${(err as Error).message}`);
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const onActivateProject = async (id: string) => {
|
|
576
|
+
try {
|
|
577
|
+
await api.post(`/projects/${encodeURIComponent(id)}/activate`);
|
|
578
|
+
// WS will fire project:change and refresh; but also reflect locally
|
|
579
|
+
setSnapshot((cur) => {
|
|
580
|
+
if (!cur) return cur;
|
|
581
|
+
return {
|
|
582
|
+
...cur,
|
|
583
|
+
activeProject: cur.projects.find((p) => p.id === id) || null,
|
|
584
|
+
};
|
|
585
|
+
});
|
|
586
|
+
} catch (err) {
|
|
587
|
+
toast.error(`Activate failed: ${(err as Error).message}`);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const onSearchSelect = (r: SearchResult) => {
|
|
592
|
+
const t = r.type;
|
|
593
|
+
if (t === 'agent') setActiveTab('agents');
|
|
594
|
+
else if (t === 'task') setActiveTab('tasks');
|
|
595
|
+
else if (t === 'mod') setActiveTab('mods');
|
|
596
|
+
else if (t === 'schedule') setActiveTab('schedules');
|
|
597
|
+
else if (t === 'project') {
|
|
598
|
+
const id = (r.item as ProjectRecord).id;
|
|
599
|
+
onActivateProject(id);
|
|
600
|
+
} else if (t === 'command') {
|
|
601
|
+
// No command page; just toast
|
|
602
|
+
toast.info(`/${(r.item as { name: string }).name} — run from the TUI`, 2500);
|
|
603
|
+
} else if (t === 'setting') {
|
|
604
|
+
// v3.0.4 — Jump to Settings view and scroll to the matching row.
|
|
605
|
+
const settingId = (r.item as { id?: string; path?: string }).id
|
|
606
|
+
|| (r.item as { path?: string }).path
|
|
607
|
+
|| '';
|
|
608
|
+
setActiveTab('settings');
|
|
609
|
+
// Defer the scroll to allow the view to mount, then apply a brief
|
|
610
|
+
// CSS highlight. We poll a few times because the Settings view
|
|
611
|
+
// mounts lazily after tab switch.
|
|
612
|
+
const tryScroll = (tries: number) => {
|
|
613
|
+
if (tries <= 0) return;
|
|
614
|
+
const el = settingId
|
|
615
|
+
? document.querySelector(`[data-setting-id="${CSS.escape(settingId)}"]`)
|
|
616
|
+
: null;
|
|
617
|
+
if (el) {
|
|
618
|
+
(el as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
619
|
+
el.classList.remove('setting-flash');
|
|
620
|
+
// Force reflow so the animation re-triggers on rapid jumps.
|
|
621
|
+
void (el as HTMLElement).offsetWidth;
|
|
622
|
+
el.classList.add('setting-flash');
|
|
623
|
+
window.setTimeout(() => el.classList.remove('setting-flash'), 1500);
|
|
624
|
+
} else {
|
|
625
|
+
window.setTimeout(() => tryScroll(tries - 1), 80);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
window.setTimeout(() => tryScroll(15), 60);
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const layout = settings?.ui?.layout || 'topnav';
|
|
633
|
+
const showHeader = settings?.ui?.showHeader !== false;
|
|
634
|
+
|
|
635
|
+
return (
|
|
636
|
+
<div className="app" data-layout={layout} data-active-tab={activeTab}>
|
|
637
|
+
{showHeader && (
|
|
638
|
+
<Topbar
|
|
639
|
+
activeTab={activeTab}
|
|
640
|
+
onTabChange={setActiveTab}
|
|
641
|
+
wsStatus={wsStatus}
|
|
642
|
+
version={VERSION}
|
|
643
|
+
activeProject={snapshot?.activeProject || null}
|
|
644
|
+
projects={snapshot?.projects || []}
|
|
645
|
+
onProjectChange={onActivateProject}
|
|
646
|
+
onProjectsRefresh={refreshProjects}
|
|
647
|
+
onOpenSearch={() => setSearchOpen(true)}
|
|
648
|
+
settings={settings}
|
|
649
|
+
notificationsSlot={<Notifications wsSubscribe={subscribeToWs} />}
|
|
650
|
+
showTabs={layout === 'topnav'}
|
|
651
|
+
extraTabs={mergedTabs}
|
|
652
|
+
/>
|
|
653
|
+
)}
|
|
654
|
+
{stuckAgents.length > 0 && !stuckBannerDismissed && (
|
|
655
|
+
<div className="stuck-banner" role="alert">
|
|
656
|
+
<AlertTriangle size={16} />
|
|
657
|
+
<span>
|
|
658
|
+
<strong>{stuckAgents.length}</strong> agent{stuckAgents.length === 1 ? '' : 's'} stuck:{' '}
|
|
659
|
+
<span className="mono">
|
|
660
|
+
{stuckAgents.map((a) => a.name).join(', ')}
|
|
661
|
+
</span>
|
|
662
|
+
</span>
|
|
663
|
+
<Button
|
|
664
|
+
variant="secondary"
|
|
665
|
+
size="sm"
|
|
666
|
+
onClick={() => {
|
|
667
|
+
setActiveTab('agents');
|
|
668
|
+
setStuckBannerDismissed(true);
|
|
669
|
+
}}
|
|
670
|
+
>
|
|
671
|
+
Review
|
|
672
|
+
</Button>
|
|
673
|
+
<button
|
|
674
|
+
type="button"
|
|
675
|
+
className="icon-btn"
|
|
676
|
+
aria-label="Dismiss"
|
|
677
|
+
onClick={() => setStuckBannerDismissed(true)}
|
|
678
|
+
>
|
|
679
|
+
<X size={14} />
|
|
680
|
+
</button>
|
|
681
|
+
</div>
|
|
682
|
+
)}
|
|
683
|
+
<div className="layout-body">
|
|
684
|
+
{layout !== 'topnav' && (
|
|
685
|
+
<Sidebar
|
|
686
|
+
tabs={mergedTabs}
|
|
687
|
+
activeTab={activeTab}
|
|
688
|
+
onTabChange={setActiveTab}
|
|
689
|
+
/>
|
|
690
|
+
)}
|
|
691
|
+
<main className="content">
|
|
692
|
+
{bootError && (
|
|
693
|
+
<div className="boot-error">
|
|
694
|
+
<h2>Dashboard unavailable</h2>
|
|
695
|
+
<p>{bootError}</p>
|
|
696
|
+
<p className="boot-error-hint">
|
|
697
|
+
Make sure the Bizar dashboard server is running. Try{' '}
|
|
698
|
+
<code>bizar-dash start</code> in your terminal.
|
|
699
|
+
</p>
|
|
700
|
+
</div>
|
|
701
|
+
)}
|
|
702
|
+
{!bootError && (!snapshot || !settings) && (
|
|
703
|
+
<div className="loading">
|
|
704
|
+
<Spinner size="lg" />
|
|
705
|
+
<p>Loading Bizar…</p>
|
|
706
|
+
</div>
|
|
707
|
+
)}
|
|
708
|
+
{renderedView}
|
|
709
|
+
</main>
|
|
710
|
+
</div>
|
|
711
|
+
<SearchModal
|
|
712
|
+
open={searchOpen}
|
|
713
|
+
onClose={() => setSearchOpen(false)}
|
|
714
|
+
onSelect={onSearchSelect}
|
|
715
|
+
/>
|
|
716
|
+
</div>
|
|
717
|
+
);
|
|
718
|
+
}
|