@swarmclawai/swarmclaw 0.8.4 → 0.8.7
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 +9 -9
- package/bin/swarmclaw.js +5 -1
- package/bin/worker-cmd.js +73 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +17 -7
- package/src/app/api/agents/route.ts +21 -8
- package/src/app/api/approvals/route.test.ts +6 -6
- package/src/app/api/approvals/route.ts +2 -1
- package/src/app/api/auth/route.ts +2 -3
- package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
- package/src/app/api/chatrooms/[id]/route.ts +7 -6
- package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/clear/route.ts +9 -9
- package/src/app/api/chats/[id]/devserver/route.ts +2 -1
- package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
- package/src/app/api/chats/[id]/fork/route.ts +3 -5
- package/src/app/api/chats/[id]/restore/route.ts +6 -7
- package/src/app/api/chats/[id]/retry/route.ts +3 -4
- package/src/app/api/chats/[id]/route.ts +61 -62
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/connectors/[id]/route.ts +7 -8
- package/src/app/api/connectors/route.ts +5 -4
- package/src/app/api/eval/run/route.ts +2 -1
- package/src/app/api/eval/suite/route.ts +2 -1
- package/src/app/api/external-agents/route.test.ts +1 -1
- package/src/app/api/external-agents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +1 -1
- package/src/app/api/gateways/[id]/route.ts +7 -5
- package/src/app/api/gateways/route.ts +1 -1
- package/src/app/api/knowledge/upload/route.ts +1 -1
- package/src/app/api/logs/route.ts +5 -7
- package/src/app/api/memory-images/[filename]/route.ts +2 -3
- package/src/app/api/openclaw/agent-files/route.ts +4 -3
- package/src/app/api/openclaw/approvals/route.ts +3 -4
- package/src/app/api/openclaw/config-sync/route.ts +3 -2
- package/src/app/api/openclaw/cron/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
- package/src/app/api/openclaw/exec-config/route.ts +3 -2
- package/src/app/api/openclaw/gateway/route.ts +5 -4
- package/src/app/api/openclaw/history/route.ts +3 -2
- package/src/app/api/openclaw/media/route.ts +2 -1
- package/src/app/api/openclaw/permissions/route.ts +3 -2
- package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
- package/src/app/api/openclaw/skills/install/route.ts +2 -1
- package/src/app/api/openclaw/skills/remove/route.ts +2 -1
- package/src/app/api/openclaw/skills/route.ts +3 -2
- package/src/app/api/orchestrator/run/route.ts +5 -14
- package/src/app/api/perf/route.ts +43 -0
- package/src/app/api/plugins/dependencies/route.ts +2 -1
- package/src/app/api/plugins/install/route.ts +2 -1
- package/src/app/api/plugins/marketplace/route.ts +3 -2
- package/src/app/api/plugins/settings/route.ts +2 -1
- package/src/app/api/preview-server/route.ts +11 -10
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/schedules/[id]/route.test.ts +128 -0
- package/src/app/api/schedules/[id]/route.ts +43 -43
- package/src/app/api/schedules/[id]/run/route.ts +11 -62
- package/src/app/api/schedules/route.ts +21 -87
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/doctor/route.ts +9 -8
- package/src/app/api/tasks/[id]/approve/route.ts +33 -30
- package/src/app/api/tasks/[id]/route.ts +12 -35
- package/src/app/api/tasks/import/github/route.ts +2 -1
- package/src/app/api/tasks/route.ts +79 -91
- package/src/app/api/wallets/[id]/approve/route.ts +2 -1
- package/src/app/api/wallets/[id]/route.ts +13 -19
- package/src/app/api/wallets/[id]/send/route.ts +2 -1
- package/src/app/api/wallets/route.ts +2 -1
- package/src/app/api/webhooks/[id]/route.ts +2 -1
- package/src/app/api/webhooks/route.test.ts +3 -1
- package/src/app/page.tsx +23 -331
- package/src/cli/index.js +19 -0
- package/src/cli/index.ts +38 -7
- package/src/cli/spec.js +9 -0
- package/src/components/activity/activity-feed.tsx +7 -4
- package/src/components/agents/agent-card.tsx +32 -6
- package/src/components/agents/agent-chat-list.tsx +55 -22
- package/src/components/agents/agent-files-editor.tsx +3 -2
- package/src/components/agents/agent-sheet.tsx +123 -22
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/openclaw-skills-panel.tsx +2 -1
- package/src/components/agents/trash-list.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +8 -2
- package/src/components/auth/setup-wizard.tsx +10 -9
- package/src/components/auth/user-picker.tsx +3 -2
- package/src/components/chat/chat-area.tsx +20 -1
- package/src/components/chat/chat-card.tsx +18 -3
- package/src/components/chat/chat-header.tsx +24 -4
- package/src/components/chat/chat-list.tsx +2 -11
- package/src/components/chat/heartbeat-history-panel.tsx +2 -1
- package/src/components/chat/message-bubble.tsx +45 -6
- package/src/components/chat/message-list.tsx +280 -145
- package/src/components/chat/streaming-bubble.tsx +217 -60
- package/src/components/chat/swarm-panel.test.ts +274 -0
- package/src/components/chat/swarm-panel.tsx +410 -0
- package/src/components/chat/swarm-status-card.tsx +346 -0
- package/src/components/chat/tool-call-bubble.tsx +48 -23
- package/src/components/chatrooms/chatroom-list.tsx +8 -5
- package/src/components/chatrooms/chatroom-message.tsx +10 -7
- package/src/components/chatrooms/chatroom-view.tsx +12 -9
- package/src/components/connectors/connector-health.tsx +6 -4
- package/src/components/connectors/connector-list.tsx +16 -11
- package/src/components/connectors/connector-sheet.tsx +12 -6
- package/src/components/home/home-view.tsx +38 -24
- package/src/components/input/chat-input.tsx +10 -1
- package/src/components/layout/app-layout.tsx +2 -38
- package/src/components/layout/sheet-layer.tsx +50 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
- package/src/components/plugins/plugin-list.tsx +8 -4
- package/src/components/plugins/plugin-sheet.tsx +2 -1
- package/src/components/providers/provider-list.tsx +3 -2
- package/src/components/providers/provider-sheet.tsx +2 -1
- package/src/components/runs/run-list.tsx +11 -7
- package/src/components/schedules/schedule-card.tsx +5 -3
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/attachment-chip.tsx +19 -3
- package/src/components/shared/notification-center.tsx +6 -3
- package/src/components/shared/settings/plugin-manager.tsx +3 -2
- package/src/components/shared/settings/section-embedding.tsx +2 -1
- package/src/components/shared/settings/section-orchestrator.tsx +2 -1
- package/src/components/shared/settings/section-user-preferences.tsx +107 -0
- package/src/components/shared/settings/settings-page.tsx +13 -9
- package/src/components/skills/clawhub-browser.tsx +15 -4
- package/src/components/skills/skill-list.tsx +15 -4
- package/src/components/tasks/approvals-panel.tsx +2 -1
- package/src/components/tasks/task-board.tsx +35 -37
- package/src/components/tasks/task-sheet.tsx +4 -3
- package/src/components/ui/full-screen-loader.tsx +164 -0
- package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
- package/src/components/wallets/wallet-panel.tsx +6 -5
- package/src/components/wallets/wallet-section.tsx +3 -2
- package/src/components/webhooks/webhook-list.tsx +4 -5
- package/src/components/webhooks/webhook-sheet.tsx +6 -6
- package/src/hooks/use-app-bootstrap.ts +202 -0
- package/src/hooks/use-mounted-ref.ts +14 -0
- package/src/hooks/use-now.ts +31 -0
- package/src/hooks/use-openclaw-gateway.ts +2 -1
- package/src/instrumentation.ts +20 -8
- package/src/lib/agent-default-tools.test.ts +52 -0
- package/src/lib/agent-default-tools.ts +40 -0
- package/src/lib/api-client.test.ts +21 -0
- package/src/lib/api-client.ts +6 -11
- package/src/lib/canvas-content.test.ts +360 -0
- package/src/lib/chat-streaming-state.test.ts +49 -2
- package/src/lib/chat-streaming-state.ts +26 -10
- package/src/lib/fetch-timeout.test.ts +54 -0
- package/src/lib/fetch-timeout.ts +60 -3
- package/src/lib/live-tool-events.test.ts +77 -0
- package/src/lib/live-tool-events.ts +73 -0
- package/src/lib/local-observability.test.ts +2 -2
- package/src/lib/openclaw-endpoint.test.ts +1 -1
- package/src/lib/providers/anthropic.ts +12 -16
- package/src/lib/providers/index.ts +4 -2
- package/src/lib/providers/ollama.ts +9 -6
- package/src/lib/providers/openai.ts +11 -14
- package/src/lib/runtime-env.test.ts +8 -8
- package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
- package/src/lib/schedule-dedupe.test.ts +1 -1
- package/src/lib/schedule-dedupe.ts +3 -2
- package/src/lib/server/agent-thread-session.test.ts +6 -6
- package/src/lib/server/agent-thread-session.ts +6 -9
- package/src/lib/server/alert-dispatch.ts +2 -1
- package/src/lib/server/api-routes.test.ts +6 -6
- package/src/lib/server/approval-connector-notify.test.ts +4 -4
- package/src/lib/server/approvals-auto-approve.test.ts +29 -29
- package/src/lib/server/approvals.test.ts +317 -0
- package/src/lib/server/approvals.ts +5 -4
- package/src/lib/server/autonomy-runtime.test.ts +11 -11
- package/src/lib/server/browser-state.ts +2 -2
- package/src/lib/server/capability-router.test.ts +1 -1
- package/src/lib/server/capability-router.ts +3 -2
- package/src/lib/server/chat-execution-advanced.test.ts +15 -2
- package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
- package/src/lib/server/chat-execution-disabled.test.ts +3 -3
- package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
- package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
- package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
- package/src/lib/server/chat-execution-tool-events.ts +116 -0
- package/src/lib/server/chat-execution-utils.test.ts +479 -0
- package/src/lib/server/chat-execution-utils.ts +533 -0
- package/src/lib/server/chat-execution.ts +153 -748
- package/src/lib/server/chat-streaming-utils.ts +174 -0
- package/src/lib/server/chat-turn-tool-routing.ts +310 -0
- package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
- package/src/lib/server/clawhub-client.ts +2 -1
- package/src/lib/server/collection-helpers.test.ts +92 -0
- package/src/lib/server/collection-helpers.ts +25 -3
- package/src/lib/server/connectors/access.ts +146 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
- package/src/lib/server/connectors/bluebubbles.ts +4 -4
- package/src/lib/server/connectors/commands.ts +367 -0
- package/src/lib/server/connectors/connector-routing.test.ts +4 -4
- package/src/lib/server/connectors/delivery.ts +142 -0
- package/src/lib/server/connectors/discord.ts +37 -40
- package/src/lib/server/connectors/email.ts +11 -10
- package/src/lib/server/connectors/googlechat.ts +4 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
- package/src/lib/server/connectors/ingress-delivery.ts +23 -0
- package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
- package/src/lib/server/connectors/manager.test.ts +352 -77
- package/src/lib/server/connectors/manager.ts +134 -673
- package/src/lib/server/connectors/matrix.ts +4 -4
- package/src/lib/server/connectors/message-sentinel.ts +7 -0
- package/src/lib/server/connectors/openclaw.test.ts +1 -1
- package/src/lib/server/connectors/openclaw.ts +8 -10
- package/src/lib/server/connectors/outbox.test.ts +192 -0
- package/src/lib/server/connectors/outbox.ts +369 -0
- package/src/lib/server/connectors/pairing.test.ts +18 -1
- package/src/lib/server/connectors/pairing.ts +49 -4
- package/src/lib/server/connectors/policy.ts +9 -3
- package/src/lib/server/connectors/reconnect-state.ts +71 -0
- package/src/lib/server/connectors/response-media.ts +256 -0
- package/src/lib/server/connectors/runtime-state.ts +67 -0
- package/src/lib/server/connectors/session.test.ts +357 -0
- package/src/lib/server/connectors/session.ts +422 -0
- package/src/lib/server/connectors/signal.ts +7 -7
- package/src/lib/server/connectors/slack.ts +43 -43
- package/src/lib/server/connectors/teams.ts +4 -4
- package/src/lib/server/connectors/telegram.ts +37 -43
- package/src/lib/server/connectors/types.ts +31 -1
- package/src/lib/server/connectors/whatsapp.test.ts +108 -0
- package/src/lib/server/connectors/whatsapp.ts +106 -34
- package/src/lib/server/context-manager.test.ts +409 -0
- package/src/lib/server/cost.test.ts +1 -1
- package/src/lib/server/daemon-policy.ts +78 -0
- package/src/lib/server/daemon-state-connectors.test.ts +167 -0
- package/src/lib/server/daemon-state.test.ts +283 -55
- package/src/lib/server/daemon-state.ts +106 -109
- package/src/lib/server/data-dir.test.ts +5 -5
- package/src/lib/server/data-dir.ts +4 -0
- package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
- package/src/lib/server/delegation-jobs.test.ts +87 -0
- package/src/lib/server/delegation-jobs.ts +42 -48
- package/src/lib/server/devserver-launch.ts +1 -1
- package/src/lib/server/document-utils.ts +7 -9
- package/src/lib/server/elevenlabs.ts +2 -1
- package/src/lib/server/embeddings.test.ts +105 -0
- package/src/lib/server/ethereum.ts +3 -2
- package/src/lib/server/eval/agent-regression.ts +3 -2
- package/src/lib/server/eval/runner.ts +2 -1
- package/src/lib/server/eval/scorer.ts +2 -1
- package/src/lib/server/evm-swap.ts +2 -1
- package/src/lib/server/gateway/protocol.test.ts +1 -1
- package/src/lib/server/guardian.ts +2 -1
- package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
- package/src/lib/server/heartbeat-service.test.ts +406 -0
- package/src/lib/server/heartbeat-service.ts +54 -7
- package/src/lib/server/heartbeat-wake.test.ts +19 -0
- package/src/lib/server/heartbeat-wake.ts +17 -16
- package/src/lib/server/integrity-monitor.test.ts +149 -0
- package/src/lib/server/json-utils.ts +22 -0
- package/src/lib/server/knowledge-db.test.ts +13 -13
- package/src/lib/server/link-understanding.ts +2 -1
- package/src/lib/server/llm-response-cache.test.ts +1 -1
- package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
- package/src/lib/server/main-agent-loop.test.ts +6 -6
- package/src/lib/server/main-agent-loop.ts +21 -7
- package/src/lib/server/mcp-client.test.ts +1 -1
- package/src/lib/server/mcp-conformance.test.ts +1 -1
- package/src/lib/server/mcp-conformance.ts +3 -2
- package/src/lib/server/memory-consolidation.ts +2 -1
- package/src/lib/server/memory-db.test.ts +485 -0
- package/src/lib/server/memory-db.ts +39 -26
- package/src/lib/server/memory-graph.test.ts +2 -2
- package/src/lib/server/memory-policy.test.ts +7 -7
- package/src/lib/server/memory-retrieval.test.ts +1 -1
- package/src/lib/server/openclaw-config-sync.ts +2 -1
- package/src/lib/server/openclaw-deploy.test.ts +1 -1
- package/src/lib/server/openclaw-deploy.ts +8 -12
- package/src/lib/server/openclaw-exec-config.ts +2 -1
- package/src/lib/server/openclaw-gateway.ts +6 -7
- package/src/lib/server/openclaw-skills-normalize.ts +2 -1
- package/src/lib/server/openclaw-sync.ts +7 -5
- package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
- package/src/lib/server/orchestrator-lg.ts +199 -327
- package/src/lib/server/path-utils.ts +31 -0
- package/src/lib/server/perf.ts +161 -0
- package/src/lib/server/plugins-approval-guidance.ts +115 -0
- package/src/lib/server/plugins.test.ts +1 -1
- package/src/lib/server/plugins.ts +22 -132
- package/src/lib/server/process-manager.ts +5 -8
- package/src/lib/server/provider-health.test.ts +137 -0
- package/src/lib/server/provider-health.ts +3 -3
- package/src/lib/server/provider-model-discovery.ts +3 -12
- package/src/lib/server/queue-followups.test.ts +9 -9
- package/src/lib/server/queue-reconcile.test.ts +2 -2
- package/src/lib/server/queue-recovery.test.ts +269 -0
- package/src/lib/server/queue.test.ts +570 -0
- package/src/lib/server/queue.ts +62 -455
- package/src/lib/server/resolve-image.ts +30 -0
- package/src/lib/server/runtime-settings.test.ts +4 -4
- package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
- package/src/lib/server/schedule-normalization.test.ts +279 -0
- package/src/lib/server/schedule-service.ts +263 -0
- package/src/lib/server/scheduler.ts +17 -74
- package/src/lib/server/session-mailbox.test.ts +191 -0
- package/src/lib/server/session-run-manager.test.ts +640 -0
- package/src/lib/server/session-run-manager.ts +59 -15
- package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
- package/src/lib/server/session-tools/calendar.ts +2 -1
- package/src/lib/server/session-tools/canvas.ts +2 -1
- package/src/lib/server/session-tools/chatroom.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +26 -28
- package/src/lib/server/session-tools/context-mgmt.ts +3 -2
- package/src/lib/server/session-tools/crawl.ts +4 -3
- package/src/lib/server/session-tools/crud.ts +105 -324
- package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
- package/src/lib/server/session-tools/delegate.ts +6 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
- package/src/lib/server/session-tools/discovery.ts +4 -3
- package/src/lib/server/session-tools/document.ts +2 -1
- package/src/lib/server/session-tools/email.ts +2 -1
- package/src/lib/server/session-tools/extract.ts +2 -1
- package/src/lib/server/session-tools/file.ts +4 -3
- package/src/lib/server/session-tools/http.ts +2 -1
- package/src/lib/server/session-tools/human-loop.ts +2 -1
- package/src/lib/server/session-tools/image-gen.ts +4 -3
- package/src/lib/server/session-tools/index.ts +26 -30
- package/src/lib/server/session-tools/mailbox.ts +2 -1
- package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
- package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
- package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
- package/src/lib/server/session-tools/monitor.ts +2 -1
- package/src/lib/server/session-tools/platform.ts +2 -1
- package/src/lib/server/session-tools/plugin-creator.ts +2 -1
- package/src/lib/server/session-tools/replicate.ts +3 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
- package/src/lib/server/session-tools/shell.ts +4 -9
- package/src/lib/server/session-tools/subagent.ts +322 -170
- package/src/lib/server/session-tools/table.ts +6 -5
- package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
- package/src/lib/server/session-tools/wallet.ts +7 -6
- package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
- package/src/lib/server/session-tools/web-utils.ts +317 -0
- package/src/lib/server/session-tools/web.ts +62 -328
- package/src/lib/server/skill-prompt-budget.test.ts +1 -1
- package/src/lib/server/skills-normalize.ts +2 -1
- package/src/lib/server/storage-item-access.test.ts +302 -0
- package/src/lib/server/storage.ts +366 -314
- package/src/lib/server/stream-agent-chat.test.ts +82 -3
- package/src/lib/server/stream-agent-chat.ts +146 -510
- package/src/lib/server/stream-continuation.ts +412 -0
- package/src/lib/server/subagent-lineage.test.ts +647 -0
- package/src/lib/server/subagent-lineage.ts +435 -0
- package/src/lib/server/subagent-runtime.test.ts +484 -0
- package/src/lib/server/subagent-runtime.ts +419 -0
- package/src/lib/server/subagent-swarm.test.ts +391 -0
- package/src/lib/server/subagent-swarm.ts +564 -0
- package/src/lib/server/system-events.ts +3 -3
- package/src/lib/server/task-followups.test.ts +491 -0
- package/src/lib/server/task-followups.ts +391 -0
- package/src/lib/server/task-lifecycle.test.ts +205 -0
- package/src/lib/server/task-lifecycle.ts +200 -0
- package/src/lib/server/task-quality-gate.test.ts +1 -1
- package/src/lib/server/task-resume.ts +208 -0
- package/src/lib/server/task-service.test.ts +108 -0
- package/src/lib/server/task-service.ts +264 -0
- package/src/lib/server/task-validation.test.ts +1 -1
- package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
- package/src/lib/server/tool-capability-policy.test.ts +2 -2
- package/src/lib/server/tool-capability-policy.ts +3 -2
- package/src/lib/server/tool-planning.ts +2 -1
- package/src/lib/server/tool-retry.ts +2 -3
- package/src/lib/server/wake-dispatcher.test.ts +303 -0
- package/src/lib/server/wake-dispatcher.ts +318 -0
- package/src/lib/server/wake-mode.test.ts +161 -0
- package/src/lib/server/wake-mode.ts +174 -0
- package/src/lib/server/wallet-service.ts +8 -9
- package/src/lib/server/watch-jobs.ts +2 -1
- package/src/lib/server/workspace-context.ts +2 -2
- package/src/lib/shared-utils.test.ts +142 -0
- package/src/lib/shared-utils.ts +62 -0
- package/src/lib/tool-event-summary.ts +2 -1
- package/src/lib/view-routes.test.ts +100 -0
- package/src/lib/wallet.test.ts +322 -6
- package/src/proxy.test.ts +4 -4
- package/src/proxy.ts +2 -3
- package/src/stores/set-if-changed.ts +40 -0
- package/src/stores/slices/agent-slice.ts +111 -0
- package/src/stores/slices/auth-slice.ts +25 -0
- package/src/stores/slices/data-slice.ts +301 -0
- package/src/stores/slices/index.ts +7 -0
- package/src/stores/slices/session-slice.ts +112 -0
- package/src/stores/slices/task-slice.ts +63 -0
- package/src/stores/slices/ui-slice.ts +192 -0
- package/src/stores/use-app-store.ts +17 -822
- package/src/stores/use-approval-store.ts +2 -1
- package/src/stores/use-chat-store.ts +8 -1
- package/src/types/index.ts +10 -0
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useState } from 'react'
|
|
3
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
4
5
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
5
6
|
import type { SettingsSectionProps } from './types'
|
|
6
7
|
|
|
8
|
+
function buildWhatsAppContactId(): string {
|
|
9
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
10
|
+
return crypto.randomUUID()
|
|
11
|
+
}
|
|
12
|
+
return `wa-contact-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
export function UserPreferencesSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
|
|
8
16
|
const agents = useAppStore((s) => s.agents)
|
|
9
17
|
const sortedAgents = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
|
|
18
|
+
const whatsappApprovedContacts = Array.isArray(appSettings.whatsappApprovedContacts) ? appSettings.whatsappApprovedContacts : []
|
|
19
|
+
const [nextWhatsAppLabel, setNextWhatsAppLabel] = useState('')
|
|
20
|
+
const [nextWhatsAppPhone, setNextWhatsAppPhone] = useState('')
|
|
21
|
+
|
|
22
|
+
const addWhatsAppContact = () => {
|
|
23
|
+
const phone = nextWhatsAppPhone.trim()
|
|
24
|
+
if (!phone) return
|
|
25
|
+
const label = nextWhatsAppLabel.trim() || phone
|
|
26
|
+
patchSettings({
|
|
27
|
+
whatsappApprovedContacts: [
|
|
28
|
+
...whatsappApprovedContacts,
|
|
29
|
+
{ id: buildWhatsAppContactId(), label, phone },
|
|
30
|
+
],
|
|
31
|
+
})
|
|
32
|
+
setNextWhatsAppLabel('')
|
|
33
|
+
setNextWhatsAppPhone('')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const removeWhatsAppContact = (id: string) => {
|
|
37
|
+
patchSettings({
|
|
38
|
+
whatsappApprovedContacts: whatsappApprovedContacts.filter((entry) => entry.id !== id),
|
|
39
|
+
})
|
|
40
|
+
}
|
|
10
41
|
|
|
11
42
|
return (
|
|
12
43
|
<div className="mb-10">
|
|
@@ -51,6 +82,7 @@ export function UserPreferencesSection({ appSettings, patchSettings, inputClass
|
|
|
51
82
|
</p>
|
|
52
83
|
<div className="flex flex-wrap gap-2">
|
|
53
84
|
<button
|
|
85
|
+
type="button"
|
|
54
86
|
onClick={() => patchSettings({ defaultAgentId: null })}
|
|
55
87
|
className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[12px] font-600 cursor-pointer transition-all border
|
|
56
88
|
${!appSettings.defaultAgentId
|
|
@@ -76,6 +108,81 @@ export function UserPreferencesSection({ appSettings, patchSettings, inputClass
|
|
|
76
108
|
))}
|
|
77
109
|
</div>
|
|
78
110
|
</div>
|
|
111
|
+
|
|
112
|
+
<div className="mt-6">
|
|
113
|
+
<label className="text-[12px] font-600 text-text-2 block mb-1.5">WhatsApp Approved Users</label>
|
|
114
|
+
<p className="text-[11px] text-text-3/60 mb-3">
|
|
115
|
+
These numbers or JIDs are globally approved for WhatsApp DMs. They bypass per-connector pairing and are merged into WhatsApp allowlists.
|
|
116
|
+
</p>
|
|
117
|
+
|
|
118
|
+
{whatsappApprovedContacts.length > 0 ? (
|
|
119
|
+
<div className="space-y-2 mb-3">
|
|
120
|
+
{whatsappApprovedContacts.map((entry) => (
|
|
121
|
+
<div
|
|
122
|
+
key={entry.id}
|
|
123
|
+
className="flex items-center justify-between gap-3 rounded-[12px] border border-white/[0.06] bg-white/[0.03] px-3 py-2"
|
|
124
|
+
>
|
|
125
|
+
<div className="min-w-0">
|
|
126
|
+
<div className="text-[12px] font-600 text-text truncate">{entry.label}</div>
|
|
127
|
+
<div className="text-[11px] text-text-3/70 truncate">{entry.phone}</div>
|
|
128
|
+
</div>
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={() => removeWhatsAppContact(entry.id)}
|
|
132
|
+
className="shrink-0 px-2.5 py-1.5 rounded-[8px] bg-white/[0.04] text-[11px] text-text-3 hover:text-text hover:bg-white/[0.08] transition-colors border-none cursor-pointer"
|
|
133
|
+
style={{ fontFamily: 'inherit' }}
|
|
134
|
+
>
|
|
135
|
+
Remove
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
) : (
|
|
141
|
+
<div className="mb-3 rounded-[12px] border border-dashed border-white/[0.08] bg-white/[0.02] px-3 py-3 text-[11px] text-text-3/70">
|
|
142
|
+
No globally approved WhatsApp users yet.
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
<div className="grid grid-cols-1 md:grid-cols-[minmax(0,1fr)_minmax(0,1.2fr)_auto] gap-2">
|
|
147
|
+
<input
|
|
148
|
+
type="text"
|
|
149
|
+
value={nextWhatsAppLabel}
|
|
150
|
+
onChange={(e) => setNextWhatsAppLabel(e.target.value)}
|
|
151
|
+
onKeyDown={(e) => {
|
|
152
|
+
if (e.key === 'Enter') {
|
|
153
|
+
e.preventDefault()
|
|
154
|
+
addWhatsAppContact()
|
|
155
|
+
}
|
|
156
|
+
}}
|
|
157
|
+
placeholder="Label (e.g. Family, Alice)"
|
|
158
|
+
className={inputClass}
|
|
159
|
+
style={{ fontFamily: 'inherit' }}
|
|
160
|
+
/>
|
|
161
|
+
<input
|
|
162
|
+
type="text"
|
|
163
|
+
value={nextWhatsAppPhone}
|
|
164
|
+
onChange={(e) => setNextWhatsAppPhone(e.target.value)}
|
|
165
|
+
onKeyDown={(e) => {
|
|
166
|
+
if (e.key === 'Enter') {
|
|
167
|
+
e.preventDefault()
|
|
168
|
+
addWhatsAppContact()
|
|
169
|
+
}
|
|
170
|
+
}}
|
|
171
|
+
placeholder="+15551234567 or 15551234567@s.whatsapp.net"
|
|
172
|
+
className={inputClass}
|
|
173
|
+
style={{ fontFamily: 'inherit' }}
|
|
174
|
+
/>
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
onClick={addWhatsAppContact}
|
|
178
|
+
disabled={!nextWhatsAppPhone.trim()}
|
|
179
|
+
className="px-3 py-2 rounded-[10px] text-[12px] font-600 border border-white/[0.06] bg-white/[0.04] text-text transition-colors disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer hover:bg-white/[0.08]"
|
|
180
|
+
style={{ fontFamily: 'inherit' }}
|
|
181
|
+
>
|
|
182
|
+
Add User
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
79
186
|
</div>
|
|
80
187
|
)
|
|
81
188
|
}
|
|
@@ -72,6 +72,8 @@ const TABS: Tab[] = [
|
|
|
72
72
|
},
|
|
73
73
|
]
|
|
74
74
|
|
|
75
|
+
const VALID_TAB_IDS = TABS.map((t) => t.id)
|
|
76
|
+
|
|
75
77
|
export function SettingsPage() {
|
|
76
78
|
const loadProviders = useAppStore((s) => s.loadProviders)
|
|
77
79
|
const loadCredentials = useAppStore((s) => s.loadCredentials)
|
|
@@ -81,13 +83,7 @@ export function SettingsPage() {
|
|
|
81
83
|
const loadSecrets = useAppStore((s) => s.loadSecrets)
|
|
82
84
|
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
83
85
|
const credentials = useAppStore((s) => s.credentials)
|
|
84
|
-
const
|
|
85
|
-
const [activeTab, setActiveTabRaw] = useState(() => {
|
|
86
|
-
if (typeof window === 'undefined') return 'general'
|
|
87
|
-
const params = new URLSearchParams(window.location.search)
|
|
88
|
-
const tab = params.get('tab')
|
|
89
|
-
return tab && validTabIds.includes(tab) ? tab : 'general'
|
|
90
|
-
})
|
|
86
|
+
const [activeTab, setActiveTabRaw] = useState('general')
|
|
91
87
|
const contentRef = useRef<HTMLDivElement>(null)
|
|
92
88
|
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
|
93
89
|
const [pendingSectionId, setPendingSectionId] = useState<string | null>(null)
|
|
@@ -109,6 +105,14 @@ export function SettingsPage() {
|
|
|
109
105
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
110
106
|
}, [])
|
|
111
107
|
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const params = new URLSearchParams(window.location.search)
|
|
110
|
+
const tab = params.get('tab')
|
|
111
|
+
if (tab && VALID_TAB_IDS.includes(tab)) {
|
|
112
|
+
setActiveTabRaw(tab)
|
|
113
|
+
}
|
|
114
|
+
}, [])
|
|
115
|
+
|
|
112
116
|
// Scroll to top when switching tabs
|
|
113
117
|
useEffect(() => {
|
|
114
118
|
contentRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
|
@@ -124,8 +128,8 @@ export function SettingsPage() {
|
|
|
124
128
|
id: 'user-preferences',
|
|
125
129
|
tabId: 'general',
|
|
126
130
|
title: 'Profile & Default Chat',
|
|
127
|
-
description: 'User identity, language,
|
|
128
|
-
keywords: ['profile', 'default chat', 'default agent', 'shortcut', 'user', 'language', 'main chat'],
|
|
131
|
+
description: 'User identity, language, default-chat behavior, and global WhatsApp approvals.',
|
|
132
|
+
keywords: ['profile', 'default chat', 'default agent', 'shortcut', 'user', 'language', 'main chat', 'whatsapp', 'contacts', 'approved users'],
|
|
129
133
|
render: () => <UserPreferencesSection {...sectionProps} />,
|
|
130
134
|
},
|
|
131
135
|
{
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react'
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
4
4
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
|
5
5
|
import { Input } from '@/components/ui/input'
|
|
6
6
|
import { Button } from '@/components/ui/button'
|
|
7
7
|
import { Badge } from '@/components/ui/badge'
|
|
8
8
|
import { api } from '@/lib/api-client'
|
|
9
9
|
import { toast } from 'sonner'
|
|
10
|
+
import { useMountedRef } from '@/hooks/use-mounted-ref'
|
|
10
11
|
|
|
11
12
|
interface ClawHubSkill {
|
|
12
13
|
id: string
|
|
@@ -33,6 +34,7 @@ interface ClawHubBrowserProps {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function ClawHubBrowser({ open, onOpenChange, onInstalled }: ClawHubBrowserProps) {
|
|
37
|
+
const mountedRef = useMountedRef()
|
|
36
38
|
const [query, setQuery] = useState('')
|
|
37
39
|
const [skills, setSkills] = useState<ClawHubSkill[]>([])
|
|
38
40
|
const [page, setPage] = useState(1)
|
|
@@ -41,12 +43,16 @@ export function ClawHubBrowser({ open, onOpenChange, onInstalled }: ClawHubBrows
|
|
|
41
43
|
const [error, setError] = useState<string | null>(null)
|
|
42
44
|
const [installing, setInstalling] = useState<string | null>(null)
|
|
43
45
|
const [searched, setSearched] = useState(false)
|
|
46
|
+
const searchRequestIdRef = useRef(0)
|
|
44
47
|
|
|
45
48
|
const search = useCallback(async (q: string, p: number, append = false) => {
|
|
49
|
+
const requestId = searchRequestIdRef.current + 1
|
|
50
|
+
searchRequestIdRef.current = requestId
|
|
46
51
|
setLoading(true)
|
|
47
52
|
setError(null)
|
|
48
53
|
try {
|
|
49
54
|
const res = await api<SearchResponse>('GET', `/clawhub/search?q=${encodeURIComponent(q)}&page=${p}`)
|
|
55
|
+
if (!mountedRef.current || searchRequestIdRef.current !== requestId) return
|
|
50
56
|
if (append) {
|
|
51
57
|
setSkills(prev => [...prev, ...res.skills])
|
|
52
58
|
} else {
|
|
@@ -56,11 +62,14 @@ export function ClawHubBrowser({ open, onOpenChange, onInstalled }: ClawHubBrows
|
|
|
56
62
|
setPage(res.page)
|
|
57
63
|
setSearched(true)
|
|
58
64
|
} catch (err) {
|
|
65
|
+
if (!mountedRef.current || searchRequestIdRef.current !== requestId) return
|
|
59
66
|
setError(err instanceof Error ? err.message : 'Failed to search ClawHub')
|
|
60
67
|
} finally {
|
|
61
|
-
|
|
68
|
+
if (mountedRef.current && searchRequestIdRef.current === requestId) {
|
|
69
|
+
setLoading(false)
|
|
70
|
+
}
|
|
62
71
|
}
|
|
63
|
-
}, [])
|
|
72
|
+
}, [mountedRef])
|
|
64
73
|
|
|
65
74
|
useEffect(() => {
|
|
66
75
|
if (open) {
|
|
@@ -101,7 +110,9 @@ export function ClawHubBrowser({ open, onOpenChange, onInstalled }: ClawHubBrows
|
|
|
101
110
|
} catch (err) {
|
|
102
111
|
toast.error(err instanceof Error ? err.message : 'Install failed')
|
|
103
112
|
} finally {
|
|
104
|
-
|
|
113
|
+
if (mountedRef.current) {
|
|
114
|
+
setInstalling(null)
|
|
115
|
+
}
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
118
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState, useCallback } from 'react'
|
|
3
|
+
import { useEffect, useState, useCallback, useRef } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
6
|
import { Badge } from '@/components/ui/badge'
|
|
7
7
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
8
8
|
import { ClawHubBrowser } from './clawhub-browser'
|
|
9
9
|
import { toast } from 'sonner'
|
|
10
|
+
import { useMountedRef } from '@/hooks/use-mounted-ref'
|
|
10
11
|
|
|
11
12
|
interface ClawHubSkill {
|
|
12
13
|
id: string
|
|
@@ -26,6 +27,7 @@ interface SearchResponse {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
|
|
30
|
+
const mountedRef = useMountedRef()
|
|
29
31
|
const skills = useAppStore((s) => s.skills)
|
|
30
32
|
const loadSkills = useAppStore((s) => s.loadSkills)
|
|
31
33
|
const agents = useAppStore((s) => s.agents)
|
|
@@ -45,6 +47,7 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
45
47
|
const [hubSearched, setHubSearched] = useState(false)
|
|
46
48
|
const [hubError, setHubError] = useState<string | null>(null)
|
|
47
49
|
const [installing, setInstalling] = useState<string | null>(null)
|
|
50
|
+
const hubSearchRequestIdRef = useRef(0)
|
|
48
51
|
|
|
49
52
|
useEffect(() => {
|
|
50
53
|
loadSkills()
|
|
@@ -67,10 +70,13 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
67
70
|
|
|
68
71
|
// Embedded ClawHub search
|
|
69
72
|
const searchHub = useCallback(async (q: string, p: number, append = false) => {
|
|
73
|
+
const requestId = hubSearchRequestIdRef.current + 1
|
|
74
|
+
hubSearchRequestIdRef.current = requestId
|
|
70
75
|
setHubLoading(true)
|
|
71
76
|
setHubError(null)
|
|
72
77
|
try {
|
|
73
78
|
const res = await api<SearchResponse>('GET', `/clawhub/search?q=${encodeURIComponent(q)}&page=${p}`)
|
|
79
|
+
if (!mountedRef.current || hubSearchRequestIdRef.current !== requestId) return
|
|
74
80
|
if (append) {
|
|
75
81
|
setHubSkills(prev => [...prev, ...res.skills])
|
|
76
82
|
} else {
|
|
@@ -80,11 +86,14 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
80
86
|
setHubPage(res.page)
|
|
81
87
|
setHubSearched(true)
|
|
82
88
|
} catch (err) {
|
|
89
|
+
if (!mountedRef.current || hubSearchRequestIdRef.current !== requestId) return
|
|
83
90
|
setHubError(err instanceof Error ? err.message : 'Failed to search ClawHub')
|
|
84
91
|
} finally {
|
|
85
|
-
|
|
92
|
+
if (mountedRef.current && hubSearchRequestIdRef.current === requestId) {
|
|
93
|
+
setHubLoading(false)
|
|
94
|
+
}
|
|
86
95
|
}
|
|
87
|
-
}, [])
|
|
96
|
+
}, [mountedRef])
|
|
88
97
|
|
|
89
98
|
useEffect(() => {
|
|
90
99
|
if (!inSidebar && tab === 'clawhub' && !hubSearched) {
|
|
@@ -111,7 +120,9 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
111
120
|
} catch (err) {
|
|
112
121
|
toast.error(err instanceof Error ? err.message : 'Install failed')
|
|
113
122
|
} finally {
|
|
114
|
-
|
|
123
|
+
if (mountedRef.current) {
|
|
124
|
+
setInstalling(null)
|
|
125
|
+
}
|
|
115
126
|
}
|
|
116
127
|
}
|
|
117
128
|
|
|
@@ -9,6 +9,7 @@ import { useWs } from '@/hooks/use-ws'
|
|
|
9
9
|
import { ExecApprovalCard } from '@/components/chat/exec-approval-card'
|
|
10
10
|
import { getApprovalPayload, getApprovalTitle } from '@/lib/approval-display'
|
|
11
11
|
import type { AppSettings, ApprovalCategory, ApprovalRequest } from '@/types'
|
|
12
|
+
import { dedup } from '@/lib/shared-utils'
|
|
12
13
|
|
|
13
14
|
const CATEGORY_LABELS: Record<string, string> = {
|
|
14
15
|
tool_access: 'Plugin Access',
|
|
@@ -137,7 +138,7 @@ export function ApprovalsPanel() {
|
|
|
137
138
|
const searchTerm = search.trim().toLowerCase()
|
|
138
139
|
|
|
139
140
|
const workflowCategories = useMemo(() => (
|
|
140
|
-
|
|
141
|
+
dedup(workflowApprovals.map((req) => req.category)).sort()
|
|
141
142
|
), [workflowApprovals])
|
|
142
143
|
|
|
143
144
|
const filteredExecApprovals = useMemo(() => {
|
|
@@ -10,7 +10,9 @@ import { Skeleton } from '@/components/shared/skeleton'
|
|
|
10
10
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
11
11
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
12
12
|
import { inputClass } from '@/components/shared/form-styles'
|
|
13
|
+
import { useNow } from '@/hooks/use-now'
|
|
13
14
|
import type { BoardTask, BoardTaskStatus } from '@/types'
|
|
15
|
+
import { dedup } from '@/lib/shared-utils'
|
|
14
16
|
import { toast } from 'sonner'
|
|
15
17
|
|
|
16
18
|
const ACTIVE_COLUMNS: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed']
|
|
@@ -18,8 +20,8 @@ type BoardViewMode = 'board' | 'list'
|
|
|
18
20
|
type AttentionFilter = 'all' | 'needs-attention' | 'approval' | 'blocked' | 'overdue' | 'failed'
|
|
19
21
|
type TaskScopeFilter = 'user-facing' | 'all' | 'agent'
|
|
20
22
|
|
|
21
|
-
function isTaskOverdue(task: BoardTask): boolean {
|
|
22
|
-
return !!task.dueAt && task.dueAt <
|
|
23
|
+
function isTaskOverdue(task: BoardTask, now: number | null): boolean {
|
|
24
|
+
return !!now && !!task.dueAt && task.dueAt < now && task.status !== 'completed' && task.status !== 'archived'
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
function isInternalAgentTask(task: BoardTask): boolean {
|
|
@@ -33,10 +35,10 @@ function isTaskRelevantToAgent(task: BoardTask, agentId: string): boolean {
|
|
|
33
35
|
|| task.delegatedByAgentId === agentId
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
function matchesAttentionFilter(task: BoardTask, filter: AttentionFilter): boolean {
|
|
38
|
+
function matchesAttentionFilter(task: BoardTask, filter: AttentionFilter, now: number | null): boolean {
|
|
37
39
|
const blocked = !!task.blockedBy?.length
|
|
38
40
|
const pendingApproval = !!task.pendingApproval
|
|
39
|
-
const overdue = isTaskOverdue(task)
|
|
41
|
+
const overdue = isTaskOverdue(task, now)
|
|
40
42
|
const failed = task.status === 'failed'
|
|
41
43
|
if (filter === 'all') return true
|
|
42
44
|
if (filter === 'approval') return pendingApproval
|
|
@@ -46,17 +48,18 @@ function matchesAttentionFilter(task: BoardTask, filter: AttentionFilter): boole
|
|
|
46
48
|
return blocked || pendingApproval || overdue || failed
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
function attentionRank(task: BoardTask): number {
|
|
51
|
+
function attentionRank(task: BoardTask, now: number | null): number {
|
|
50
52
|
if (task.pendingApproval) return 0
|
|
51
53
|
if (task.status === 'failed') return 1
|
|
52
54
|
if (task.blockedBy?.length) return 2
|
|
53
|
-
if (isTaskOverdue(task)) return 3
|
|
55
|
+
if (isTaskOverdue(task, now)) return 3
|
|
54
56
|
if (task.status === 'running') return 4
|
|
55
57
|
if (task.status === 'queued') return 5
|
|
56
58
|
return 6
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
export function TaskBoard() {
|
|
62
|
+
const now = useNow()
|
|
60
63
|
const tasks = useAppStore((s) => s.tasks)
|
|
61
64
|
const loadTasks = useAppStore((s) => s.loadTasks)
|
|
62
65
|
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
@@ -145,21 +148,10 @@ export function TaskBoard() {
|
|
|
145
148
|
const bulkStatusRef = useRef<HTMLDivElement>(null)
|
|
146
149
|
|
|
147
150
|
// URL-based filter state
|
|
148
|
-
const [filterAgentId, setFilterAgentId] = useState<string>(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const [filterTag, setFilterTag] = useState<string>(() => {
|
|
153
|
-
if (typeof window === 'undefined') return ''
|
|
154
|
-
return new URLSearchParams(window.location.search).get('tag') || ''
|
|
155
|
-
})
|
|
156
|
-
const [taskScopeFilter, setTaskScopeFilter] = useState<TaskScopeFilter>(() => {
|
|
157
|
-
if (typeof window === 'undefined') return 'user-facing'
|
|
158
|
-
const params = new URLSearchParams(window.location.search)
|
|
159
|
-
if (params.get('agent')) return 'agent'
|
|
160
|
-
const raw = params.get('taskView')
|
|
161
|
-
return raw === 'all' ? 'all' : 'user-facing'
|
|
162
|
-
})
|
|
151
|
+
const [filterAgentId, setFilterAgentId] = useState<string>('')
|
|
152
|
+
const [filterTag, setFilterTag] = useState<string>('')
|
|
153
|
+
const [taskScopeFilter, setTaskScopeFilter] = useState<TaskScopeFilter>('user-facing')
|
|
154
|
+
const [filtersHydrated, setFiltersHydrated] = useState(false)
|
|
163
155
|
const [viewMode, setViewMode] = useState<BoardViewMode>('board')
|
|
164
156
|
const [attentionFilter, setAttentionFilter] = useState<AttentionFilter>('all')
|
|
165
157
|
const [githubImportOpen, setGitHubImportOpen] = useState(false)
|
|
@@ -172,19 +164,25 @@ export function TaskBoard() {
|
|
|
172
164
|
const [githubImportError, setGitHubImportError] = useState<string | null>(null)
|
|
173
165
|
const [githubImportResult, setGitHubImportResult] = useState<GitHubIssueImportResult | null>(null)
|
|
174
166
|
|
|
175
|
-
// Seed
|
|
167
|
+
// Seed URL-backed filters after hydration so the initial tree stays deterministic.
|
|
176
168
|
useEffect(() => {
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
169
|
+
const params = new URLSearchParams(window.location.search)
|
|
170
|
+
const urlAgent = params.get('agent') || ''
|
|
171
|
+
const urlTag = params.get('tag') || ''
|
|
172
|
+
const urlProject = params.get('project')
|
|
173
|
+
const rawTaskView = params.get('taskView')
|
|
174
|
+
|
|
175
|
+
setFilterAgentId(urlAgent)
|
|
176
|
+
setFilterTag(urlTag)
|
|
177
|
+
setTaskScopeFilter(urlAgent ? 'agent' : rawTaskView === 'all' ? 'all' : 'user-facing')
|
|
178
|
+
if (urlProject && !activeProjectFilter) setActiveProjectFilter(urlProject)
|
|
179
|
+
setFiltersHydrated(true)
|
|
182
180
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
183
181
|
}, [])
|
|
184
182
|
|
|
185
183
|
// Sync filters to URL
|
|
186
184
|
useEffect(() => {
|
|
187
|
-
if (
|
|
185
|
+
if (!filtersHydrated) return
|
|
188
186
|
const params = new URLSearchParams()
|
|
189
187
|
if (taskScopeFilter === 'agent' && filterAgentId) params.set('agent', filterAgentId)
|
|
190
188
|
else if (taskScopeFilter === 'all') params.set('taskView', 'all')
|
|
@@ -193,7 +191,7 @@ export function TaskBoard() {
|
|
|
193
191
|
const qs = params.toString()
|
|
194
192
|
const newUrl = `${window.location.pathname}${qs ? `?${qs}` : ''}`
|
|
195
193
|
window.history.replaceState(null, '', newUrl)
|
|
196
|
-
}, [filterAgentId, filterTag,
|
|
194
|
+
}, [activeProjectFilter, filterAgentId, filterTag, filtersHydrated, taskScopeFilter])
|
|
197
195
|
|
|
198
196
|
const [loaded, setLoaded] = useState(Object.keys(tasks).length > 0)
|
|
199
197
|
useEffect(() => {
|
|
@@ -202,7 +200,7 @@ export function TaskBoard() {
|
|
|
202
200
|
useWs('tasks', loadTasks, 5000)
|
|
203
201
|
|
|
204
202
|
// Collect all unique tags across tasks
|
|
205
|
-
const allTags =
|
|
203
|
+
const allTags = dedup(Object.values(tasks).flatMap((t) => t.tags || [])).sort()
|
|
206
204
|
|
|
207
205
|
const columns: BoardTaskStatus[] = showArchived ? [...ACTIVE_COLUMNS, 'archived'] : ACTIVE_COLUMNS
|
|
208
206
|
|
|
@@ -217,9 +215,9 @@ export function TaskBoard() {
|
|
|
217
215
|
|
|
218
216
|
const matchesBaseFilters = useCallback((task: BoardTask) => {
|
|
219
217
|
if (!matchesScopeFilters(task)) return false
|
|
220
|
-
if (!matchesAttentionFilter(task, attentionFilter)) return false
|
|
218
|
+
if (!matchesAttentionFilter(task, attentionFilter, now)) return false
|
|
221
219
|
return true
|
|
222
|
-
}, [attentionFilter, matchesScopeFilters])
|
|
220
|
+
}, [attentionFilter, matchesScopeFilters, now])
|
|
223
221
|
|
|
224
222
|
const scopedTasks = useMemo(
|
|
225
223
|
() => Object.values(tasks).filter(matchesScopeFilters),
|
|
@@ -230,13 +228,13 @@ export function TaskBoard() {
|
|
|
230
228
|
scopedTasks
|
|
231
229
|
.filter(matchesBaseFilters)
|
|
232
230
|
.sort((a, b) => {
|
|
233
|
-
const rankDiff = attentionRank(a) - attentionRank(b)
|
|
231
|
+
const rankDiff = attentionRank(a, now) - attentionRank(b, now)
|
|
234
232
|
if (rankDiff !== 0) return rankDiff
|
|
235
233
|
const dueDiff = (a.dueAt || Number.MAX_SAFE_INTEGER) - (b.dueAt || Number.MAX_SAFE_INTEGER)
|
|
236
234
|
if (dueDiff !== 0) return dueDiff
|
|
237
235
|
return b.updatedAt - a.updatedAt
|
|
238
236
|
})
|
|
239
|
-
), [scopedTasks, matchesBaseFilters])
|
|
237
|
+
), [scopedTasks, matchesBaseFilters, now])
|
|
240
238
|
|
|
241
239
|
const tasksByStatus = useCallback((status: BoardTaskStatus) =>
|
|
242
240
|
filteredTasks
|
|
@@ -334,12 +332,12 @@ export function TaskBoard() {
|
|
|
334
332
|
running: all.filter((t) => t.status === 'running').length,
|
|
335
333
|
completed: all.filter((t) => t.status === 'completed').length,
|
|
336
334
|
failed: all.filter((t) => t.status === 'failed').length,
|
|
337
|
-
overdue: all.filter((t) => isTaskOverdue(t)).length,
|
|
335
|
+
overdue: all.filter((t) => isTaskOverdue(t, now)).length,
|
|
338
336
|
blocked: all.filter((t) => (t.blockedBy?.length || 0) > 0).length,
|
|
339
337
|
approvals: all.filter((t) => !!t.pendingApproval).length,
|
|
340
|
-
attention: all.filter((t) => matchesAttentionFilter(t, 'needs-attention')).length,
|
|
338
|
+
attention: all.filter((t) => matchesAttentionFilter(t, 'needs-attention', now)).length,
|
|
341
339
|
}
|
|
342
|
-
}, [scopedTasks])
|
|
340
|
+
}, [now, scopedTasks])
|
|
343
341
|
|
|
344
342
|
const activeScopeLabel = useMemo(() => {
|
|
345
343
|
if (taskScopeFilter === 'all') return 'All tasks'
|
|
@@ -11,6 +11,7 @@ import { DirBrowser } from '@/components/shared/dir-browser'
|
|
|
11
11
|
import { SheetFooter } from '@/components/shared/sheet-footer'
|
|
12
12
|
import { inputClass } from '@/components/shared/form-styles'
|
|
13
13
|
import type { BoardTask, TaskComment, TaskQualityGateConfig } from '@/types'
|
|
14
|
+
import { dedup, errorMessage } from '@/lib/shared-utils'
|
|
14
15
|
import { SectionLabel } from '@/components/shared/section-label'
|
|
15
16
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
16
17
|
|
|
@@ -187,7 +188,7 @@ export function TaskSheet() {
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
} catch (err: unknown) {
|
|
190
|
-
setDepError(
|
|
191
|
+
setDepError(errorMessage(err))
|
|
191
192
|
return
|
|
192
193
|
}
|
|
193
194
|
setDepError(null)
|
|
@@ -208,7 +209,7 @@ export function TaskSheet() {
|
|
|
208
209
|
const data = await res.json()
|
|
209
210
|
if (data.url) setImages((prev) => [...prev, data.url])
|
|
210
211
|
} catch (err: unknown) {
|
|
211
|
-
console.error('Image upload failed:',
|
|
212
|
+
console.error('Image upload failed:', errorMessage(err))
|
|
212
213
|
}
|
|
213
214
|
setUploading(false)
|
|
214
215
|
e.target.value = ''
|
|
@@ -758,7 +759,7 @@ export function TaskSheet() {
|
|
|
758
759
|
list="tag-suggestions"
|
|
759
760
|
/>
|
|
760
761
|
<datalist id="tag-suggestions">
|
|
761
|
-
{
|
|
762
|
+
{dedup(Object.values(tasks).flatMap((t) => t.tags || []))
|
|
762
763
|
.filter((t) => !tags.includes(t) && t.includes(tagInput.toLowerCase()))
|
|
763
764
|
.slice(0, 10)
|
|
764
765
|
.map((t) => <option key={t} value={t} />)}
|