cowork-os 0.3.21
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/LICENSE +21 -0
- package/README.md +1638 -0
- package/bin/cowork.js +42 -0
- package/build/entitlements.mac.plist +16 -0
- package/build/icon.icns +0 -0
- package/build/icon.png +0 -0
- package/dist/electron/electron/activity/ActivityRepository.js +190 -0
- package/dist/electron/electron/agent/browser/browser-service.js +639 -0
- package/dist/electron/electron/agent/context-manager.js +225 -0
- package/dist/electron/electron/agent/custom-skill-loader.js +566 -0
- package/dist/electron/electron/agent/daemon.js +975 -0
- package/dist/electron/electron/agent/executor.js +3561 -0
- package/dist/electron/electron/agent/llm/anthropic-provider.js +155 -0
- package/dist/electron/electron/agent/llm/bedrock-provider.js +202 -0
- package/dist/electron/electron/agent/llm/gemini-provider.js +375 -0
- package/dist/electron/electron/agent/llm/index.js +34 -0
- package/dist/electron/electron/agent/llm/ollama-provider.js +263 -0
- package/dist/electron/electron/agent/llm/openai-oauth.js +101 -0
- package/dist/electron/electron/agent/llm/openai-provider.js +657 -0
- package/dist/electron/electron/agent/llm/openrouter-provider.js +232 -0
- package/dist/electron/electron/agent/llm/pricing.js +160 -0
- package/dist/electron/electron/agent/llm/provider-factory.js +880 -0
- package/dist/electron/electron/agent/llm/types.js +178 -0
- package/dist/electron/electron/agent/queue-manager.js +378 -0
- package/dist/electron/electron/agent/sandbox/docker-sandbox.js +402 -0
- package/dist/electron/electron/agent/sandbox/macos-sandbox.js +407 -0
- package/dist/electron/electron/agent/sandbox/runner.js +410 -0
- package/dist/electron/electron/agent/sandbox/sandbox-factory.js +228 -0
- package/dist/electron/electron/agent/sandbox/security-utils.js +258 -0
- package/dist/electron/electron/agent/search/brave-provider.js +119 -0
- package/dist/electron/electron/agent/search/google-provider.js +100 -0
- package/dist/electron/electron/agent/search/index.js +28 -0
- package/dist/electron/electron/agent/search/provider-factory.js +395 -0
- package/dist/electron/electron/agent/search/serpapi-provider.js +112 -0
- package/dist/electron/electron/agent/search/tavily-provider.js +90 -0
- package/dist/electron/electron/agent/search/types.js +40 -0
- package/dist/electron/electron/agent/security/index.js +12 -0
- package/dist/electron/electron/agent/security/input-sanitizer.js +303 -0
- package/dist/electron/electron/agent/security/output-filter.js +217 -0
- package/dist/electron/electron/agent/skill-eligibility.js +281 -0
- package/dist/electron/electron/agent/skill-registry.js +396 -0
- package/dist/electron/electron/agent/skills/document.js +878 -0
- package/dist/electron/electron/agent/skills/image-generator.js +225 -0
- package/dist/electron/electron/agent/skills/organizer.js +141 -0
- package/dist/electron/electron/agent/skills/presentation.js +367 -0
- package/dist/electron/electron/agent/skills/spreadsheet.js +165 -0
- package/dist/electron/electron/agent/tools/browser-tools.js +523 -0
- package/dist/electron/electron/agent/tools/builtin-settings.js +384 -0
- package/dist/electron/electron/agent/tools/canvas-tools.js +530 -0
- package/dist/electron/electron/agent/tools/cron-tools.js +577 -0
- package/dist/electron/electron/agent/tools/edit-tools.js +194 -0
- package/dist/electron/electron/agent/tools/file-tools.js +719 -0
- package/dist/electron/electron/agent/tools/glob-tools.js +283 -0
- package/dist/electron/electron/agent/tools/grep-tools.js +387 -0
- package/dist/electron/electron/agent/tools/image-tools.js +111 -0
- package/dist/electron/electron/agent/tools/mention-tools.js +282 -0
- package/dist/electron/electron/agent/tools/node-tools.js +476 -0
- package/dist/electron/electron/agent/tools/registry.js +2719 -0
- package/dist/electron/electron/agent/tools/search-tools.js +91 -0
- package/dist/electron/electron/agent/tools/shell-tools.js +574 -0
- package/dist/electron/electron/agent/tools/skill-tools.js +274 -0
- package/dist/electron/electron/agent/tools/system-tools.js +578 -0
- package/dist/electron/electron/agent/tools/web-fetch-tools.js +444 -0
- package/dist/electron/electron/agent/tools/x-tools.js +264 -0
- package/dist/electron/electron/agents/AgentRoleRepository.js +420 -0
- package/dist/electron/electron/agents/HeartbeatService.js +356 -0
- package/dist/electron/electron/agents/MentionRepository.js +197 -0
- package/dist/electron/electron/agents/TaskSubscriptionRepository.js +168 -0
- package/dist/electron/electron/agents/WorkingStateRepository.js +229 -0
- package/dist/electron/electron/canvas/canvas-manager.js +714 -0
- package/dist/electron/electron/canvas/canvas-preload.js +53 -0
- package/dist/electron/electron/canvas/canvas-protocol.js +195 -0
- package/dist/electron/electron/canvas/canvas-store.js +174 -0
- package/dist/electron/electron/canvas/index.js +13 -0
- package/dist/electron/electron/control-plane/client.js +364 -0
- package/dist/electron/electron/control-plane/handlers.js +572 -0
- package/dist/electron/electron/control-plane/index.js +41 -0
- package/dist/electron/electron/control-plane/node-manager.js +264 -0
- package/dist/electron/electron/control-plane/protocol.js +194 -0
- package/dist/electron/electron/control-plane/remote-client.js +437 -0
- package/dist/electron/electron/control-plane/server.js +640 -0
- package/dist/electron/electron/control-plane/settings.js +369 -0
- package/dist/electron/electron/control-plane/ssh-tunnel.js +549 -0
- package/dist/electron/electron/cron/index.js +30 -0
- package/dist/electron/electron/cron/schedule.js +190 -0
- package/dist/electron/electron/cron/service.js +614 -0
- package/dist/electron/electron/cron/store.js +155 -0
- package/dist/electron/electron/cron/types.js +82 -0
- package/dist/electron/electron/cron/webhook.js +258 -0
- package/dist/electron/electron/database/SecureSettingsRepository.js +444 -0
- package/dist/electron/electron/database/TaskLabelRepository.js +120 -0
- package/dist/electron/electron/database/repositories.js +1781 -0
- package/dist/electron/electron/database/schema.js +978 -0
- package/dist/electron/electron/extensions/index.js +33 -0
- package/dist/electron/electron/extensions/loader.js +313 -0
- package/dist/electron/electron/extensions/registry.js +485 -0
- package/dist/electron/electron/extensions/types.js +11 -0
- package/dist/electron/electron/gateway/channel-registry.js +1102 -0
- package/dist/electron/electron/gateway/channels/bluebubbles-client.js +479 -0
- package/dist/electron/electron/gateway/channels/bluebubbles.js +432 -0
- package/dist/electron/electron/gateway/channels/discord.js +975 -0
- package/dist/electron/electron/gateway/channels/email-client.js +593 -0
- package/dist/electron/electron/gateway/channels/email.js +443 -0
- package/dist/electron/electron/gateway/channels/google-chat.js +631 -0
- package/dist/electron/electron/gateway/channels/imessage-client.js +363 -0
- package/dist/electron/electron/gateway/channels/imessage.js +465 -0
- package/dist/electron/electron/gateway/channels/index.js +36 -0
- package/dist/electron/electron/gateway/channels/line-client.js +470 -0
- package/dist/electron/electron/gateway/channels/line.js +479 -0
- package/dist/electron/electron/gateway/channels/matrix-client.js +432 -0
- package/dist/electron/electron/gateway/channels/matrix.js +592 -0
- package/dist/electron/electron/gateway/channels/mattermost-client.js +394 -0
- package/dist/electron/electron/gateway/channels/mattermost.js +496 -0
- package/dist/electron/electron/gateway/channels/signal-client.js +500 -0
- package/dist/electron/electron/gateway/channels/signal.js +582 -0
- package/dist/electron/electron/gateway/channels/slack.js +415 -0
- package/dist/electron/electron/gateway/channels/teams.js +596 -0
- package/dist/electron/electron/gateway/channels/telegram.js +1390 -0
- package/dist/electron/electron/gateway/channels/twitch-client.js +502 -0
- package/dist/electron/electron/gateway/channels/twitch.js +396 -0
- package/dist/electron/electron/gateway/channels/types.js +8 -0
- package/dist/electron/electron/gateway/channels/whatsapp.js +953 -0
- package/dist/electron/electron/gateway/context-policy.js +268 -0
- package/dist/electron/electron/gateway/index.js +1063 -0
- package/dist/electron/electron/gateway/infrastructure.js +496 -0
- package/dist/electron/electron/gateway/router.js +2700 -0
- package/dist/electron/electron/gateway/security.js +375 -0
- package/dist/electron/electron/gateway/session.js +115 -0
- package/dist/electron/electron/gateway/tunnel.js +503 -0
- package/dist/electron/electron/guardrails/guardrail-manager.js +348 -0
- package/dist/electron/electron/hooks/gmail-watcher.js +300 -0
- package/dist/electron/electron/hooks/index.js +46 -0
- package/dist/electron/electron/hooks/mappings.js +381 -0
- package/dist/electron/electron/hooks/server.js +480 -0
- package/dist/electron/electron/hooks/settings.js +447 -0
- package/dist/electron/electron/hooks/types.js +41 -0
- package/dist/electron/electron/ipc/canvas-handlers.js +158 -0
- package/dist/electron/electron/ipc/handlers.js +3138 -0
- package/dist/electron/electron/ipc/mission-control-handlers.js +141 -0
- package/dist/electron/electron/main.js +448 -0
- package/dist/electron/electron/mcp/client/MCPClientManager.js +330 -0
- package/dist/electron/electron/mcp/client/MCPServerConnection.js +437 -0
- package/dist/electron/electron/mcp/client/transports/SSETransport.js +304 -0
- package/dist/electron/electron/mcp/client/transports/StdioTransport.js +307 -0
- package/dist/electron/electron/mcp/client/transports/WebSocketTransport.js +329 -0
- package/dist/electron/electron/mcp/host/MCPHostServer.js +354 -0
- package/dist/electron/electron/mcp/host/ToolAdapter.js +100 -0
- package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +497 -0
- package/dist/electron/electron/mcp/settings.js +446 -0
- package/dist/electron/electron/mcp/types.js +59 -0
- package/dist/electron/electron/memory/MemoryService.js +435 -0
- package/dist/electron/electron/notifications/index.js +17 -0
- package/dist/electron/electron/notifications/service.js +118 -0
- package/dist/electron/electron/notifications/store.js +144 -0
- package/dist/electron/electron/preload.js +842 -0
- package/dist/electron/electron/reports/StandupReportService.js +272 -0
- package/dist/electron/electron/security/concurrency.js +293 -0
- package/dist/electron/electron/security/index.js +15 -0
- package/dist/electron/electron/security/policy-manager.js +435 -0
- package/dist/electron/electron/settings/appearance-manager.js +193 -0
- package/dist/electron/electron/settings/personality-manager.js +724 -0
- package/dist/electron/electron/settings/x-manager.js +58 -0
- package/dist/electron/electron/tailscale/exposure.js +188 -0
- package/dist/electron/electron/tailscale/index.js +28 -0
- package/dist/electron/electron/tailscale/settings.js +205 -0
- package/dist/electron/electron/tailscale/tailscale.js +355 -0
- package/dist/electron/electron/tray/QuickInputWindow.js +568 -0
- package/dist/electron/electron/tray/TrayManager.js +895 -0
- package/dist/electron/electron/tray/index.js +9 -0
- package/dist/electron/electron/updater/index.js +6 -0
- package/dist/electron/electron/updater/update-manager.js +418 -0
- package/dist/electron/electron/utils/env-migration.js +209 -0
- package/dist/electron/electron/utils/process.js +102 -0
- package/dist/electron/electron/utils/rate-limiter.js +104 -0
- package/dist/electron/electron/utils/validation.js +419 -0
- package/dist/electron/electron/utils/x-cli.js +177 -0
- package/dist/electron/electron/voice/VoiceService.js +507 -0
- package/dist/electron/electron/voice/index.js +14 -0
- package/dist/electron/electron/voice/voice-settings-manager.js +359 -0
- package/dist/electron/shared/channelMessages.js +170 -0
- package/dist/electron/shared/types.js +1185 -0
- package/package.json +159 -0
- package/resources/skills/1password.json +10 -0
- package/resources/skills/add-documentation.json +31 -0
- package/resources/skills/analyze-csv.json +17 -0
- package/resources/skills/apple-notes.json +10 -0
- package/resources/skills/apple-reminders.json +10 -0
- package/resources/skills/auto-commenter.json +10 -0
- package/resources/skills/bear-notes.json +10 -0
- package/resources/skills/bird.json +35 -0
- package/resources/skills/blogwatcher.json +10 -0
- package/resources/skills/blucli.json +10 -0
- package/resources/skills/bluebubbles.json +10 -0
- package/resources/skills/camsnap.json +10 -0
- package/resources/skills/clean-imports.json +18 -0
- package/resources/skills/code-review.json +18 -0
- package/resources/skills/coding-agent.json +10 -0
- package/resources/skills/compare-files.json +23 -0
- package/resources/skills/convert-code.json +34 -0
- package/resources/skills/create-changelog.json +24 -0
- package/resources/skills/debug-error.json +17 -0
- package/resources/skills/dependency-check.json +10 -0
- package/resources/skills/discord.json +10 -0
- package/resources/skills/eightctl.json +10 -0
- package/resources/skills/explain-code.json +29 -0
- package/resources/skills/extract-todos.json +18 -0
- package/resources/skills/food-order.json +10 -0
- package/resources/skills/gemini.json +10 -0
- package/resources/skills/generate-readme.json +10 -0
- package/resources/skills/gifgrep.json +10 -0
- package/resources/skills/git-commit.json +10 -0
- package/resources/skills/github.json +10 -0
- package/resources/skills/gog.json +10 -0
- package/resources/skills/goplaces.json +10 -0
- package/resources/skills/himalaya.json +10 -0
- package/resources/skills/imsg.json +10 -0
- package/resources/skills/karpathy-guidelines.json +12 -0
- package/resources/skills/last30days.json +26 -0
- package/resources/skills/local-places.json +10 -0
- package/resources/skills/mcporter.json +10 -0
- package/resources/skills/model-usage.json +10 -0
- package/resources/skills/nano-banana-pro.json +10 -0
- package/resources/skills/nano-pdf.json +10 -0
- package/resources/skills/notion.json +10 -0
- package/resources/skills/obsidian.json +10 -0
- package/resources/skills/openai-image-gen.json +10 -0
- package/resources/skills/openai-whisper-api.json +10 -0
- package/resources/skills/openai-whisper.json +10 -0
- package/resources/skills/openhue.json +10 -0
- package/resources/skills/oracle.json +10 -0
- package/resources/skills/ordercli.json +10 -0
- package/resources/skills/peekaboo.json +10 -0
- package/resources/skills/project-structure.json +10 -0
- package/resources/skills/proofread.json +17 -0
- package/resources/skills/refactor-code.json +31 -0
- package/resources/skills/rename-symbol.json +23 -0
- package/resources/skills/sag.json +10 -0
- package/resources/skills/security-audit.json +18 -0
- package/resources/skills/session-logs.json +10 -0
- package/resources/skills/sherpa-onnx-tts.json +10 -0
- package/resources/skills/skill-creator.json +15 -0
- package/resources/skills/skill-hub.json +29 -0
- package/resources/skills/slack.json +10 -0
- package/resources/skills/songsee.json +10 -0
- package/resources/skills/sonoscli.json +10 -0
- package/resources/skills/spotify-player.json +10 -0
- package/resources/skills/startup-cfo.json +55 -0
- package/resources/skills/summarize-folder.json +18 -0
- package/resources/skills/summarize.json +10 -0
- package/resources/skills/things-mac.json +10 -0
- package/resources/skills/tmux.json +10 -0
- package/resources/skills/translate.json +36 -0
- package/resources/skills/trello.json +10 -0
- package/resources/skills/video-frames.json +10 -0
- package/resources/skills/voice-call.json +10 -0
- package/resources/skills/wacli.json +10 -0
- package/resources/skills/weather.json +10 -0
- package/resources/skills/write-tests.json +31 -0
- package/src/electron/activity/ActivityRepository.ts +238 -0
- package/src/electron/agent/browser/browser-service.ts +721 -0
- package/src/electron/agent/context-manager.ts +257 -0
- package/src/electron/agent/custom-skill-loader.ts +634 -0
- package/src/electron/agent/daemon.ts +1097 -0
- package/src/electron/agent/executor.ts +4017 -0
- package/src/electron/agent/llm/anthropic-provider.ts +175 -0
- package/src/electron/agent/llm/bedrock-provider.ts +236 -0
- package/src/electron/agent/llm/gemini-provider.ts +422 -0
- package/src/electron/agent/llm/index.ts +9 -0
- package/src/electron/agent/llm/ollama-provider.ts +347 -0
- package/src/electron/agent/llm/openai-oauth.ts +127 -0
- package/src/electron/agent/llm/openai-provider.ts +686 -0
- package/src/electron/agent/llm/openrouter-provider.ts +273 -0
- package/src/electron/agent/llm/pricing.ts +180 -0
- package/src/electron/agent/llm/provider-factory.ts +971 -0
- package/src/electron/agent/llm/types.ts +291 -0
- package/src/electron/agent/queue-manager.ts +408 -0
- package/src/electron/agent/sandbox/docker-sandbox.ts +453 -0
- package/src/electron/agent/sandbox/macos-sandbox.ts +426 -0
- package/src/electron/agent/sandbox/runner.ts +453 -0
- package/src/electron/agent/sandbox/sandbox-factory.ts +337 -0
- package/src/electron/agent/sandbox/security-utils.ts +251 -0
- package/src/electron/agent/search/brave-provider.ts +141 -0
- package/src/electron/agent/search/google-provider.ts +131 -0
- package/src/electron/agent/search/index.ts +6 -0
- package/src/electron/agent/search/provider-factory.ts +450 -0
- package/src/electron/agent/search/serpapi-provider.ts +138 -0
- package/src/electron/agent/search/tavily-provider.ts +108 -0
- package/src/electron/agent/search/types.ts +118 -0
- package/src/electron/agent/security/index.ts +20 -0
- package/src/electron/agent/security/input-sanitizer.ts +380 -0
- package/src/electron/agent/security/output-filter.ts +259 -0
- package/src/electron/agent/skill-eligibility.ts +334 -0
- package/src/electron/agent/skill-registry.ts +457 -0
- package/src/electron/agent/skills/document.ts +1070 -0
- package/src/electron/agent/skills/image-generator.ts +272 -0
- package/src/electron/agent/skills/organizer.ts +131 -0
- package/src/electron/agent/skills/presentation.ts +418 -0
- package/src/electron/agent/skills/spreadsheet.ts +166 -0
- package/src/electron/agent/tools/browser-tools.ts +546 -0
- package/src/electron/agent/tools/builtin-settings.ts +422 -0
- package/src/electron/agent/tools/canvas-tools.ts +572 -0
- package/src/electron/agent/tools/cron-tools.ts +723 -0
- package/src/electron/agent/tools/edit-tools.ts +196 -0
- package/src/electron/agent/tools/file-tools.ts +811 -0
- package/src/electron/agent/tools/glob-tools.ts +303 -0
- package/src/electron/agent/tools/grep-tools.ts +432 -0
- package/src/electron/agent/tools/image-tools.ts +126 -0
- package/src/electron/agent/tools/mention-tools.ts +371 -0
- package/src/electron/agent/tools/node-tools.ts +550 -0
- package/src/electron/agent/tools/registry.ts +3052 -0
- package/src/electron/agent/tools/search-tools.ts +111 -0
- package/src/electron/agent/tools/shell-tools.ts +651 -0
- package/src/electron/agent/tools/skill-tools.ts +340 -0
- package/src/electron/agent/tools/system-tools.ts +665 -0
- package/src/electron/agent/tools/web-fetch-tools.ts +528 -0
- package/src/electron/agent/tools/x-tools.ts +267 -0
- package/src/electron/agents/AgentRoleRepository.ts +557 -0
- package/src/electron/agents/HeartbeatService.ts +469 -0
- package/src/electron/agents/MentionRepository.ts +242 -0
- package/src/electron/agents/TaskSubscriptionRepository.ts +231 -0
- package/src/electron/agents/WorkingStateRepository.ts +278 -0
- package/src/electron/canvas/canvas-manager.ts +818 -0
- package/src/electron/canvas/canvas-preload.ts +102 -0
- package/src/electron/canvas/canvas-protocol.ts +174 -0
- package/src/electron/canvas/canvas-store.ts +200 -0
- package/src/electron/canvas/index.ts +8 -0
- package/src/electron/control-plane/client.ts +527 -0
- package/src/electron/control-plane/handlers.ts +723 -0
- package/src/electron/control-plane/index.ts +51 -0
- package/src/electron/control-plane/node-manager.ts +322 -0
- package/src/electron/control-plane/protocol.ts +269 -0
- package/src/electron/control-plane/remote-client.ts +517 -0
- package/src/electron/control-plane/server.ts +853 -0
- package/src/electron/control-plane/settings.ts +401 -0
- package/src/electron/control-plane/ssh-tunnel.ts +624 -0
- package/src/electron/cron/index.ts +9 -0
- package/src/electron/cron/schedule.ts +217 -0
- package/src/electron/cron/service.ts +743 -0
- package/src/electron/cron/store.ts +165 -0
- package/src/electron/cron/types.ts +291 -0
- package/src/electron/cron/webhook.ts +303 -0
- package/src/electron/database/SecureSettingsRepository.ts +514 -0
- package/src/electron/database/TaskLabelRepository.ts +148 -0
- package/src/electron/database/repositories.ts +2397 -0
- package/src/electron/database/schema.ts +1017 -0
- package/src/electron/extensions/index.ts +18 -0
- package/src/electron/extensions/loader.ts +336 -0
- package/src/electron/extensions/registry.ts +546 -0
- package/src/electron/extensions/types.ts +372 -0
- package/src/electron/gateway/channel-registry.ts +1267 -0
- package/src/electron/gateway/channels/bluebubbles-client.ts +641 -0
- package/src/electron/gateway/channels/bluebubbles.ts +509 -0
- package/src/electron/gateway/channels/discord.ts +1150 -0
- package/src/electron/gateway/channels/email-client.ts +708 -0
- package/src/electron/gateway/channels/email.ts +516 -0
- package/src/electron/gateway/channels/google-chat.ts +760 -0
- package/src/electron/gateway/channels/imessage-client.ts +473 -0
- package/src/electron/gateway/channels/imessage.ts +520 -0
- package/src/electron/gateway/channels/index.ts +21 -0
- package/src/electron/gateway/channels/line-client.ts +598 -0
- package/src/electron/gateway/channels/line.ts +559 -0
- package/src/electron/gateway/channels/matrix-client.ts +632 -0
- package/src/electron/gateway/channels/matrix.ts +655 -0
- package/src/electron/gateway/channels/mattermost-client.ts +526 -0
- package/src/electron/gateway/channels/mattermost.ts +550 -0
- package/src/electron/gateway/channels/signal-client.ts +722 -0
- package/src/electron/gateway/channels/signal.ts +666 -0
- package/src/electron/gateway/channels/slack.ts +458 -0
- package/src/electron/gateway/channels/teams.ts +681 -0
- package/src/electron/gateway/channels/telegram.ts +1727 -0
- package/src/electron/gateway/channels/twitch-client.ts +665 -0
- package/src/electron/gateway/channels/twitch.ts +468 -0
- package/src/electron/gateway/channels/types.ts +1002 -0
- package/src/electron/gateway/channels/whatsapp.ts +1101 -0
- package/src/electron/gateway/context-policy.ts +382 -0
- package/src/electron/gateway/index.ts +1274 -0
- package/src/electron/gateway/infrastructure.ts +645 -0
- package/src/electron/gateway/router.ts +3206 -0
- package/src/electron/gateway/security.ts +422 -0
- package/src/electron/gateway/session.ts +144 -0
- package/src/electron/gateway/tunnel.ts +626 -0
- package/src/electron/guardrails/guardrail-manager.ts +380 -0
- package/src/electron/hooks/gmail-watcher.ts +355 -0
- package/src/electron/hooks/index.ts +30 -0
- package/src/electron/hooks/mappings.ts +404 -0
- package/src/electron/hooks/server.ts +574 -0
- package/src/electron/hooks/settings.ts +466 -0
- package/src/electron/hooks/types.ts +245 -0
- package/src/electron/ipc/canvas-handlers.ts +223 -0
- package/src/electron/ipc/handlers.ts +3661 -0
- package/src/electron/ipc/mission-control-handlers.ts +182 -0
- package/src/electron/main.ts +496 -0
- package/src/electron/mcp/client/MCPClientManager.ts +406 -0
- package/src/electron/mcp/client/MCPServerConnection.ts +514 -0
- package/src/electron/mcp/client/transports/SSETransport.ts +360 -0
- package/src/electron/mcp/client/transports/StdioTransport.ts +355 -0
- package/src/electron/mcp/client/transports/WebSocketTransport.ts +384 -0
- package/src/electron/mcp/host/MCPHostServer.ts +388 -0
- package/src/electron/mcp/host/ToolAdapter.ts +140 -0
- package/src/electron/mcp/registry/MCPRegistryManager.ts +565 -0
- package/src/electron/mcp/settings.ts +468 -0
- package/src/electron/mcp/types.ts +371 -0
- package/src/electron/memory/MemoryService.ts +523 -0
- package/src/electron/notifications/index.ts +16 -0
- package/src/electron/notifications/service.ts +161 -0
- package/src/electron/notifications/store.ts +163 -0
- package/src/electron/preload.ts +2845 -0
- package/src/electron/reports/StandupReportService.ts +356 -0
- package/src/electron/security/concurrency.ts +333 -0
- package/src/electron/security/index.ts +17 -0
- package/src/electron/security/policy-manager.ts +539 -0
- package/src/electron/settings/appearance-manager.ts +182 -0
- package/src/electron/settings/personality-manager.ts +800 -0
- package/src/electron/settings/x-manager.ts +62 -0
- package/src/electron/tailscale/exposure.ts +262 -0
- package/src/electron/tailscale/index.ts +34 -0
- package/src/electron/tailscale/settings.ts +218 -0
- package/src/electron/tailscale/tailscale.ts +379 -0
- package/src/electron/tray/QuickInputWindow.ts +609 -0
- package/src/electron/tray/TrayManager.ts +1005 -0
- package/src/electron/tray/index.ts +6 -0
- package/src/electron/updater/index.ts +1 -0
- package/src/electron/updater/update-manager.ts +447 -0
- package/src/electron/utils/env-migration.ts +203 -0
- package/src/electron/utils/process.ts +124 -0
- package/src/electron/utils/rate-limiter.ts +130 -0
- package/src/electron/utils/validation.ts +493 -0
- package/src/electron/utils/x-cli.ts +198 -0
- package/src/electron/voice/VoiceService.ts +583 -0
- package/src/electron/voice/index.ts +9 -0
- package/src/electron/voice/voice-settings-manager.ts +403 -0
- package/src/renderer/App.tsx +775 -0
- package/src/renderer/components/ActivityFeed.tsx +407 -0
- package/src/renderer/components/ActivityFeedItem.tsx +285 -0
- package/src/renderer/components/AgentRoleCard.tsx +343 -0
- package/src/renderer/components/AgentRoleEditor.tsx +805 -0
- package/src/renderer/components/AgentSquadSettings.tsx +295 -0
- package/src/renderer/components/AgentWorkingStatePanel.tsx +411 -0
- package/src/renderer/components/AppearanceSettings.tsx +122 -0
- package/src/renderer/components/ApprovalDialog.tsx +100 -0
- package/src/renderer/components/BlueBubblesSettings.tsx +505 -0
- package/src/renderer/components/BuiltinToolsSettings.tsx +307 -0
- package/src/renderer/components/CanvasPreview.tsx +1189 -0
- package/src/renderer/components/CommandOutput.tsx +202 -0
- package/src/renderer/components/ContextPolicySettings.tsx +523 -0
- package/src/renderer/components/ControlPlaneSettings.tsx +1134 -0
- package/src/renderer/components/DisclaimerModal.tsx +124 -0
- package/src/renderer/components/DiscordSettings.tsx +436 -0
- package/src/renderer/components/EmailSettings.tsx +606 -0
- package/src/renderer/components/ExtensionsSettings.tsx +542 -0
- package/src/renderer/components/FileViewer.tsx +224 -0
- package/src/renderer/components/GoogleChatSettings.tsx +535 -0
- package/src/renderer/components/GuardrailSettings.tsx +487 -0
- package/src/renderer/components/HooksSettings.tsx +581 -0
- package/src/renderer/components/ImessageSettings.tsx +484 -0
- package/src/renderer/components/LineSettings.tsx +483 -0
- package/src/renderer/components/MCPRegistryBrowser.tsx +386 -0
- package/src/renderer/components/MCPSettings.tsx +943 -0
- package/src/renderer/components/MainContent.tsx +2433 -0
- package/src/renderer/components/MatrixSettings.tsx +510 -0
- package/src/renderer/components/MattermostSettings.tsx +473 -0
- package/src/renderer/components/MemorySettings.tsx +247 -0
- package/src/renderer/components/MentionBadge.tsx +87 -0
- package/src/renderer/components/MentionInput.tsx +409 -0
- package/src/renderer/components/MentionList.tsx +476 -0
- package/src/renderer/components/MissionControlPanel.tsx +1995 -0
- package/src/renderer/components/NodesSettings.tsx +316 -0
- package/src/renderer/components/NotificationPanel.tsx +481 -0
- package/src/renderer/components/Onboarding/AwakeningOrb.tsx +44 -0
- package/src/renderer/components/Onboarding/Onboarding.tsx +443 -0
- package/src/renderer/components/Onboarding/TypewriterText.tsx +102 -0
- package/src/renderer/components/Onboarding/index.ts +3 -0
- package/src/renderer/components/OnboardingModal.tsx +698 -0
- package/src/renderer/components/PairingCodeDisplay.tsx +324 -0
- package/src/renderer/components/PersonalitySettings.tsx +597 -0
- package/src/renderer/components/QueueSettings.tsx +119 -0
- package/src/renderer/components/QuickTaskFAB.tsx +71 -0
- package/src/renderer/components/RightPanel.tsx +413 -0
- package/src/renderer/components/ScheduledTasksSettings.tsx +1328 -0
- package/src/renderer/components/SearchSettings.tsx +328 -0
- package/src/renderer/components/Settings.tsx +1504 -0
- package/src/renderer/components/Sidebar.tsx +344 -0
- package/src/renderer/components/SignalSettings.tsx +673 -0
- package/src/renderer/components/SkillHubBrowser.tsx +458 -0
- package/src/renderer/components/SkillParameterModal.tsx +185 -0
- package/src/renderer/components/SkillsSettings.tsx +451 -0
- package/src/renderer/components/SlackSettings.tsx +442 -0
- package/src/renderer/components/StandupReportViewer.tsx +614 -0
- package/src/renderer/components/TaskBoard.tsx +498 -0
- package/src/renderer/components/TaskBoardCard.tsx +357 -0
- package/src/renderer/components/TaskBoardColumn.tsx +211 -0
- package/src/renderer/components/TaskLabelManager.tsx +472 -0
- package/src/renderer/components/TaskQueuePanel.tsx +144 -0
- package/src/renderer/components/TaskQuickActions.tsx +492 -0
- package/src/renderer/components/TaskTimeline.tsx +216 -0
- package/src/renderer/components/TaskView.tsx +162 -0
- package/src/renderer/components/TeamsSettings.tsx +518 -0
- package/src/renderer/components/TelegramSettings.tsx +421 -0
- package/src/renderer/components/Toast.tsx +76 -0
- package/src/renderer/components/TraySettings.tsx +189 -0
- package/src/renderer/components/TwitchSettings.tsx +511 -0
- package/src/renderer/components/UpdateSettings.tsx +295 -0
- package/src/renderer/components/VoiceIndicator.tsx +270 -0
- package/src/renderer/components/VoiceSettings.tsx +867 -0
- package/src/renderer/components/WhatsAppSettings.tsx +721 -0
- package/src/renderer/components/WorkingStateEditor.tsx +309 -0
- package/src/renderer/components/WorkingStateHistory.tsx +481 -0
- package/src/renderer/components/WorkspaceSelector.tsx +150 -0
- package/src/renderer/components/XSettings.tsx +311 -0
- package/src/renderer/global.d.ts +9 -0
- package/src/renderer/hooks/useAgentContext.ts +153 -0
- package/src/renderer/hooks/useOnboardingFlow.ts +548 -0
- package/src/renderer/hooks/useVoiceInput.ts +268 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +10 -0
- package/src/renderer/public/cowork-os-logo.png +0 -0
- package/src/renderer/quick-input.html +164 -0
- package/src/renderer/styles/index.css +14504 -0
- package/src/renderer/utils/agentMessages.ts +749 -0
- package/src/renderer/utils/voice-directives.ts +169 -0
- package/src/shared/channelMessages.ts +213 -0
- package/src/shared/types.ts +3608 -0
- package/tsconfig.electron.json +26 -0
- package/tsconfig.json +26 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +23 -0
|
@@ -0,0 +1,2433 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback, useMemo, Fragment } from 'react';
|
|
2
|
+
import ReactMarkdown from 'react-markdown';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import remarkBreaks from 'remark-breaks';
|
|
5
|
+
import { Task, TaskEvent, Workspace, ApprovalRequest, LLMModelInfo, SuccessCriteria, CustomSkill, EventType, TEMP_WORKSPACE_ID, DEFAULT_QUIRKS, CanvasSession } from '../../shared/types';
|
|
6
|
+
import type { AgentRoleData } from '../../electron/preload';
|
|
7
|
+
import { useVoiceInput } from '../hooks/useVoiceInput';
|
|
8
|
+
import { useAgentContext, type AgentContext } from '../hooks/useAgentContext';
|
|
9
|
+
import { getMessage } from '../utils/agentMessages';
|
|
10
|
+
|
|
11
|
+
// localStorage key for verbose mode
|
|
12
|
+
const VERBOSE_STEPS_KEY = 'cowork:verboseSteps';
|
|
13
|
+
const TASK_TITLE_MAX_LENGTH = 50;
|
|
14
|
+
const TITLE_ELLIPSIS_REGEX = /(\.\.\.|\u2026)$/u;
|
|
15
|
+
|
|
16
|
+
// Important event types shown in non-verbose mode
|
|
17
|
+
// These are high-level steps that represent meaningful progress
|
|
18
|
+
const IMPORTANT_EVENT_TYPES: EventType[] = [
|
|
19
|
+
'task_created',
|
|
20
|
+
'task_completed',
|
|
21
|
+
'task_cancelled',
|
|
22
|
+
'plan_created',
|
|
23
|
+
'step_started',
|
|
24
|
+
'step_completed',
|
|
25
|
+
'step_failed',
|
|
26
|
+
'assistant_message',
|
|
27
|
+
'user_message',
|
|
28
|
+
'file_created',
|
|
29
|
+
'file_modified',
|
|
30
|
+
'file_deleted',
|
|
31
|
+
'error',
|
|
32
|
+
'verification_started',
|
|
33
|
+
'verification_passed',
|
|
34
|
+
'verification_failed',
|
|
35
|
+
'retry_started',
|
|
36
|
+
'approval_requested',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Helper to check if an event is important (shown in non-verbose mode)
|
|
40
|
+
const isImportantEvent = (event: TaskEvent): boolean => {
|
|
41
|
+
return IMPORTANT_EVENT_TYPES.includes(event.type);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const buildTaskTitle = (text: string): string => {
|
|
45
|
+
const trimmed = text.trim();
|
|
46
|
+
if (trimmed.length <= TASK_TITLE_MAX_LENGTH) {
|
|
47
|
+
return trimmed;
|
|
48
|
+
}
|
|
49
|
+
return `${trimmed.slice(0, TASK_TITLE_MAX_LENGTH)}...`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type MentionOption = {
|
|
53
|
+
type: 'agent' | 'everyone';
|
|
54
|
+
id: string;
|
|
55
|
+
label: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
icon?: string;
|
|
58
|
+
color?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const normalizeMentionSearch = (value: string): string =>
|
|
62
|
+
value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
63
|
+
import { ApprovalDialog } from './ApprovalDialog';
|
|
64
|
+
import { SkillParameterModal } from './SkillParameterModal';
|
|
65
|
+
import { FileViewer } from './FileViewer';
|
|
66
|
+
import { CommandOutput } from './CommandOutput';
|
|
67
|
+
import { CanvasPreview } from './CanvasPreview';
|
|
68
|
+
|
|
69
|
+
// Code block component with copy button
|
|
70
|
+
interface CodeBlockProps {
|
|
71
|
+
children?: React.ReactNode;
|
|
72
|
+
className?: string;
|
|
73
|
+
node?: unknown;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function CodeBlock({ children, className, ...props }: CodeBlockProps) {
|
|
77
|
+
const [copied, setCopied] = useState(false);
|
|
78
|
+
|
|
79
|
+
// Check if this is a code block (has language class) vs inline code
|
|
80
|
+
const isCodeBlock = className?.startsWith('language-');
|
|
81
|
+
const language = className?.replace('language-', '') || '';
|
|
82
|
+
|
|
83
|
+
// Get the text content for copying
|
|
84
|
+
const getTextContent = (node: React.ReactNode): string => {
|
|
85
|
+
if (typeof node === 'string') return node;
|
|
86
|
+
if (Array.isArray(node)) return node.map(getTextContent).join('');
|
|
87
|
+
if (node && typeof node === 'object' && 'props' in node) {
|
|
88
|
+
return getTextContent((node as { props: { children?: React.ReactNode } }).props.children);
|
|
89
|
+
}
|
|
90
|
+
return '';
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleCopy = async () => {
|
|
94
|
+
const text = getTextContent(children);
|
|
95
|
+
try {
|
|
96
|
+
await navigator.clipboard.writeText(text);
|
|
97
|
+
setCopied(true);
|
|
98
|
+
setTimeout(() => setCopied(false), 2000);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error('Failed to copy:', err);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// For inline code, just render normally
|
|
105
|
+
if (!isCodeBlock) {
|
|
106
|
+
return <code className={className} {...props}>{children}</code>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// For code blocks, wrap with copy button
|
|
110
|
+
return (
|
|
111
|
+
<div className="code-block-wrapper">
|
|
112
|
+
<div className="code-block-header">
|
|
113
|
+
{language && <span className="code-block-language">{language}</span>}
|
|
114
|
+
<button
|
|
115
|
+
className={`code-block-copy ${copied ? 'copied' : ''}`}
|
|
116
|
+
onClick={handleCopy}
|
|
117
|
+
title={copied ? 'Copied!' : 'Copy code'}
|
|
118
|
+
>
|
|
119
|
+
{copied ? (
|
|
120
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
121
|
+
<path d="M20 6L9 17l-5-5" />
|
|
122
|
+
</svg>
|
|
123
|
+
) : (
|
|
124
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
125
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
126
|
+
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
|
127
|
+
</svg>
|
|
128
|
+
)}
|
|
129
|
+
<span>{copied ? 'Copied!' : 'Copy'}</span>
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
<code className={className} {...props}>{children}</code>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Copy button for user messages
|
|
138
|
+
function MessageCopyButton({ text }: { text: string }) {
|
|
139
|
+
const [copied, setCopied] = useState(false);
|
|
140
|
+
|
|
141
|
+
const handleCopy = async () => {
|
|
142
|
+
try {
|
|
143
|
+
await navigator.clipboard.writeText(text);
|
|
144
|
+
setCopied(true);
|
|
145
|
+
setTimeout(() => setCopied(false), 2000);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error('Failed to copy:', err);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<button
|
|
153
|
+
className={`message-copy-btn ${copied ? 'copied' : ''}`}
|
|
154
|
+
onClick={handleCopy}
|
|
155
|
+
title={copied ? 'Copied!' : 'Copy message'}
|
|
156
|
+
>
|
|
157
|
+
{copied ? (
|
|
158
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
159
|
+
<path d="M20 6L9 17l-5-5" />
|
|
160
|
+
</svg>
|
|
161
|
+
) : (
|
|
162
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
163
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
164
|
+
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
|
165
|
+
</svg>
|
|
166
|
+
)}
|
|
167
|
+
<span>{copied ? 'Copied' : 'Copy'}</span>
|
|
168
|
+
</button>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Global audio state to ensure only one audio plays at a time
|
|
173
|
+
let currentAudioContext: AudioContext | null = null;
|
|
174
|
+
let currentAudioSource: AudioBufferSourceNode | null = null;
|
|
175
|
+
let currentSpeakingCallback: (() => void) | null = null;
|
|
176
|
+
|
|
177
|
+
function stopCurrentAudio() {
|
|
178
|
+
if (currentAudioSource) {
|
|
179
|
+
try {
|
|
180
|
+
currentAudioSource.stop();
|
|
181
|
+
} catch {
|
|
182
|
+
// Already stopped
|
|
183
|
+
}
|
|
184
|
+
currentAudioSource = null;
|
|
185
|
+
}
|
|
186
|
+
if (currentAudioContext) {
|
|
187
|
+
try {
|
|
188
|
+
currentAudioContext.close();
|
|
189
|
+
} catch {
|
|
190
|
+
// Already closed
|
|
191
|
+
}
|
|
192
|
+
currentAudioContext = null;
|
|
193
|
+
}
|
|
194
|
+
if (currentSpeakingCallback) {
|
|
195
|
+
currentSpeakingCallback();
|
|
196
|
+
currentSpeakingCallback = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Speak button for assistant messages
|
|
201
|
+
function MessageSpeakButton({ text, voiceEnabled }: { text: string; voiceEnabled: boolean }) {
|
|
202
|
+
const [speaking, setSpeaking] = useState(false);
|
|
203
|
+
const [loading, setLoading] = useState(false);
|
|
204
|
+
|
|
205
|
+
const handleClick = async () => {
|
|
206
|
+
if (!voiceEnabled) return;
|
|
207
|
+
|
|
208
|
+
// If already speaking, stop the audio
|
|
209
|
+
if (speaking) {
|
|
210
|
+
stopCurrentAudio();
|
|
211
|
+
setSpeaking(false);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
setLoading(true);
|
|
217
|
+
// Strip markdown for cleaner speech
|
|
218
|
+
const cleanText = text
|
|
219
|
+
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
|
220
|
+
.replace(/`[^`]+`/g, '') // Remove inline code
|
|
221
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Keep link text only
|
|
222
|
+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '') // Remove images
|
|
223
|
+
.replace(/^#{1,6}\s+/gm, '') // Remove headers
|
|
224
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1') // Remove bold
|
|
225
|
+
.replace(/\*([^*]+)\*/g, '$1') // Remove italic
|
|
226
|
+
.replace(/\[\[speak\]\]([\s\S]*?)\[\[\/speak\]\]/gi, '$1') // Extract speak tags
|
|
227
|
+
.trim();
|
|
228
|
+
|
|
229
|
+
if (cleanText) {
|
|
230
|
+
// Stop any currently playing audio first
|
|
231
|
+
stopCurrentAudio();
|
|
232
|
+
|
|
233
|
+
const result = await window.electronAPI.voiceSpeak(cleanText);
|
|
234
|
+
if (result.success && result.audioData) {
|
|
235
|
+
// Convert number array back to ArrayBuffer and play
|
|
236
|
+
const audioBuffer = new Uint8Array(result.audioData).buffer;
|
|
237
|
+
const audioContext = new AudioContext();
|
|
238
|
+
const decodedAudio = await audioContext.decodeAudioData(audioBuffer);
|
|
239
|
+
const source = audioContext.createBufferSource();
|
|
240
|
+
source.buffer = decodedAudio;
|
|
241
|
+
source.connect(audioContext.destination);
|
|
242
|
+
|
|
243
|
+
// Store references for stopping
|
|
244
|
+
currentAudioContext = audioContext;
|
|
245
|
+
currentAudioSource = source;
|
|
246
|
+
currentSpeakingCallback = () => setSpeaking(false);
|
|
247
|
+
|
|
248
|
+
source.onended = () => {
|
|
249
|
+
setSpeaking(false);
|
|
250
|
+
currentAudioContext = null;
|
|
251
|
+
currentAudioSource = null;
|
|
252
|
+
currentSpeakingCallback = null;
|
|
253
|
+
try {
|
|
254
|
+
audioContext.close();
|
|
255
|
+
} catch {
|
|
256
|
+
// Already closed
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
setLoading(false);
|
|
261
|
+
setSpeaking(true);
|
|
262
|
+
source.start(0);
|
|
263
|
+
return;
|
|
264
|
+
} else if (!result.success) {
|
|
265
|
+
console.error('TTS failed:', result.error);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error('Failed to speak:', err);
|
|
270
|
+
} finally {
|
|
271
|
+
setLoading(false);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
if (!voiceEnabled) return null;
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<button
|
|
279
|
+
className={`message-speak-btn ${speaking ? 'speaking' : ''}`}
|
|
280
|
+
onClick={handleClick}
|
|
281
|
+
title={speaking ? 'Stop speaking' : loading ? 'Loading...' : 'Speak message'}
|
|
282
|
+
disabled={loading}
|
|
283
|
+
>
|
|
284
|
+
{speaking ? (
|
|
285
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2">
|
|
286
|
+
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
287
|
+
</svg>
|
|
288
|
+
) : loading ? (
|
|
289
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="spin">
|
|
290
|
+
<circle cx="12" cy="12" r="10" strokeDasharray="32" strokeDashoffset="12" />
|
|
291
|
+
</svg>
|
|
292
|
+
) : (
|
|
293
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
294
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
295
|
+
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
|
296
|
+
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
|
|
297
|
+
</svg>
|
|
298
|
+
)}
|
|
299
|
+
<span>{speaking ? 'Stop' : loading ? 'Loading' : 'Speak'}</span>
|
|
300
|
+
</button>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Custom components for ReactMarkdown
|
|
305
|
+
const markdownComponents = {
|
|
306
|
+
code: CodeBlock,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const userMarkdownPlugins = [remarkGfm, remarkBreaks];
|
|
310
|
+
|
|
311
|
+
// Searchable Model Dropdown Component
|
|
312
|
+
interface ModelDropdownProps {
|
|
313
|
+
models: LLMModelInfo[];
|
|
314
|
+
selectedModel: string;
|
|
315
|
+
onModelChange: (model: string) => void;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function ModelDropdown({ models, selectedModel, onModelChange }: ModelDropdownProps) {
|
|
319
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
320
|
+
const [search, setSearch] = useState('');
|
|
321
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
322
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
323
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
324
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
325
|
+
|
|
326
|
+
const selectedModelInfo = models.find(m => m.key === selectedModel);
|
|
327
|
+
|
|
328
|
+
const filteredModels = models.filter(model =>
|
|
329
|
+
model.displayName.toLowerCase().includes(search.toLowerCase()) ||
|
|
330
|
+
model.key.toLowerCase().includes(search.toLowerCase()) ||
|
|
331
|
+
model.description.toLowerCase().includes(search.toLowerCase())
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Reset highlighted index when search changes
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
setHighlightedIndex(0);
|
|
337
|
+
}, [search]);
|
|
338
|
+
|
|
339
|
+
// Scroll highlighted option into view
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
if (isOpen && listRef.current) {
|
|
342
|
+
const highlightedEl = listRef.current.querySelector(`[data-index="${highlightedIndex}"]`);
|
|
343
|
+
if (highlightedEl) {
|
|
344
|
+
highlightedEl.scrollIntoView({ block: 'nearest' });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}, [highlightedIndex, isOpen]);
|
|
348
|
+
|
|
349
|
+
// Close on click outside
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
352
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
353
|
+
setIsOpen(false);
|
|
354
|
+
setSearch('');
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
358
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
359
|
+
}, []);
|
|
360
|
+
|
|
361
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
362
|
+
if (!isOpen) {
|
|
363
|
+
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
setIsOpen(true);
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
switch (e.key) {
|
|
371
|
+
case 'ArrowDown':
|
|
372
|
+
e.preventDefault();
|
|
373
|
+
setHighlightedIndex(i => Math.min(i + 1, filteredModels.length - 1));
|
|
374
|
+
break;
|
|
375
|
+
case 'ArrowUp':
|
|
376
|
+
e.preventDefault();
|
|
377
|
+
setHighlightedIndex(i => Math.max(i - 1, 0));
|
|
378
|
+
break;
|
|
379
|
+
case 'Enter':
|
|
380
|
+
e.preventDefault();
|
|
381
|
+
if (filteredModels[highlightedIndex]) {
|
|
382
|
+
onModelChange(filteredModels[highlightedIndex].key);
|
|
383
|
+
setIsOpen(false);
|
|
384
|
+
setSearch('');
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
case 'Escape':
|
|
388
|
+
e.preventDefault();
|
|
389
|
+
setIsOpen(false);
|
|
390
|
+
setSearch('');
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const handleSelect = (modelKey: string) => {
|
|
396
|
+
onModelChange(modelKey);
|
|
397
|
+
setIsOpen(false);
|
|
398
|
+
setSearch('');
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<div className="model-dropdown-container" ref={containerRef}>
|
|
403
|
+
<button
|
|
404
|
+
className={`model-selector ${isOpen ? 'open' : ''}`}
|
|
405
|
+
onClick={() => {
|
|
406
|
+
setIsOpen(!isOpen);
|
|
407
|
+
if (!isOpen) {
|
|
408
|
+
setTimeout(() => inputRef.current?.focus(), 0);
|
|
409
|
+
}
|
|
410
|
+
}}
|
|
411
|
+
onKeyDown={handleKeyDown}
|
|
412
|
+
>
|
|
413
|
+
{selectedModelInfo?.displayName || 'Select Model'}
|
|
414
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
415
|
+
<path d="M6 9l6 6 6-6" />
|
|
416
|
+
</svg>
|
|
417
|
+
</button>
|
|
418
|
+
{isOpen && (
|
|
419
|
+
<div className="model-dropdown">
|
|
420
|
+
<div className="model-dropdown-search">
|
|
421
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
422
|
+
<circle cx="11" cy="11" r="8" />
|
|
423
|
+
<path d="M21 21l-4.35-4.35" />
|
|
424
|
+
</svg>
|
|
425
|
+
<input
|
|
426
|
+
ref={inputRef}
|
|
427
|
+
type="text"
|
|
428
|
+
value={search}
|
|
429
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
430
|
+
onKeyDown={handleKeyDown}
|
|
431
|
+
placeholder="Search models..."
|
|
432
|
+
autoFocus
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
435
|
+
<div ref={listRef} className="model-dropdown-list">
|
|
436
|
+
{filteredModels.length === 0 ? (
|
|
437
|
+
<div className="model-dropdown-no-results">No models found</div>
|
|
438
|
+
) : (
|
|
439
|
+
filteredModels.map((model, index) => (
|
|
440
|
+
<button
|
|
441
|
+
key={model.key}
|
|
442
|
+
data-index={index}
|
|
443
|
+
className={`model-dropdown-item ${model.key === selectedModel ? 'selected' : ''} ${index === highlightedIndex ? 'highlighted' : ''}`}
|
|
444
|
+
onClick={() => handleSelect(model.key)}
|
|
445
|
+
onMouseEnter={() => setHighlightedIndex(index)}
|
|
446
|
+
>
|
|
447
|
+
<div className="model-dropdown-item-content">
|
|
448
|
+
<span className="model-dropdown-item-name">{model.displayName}</span>
|
|
449
|
+
<span className="model-dropdown-item-desc">{model.description}</span>
|
|
450
|
+
</div>
|
|
451
|
+
{model.key === selectedModel && (
|
|
452
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
453
|
+
<path d="M20 6L9 17l-5-5" />
|
|
454
|
+
</svg>
|
|
455
|
+
)}
|
|
456
|
+
</button>
|
|
457
|
+
))
|
|
458
|
+
)}
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
)}
|
|
462
|
+
</div>
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Clickable file path component - opens file viewer on click, shows in Finder on right-click
|
|
467
|
+
function ClickableFilePath({
|
|
468
|
+
path,
|
|
469
|
+
workspacePath,
|
|
470
|
+
className = '',
|
|
471
|
+
onOpenViewer
|
|
472
|
+
}: {
|
|
473
|
+
path: string;
|
|
474
|
+
workspacePath?: string;
|
|
475
|
+
className?: string;
|
|
476
|
+
onOpenViewer?: (path: string) => void;
|
|
477
|
+
}) {
|
|
478
|
+
const handleClick = async (e: React.MouseEvent) => {
|
|
479
|
+
e.preventDefault();
|
|
480
|
+
e.stopPropagation();
|
|
481
|
+
|
|
482
|
+
// If viewer callback is provided and we have a workspace, use the in-app viewer
|
|
483
|
+
if (onOpenViewer && workspacePath) {
|
|
484
|
+
onOpenViewer(path);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Fallback to external app
|
|
489
|
+
try {
|
|
490
|
+
const error = await window.electronAPI.openFile(path, workspacePath);
|
|
491
|
+
if (error) {
|
|
492
|
+
console.error('Failed to open file:', error);
|
|
493
|
+
}
|
|
494
|
+
} catch (err) {
|
|
495
|
+
console.error('Error opening file:', err);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const handleContextMenu = async (e: React.MouseEvent) => {
|
|
500
|
+
e.preventDefault();
|
|
501
|
+
e.stopPropagation();
|
|
502
|
+
try {
|
|
503
|
+
await window.electronAPI.showInFinder(path, workspacePath);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
console.error('Error showing in Finder:', err);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// Extract filename for display
|
|
510
|
+
const fileName = path.split('/').pop() || path;
|
|
511
|
+
|
|
512
|
+
return (
|
|
513
|
+
<span
|
|
514
|
+
className={`clickable-file-path ${className}`}
|
|
515
|
+
onClick={handleClick}
|
|
516
|
+
onContextMenu={handleContextMenu}
|
|
517
|
+
title={`${path}\n\nClick to preview • Right-click to show in Finder`}
|
|
518
|
+
>
|
|
519
|
+
{fileName}
|
|
520
|
+
</span>
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
interface GoalModeOptions {
|
|
525
|
+
successCriteria?: SuccessCriteria;
|
|
526
|
+
maxAttempts?: number;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
type SettingsTab = 'appearance' | 'llm' | 'search' | 'telegram' | 'slack' | 'whatsapp' | 'teams' | 'morechannels' | 'updates' | 'guardrails' | 'queue' | 'skills' | 'voice';
|
|
530
|
+
|
|
531
|
+
interface MainContentProps {
|
|
532
|
+
task: Task | undefined;
|
|
533
|
+
selectedTaskId: string | null; // Added to distinguish "no task" from "task not in list"
|
|
534
|
+
workspace: Workspace | null;
|
|
535
|
+
events: TaskEvent[];
|
|
536
|
+
onSendMessage: (message: string) => void;
|
|
537
|
+
onCreateTask?: (title: string, prompt: string, options?: GoalModeOptions) => void;
|
|
538
|
+
onChangeWorkspace?: () => void;
|
|
539
|
+
onSelectWorkspace?: (workspace: Workspace) => void;
|
|
540
|
+
onOpenSettings?: (tab?: SettingsTab) => void;
|
|
541
|
+
onStopTask?: () => void;
|
|
542
|
+
selectedModel: string;
|
|
543
|
+
availableModels: LLMModelInfo[];
|
|
544
|
+
onModelChange: (model: string) => void;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Track active command execution state
|
|
548
|
+
interface ActiveCommand {
|
|
549
|
+
command: string;
|
|
550
|
+
output: string;
|
|
551
|
+
isRunning: boolean;
|
|
552
|
+
exitCode: number | null;
|
|
553
|
+
startTimestamp: number; // When the command started, for positioning in timeline
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function MainContent({ task, selectedTaskId, workspace, events, onSendMessage, onCreateTask, onChangeWorkspace, onSelectWorkspace, onOpenSettings, onStopTask, selectedModel, availableModels, onModelChange }: MainContentProps) {
|
|
557
|
+
// Agent personality context for personalized messages
|
|
558
|
+
const agentContext = useAgentContext();
|
|
559
|
+
const [pendingApproval, setPendingApproval] = useState<ApprovalRequest | null>(null);
|
|
560
|
+
const [inputValue, setInputValue] = useState('');
|
|
561
|
+
const [agentRoles, setAgentRoles] = useState<AgentRoleData[]>([]);
|
|
562
|
+
const [mentionQuery, setMentionQuery] = useState('');
|
|
563
|
+
const [mentionTarget, setMentionTarget] = useState<{ start: number; end: number } | null>(null);
|
|
564
|
+
const [mentionOpen, setMentionOpen] = useState(false);
|
|
565
|
+
const [mentionSelectedIndex, setMentionSelectedIndex] = useState(0);
|
|
566
|
+
// Shell permission state - tracks current workspace's shell permission
|
|
567
|
+
const [shellEnabled, setShellEnabled] = useState(workspace?.permissions?.shell ?? false);
|
|
568
|
+
// Active command execution state
|
|
569
|
+
const [activeCommand, setActiveCommand] = useState<ActiveCommand | null>(null);
|
|
570
|
+
// Track dismissed command outputs by task ID (persisted in localStorage)
|
|
571
|
+
const [dismissedCommandOutputs, setDismissedCommandOutputs] = useState<Set<string>>(() => {
|
|
572
|
+
try {
|
|
573
|
+
const saved = localStorage.getItem('dismissedCommandOutputs');
|
|
574
|
+
return saved ? new Set(JSON.parse(saved)) : new Set();
|
|
575
|
+
} catch {
|
|
576
|
+
return new Set();
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
// Goal Mode state
|
|
580
|
+
const [goalModeEnabled, setGoalModeEnabled] = useState(false);
|
|
581
|
+
const [verificationCommand, setVerificationCommand] = useState('');
|
|
582
|
+
const [maxAttempts, setMaxAttempts] = useState(3);
|
|
583
|
+
const [showSteps, setShowSteps] = useState(true);
|
|
584
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
585
|
+
const [queuedMessage, setQueuedMessage] = useState<string | null>(null);
|
|
586
|
+
// Track toggled events by ID for stable state across filtering
|
|
587
|
+
const [toggledEvents, setToggledEvents] = useState<Set<string>>(new Set());
|
|
588
|
+
const [appVersion, setAppVersion] = useState<string>('');
|
|
589
|
+
const [customSkills, setCustomSkills] = useState<CustomSkill[]>([]);
|
|
590
|
+
const [showSkillsMenu, setShowSkillsMenu] = useState(false);
|
|
591
|
+
const [skillsSearchQuery, setSkillsSearchQuery] = useState('');
|
|
592
|
+
const [selectedSkillForParams, setSelectedSkillForParams] = useState<CustomSkill | null>(null);
|
|
593
|
+
|
|
594
|
+
// Voice input hook
|
|
595
|
+
const [showVoiceNotConfigured, setShowVoiceNotConfigured] = useState(false);
|
|
596
|
+
const voiceInput = useVoiceInput({
|
|
597
|
+
onTranscript: (text) => {
|
|
598
|
+
// Append transcribed text to input
|
|
599
|
+
setInputValue(prev => prev ? `${prev} ${text}` : text);
|
|
600
|
+
},
|
|
601
|
+
onError: (error) => {
|
|
602
|
+
console.error('Voice input error:', error);
|
|
603
|
+
},
|
|
604
|
+
onNotConfigured: () => {
|
|
605
|
+
setShowVoiceNotConfigured(true);
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
const [viewerFilePath, setViewerFilePath] = useState<string | null>(null);
|
|
609
|
+
// Canvas sessions state - track active canvas sessions for current task
|
|
610
|
+
const [canvasSessions, setCanvasSessions] = useState<CanvasSession[]>([]);
|
|
611
|
+
// Workspace dropdown state
|
|
612
|
+
const [showWorkspaceDropdown, setShowWorkspaceDropdown] = useState(false);
|
|
613
|
+
const [workspacesList, setWorkspacesList] = useState<Workspace[]>([]);
|
|
614
|
+
// Verbose mode - when false, only show important steps
|
|
615
|
+
const [verboseSteps, setVerboseSteps] = useState(() => {
|
|
616
|
+
const saved = localStorage.getItem(VERBOSE_STEPS_KEY);
|
|
617
|
+
return saved === 'true';
|
|
618
|
+
});
|
|
619
|
+
// Voice state - track if voice is enabled
|
|
620
|
+
const [voiceEnabled, setVoiceEnabled] = useState(false);
|
|
621
|
+
const [voiceResponseMode, setVoiceResponseMode] = useState<'auto' | 'manual' | 'smart'>('manual');
|
|
622
|
+
const lastSpokenMessageRef = useRef<string | null>(null);
|
|
623
|
+
const skillsMenuRef = useRef<HTMLDivElement>(null);
|
|
624
|
+
const workspaceDropdownRef = useRef<HTMLDivElement>(null);
|
|
625
|
+
|
|
626
|
+
// Filter events based on verbose mode
|
|
627
|
+
const filteredEvents = useMemo(() => {
|
|
628
|
+
const visibleEvents = verboseSteps ? events : events.filter(isImportantEvent);
|
|
629
|
+
// Command output is rendered separately via CommandOutput component
|
|
630
|
+
return visibleEvents.filter(event => event.type !== 'command_output');
|
|
631
|
+
}, [events, verboseSteps]);
|
|
632
|
+
|
|
633
|
+
const latestUserMessageTimestamp = useMemo(() => {
|
|
634
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
635
|
+
if (events[i].type === 'user_message') {
|
|
636
|
+
return events[i].timestamp;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return null;
|
|
640
|
+
}, [events]);
|
|
641
|
+
|
|
642
|
+
const latestCanvasSessionId = useMemo(() => {
|
|
643
|
+
if (canvasSessions.length === 0) return null;
|
|
644
|
+
const eligibleSessions = latestUserMessageTimestamp
|
|
645
|
+
? canvasSessions.filter(session => session.createdAt >= latestUserMessageTimestamp)
|
|
646
|
+
: canvasSessions;
|
|
647
|
+
const pool = eligibleSessions.length > 0 ? eligibleSessions : canvasSessions;
|
|
648
|
+
return pool.reduce((latest, session) => {
|
|
649
|
+
return session.createdAt > latest.createdAt ? session : latest;
|
|
650
|
+
}, pool[0]).id;
|
|
651
|
+
}, [canvasSessions, latestUserMessageTimestamp]);
|
|
652
|
+
|
|
653
|
+
const timelineItems = useMemo(() => {
|
|
654
|
+
const eventItems = filteredEvents.map((event, index) => ({
|
|
655
|
+
kind: 'event' as const,
|
|
656
|
+
event,
|
|
657
|
+
eventIndex: index,
|
|
658
|
+
timestamp: event.timestamp,
|
|
659
|
+
}));
|
|
660
|
+
|
|
661
|
+
const freezeBefore = latestUserMessageTimestamp;
|
|
662
|
+
const canvasItems = canvasSessions
|
|
663
|
+
.map((session) => ({
|
|
664
|
+
kind: 'canvas' as const,
|
|
665
|
+
session,
|
|
666
|
+
timestamp: session.createdAt,
|
|
667
|
+
forceSnapshot: Boolean(
|
|
668
|
+
(freezeBefore && session.createdAt < freezeBefore) ||
|
|
669
|
+
(latestCanvasSessionId && session.id !== latestCanvasSessionId)
|
|
670
|
+
),
|
|
671
|
+
}))
|
|
672
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
673
|
+
|
|
674
|
+
if (canvasItems.length === 0) return eventItems;
|
|
675
|
+
|
|
676
|
+
const merged: Array<typeof eventItems[number] | typeof canvasItems[number]> = [];
|
|
677
|
+
let canvasIndex = 0;
|
|
678
|
+
|
|
679
|
+
for (const eventItem of eventItems) {
|
|
680
|
+
while (canvasIndex < canvasItems.length && canvasItems[canvasIndex].timestamp <= eventItem.timestamp) {
|
|
681
|
+
merged.push(canvasItems[canvasIndex]);
|
|
682
|
+
canvasIndex += 1;
|
|
683
|
+
}
|
|
684
|
+
merged.push(eventItem);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
while (canvasIndex < canvasItems.length) {
|
|
688
|
+
merged.push(canvasItems[canvasIndex]);
|
|
689
|
+
canvasIndex += 1;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return merged;
|
|
693
|
+
}, [filteredEvents, canvasSessions, latestCanvasSessionId, latestUserMessageTimestamp]);
|
|
694
|
+
|
|
695
|
+
// Find the index where command output should be inserted (after the last event before command started)
|
|
696
|
+
const commandOutputInsertIndex = useMemo(() => {
|
|
697
|
+
if (!activeCommand || !activeCommand.startTimestamp) return -1;
|
|
698
|
+
// Find the last event that started before or at the same time as the command
|
|
699
|
+
for (let i = filteredEvents.length - 1; i >= 0; i--) {
|
|
700
|
+
if (filteredEvents[i].timestamp <= activeCommand.startTimestamp) {
|
|
701
|
+
return i;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// If no events before command, insert at beginning (index -1 means render before all events)
|
|
705
|
+
return -1;
|
|
706
|
+
}, [filteredEvents, activeCommand]);
|
|
707
|
+
|
|
708
|
+
// Toggle verbose mode and persist to localStorage
|
|
709
|
+
const toggleVerboseSteps = () => {
|
|
710
|
+
setVerboseSteps(prev => {
|
|
711
|
+
const newValue = !prev;
|
|
712
|
+
localStorage.setItem(VERBOSE_STEPS_KEY, String(newValue));
|
|
713
|
+
return newValue;
|
|
714
|
+
});
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
// Load app version
|
|
718
|
+
useEffect(() => {
|
|
719
|
+
window.electronAPI.getAppVersion()
|
|
720
|
+
.then(info => setAppVersion(info.version))
|
|
721
|
+
.catch(err => console.error('Failed to load version:', err));
|
|
722
|
+
}, []);
|
|
723
|
+
|
|
724
|
+
// Load voice settings
|
|
725
|
+
useEffect(() => {
|
|
726
|
+
window.electronAPI.getVoiceSettings()
|
|
727
|
+
.then(settings => {
|
|
728
|
+
setVoiceEnabled(settings.enabled);
|
|
729
|
+
setVoiceResponseMode(settings.responseMode);
|
|
730
|
+
})
|
|
731
|
+
.catch(err => console.error('Failed to load voice settings:', err));
|
|
732
|
+
|
|
733
|
+
// Subscribe to voice state changes
|
|
734
|
+
const unsubscribe = window.electronAPI.onVoiceEvent((event) => {
|
|
735
|
+
if (event.type === 'voice:state-changed' && typeof event.data === 'object' && 'isActive' in event.data) {
|
|
736
|
+
setVoiceEnabled(event.data.isActive);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
return () => unsubscribe();
|
|
741
|
+
}, []);
|
|
742
|
+
|
|
743
|
+
// Auto-speak new assistant messages based on response mode
|
|
744
|
+
useEffect(() => {
|
|
745
|
+
if (!voiceEnabled || voiceResponseMode === 'manual') return;
|
|
746
|
+
|
|
747
|
+
const assistantMessages = events.filter(e => e.type === 'assistant_message');
|
|
748
|
+
if (assistantMessages.length === 0) return;
|
|
749
|
+
|
|
750
|
+
const lastMessage = assistantMessages[assistantMessages.length - 1];
|
|
751
|
+
const messageText = lastMessage.payload?.message || '';
|
|
752
|
+
|
|
753
|
+
// Skip if already spoken
|
|
754
|
+
if (lastSpokenMessageRef.current === messageText) return;
|
|
755
|
+
|
|
756
|
+
// Check if should speak based on mode
|
|
757
|
+
const hasDirective = /\[\[speak\]\]/i.test(messageText);
|
|
758
|
+
|
|
759
|
+
if (voiceResponseMode === 'auto' || (voiceResponseMode === 'smart' && hasDirective)) {
|
|
760
|
+
// Extract text to speak
|
|
761
|
+
let textToSpeak = messageText;
|
|
762
|
+
|
|
763
|
+
// If smart mode, only speak content within [[speak]] tags
|
|
764
|
+
if (voiceResponseMode === 'smart' && hasDirective) {
|
|
765
|
+
const matches = messageText.match(/\[\[speak\]\]([\s\S]*?)\[\[\/speak\]\]/gi);
|
|
766
|
+
if (matches) {
|
|
767
|
+
textToSpeak = matches
|
|
768
|
+
.map((m: string) => m.replace(/\[\[speak\]\]/gi, '').replace(/\[\[\/speak\]\]/gi, ''))
|
|
769
|
+
.join(' ')
|
|
770
|
+
.trim();
|
|
771
|
+
}
|
|
772
|
+
} else {
|
|
773
|
+
// Strip markdown for cleaner speech
|
|
774
|
+
textToSpeak = textToSpeak
|
|
775
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
776
|
+
.replace(/`[^`]+`/g, '')
|
|
777
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
778
|
+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '')
|
|
779
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
780
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
781
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
782
|
+
.trim();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (textToSpeak) {
|
|
786
|
+
lastSpokenMessageRef.current = messageText;
|
|
787
|
+
window.electronAPI.voiceSpeak(textToSpeak).catch(err => {
|
|
788
|
+
console.error('Failed to auto-speak:', err);
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}, [events, voiceEnabled, voiceResponseMode]);
|
|
793
|
+
|
|
794
|
+
// Load custom skills (task skills only, excludes guidelines)
|
|
795
|
+
useEffect(() => {
|
|
796
|
+
window.electronAPI.listTaskSkills()
|
|
797
|
+
.then(skills => setCustomSkills(skills.filter(s => s.enabled !== false)))
|
|
798
|
+
.catch(err => console.error('Failed to load custom skills:', err));
|
|
799
|
+
}, []);
|
|
800
|
+
|
|
801
|
+
// Load active agent roles for @mention autocomplete
|
|
802
|
+
useEffect(() => {
|
|
803
|
+
window.electronAPI.getAgentRoles()
|
|
804
|
+
.then((roles) => setAgentRoles(roles.filter((role) => role.isActive)))
|
|
805
|
+
.catch(err => console.error('Failed to load agent roles:', err));
|
|
806
|
+
}, []);
|
|
807
|
+
|
|
808
|
+
// Load canvas sessions when task changes
|
|
809
|
+
useEffect(() => {
|
|
810
|
+
if (!task?.id) {
|
|
811
|
+
setCanvasSessions([]);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Load existing canvas sessions for this task
|
|
816
|
+
window.electronAPI.canvasListSessions(task.id)
|
|
817
|
+
.then(sessions => {
|
|
818
|
+
// Filter to only active/paused sessions
|
|
819
|
+
setCanvasSessions(sessions.filter(s => s.status !== 'closed'));
|
|
820
|
+
})
|
|
821
|
+
.catch(err => console.error('Failed to load canvas sessions:', err));
|
|
822
|
+
}, [task?.id]);
|
|
823
|
+
|
|
824
|
+
// Subscribe to canvas events
|
|
825
|
+
useEffect(() => {
|
|
826
|
+
const unsubscribe = window.electronAPI.onCanvasEvent((event) => {
|
|
827
|
+
// Only process events for the current task
|
|
828
|
+
if (task?.id && event.taskId === task.id) {
|
|
829
|
+
// Don't show preview on session_created - wait until content is actually pushed
|
|
830
|
+
if (event.type === 'content_pushed') {
|
|
831
|
+
// Content has been pushed, now show the preview if not already showing
|
|
832
|
+
// Fetch the session info and add it to the list
|
|
833
|
+
window.electronAPI.canvasGetSession(event.sessionId)
|
|
834
|
+
.then(session => {
|
|
835
|
+
if (session && session.status !== 'closed') {
|
|
836
|
+
setCanvasSessions(prev => {
|
|
837
|
+
// Only add if not already in the list
|
|
838
|
+
if (prev.some(s => s.id === session.id)) {
|
|
839
|
+
return prev;
|
|
840
|
+
}
|
|
841
|
+
return [...prev, session];
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
})
|
|
845
|
+
.catch(err => console.error('Failed to get canvas session:', err));
|
|
846
|
+
} else if (event.type === 'session_updated' && event.session) {
|
|
847
|
+
const updatedSession = event.session;
|
|
848
|
+
setCanvasSessions(prev => {
|
|
849
|
+
const exists = prev.some(s => s.id === event.sessionId);
|
|
850
|
+
if (!exists && updatedSession.status !== 'closed') {
|
|
851
|
+
return [...prev, updatedSession];
|
|
852
|
+
}
|
|
853
|
+
return prev.map(s => s.id === event.sessionId ? updatedSession : s);
|
|
854
|
+
});
|
|
855
|
+
} else if (event.type === 'session_closed') {
|
|
856
|
+
setCanvasSessions(prev => prev.filter(s => s.id !== event.sessionId));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
return unsubscribe;
|
|
862
|
+
}, [task?.id]);
|
|
863
|
+
|
|
864
|
+
// Handle removing a canvas session from the UI
|
|
865
|
+
const handleCanvasClose = useCallback((sessionId: string) => {
|
|
866
|
+
setCanvasSessions(prev => prev.filter(s => s.id !== sessionId));
|
|
867
|
+
}, []);
|
|
868
|
+
|
|
869
|
+
// Handle dismissing command output for current task
|
|
870
|
+
const handleDismissCommandOutput = useCallback(() => {
|
|
871
|
+
if (!task?.id) return;
|
|
872
|
+
setDismissedCommandOutputs(prev => {
|
|
873
|
+
const updated = new Set(prev);
|
|
874
|
+
updated.add(task.id);
|
|
875
|
+
// Persist to localStorage
|
|
876
|
+
localStorage.setItem('dismissedCommandOutputs', JSON.stringify([...updated]));
|
|
877
|
+
return updated;
|
|
878
|
+
});
|
|
879
|
+
setActiveCommand(null);
|
|
880
|
+
}, [task?.id]);
|
|
881
|
+
|
|
882
|
+
// Filter skills based on search query
|
|
883
|
+
const filteredSkills = useMemo(() => {
|
|
884
|
+
if (!skillsSearchQuery.trim()) return customSkills;
|
|
885
|
+
const query = skillsSearchQuery.toLowerCase();
|
|
886
|
+
return customSkills.filter(skill =>
|
|
887
|
+
skill.name.toLowerCase().includes(query) ||
|
|
888
|
+
skill.description?.toLowerCase().includes(query) ||
|
|
889
|
+
skill.category?.toLowerCase().includes(query)
|
|
890
|
+
);
|
|
891
|
+
}, [customSkills, skillsSearchQuery]);
|
|
892
|
+
|
|
893
|
+
// Sync shell permission state when workspace changes
|
|
894
|
+
useEffect(() => {
|
|
895
|
+
setShellEnabled(workspace?.permissions?.shell ?? false);
|
|
896
|
+
}, [workspace?.id, workspace?.permissions?.shell]);
|
|
897
|
+
|
|
898
|
+
// Toggle shell permission for current workspace
|
|
899
|
+
const handleShellToggle = async () => {
|
|
900
|
+
if (!workspace) return;
|
|
901
|
+
const newValue = !shellEnabled;
|
|
902
|
+
setShellEnabled(newValue);
|
|
903
|
+
try {
|
|
904
|
+
await window.electronAPI.updateWorkspacePermissions(workspace.id, { shell: newValue });
|
|
905
|
+
} catch (err) {
|
|
906
|
+
console.error('Failed to update shell permission:', err);
|
|
907
|
+
setShellEnabled(!newValue); // Revert on error
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
// Close skills menu on click outside
|
|
912
|
+
useEffect(() => {
|
|
913
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
914
|
+
if (skillsMenuRef.current && !skillsMenuRef.current.contains(e.target as Node)) {
|
|
915
|
+
setShowSkillsMenu(false);
|
|
916
|
+
setSkillsSearchQuery('');
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
if (showSkillsMenu) {
|
|
920
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
921
|
+
}
|
|
922
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
923
|
+
}, [showSkillsMenu]);
|
|
924
|
+
|
|
925
|
+
// Close workspace dropdown on click outside
|
|
926
|
+
useEffect(() => {
|
|
927
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
928
|
+
if (workspaceDropdownRef.current && !workspaceDropdownRef.current.contains(e.target as Node)) {
|
|
929
|
+
setShowWorkspaceDropdown(false);
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
if (showWorkspaceDropdown) {
|
|
933
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
934
|
+
}
|
|
935
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
936
|
+
}, [showWorkspaceDropdown]);
|
|
937
|
+
|
|
938
|
+
// Handle workspace dropdown toggle - load workspaces when opening
|
|
939
|
+
const handleWorkspaceDropdownToggle = async () => {
|
|
940
|
+
if (!showWorkspaceDropdown) {
|
|
941
|
+
try {
|
|
942
|
+
const workspaces = await window.electronAPI.listWorkspaces();
|
|
943
|
+
// Filter out temp workspace and sort by most recently created
|
|
944
|
+
const filteredWorkspaces = workspaces
|
|
945
|
+
.filter((w: Workspace) => w.id !== TEMP_WORKSPACE_ID)
|
|
946
|
+
.sort((a: Workspace, b: Workspace) => b.createdAt - a.createdAt);
|
|
947
|
+
setWorkspacesList(filteredWorkspaces);
|
|
948
|
+
} catch (error) {
|
|
949
|
+
console.error('Failed to load workspaces:', error);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
setShowWorkspaceDropdown(!showWorkspaceDropdown);
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
// Handle selecting an existing workspace from dropdown
|
|
956
|
+
const handleWorkspaceSelect = (selectedWorkspace: Workspace) => {
|
|
957
|
+
setShowWorkspaceDropdown(false);
|
|
958
|
+
onSelectWorkspace?.(selectedWorkspace);
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
// Handle selecting a new folder via Finder
|
|
962
|
+
const handleSelectNewFolder = () => {
|
|
963
|
+
setShowWorkspaceDropdown(false);
|
|
964
|
+
onChangeWorkspace?.();
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
const handleSkillSelect = (skill: CustomSkill) => {
|
|
968
|
+
setShowSkillsMenu(false);
|
|
969
|
+
setSkillsSearchQuery('');
|
|
970
|
+
// If skill has parameters, show the parameter modal
|
|
971
|
+
if (skill.parameters && skill.parameters.length > 0) {
|
|
972
|
+
setSelectedSkillForParams(skill);
|
|
973
|
+
} else {
|
|
974
|
+
// No parameters, just set the prompt directly
|
|
975
|
+
setInputValue(skill.prompt);
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
const handleSkillParamSubmit = (expandedPrompt: string) => {
|
|
980
|
+
setSelectedSkillForParams(null);
|
|
981
|
+
// Create task directly with the expanded prompt
|
|
982
|
+
if (onCreateTask) {
|
|
983
|
+
const title = buildTaskTitle(expandedPrompt);
|
|
984
|
+
onCreateTask(title, expandedPrompt);
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
const handleSkillParamCancel = () => {
|
|
989
|
+
setSelectedSkillForParams(null);
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
// Toggle an event's expanded state using its ID
|
|
993
|
+
const toggleEventExpanded = (eventId: string) => {
|
|
994
|
+
setToggledEvents(prev => {
|
|
995
|
+
const next = new Set(prev);
|
|
996
|
+
if (next.has(eventId)) {
|
|
997
|
+
next.delete(eventId);
|
|
998
|
+
} else {
|
|
999
|
+
next.add(eventId);
|
|
1000
|
+
}
|
|
1001
|
+
return next;
|
|
1002
|
+
});
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
// Check if an event has details to show
|
|
1006
|
+
const hasEventDetails = (event: TaskEvent): boolean => {
|
|
1007
|
+
return ['plan_created', 'tool_call', 'tool_result', 'assistant_message', 'error'].includes(event.type);
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// Determine if an event should be expanded by default
|
|
1011
|
+
// Important events (plan, assistant responses, errors) should be expanded
|
|
1012
|
+
// Verbose events (tool calls/results) should be collapsed
|
|
1013
|
+
const shouldDefaultExpand = (event: TaskEvent): boolean => {
|
|
1014
|
+
return ['plan_created', 'assistant_message', 'error'].includes(event.type);
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
// Check if an event is currently expanded using its ID
|
|
1018
|
+
// If the event should default expand, clicking toggles it to collapsed (and vice versa)
|
|
1019
|
+
const isEventExpanded = (event: TaskEvent): boolean => {
|
|
1020
|
+
const defaultExpanded = shouldDefaultExpand(event);
|
|
1021
|
+
const isToggled = toggledEvents.has(event.id);
|
|
1022
|
+
// XOR: if toggled, invert the default state
|
|
1023
|
+
return defaultExpanded ? !isToggled : isToggled;
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
const timelineRef = useRef<HTMLDivElement>(null);
|
|
1027
|
+
const mainBodyRef = useRef<HTMLDivElement>(null);
|
|
1028
|
+
const prevTaskStatusRef = useRef<Task['status'] | undefined>(undefined);
|
|
1029
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
1030
|
+
const mentionContainerRef = useRef<HTMLDivElement>(null);
|
|
1031
|
+
const mentionDropdownRef = useRef<HTMLDivElement>(null);
|
|
1032
|
+
const placeholderMeasureRef = useRef<HTMLSpanElement>(null);
|
|
1033
|
+
const [cursorLeft, setCursorLeft] = useState<number>(0);
|
|
1034
|
+
|
|
1035
|
+
// Auto-resize textarea as content changes
|
|
1036
|
+
const autoResizeTextarea = useCallback(() => {
|
|
1037
|
+
const textarea = textareaRef.current;
|
|
1038
|
+
if (textarea) {
|
|
1039
|
+
textarea.style.height = 'auto';
|
|
1040
|
+
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
|
|
1041
|
+
}
|
|
1042
|
+
}, []);
|
|
1043
|
+
|
|
1044
|
+
// Auto-resize when input value changes
|
|
1045
|
+
useEffect(() => {
|
|
1046
|
+
autoResizeTextarea();
|
|
1047
|
+
}, [inputValue, autoResizeTextarea]);
|
|
1048
|
+
|
|
1049
|
+
// Calculate cursor position based on placeholder text width
|
|
1050
|
+
const placeholder = agentContext.getPlaceholder();
|
|
1051
|
+
useEffect(() => {
|
|
1052
|
+
if (placeholderMeasureRef.current) {
|
|
1053
|
+
// Measure the placeholder text width
|
|
1054
|
+
const measureEl = placeholderMeasureRef.current;
|
|
1055
|
+
measureEl.textContent = placeholder;
|
|
1056
|
+
// Get the width and add offset for: padding (16px) + prompt (~$ = ~24px) + gap (10px)
|
|
1057
|
+
const padding = 16; // wrapper left padding
|
|
1058
|
+
const promptWidth = 24; // ~$ prompt width
|
|
1059
|
+
const gap = 10;
|
|
1060
|
+
const textWidth = measureEl.offsetWidth;
|
|
1061
|
+
setCursorLeft(padding + promptWidth + gap + textWidth);
|
|
1062
|
+
}
|
|
1063
|
+
}, [placeholder]);
|
|
1064
|
+
|
|
1065
|
+
// Check if user is near the bottom of the scroll container
|
|
1066
|
+
const isNearBottom = useCallback((element: HTMLElement, threshold = 100) => {
|
|
1067
|
+
const { scrollTop, scrollHeight, clientHeight } = element;
|
|
1068
|
+
return scrollHeight - scrollTop - clientHeight < threshold;
|
|
1069
|
+
}, []);
|
|
1070
|
+
|
|
1071
|
+
// Handle scroll events to detect manual scrolling
|
|
1072
|
+
const handleScroll = useCallback(() => {
|
|
1073
|
+
const container = mainBodyRef.current;
|
|
1074
|
+
if (!container) return;
|
|
1075
|
+
|
|
1076
|
+
// If user scrolls to near bottom, re-enable auto-scroll
|
|
1077
|
+
// If user scrolls away from bottom, disable auto-scroll
|
|
1078
|
+
setAutoScroll(isNearBottom(container));
|
|
1079
|
+
}, [isNearBottom]);
|
|
1080
|
+
|
|
1081
|
+
// Auto-scroll to bottom when new events arrive
|
|
1082
|
+
useEffect(() => {
|
|
1083
|
+
if (autoScroll && timelineRef.current && mainBodyRef.current) {
|
|
1084
|
+
// Scroll the main body to show the latest event
|
|
1085
|
+
mainBodyRef.current.scrollTop = mainBodyRef.current.scrollHeight;
|
|
1086
|
+
}
|
|
1087
|
+
}, [events, autoScroll]);
|
|
1088
|
+
|
|
1089
|
+
// Reset auto-scroll when task changes
|
|
1090
|
+
useEffect(() => {
|
|
1091
|
+
setAutoScroll(true);
|
|
1092
|
+
}, [task?.id]);
|
|
1093
|
+
|
|
1094
|
+
// Send queued message when task finishes executing
|
|
1095
|
+
useEffect(() => {
|
|
1096
|
+
const prevStatus = prevTaskStatusRef.current;
|
|
1097
|
+
const currentStatus = task?.status;
|
|
1098
|
+
|
|
1099
|
+
// If task was executing and now it's not, send the queued message
|
|
1100
|
+
if (prevStatus === 'executing' && currentStatus !== 'executing' && queuedMessage) {
|
|
1101
|
+
onSendMessage(queuedMessage);
|
|
1102
|
+
setQueuedMessage(null);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
prevTaskStatusRef.current = currentStatus;
|
|
1106
|
+
}, [task?.status, queuedMessage, onSendMessage]);
|
|
1107
|
+
|
|
1108
|
+
// Process command_output events to track live command execution
|
|
1109
|
+
useEffect(() => {
|
|
1110
|
+
// Get the last command_output event
|
|
1111
|
+
const commandOutputEvents = events.filter(e => e.type === 'command_output');
|
|
1112
|
+
if (commandOutputEvents.length === 0) {
|
|
1113
|
+
setActiveCommand(null);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Build the command state from events
|
|
1118
|
+
let currentCommand: string | null = null;
|
|
1119
|
+
let output = '';
|
|
1120
|
+
let isRunning = false;
|
|
1121
|
+
let exitCode: number | null = null;
|
|
1122
|
+
let startTimestamp: number = 0;
|
|
1123
|
+
|
|
1124
|
+
for (const event of commandOutputEvents) {
|
|
1125
|
+
const payload = event.payload;
|
|
1126
|
+
if (payload.type === 'start') {
|
|
1127
|
+
// New command started
|
|
1128
|
+
currentCommand = payload.command;
|
|
1129
|
+
output = payload.output || '';
|
|
1130
|
+
isRunning = true;
|
|
1131
|
+
exitCode = null;
|
|
1132
|
+
startTimestamp = event.timestamp;
|
|
1133
|
+
} else if (payload.type === 'stdout' || payload.type === 'stderr' || payload.type === 'stdin') {
|
|
1134
|
+
// Append output (stdin shows what user typed)
|
|
1135
|
+
output += payload.output || '';
|
|
1136
|
+
} else if (payload.type === 'end') {
|
|
1137
|
+
// Command finished
|
|
1138
|
+
isRunning = false;
|
|
1139
|
+
exitCode = payload.exitCode;
|
|
1140
|
+
} else if (payload.type === 'error') {
|
|
1141
|
+
// Error output
|
|
1142
|
+
output += payload.output || '';
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Check if this task's command output was dismissed
|
|
1147
|
+
const isDismissed = task?.id ? dismissedCommandOutputs.has(task.id) : false;
|
|
1148
|
+
|
|
1149
|
+
// If a new command is running, clear the dismissed state for this task
|
|
1150
|
+
if (isRunning && task?.id && isDismissed) {
|
|
1151
|
+
setDismissedCommandOutputs(prev => {
|
|
1152
|
+
const updated = new Set(prev);
|
|
1153
|
+
updated.delete(task.id);
|
|
1154
|
+
localStorage.setItem('dismissedCommandOutputs', JSON.stringify([...updated]));
|
|
1155
|
+
return updated;
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Show command output if:
|
|
1160
|
+
// 1. There's a command AND it's not dismissed, OR
|
|
1161
|
+
// 2. Command is currently running (always show while running)
|
|
1162
|
+
const shouldShowOutput = currentCommand && (isRunning || !isDismissed);
|
|
1163
|
+
|
|
1164
|
+
// Limit output size in UI to prevent performance issues (keep last 50KB)
|
|
1165
|
+
const MAX_UI_OUTPUT = 50 * 1024;
|
|
1166
|
+
let truncatedOutput = output;
|
|
1167
|
+
if (output.length > MAX_UI_OUTPUT) {
|
|
1168
|
+
truncatedOutput = '[... earlier output truncated ...]\n\n' + output.slice(-MAX_UI_OUTPUT);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
if (shouldShowOutput) {
|
|
1172
|
+
setActiveCommand({
|
|
1173
|
+
command: currentCommand!,
|
|
1174
|
+
output: truncatedOutput,
|
|
1175
|
+
isRunning,
|
|
1176
|
+
exitCode,
|
|
1177
|
+
startTimestamp,
|
|
1178
|
+
});
|
|
1179
|
+
} else {
|
|
1180
|
+
setActiveCommand(null);
|
|
1181
|
+
}
|
|
1182
|
+
}, [events, task?.id, task?.status, dismissedCommandOutputs]);
|
|
1183
|
+
|
|
1184
|
+
// Check for approval requests in events
|
|
1185
|
+
useEffect(() => {
|
|
1186
|
+
// Get all approval IDs that have been resolved (granted or denied)
|
|
1187
|
+
const resolvedApprovalIds = new Set(
|
|
1188
|
+
events
|
|
1189
|
+
.filter(e => e.type === 'approval_granted' || e.type === 'approval_denied')
|
|
1190
|
+
.map(e => e.payload?.approvalId || e.payload?.approval?.id)
|
|
1191
|
+
.filter(Boolean)
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
// Find an approval request that hasn't been resolved yet
|
|
1195
|
+
const pendingApprovalEvent = events.find(e => {
|
|
1196
|
+
if (e.type !== 'approval_requested' || !e.payload?.approval) return false;
|
|
1197
|
+
const approvalId = e.payload.approval.id;
|
|
1198
|
+
// Only show if not already resolved
|
|
1199
|
+
return !resolvedApprovalIds.has(approvalId);
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
if (pendingApprovalEvent) {
|
|
1203
|
+
setPendingApproval(pendingApprovalEvent.payload.approval);
|
|
1204
|
+
} else {
|
|
1205
|
+
// No pending approvals - clear the state
|
|
1206
|
+
setPendingApproval(null);
|
|
1207
|
+
}
|
|
1208
|
+
}, [events]);
|
|
1209
|
+
|
|
1210
|
+
const handleApprovalResponse = async (approved: boolean) => {
|
|
1211
|
+
if (!pendingApproval) return;
|
|
1212
|
+
try {
|
|
1213
|
+
await window.electronAPI.respondToApproval({
|
|
1214
|
+
approvalId: pendingApproval.id,
|
|
1215
|
+
approved,
|
|
1216
|
+
});
|
|
1217
|
+
setPendingApproval(null);
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
console.error('Failed to respond to approval:', error);
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
const handleSend = () => {
|
|
1224
|
+
if (inputValue.trim()) {
|
|
1225
|
+
// Use selectedTaskId to determine if we should follow-up or create new task
|
|
1226
|
+
// This fixes the bug where old tasks (beyond the 100 most recent) would create new tasks
|
|
1227
|
+
// instead of sending follow-up messages
|
|
1228
|
+
if (!selectedTaskId && onCreateTask) {
|
|
1229
|
+
// No task selected - create new task with optional Goal Mode options
|
|
1230
|
+
const trimmedInput = inputValue.trim();
|
|
1231
|
+
const title = buildTaskTitle(trimmedInput);
|
|
1232
|
+
const options: GoalModeOptions | undefined = goalModeEnabled && verificationCommand
|
|
1233
|
+
? {
|
|
1234
|
+
successCriteria: { type: 'shell_command' as const, command: verificationCommand },
|
|
1235
|
+
maxAttempts,
|
|
1236
|
+
}
|
|
1237
|
+
: undefined;
|
|
1238
|
+
onCreateTask(title, trimmedInput, options);
|
|
1239
|
+
// Reset Goal Mode state
|
|
1240
|
+
setGoalModeEnabled(false);
|
|
1241
|
+
setVerificationCommand('');
|
|
1242
|
+
setMaxAttempts(3);
|
|
1243
|
+
} else {
|
|
1244
|
+
// Task is selected (even if not in current list) - send follow-up message
|
|
1245
|
+
onSendMessage(inputValue.trim());
|
|
1246
|
+
}
|
|
1247
|
+
setInputValue('');
|
|
1248
|
+
setMentionOpen(false);
|
|
1249
|
+
setMentionQuery('');
|
|
1250
|
+
setMentionTarget(null);
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
const handleClearQueue = () => {
|
|
1255
|
+
setQueuedMessage(null);
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
const findMentionAtCursor = (value: string, cursor: number | null) => {
|
|
1259
|
+
if (cursor === null) return null;
|
|
1260
|
+
const uptoCursor = value.slice(0, cursor);
|
|
1261
|
+
const atIndex = uptoCursor.lastIndexOf('@');
|
|
1262
|
+
if (atIndex === -1) return null;
|
|
1263
|
+
if (atIndex > 0 && /[a-zA-Z0-9]/.test(uptoCursor[atIndex - 1])) {
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
const query = uptoCursor.slice(atIndex + 1);
|
|
1267
|
+
if (query.startsWith(' ')) return null;
|
|
1268
|
+
if (query.includes('\n') || query.includes('\r')) return null;
|
|
1269
|
+
return { query, start: atIndex, end: cursor };
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
const mentionOptions = useMemo<MentionOption[]>(() => {
|
|
1273
|
+
if (!mentionOpen) return [];
|
|
1274
|
+
const query = normalizeMentionSearch(mentionQuery);
|
|
1275
|
+
const options: MentionOption[] = [];
|
|
1276
|
+
const includeEveryone = query.length > 0 && ['everybody', 'everyone', 'all'].some((alias) => alias.startsWith(query));
|
|
1277
|
+
if (includeEveryone) {
|
|
1278
|
+
options.push({
|
|
1279
|
+
type: 'everyone',
|
|
1280
|
+
id: 'everyone',
|
|
1281
|
+
label: 'Everybody',
|
|
1282
|
+
description: 'Auto-pick the best agents for this task',
|
|
1283
|
+
icon: '👥',
|
|
1284
|
+
color: '#64748b',
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const filteredAgents = agentRoles
|
|
1289
|
+
.filter((role) => role.isActive)
|
|
1290
|
+
.filter((role) => {
|
|
1291
|
+
if (!query) return true;
|
|
1292
|
+
const haystacks = [role.displayName, role.name, role.description ?? ''];
|
|
1293
|
+
return haystacks.some((text) => normalizeMentionSearch(text).includes(query));
|
|
1294
|
+
})
|
|
1295
|
+
.sort((a, b) => {
|
|
1296
|
+
if (a.sortOrder !== b.sortOrder) {
|
|
1297
|
+
return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
|
|
1298
|
+
}
|
|
1299
|
+
return a.displayName.localeCompare(b.displayName);
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
filteredAgents.forEach((role) => {
|
|
1303
|
+
options.push({
|
|
1304
|
+
type: 'agent',
|
|
1305
|
+
id: role.id,
|
|
1306
|
+
label: role.displayName,
|
|
1307
|
+
description: role.description,
|
|
1308
|
+
icon: role.icon,
|
|
1309
|
+
color: role.color,
|
|
1310
|
+
});
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
return options;
|
|
1314
|
+
}, [mentionOpen, mentionQuery, agentRoles]);
|
|
1315
|
+
|
|
1316
|
+
useEffect(() => {
|
|
1317
|
+
if (mentionSelectedIndex >= mentionOptions.length) {
|
|
1318
|
+
setMentionSelectedIndex(0);
|
|
1319
|
+
}
|
|
1320
|
+
}, [mentionOptions, mentionSelectedIndex]);
|
|
1321
|
+
|
|
1322
|
+
useEffect(() => {
|
|
1323
|
+
if (!mentionOpen) return;
|
|
1324
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
1325
|
+
if (mentionContainerRef.current && !mentionContainerRef.current.contains(e.target as Node)) {
|
|
1326
|
+
setMentionOpen(false);
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
1330
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
1331
|
+
}, [mentionOpen]);
|
|
1332
|
+
|
|
1333
|
+
const updateMentionState = useCallback((value: string, cursor: number | null) => {
|
|
1334
|
+
const mention = findMentionAtCursor(value, cursor);
|
|
1335
|
+
if (!mention) {
|
|
1336
|
+
setMentionOpen(false);
|
|
1337
|
+
setMentionQuery('');
|
|
1338
|
+
setMentionTarget(null);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
setMentionOpen(true);
|
|
1342
|
+
setMentionQuery(mention.query);
|
|
1343
|
+
setMentionTarget({ start: mention.start, end: mention.end });
|
|
1344
|
+
setMentionSelectedIndex(0);
|
|
1345
|
+
}, []);
|
|
1346
|
+
|
|
1347
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
1348
|
+
const value = e.target.value;
|
|
1349
|
+
setInputValue(value);
|
|
1350
|
+
updateMentionState(value, e.target.selectionStart);
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
const handleInputClick = (e: React.MouseEvent<HTMLTextAreaElement>) => {
|
|
1354
|
+
updateMentionState(inputValue, e.currentTarget.selectionStart);
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
const handleInputKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
1358
|
+
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key)) {
|
|
1359
|
+
updateMentionState(inputValue, (e.currentTarget as HTMLTextAreaElement).selectionStart);
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
const handleMentionSelect = (option: MentionOption) => {
|
|
1364
|
+
if (!mentionTarget) return;
|
|
1365
|
+
const insertText = option.type === 'everyone' ? '@everybody' : `@${option.label}`;
|
|
1366
|
+
const before = inputValue.slice(0, mentionTarget.start);
|
|
1367
|
+
const after = inputValue.slice(mentionTarget.end);
|
|
1368
|
+
const needsSpace = after.length === 0 ? true : !after.startsWith(' ');
|
|
1369
|
+
const nextValue = `${before}${insertText}${needsSpace ? ' ' : ''}${after}`;
|
|
1370
|
+
setInputValue(nextValue);
|
|
1371
|
+
setMentionOpen(false);
|
|
1372
|
+
setMentionQuery('');
|
|
1373
|
+
setMentionTarget(null);
|
|
1374
|
+
|
|
1375
|
+
requestAnimationFrame(() => {
|
|
1376
|
+
const textarea = textareaRef.current;
|
|
1377
|
+
if (textarea) {
|
|
1378
|
+
const cursorPosition = before.length + insertText.length + (needsSpace ? 1 : 0);
|
|
1379
|
+
textarea.focus();
|
|
1380
|
+
textarea.setSelectionRange(cursorPosition, cursorPosition);
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
const renderMentionDropdown = () => {
|
|
1386
|
+
if (!mentionOpen || mentionOptions.length === 0) return null;
|
|
1387
|
+
return (
|
|
1388
|
+
<div className="mention-autocomplete-dropdown" ref={mentionDropdownRef}>
|
|
1389
|
+
{mentionOptions.map((option, index) => {
|
|
1390
|
+
const displayLabel = option.type === 'everyone' ? '@everybody' : `@${option.label}`;
|
|
1391
|
+
return (
|
|
1392
|
+
<button
|
|
1393
|
+
key={`${option.type}-${option.id}`}
|
|
1394
|
+
className={`mention-autocomplete-item ${index === mentionSelectedIndex ? 'selected' : ''}`}
|
|
1395
|
+
onMouseDown={(e) => {
|
|
1396
|
+
e.preventDefault();
|
|
1397
|
+
handleMentionSelect(option);
|
|
1398
|
+
}}
|
|
1399
|
+
onMouseEnter={() => setMentionSelectedIndex(index)}
|
|
1400
|
+
>
|
|
1401
|
+
<span
|
|
1402
|
+
className="mention-autocomplete-icon"
|
|
1403
|
+
style={{ backgroundColor: option.color || '#64748b' }}
|
|
1404
|
+
>
|
|
1405
|
+
{option.icon || '👥'}
|
|
1406
|
+
</span>
|
|
1407
|
+
<div className="mention-autocomplete-details">
|
|
1408
|
+
<span className="mention-autocomplete-name">{displayLabel}</span>
|
|
1409
|
+
{option.description && (
|
|
1410
|
+
<span className="mention-autocomplete-desc">{option.description}</span>
|
|
1411
|
+
)}
|
|
1412
|
+
</div>
|
|
1413
|
+
</button>
|
|
1414
|
+
);
|
|
1415
|
+
})}
|
|
1416
|
+
</div>
|
|
1417
|
+
);
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
1421
|
+
if (mentionOpen && mentionOptions.length > 0) {
|
|
1422
|
+
switch (e.key) {
|
|
1423
|
+
case 'ArrowDown':
|
|
1424
|
+
e.preventDefault();
|
|
1425
|
+
setMentionSelectedIndex((prev) => (prev + 1) % mentionOptions.length);
|
|
1426
|
+
return;
|
|
1427
|
+
case 'ArrowUp':
|
|
1428
|
+
e.preventDefault();
|
|
1429
|
+
setMentionSelectedIndex((prev) => (prev - 1 + mentionOptions.length) % mentionOptions.length);
|
|
1430
|
+
return;
|
|
1431
|
+
case 'Enter':
|
|
1432
|
+
case 'Tab':
|
|
1433
|
+
e.preventDefault();
|
|
1434
|
+
handleMentionSelect(mentionOptions[mentionSelectedIndex]);
|
|
1435
|
+
return;
|
|
1436
|
+
case 'Escape':
|
|
1437
|
+
e.preventDefault();
|
|
1438
|
+
setMentionOpen(false);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1444
|
+
e.preventDefault();
|
|
1445
|
+
handleSend();
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
const handleQuickAction = (action: string) => {
|
|
1450
|
+
setInputValue(action);
|
|
1451
|
+
};
|
|
1452
|
+
|
|
1453
|
+
const formatTime = (timestamp: number) => {
|
|
1454
|
+
return new Date(timestamp).toLocaleTimeString(undefined, {
|
|
1455
|
+
hour: '2-digit',
|
|
1456
|
+
minute: '2-digit',
|
|
1457
|
+
});
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
const getEventDotClass = (type: TaskEvent['type']) => {
|
|
1461
|
+
if (type === 'error' || type === 'verification_failed') return 'error';
|
|
1462
|
+
if (type === 'step_completed' || type === 'task_completed' || type === 'verification_passed') return 'success';
|
|
1463
|
+
if (type === 'step_started' || type === 'executing' || type === 'verification_started' || type === 'retry_started') return 'active';
|
|
1464
|
+
return '';
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
// Get the last assistant message to always show the response
|
|
1468
|
+
const lastAssistantMessage = useMemo(() => {
|
|
1469
|
+
const assistantMessages = events.filter(e => e.type === 'assistant_message');
|
|
1470
|
+
return assistantMessages.length > 0 ? assistantMessages[assistantMessages.length - 1] : null;
|
|
1471
|
+
}, [events]);
|
|
1472
|
+
|
|
1473
|
+
// Welcome/Empty state
|
|
1474
|
+
if (!task) {
|
|
1475
|
+
return (
|
|
1476
|
+
<div className="main-content">
|
|
1477
|
+
<div className="main-body welcome-view">
|
|
1478
|
+
<div className="welcome-content cli-style">
|
|
1479
|
+
{/* Logo */}
|
|
1480
|
+
<div className="welcome-logo">
|
|
1481
|
+
<img src="./cowork-os-logo.png" alt="CoWork OS" className="welcome-logo-img" />
|
|
1482
|
+
</div>
|
|
1483
|
+
|
|
1484
|
+
{/* ASCII Terminal Header */}
|
|
1485
|
+
<div className="cli-header">
|
|
1486
|
+
<pre className="ascii-art">{`
|
|
1487
|
+
██████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ ███████╗
|
|
1488
|
+
██╔════╝██╔═══██╗██║ ██║██╔═══██╗██╔══██╗██║ ██╔╝ ██╔═══██╗██╔════╝
|
|
1489
|
+
██║ ██║ ██║██║ █╗ ██║██║ ██║██████╔╝█████╔╝ ██║ ██║███████╗
|
|
1490
|
+
██║ ██║ ██║██║███╗██║██║ ██║██╔══██╗██╔═██╗ ██║ ██║╚════██║
|
|
1491
|
+
╚██████╗╚██████╔╝╚███╔███╔╝╚██████╔╝██║ ██║██║ ██╗ ╚██████╔╝███████║
|
|
1492
|
+
╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝`}</pre>
|
|
1493
|
+
<div className="cli-version">{appVersion ? `v${appVersion}` : ''}</div>
|
|
1494
|
+
</div>
|
|
1495
|
+
|
|
1496
|
+
{/* Terminal Info */}
|
|
1497
|
+
<div className="cli-info">
|
|
1498
|
+
<div className="cli-line">
|
|
1499
|
+
<span className="cli-prompt">$</span>
|
|
1500
|
+
<span className="cli-text" title={agentContext.getMessage('welcome')}>{agentContext.getMessage('welcome')}</span>
|
|
1501
|
+
</div>
|
|
1502
|
+
<div className="cli-line cli-line-secondary">
|
|
1503
|
+
<span className="cli-prompt">></span>
|
|
1504
|
+
<span className="cli-text">{agentContext.getMessage('welcomeSubtitle')}</span>
|
|
1505
|
+
</div>
|
|
1506
|
+
<div className="cli-line cli-line-disclosure">
|
|
1507
|
+
<span className="cli-prompt">#</span>
|
|
1508
|
+
<span className="cli-text cli-text-muted" title={agentContext.getMessage('disclaimer')}>{agentContext.getMessage('disclaimer')}</span>
|
|
1509
|
+
</div>
|
|
1510
|
+
</div>
|
|
1511
|
+
|
|
1512
|
+
{/* Quick Start */}
|
|
1513
|
+
<div className="cli-commands">
|
|
1514
|
+
<div className="cli-commands-header">
|
|
1515
|
+
<span className="cli-prompt">></span>
|
|
1516
|
+
<span>QUICK START</span>
|
|
1517
|
+
</div>
|
|
1518
|
+
<div className="quick-start-grid">
|
|
1519
|
+
<button className="quick-start-card" onClick={() => handleQuickAction('Let\'s organize the files in this folder together. Sort them by type and rename them with clear, consistent names.')} title="Let's sort and tidy up the workspace">
|
|
1520
|
+
<span className="quick-start-icon">📁</span>
|
|
1521
|
+
<span className="quick-start-title">Organize files</span>
|
|
1522
|
+
<span className="quick-start-desc">Let's sort and tidy up the workspace</span>
|
|
1523
|
+
</button>
|
|
1524
|
+
<button className="quick-start-card" onClick={() => handleQuickAction('Let\'s write a document together. I\'ll describe what I need and we can create it.')} title="Co-create reports, summaries, or notes">
|
|
1525
|
+
<span className="quick-start-icon">📝</span>
|
|
1526
|
+
<span className="quick-start-title">Write together</span>
|
|
1527
|
+
<span className="quick-start-desc">Co-create reports, summaries, or notes</span>
|
|
1528
|
+
</button>
|
|
1529
|
+
<button className="quick-start-card" onClick={() => handleQuickAction('Let\'s analyze the data files in this folder together. We\'ll summarize the key findings and create a report.')} title="Work through spreadsheets or data files">
|
|
1530
|
+
<span className="quick-start-icon">📊</span>
|
|
1531
|
+
<span className="quick-start-title">Analyze data</span>
|
|
1532
|
+
<span className="quick-start-desc">Work through spreadsheets or data files</span>
|
|
1533
|
+
</button>
|
|
1534
|
+
<button className="quick-start-card" onClick={() => handleQuickAction('Let\'s generate documentation for this project together. We can create a README, API docs, or code comments as needed.')} title="Build documentation for the project">
|
|
1535
|
+
<span className="quick-start-icon">📖</span>
|
|
1536
|
+
<span className="quick-start-title">Generate docs</span>
|
|
1537
|
+
<span className="quick-start-desc">Build documentation for the project</span>
|
|
1538
|
+
</button>
|
|
1539
|
+
<button className="quick-start-card" onClick={() => handleQuickAction('Let\'s research and summarize information from the files in this folder together.')} title="Dig through files and find insights">
|
|
1540
|
+
<span className="quick-start-icon">🔍</span>
|
|
1541
|
+
<span className="quick-start-title">Research together</span>
|
|
1542
|
+
<span className="quick-start-desc">Dig through files and find insights</span>
|
|
1543
|
+
</button>
|
|
1544
|
+
<button className="quick-start-card" onClick={() => handleQuickAction('Let\'s prepare for a meeting together. We\'ll create an agenda, talking points, and organize materials needed.')} title="Get everything ready for a clean meeting">
|
|
1545
|
+
<span className="quick-start-icon">📋</span>
|
|
1546
|
+
<span className="quick-start-title">Meeting prep</span>
|
|
1547
|
+
<span className="quick-start-desc">Get everything ready for a clean meeting</span>
|
|
1548
|
+
</button>
|
|
1549
|
+
</div>
|
|
1550
|
+
</div>
|
|
1551
|
+
|
|
1552
|
+
{/* Input Area */}
|
|
1553
|
+
<div className="welcome-input-container cli-input-container">
|
|
1554
|
+
{showVoiceNotConfigured && (
|
|
1555
|
+
<div className="voice-not-configured-banner">
|
|
1556
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1557
|
+
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
|
|
1558
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
|
1559
|
+
<line x1="12" y1="19" x2="12" y2="23" />
|
|
1560
|
+
<line x1="8" y1="23" x2="16" y2="23" />
|
|
1561
|
+
</svg>
|
|
1562
|
+
<span>Voice input is not configured.</span>
|
|
1563
|
+
<button
|
|
1564
|
+
className="voice-settings-link"
|
|
1565
|
+
onClick={() => {
|
|
1566
|
+
setShowVoiceNotConfigured(false);
|
|
1567
|
+
onOpenSettings?.('voice');
|
|
1568
|
+
}}
|
|
1569
|
+
>
|
|
1570
|
+
Open Voice Settings
|
|
1571
|
+
</button>
|
|
1572
|
+
<button
|
|
1573
|
+
className="voice-banner-close"
|
|
1574
|
+
onClick={() => setShowVoiceNotConfigured(false)}
|
|
1575
|
+
title="Dismiss"
|
|
1576
|
+
>
|
|
1577
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1578
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
1579
|
+
</svg>
|
|
1580
|
+
</button>
|
|
1581
|
+
</div>
|
|
1582
|
+
)}
|
|
1583
|
+
<div className="cli-input-wrapper">
|
|
1584
|
+
<span className="cli-input-prompt">~$</span>
|
|
1585
|
+
<span ref={placeholderMeasureRef} className="cli-placeholder-measure" aria-hidden="true" />
|
|
1586
|
+
<div className="mention-autocomplete-wrapper" ref={mentionContainerRef}>
|
|
1587
|
+
<textarea
|
|
1588
|
+
ref={textareaRef}
|
|
1589
|
+
className="welcome-input cli-input input-textarea"
|
|
1590
|
+
placeholder={placeholder}
|
|
1591
|
+
value={inputValue}
|
|
1592
|
+
onChange={handleInputChange}
|
|
1593
|
+
onKeyDown={handleKeyDown}
|
|
1594
|
+
onClick={handleInputClick}
|
|
1595
|
+
onKeyUp={handleInputKeyUp}
|
|
1596
|
+
rows={1}
|
|
1597
|
+
/>
|
|
1598
|
+
{renderMentionDropdown()}
|
|
1599
|
+
</div>
|
|
1600
|
+
{!inputValue && <span className="cli-cursor" style={{ left: cursorLeft }} />}
|
|
1601
|
+
</div>
|
|
1602
|
+
|
|
1603
|
+
{/* Goal Mode Options */}
|
|
1604
|
+
<div className="goal-mode-section">
|
|
1605
|
+
<label className="goal-mode-toggle">
|
|
1606
|
+
<input
|
|
1607
|
+
type="checkbox"
|
|
1608
|
+
checked={goalModeEnabled}
|
|
1609
|
+
onChange={(e) => setGoalModeEnabled(e.target.checked)}
|
|
1610
|
+
/>
|
|
1611
|
+
<span className="goal-mode-label">Goal Mode</span>
|
|
1612
|
+
<span className="goal-mode-hint">Verify & retry until success</span>
|
|
1613
|
+
</label>
|
|
1614
|
+
{goalModeEnabled && (
|
|
1615
|
+
<div className="goal-mode-options">
|
|
1616
|
+
<div className="goal-mode-command">
|
|
1617
|
+
<span className="goal-mode-prompt">$</span>
|
|
1618
|
+
<input
|
|
1619
|
+
type="text"
|
|
1620
|
+
className="goal-mode-input"
|
|
1621
|
+
placeholder="Verification command (e.g., npm test)"
|
|
1622
|
+
value={verificationCommand}
|
|
1623
|
+
onChange={(e) => setVerificationCommand(e.target.value)}
|
|
1624
|
+
/>
|
|
1625
|
+
</div>
|
|
1626
|
+
<div className="goal-mode-attempts">
|
|
1627
|
+
<label>
|
|
1628
|
+
Max attempts:
|
|
1629
|
+
<input
|
|
1630
|
+
type="number"
|
|
1631
|
+
min="1"
|
|
1632
|
+
max="10"
|
|
1633
|
+
value={maxAttempts}
|
|
1634
|
+
onChange={(e) => setMaxAttempts(Math.min(10, Math.max(1, Number(e.target.value))))}
|
|
1635
|
+
/>
|
|
1636
|
+
</label>
|
|
1637
|
+
</div>
|
|
1638
|
+
</div>
|
|
1639
|
+
)}
|
|
1640
|
+
</div>
|
|
1641
|
+
|
|
1642
|
+
<div className="welcome-input-footer">
|
|
1643
|
+
<div className="input-left-actions">
|
|
1644
|
+
<div className="workspace-dropdown-container" ref={workspaceDropdownRef}>
|
|
1645
|
+
<button className="folder-selector" onClick={handleWorkspaceDropdownToggle}>
|
|
1646
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1647
|
+
<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z" />
|
|
1648
|
+
</svg>
|
|
1649
|
+
<span>{workspace?.id === TEMP_WORKSPACE_ID ? 'Work in a folder' : (workspace?.name || 'Work in a folder')}</span>
|
|
1650
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className={showWorkspaceDropdown ? 'chevron-up' : ''}>
|
|
1651
|
+
<path d="M6 9l6 6 6-6" />
|
|
1652
|
+
</svg>
|
|
1653
|
+
</button>
|
|
1654
|
+
{showWorkspaceDropdown && (
|
|
1655
|
+
<div className="workspace-dropdown">
|
|
1656
|
+
{workspacesList.length > 0 && (
|
|
1657
|
+
<>
|
|
1658
|
+
<div className="workspace-dropdown-header">Recent Folders</div>
|
|
1659
|
+
<div className="workspace-dropdown-list">
|
|
1660
|
+
{workspacesList.slice(0, 5).map((w) => (
|
|
1661
|
+
<button
|
|
1662
|
+
key={w.id}
|
|
1663
|
+
className={`workspace-dropdown-item ${workspace?.id === w.id ? 'active' : ''}`}
|
|
1664
|
+
onClick={() => handleWorkspaceSelect(w)}
|
|
1665
|
+
>
|
|
1666
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1667
|
+
<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z" />
|
|
1668
|
+
</svg>
|
|
1669
|
+
<div className="workspace-item-info">
|
|
1670
|
+
<span className="workspace-item-name">{w.name}</span>
|
|
1671
|
+
<span className="workspace-item-path">{w.path}</span>
|
|
1672
|
+
</div>
|
|
1673
|
+
{workspace?.id === w.id && (
|
|
1674
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="check-icon">
|
|
1675
|
+
<path d="M20 6L9 17l-5-5" />
|
|
1676
|
+
</svg>
|
|
1677
|
+
)}
|
|
1678
|
+
</button>
|
|
1679
|
+
))}
|
|
1680
|
+
</div>
|
|
1681
|
+
<div className="workspace-dropdown-divider" />
|
|
1682
|
+
</>
|
|
1683
|
+
)}
|
|
1684
|
+
<button className="workspace-dropdown-item new-folder" onClick={handleSelectNewFolder}>
|
|
1685
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1686
|
+
<path d="M12 5v14M5 12h14" />
|
|
1687
|
+
</svg>
|
|
1688
|
+
<span>Work in another folder...</span>
|
|
1689
|
+
</button>
|
|
1690
|
+
</div>
|
|
1691
|
+
)}
|
|
1692
|
+
</div>
|
|
1693
|
+
<button
|
|
1694
|
+
className={`shell-toggle ${shellEnabled ? 'enabled' : ''}`}
|
|
1695
|
+
onClick={handleShellToggle}
|
|
1696
|
+
title={shellEnabled ? 'Shell commands enabled - click to disable' : 'Shell commands disabled - click to enable'}
|
|
1697
|
+
>
|
|
1698
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1699
|
+
<path d="M4 17l6-6-6-6M12 19h8" />
|
|
1700
|
+
</svg>
|
|
1701
|
+
<span>Shell {shellEnabled ? 'ON' : 'OFF'}</span>
|
|
1702
|
+
</button>
|
|
1703
|
+
</div>
|
|
1704
|
+
<div className="input-right-actions">
|
|
1705
|
+
<ModelDropdown
|
|
1706
|
+
models={availableModels}
|
|
1707
|
+
selectedModel={selectedModel}
|
|
1708
|
+
onModelChange={onModelChange}
|
|
1709
|
+
/>
|
|
1710
|
+
{/* Skills Menu Button */}
|
|
1711
|
+
<div className="skills-menu-container" ref={skillsMenuRef}>
|
|
1712
|
+
<button
|
|
1713
|
+
className={`skills-menu-btn ${showSkillsMenu ? 'active' : ''}`}
|
|
1714
|
+
onClick={() => setShowSkillsMenu(!showSkillsMenu)}
|
|
1715
|
+
title="Custom Skills"
|
|
1716
|
+
>
|
|
1717
|
+
<span>/</span>
|
|
1718
|
+
</button>
|
|
1719
|
+
{showSkillsMenu && (
|
|
1720
|
+
<div className="skills-dropdown">
|
|
1721
|
+
<div className="skills-dropdown-header">Custom Skills</div>
|
|
1722
|
+
<div className="skills-dropdown-search">
|
|
1723
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1724
|
+
<circle cx="11" cy="11" r="8" />
|
|
1725
|
+
<path d="M21 21l-4.35-4.35" />
|
|
1726
|
+
</svg>
|
|
1727
|
+
<input
|
|
1728
|
+
type="text"
|
|
1729
|
+
placeholder="Search skills..."
|
|
1730
|
+
value={skillsSearchQuery}
|
|
1731
|
+
onChange={(e) => setSkillsSearchQuery(e.target.value)}
|
|
1732
|
+
autoFocus
|
|
1733
|
+
/>
|
|
1734
|
+
</div>
|
|
1735
|
+
{customSkills.length > 0 ? (
|
|
1736
|
+
filteredSkills.length > 0 ? (
|
|
1737
|
+
<div className="skills-dropdown-list">
|
|
1738
|
+
{filteredSkills.map(skill => (
|
|
1739
|
+
<div
|
|
1740
|
+
key={skill.id}
|
|
1741
|
+
className="skills-dropdown-item"
|
|
1742
|
+
style={{ cursor: 'pointer' }}
|
|
1743
|
+
onClick={() => handleSkillSelect(skill)}
|
|
1744
|
+
>
|
|
1745
|
+
<span className="skills-dropdown-icon">{skill.icon}</span>
|
|
1746
|
+
<div className="skills-dropdown-info">
|
|
1747
|
+
<span className="skills-dropdown-name">{skill.name}</span>
|
|
1748
|
+
<span className="skills-dropdown-desc">{skill.description}</span>
|
|
1749
|
+
</div>
|
|
1750
|
+
</div>
|
|
1751
|
+
))}
|
|
1752
|
+
</div>
|
|
1753
|
+
) : (
|
|
1754
|
+
<div className="skills-dropdown-empty">
|
|
1755
|
+
No skills match "{skillsSearchQuery}"
|
|
1756
|
+
</div>
|
|
1757
|
+
)
|
|
1758
|
+
) : (
|
|
1759
|
+
<div className="skills-dropdown-empty">
|
|
1760
|
+
No custom skills yet.
|
|
1761
|
+
</div>
|
|
1762
|
+
)}
|
|
1763
|
+
<div className="skills-dropdown-footer">
|
|
1764
|
+
<button
|
|
1765
|
+
className="skills-dropdown-create"
|
|
1766
|
+
onClick={() => {
|
|
1767
|
+
setShowSkillsMenu(false);
|
|
1768
|
+
setSkillsSearchQuery('');
|
|
1769
|
+
onOpenSettings?.('skills');
|
|
1770
|
+
}}
|
|
1771
|
+
>
|
|
1772
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1773
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
1774
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
1775
|
+
</svg>
|
|
1776
|
+
<span>Create New Skill</span>
|
|
1777
|
+
</button>
|
|
1778
|
+
</div>
|
|
1779
|
+
</div>
|
|
1780
|
+
)}
|
|
1781
|
+
</div>
|
|
1782
|
+
<button
|
|
1783
|
+
className={`voice-input-btn ${voiceInput.state}`}
|
|
1784
|
+
onClick={voiceInput.toggleRecording}
|
|
1785
|
+
disabled={voiceInput.state === 'processing'}
|
|
1786
|
+
title={
|
|
1787
|
+
voiceInput.state === 'idle' ? 'Start voice input' :
|
|
1788
|
+
voiceInput.state === 'recording' ? 'Stop recording' :
|
|
1789
|
+
'Processing...'
|
|
1790
|
+
}
|
|
1791
|
+
>
|
|
1792
|
+
{voiceInput.state === 'processing' ? (
|
|
1793
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="voice-processing-spin">
|
|
1794
|
+
<circle cx="12" cy="12" r="10" />
|
|
1795
|
+
<path d="M12 6v6l4 2" />
|
|
1796
|
+
</svg>
|
|
1797
|
+
) : voiceInput.state === 'recording' ? (
|
|
1798
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
1799
|
+
<rect x="6" y="6" width="12" height="12" rx="2" />
|
|
1800
|
+
</svg>
|
|
1801
|
+
) : (
|
|
1802
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1803
|
+
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
|
|
1804
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
|
1805
|
+
<line x1="12" y1="19" x2="12" y2="23" />
|
|
1806
|
+
<line x1="8" y1="23" x2="16" y2="23" />
|
|
1807
|
+
</svg>
|
|
1808
|
+
)}
|
|
1809
|
+
{voiceInput.state === 'recording' && (
|
|
1810
|
+
<span className="voice-recording-indicator" style={{ width: `${voiceInput.audioLevel}%` }} />
|
|
1811
|
+
)}
|
|
1812
|
+
</button>
|
|
1813
|
+
<button
|
|
1814
|
+
className="lets-go-btn lets-go-btn-sm"
|
|
1815
|
+
onClick={handleSend}
|
|
1816
|
+
disabled={!inputValue.trim()}
|
|
1817
|
+
>
|
|
1818
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1819
|
+
<path d="M12 19V5M5 12l7-7 7 7" />
|
|
1820
|
+
</svg>
|
|
1821
|
+
</button>
|
|
1822
|
+
</div>
|
|
1823
|
+
</div>
|
|
1824
|
+
</div>
|
|
1825
|
+
</div>
|
|
1826
|
+
</div>
|
|
1827
|
+
|
|
1828
|
+
{/* Modal for skills with parameters - Welcome View */}
|
|
1829
|
+
{selectedSkillForParams && (
|
|
1830
|
+
<SkillParameterModal
|
|
1831
|
+
skill={selectedSkillForParams}
|
|
1832
|
+
onSubmit={handleSkillParamSubmit}
|
|
1833
|
+
onCancel={handleSkillParamCancel}
|
|
1834
|
+
/>
|
|
1835
|
+
)}
|
|
1836
|
+
|
|
1837
|
+
{/* File Viewer Modal - Welcome View */}
|
|
1838
|
+
{viewerFilePath && workspace?.path && (
|
|
1839
|
+
<FileViewer
|
|
1840
|
+
filePath={viewerFilePath}
|
|
1841
|
+
workspacePath={workspace.path}
|
|
1842
|
+
onClose={() => setViewerFilePath(null)}
|
|
1843
|
+
/>
|
|
1844
|
+
)}
|
|
1845
|
+
</div>
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
const trimmedPrompt = task.prompt.trim();
|
|
1850
|
+
const baseTitle = task.title || buildTaskTitle(trimmedPrompt);
|
|
1851
|
+
const normalizedTitle = baseTitle.replace(TITLE_ELLIPSIS_REGEX, '');
|
|
1852
|
+
const titleMatchesPrompt = normalizedTitle.length > 0 && trimmedPrompt.startsWith(normalizedTitle);
|
|
1853
|
+
const isTitleTruncated = titleMatchesPrompt && trimmedPrompt.length > normalizedTitle.length;
|
|
1854
|
+
const headerTitle = isTitleTruncated && !TITLE_ELLIPSIS_REGEX.test(baseTitle)
|
|
1855
|
+
? `${baseTitle}...`
|
|
1856
|
+
: baseTitle;
|
|
1857
|
+
const headerTooltip = isTitleTruncated ? trimmedPrompt : baseTitle;
|
|
1858
|
+
const latestPauseEvent = [...events].reverse().find(event => event.type === 'task_paused');
|
|
1859
|
+
const latestApprovalEvent = [...events].reverse().find(event => event.type === 'approval_requested');
|
|
1860
|
+
|
|
1861
|
+
// Task view
|
|
1862
|
+
return (
|
|
1863
|
+
<div className="main-content">
|
|
1864
|
+
{/* Header */}
|
|
1865
|
+
<div className="main-header">
|
|
1866
|
+
<div className="main-header-title" title={headerTooltip}>{headerTitle}</div>
|
|
1867
|
+
</div>
|
|
1868
|
+
|
|
1869
|
+
{/* Body */}
|
|
1870
|
+
<div className="main-body" ref={mainBodyRef} onScroll={handleScroll}>
|
|
1871
|
+
<div className="task-content">
|
|
1872
|
+
{/* User Prompt - Right aligned like chat */}
|
|
1873
|
+
<div className="chat-message user-message">
|
|
1874
|
+
<div className="chat-bubble user-bubble markdown-content">
|
|
1875
|
+
<ReactMarkdown remarkPlugins={userMarkdownPlugins} components={markdownComponents}>
|
|
1876
|
+
{task.prompt}
|
|
1877
|
+
</ReactMarkdown>
|
|
1878
|
+
</div>
|
|
1879
|
+
<MessageCopyButton text={task.prompt} />
|
|
1880
|
+
</div>
|
|
1881
|
+
|
|
1882
|
+
{/* View steps toggle - show right after original prompt */}
|
|
1883
|
+
{events.some(e => e.type !== 'user_message' && e.type !== 'assistant_message') && (
|
|
1884
|
+
<div className="timeline-controls">
|
|
1885
|
+
<button
|
|
1886
|
+
className={`view-steps-btn ${showSteps ? 'expanded' : ''}`}
|
|
1887
|
+
onClick={() => setShowSteps(!showSteps)}
|
|
1888
|
+
>
|
|
1889
|
+
{showSteps ? 'Hide steps' : 'View steps'}
|
|
1890
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1891
|
+
<path d="M9 18l6-6-6-6" />
|
|
1892
|
+
</svg>
|
|
1893
|
+
</button>
|
|
1894
|
+
{showSteps && (
|
|
1895
|
+
<button
|
|
1896
|
+
className={`verbose-toggle-btn ${verboseSteps ? 'active' : ''}`}
|
|
1897
|
+
onClick={toggleVerboseSteps}
|
|
1898
|
+
title={verboseSteps ? 'Show important steps only' : 'Show all steps (verbose)'}
|
|
1899
|
+
>
|
|
1900
|
+
{verboseSteps ? 'Verbose' : 'Summary'}
|
|
1901
|
+
</button>
|
|
1902
|
+
)}
|
|
1903
|
+
</div>
|
|
1904
|
+
)}
|
|
1905
|
+
|
|
1906
|
+
{/* Conversation Flow - renders all events in order */}
|
|
1907
|
+
{events.length > 0 && (
|
|
1908
|
+
<div className="conversation-flow" ref={timelineRef}>
|
|
1909
|
+
{/* Render CommandOutput at beginning if it should appear before all events */}
|
|
1910
|
+
{activeCommand && commandOutputInsertIndex === -1 && (
|
|
1911
|
+
<CommandOutput
|
|
1912
|
+
command={activeCommand.command}
|
|
1913
|
+
output={activeCommand.output}
|
|
1914
|
+
isRunning={activeCommand.isRunning}
|
|
1915
|
+
exitCode={activeCommand.exitCode}
|
|
1916
|
+
taskId={task?.id}
|
|
1917
|
+
onClose={handleDismissCommandOutput}
|
|
1918
|
+
/>
|
|
1919
|
+
)}
|
|
1920
|
+
{timelineItems.map((item) => {
|
|
1921
|
+
if (item.kind === 'canvas') {
|
|
1922
|
+
return (
|
|
1923
|
+
<CanvasPreview
|
|
1924
|
+
key={item.session.id}
|
|
1925
|
+
session={item.session}
|
|
1926
|
+
onClose={() => handleCanvasClose(item.session.id)}
|
|
1927
|
+
forceSnapshot={item.forceSnapshot}
|
|
1928
|
+
/>
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
const event = item.event;
|
|
1933
|
+
const isUserMessage = event.type === 'user_message';
|
|
1934
|
+
const isAssistantMessage = event.type === 'assistant_message';
|
|
1935
|
+
// Check if CommandOutput should be rendered after this event
|
|
1936
|
+
const shouldRenderCommandOutput = activeCommand && item.eventIndex === commandOutputInsertIndex;
|
|
1937
|
+
|
|
1938
|
+
// Render user messages as chat bubbles on the right
|
|
1939
|
+
if (isUserMessage) {
|
|
1940
|
+
const messageText = event.payload?.message || 'User message';
|
|
1941
|
+
return (
|
|
1942
|
+
<Fragment key={event.id || `event-${item.eventIndex}`}>
|
|
1943
|
+
<div className="chat-message user-message">
|
|
1944
|
+
<div className="chat-bubble user-bubble markdown-content">
|
|
1945
|
+
<ReactMarkdown remarkPlugins={userMarkdownPlugins} components={markdownComponents}>
|
|
1946
|
+
{messageText}
|
|
1947
|
+
</ReactMarkdown>
|
|
1948
|
+
</div>
|
|
1949
|
+
<MessageCopyButton text={messageText} />
|
|
1950
|
+
</div>
|
|
1951
|
+
{shouldRenderCommandOutput && (
|
|
1952
|
+
<CommandOutput
|
|
1953
|
+
command={activeCommand.command}
|
|
1954
|
+
output={activeCommand.output}
|
|
1955
|
+
isRunning={activeCommand.isRunning}
|
|
1956
|
+
exitCode={activeCommand.exitCode}
|
|
1957
|
+
taskId={task?.id}
|
|
1958
|
+
onClose={handleDismissCommandOutput}
|
|
1959
|
+
/>
|
|
1960
|
+
)}
|
|
1961
|
+
</Fragment>
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// Render assistant messages as chat bubbles on the left
|
|
1966
|
+
if (isAssistantMessage) {
|
|
1967
|
+
const messageText = event.payload?.message || '';
|
|
1968
|
+
const isLastAssistant = event === lastAssistantMessage;
|
|
1969
|
+
return (
|
|
1970
|
+
<Fragment key={event.id || `event-${item.eventIndex}`}>
|
|
1971
|
+
<div className="chat-message assistant-message">
|
|
1972
|
+
<div className="chat-bubble assistant-bubble">
|
|
1973
|
+
{isLastAssistant && (
|
|
1974
|
+
<div className="chat-bubble-header">
|
|
1975
|
+
{task.status === 'completed' && <span className="chat-status">{agentContext.getMessage('taskComplete')}</span>}
|
|
1976
|
+
{task.status === 'paused' && <span className="chat-status">{agentContext.getMessage('taskPaused') || 'Paused'}</span>}
|
|
1977
|
+
{task.status === 'blocked' && <span className="chat-status">{agentContext.getMessage('taskBlocked') || 'Needs approval'}</span>}
|
|
1978
|
+
{task.status === 'executing' && (
|
|
1979
|
+
<span className="chat-status executing">
|
|
1980
|
+
<svg className="spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1981
|
+
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
|
1982
|
+
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
|
|
1983
|
+
</svg>
|
|
1984
|
+
{agentContext.getMessage('taskWorking')}
|
|
1985
|
+
</span>
|
|
1986
|
+
)}
|
|
1987
|
+
</div>
|
|
1988
|
+
)}
|
|
1989
|
+
<div className="chat-bubble-content markdown-content">
|
|
1990
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
|
1991
|
+
{messageText.replace(/\[\[speak\]\]([\s\S]*?)\[\[\/speak\]\]/gi, '$1')}
|
|
1992
|
+
</ReactMarkdown>
|
|
1993
|
+
</div>
|
|
1994
|
+
</div>
|
|
1995
|
+
<div className="message-actions">
|
|
1996
|
+
<MessageCopyButton text={messageText} />
|
|
1997
|
+
<MessageSpeakButton text={messageText} voiceEnabled={voiceEnabled} />
|
|
1998
|
+
</div>
|
|
1999
|
+
</div>
|
|
2000
|
+
{shouldRenderCommandOutput && (
|
|
2001
|
+
<CommandOutput
|
|
2002
|
+
command={activeCommand.command}
|
|
2003
|
+
output={activeCommand.output}
|
|
2004
|
+
isRunning={activeCommand.isRunning}
|
|
2005
|
+
exitCode={activeCommand.exitCode}
|
|
2006
|
+
taskId={task?.id}
|
|
2007
|
+
onClose={handleDismissCommandOutput}
|
|
2008
|
+
/>
|
|
2009
|
+
)}
|
|
2010
|
+
</Fragment>
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// Technical events - only show when showSteps is true
|
|
2015
|
+
const alwaysVisibleEvents = new Set(['approval_requested', 'approval_granted', 'approval_denied']);
|
|
2016
|
+
if (!showSteps && !alwaysVisibleEvents.has(event.type)) {
|
|
2017
|
+
// Even if we're not showing steps, we may still need to render CommandOutput here
|
|
2018
|
+
if (shouldRenderCommandOutput) {
|
|
2019
|
+
return (
|
|
2020
|
+
<Fragment key={event.id || `event-${item.eventIndex}`}>
|
|
2021
|
+
<CommandOutput
|
|
2022
|
+
command={activeCommand.command}
|
|
2023
|
+
output={activeCommand.output}
|
|
2024
|
+
isRunning={activeCommand.isRunning}
|
|
2025
|
+
exitCode={activeCommand.exitCode}
|
|
2026
|
+
taskId={task?.id}
|
|
2027
|
+
onClose={handleDismissCommandOutput}
|
|
2028
|
+
/>
|
|
2029
|
+
</Fragment>
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
return null;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
const isExpandable = hasEventDetails(event);
|
|
2036
|
+
const isExpanded = isEventExpanded(event);
|
|
2037
|
+
|
|
2038
|
+
return (
|
|
2039
|
+
<Fragment key={event.id || `event-${item.eventIndex}`}>
|
|
2040
|
+
<div className="timeline-event">
|
|
2041
|
+
<div className="event-indicator">
|
|
2042
|
+
<div className={`event-dot ${getEventDotClass(event.type)}`} />
|
|
2043
|
+
</div>
|
|
2044
|
+
<div className="event-content">
|
|
2045
|
+
<div
|
|
2046
|
+
className={`event-header ${isExpandable ? 'expandable' : ''} ${isExpanded ? 'expanded' : ''}`}
|
|
2047
|
+
onClick={isExpandable ? () => toggleEventExpanded(event.id) : undefined}
|
|
2048
|
+
>
|
|
2049
|
+
<div className="event-header-left">
|
|
2050
|
+
{isExpandable && (
|
|
2051
|
+
<svg className="event-expand-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2052
|
+
<path d="M9 18l6-6-6-6" />
|
|
2053
|
+
</svg>
|
|
2054
|
+
)}
|
|
2055
|
+
<div className="event-title">{renderEventTitle(event, workspace?.path, setViewerFilePath, agentContext)}</div>
|
|
2056
|
+
</div>
|
|
2057
|
+
<div className="event-time">{formatTime(event.timestamp)}</div>
|
|
2058
|
+
</div>
|
|
2059
|
+
{isExpanded && renderEventDetails(event, voiceEnabled)}
|
|
2060
|
+
</div>
|
|
2061
|
+
</div>
|
|
2062
|
+
{shouldRenderCommandOutput && (
|
|
2063
|
+
<CommandOutput
|
|
2064
|
+
command={activeCommand.command}
|
|
2065
|
+
output={activeCommand.output}
|
|
2066
|
+
isRunning={activeCommand.isRunning}
|
|
2067
|
+
exitCode={activeCommand.exitCode}
|
|
2068
|
+
taskId={task?.id}
|
|
2069
|
+
onClose={handleDismissCommandOutput}
|
|
2070
|
+
/>
|
|
2071
|
+
)}
|
|
2072
|
+
</Fragment>
|
|
2073
|
+
);
|
|
2074
|
+
})}
|
|
2075
|
+
</div>
|
|
2076
|
+
)}
|
|
2077
|
+
|
|
2078
|
+
</div>
|
|
2079
|
+
</div>
|
|
2080
|
+
|
|
2081
|
+
{/* Footer with Input */}
|
|
2082
|
+
<div className="main-footer">
|
|
2083
|
+
<div className="input-container">
|
|
2084
|
+
{/* Queued message display */}
|
|
2085
|
+
{queuedMessage && (
|
|
2086
|
+
<div className="queued-message-frame">
|
|
2087
|
+
<div className="queued-message-content">
|
|
2088
|
+
<svg className="queued-message-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2089
|
+
<path d="M12 19V5M5 12l7-7 7 7" />
|
|
2090
|
+
</svg>
|
|
2091
|
+
<span className="queued-message-label">Queue:</span>
|
|
2092
|
+
<span className="queued-message-text">{queuedMessage}</span>
|
|
2093
|
+
</div>
|
|
2094
|
+
<button className="queued-message-clear" onClick={handleClearQueue} title="Remove from queue">
|
|
2095
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2096
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
2097
|
+
</svg>
|
|
2098
|
+
</button>
|
|
2099
|
+
</div>
|
|
2100
|
+
)}
|
|
2101
|
+
{showVoiceNotConfigured && (
|
|
2102
|
+
<div className="voice-not-configured-banner">
|
|
2103
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2104
|
+
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
|
|
2105
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
|
2106
|
+
<line x1="12" y1="19" x2="12" y2="23" />
|
|
2107
|
+
<line x1="8" y1="23" x2="16" y2="23" />
|
|
2108
|
+
</svg>
|
|
2109
|
+
<span>Voice input is not configured.</span>
|
|
2110
|
+
<button
|
|
2111
|
+
className="voice-settings-link"
|
|
2112
|
+
onClick={() => {
|
|
2113
|
+
setShowVoiceNotConfigured(false);
|
|
2114
|
+
onOpenSettings?.('voice');
|
|
2115
|
+
}}
|
|
2116
|
+
>
|
|
2117
|
+
Open Voice Settings
|
|
2118
|
+
</button>
|
|
2119
|
+
<button
|
|
2120
|
+
className="voice-banner-close"
|
|
2121
|
+
onClick={() => setShowVoiceNotConfigured(false)}
|
|
2122
|
+
title="Dismiss"
|
|
2123
|
+
>
|
|
2124
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2125
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
2126
|
+
</svg>
|
|
2127
|
+
</button>
|
|
2128
|
+
</div>
|
|
2129
|
+
)}
|
|
2130
|
+
{task.status === 'paused' && (
|
|
2131
|
+
<div className="task-status-banner task-status-banner-paused">
|
|
2132
|
+
<div className="task-status-banner-content">
|
|
2133
|
+
<strong>Paused — waiting on your input</strong>
|
|
2134
|
+
{latestPauseEvent?.payload?.message && (
|
|
2135
|
+
<span className="task-status-banner-detail">{latestPauseEvent.payload.message}</span>
|
|
2136
|
+
)}
|
|
2137
|
+
</div>
|
|
2138
|
+
<button className="btn-secondary" onClick={() => window.electronAPI.resumeTask(task.id)}>
|
|
2139
|
+
Resume
|
|
2140
|
+
</button>
|
|
2141
|
+
</div>
|
|
2142
|
+
)}
|
|
2143
|
+
{task.status === 'blocked' && (
|
|
2144
|
+
<div className="task-status-banner task-status-banner-blocked">
|
|
2145
|
+
<div className="task-status-banner-content">
|
|
2146
|
+
<strong>Blocked — needs approval</strong>
|
|
2147
|
+
{latestApprovalEvent?.payload?.approval?.description && (
|
|
2148
|
+
<span className="task-status-banner-detail">{latestApprovalEvent.payload.approval.description}</span>
|
|
2149
|
+
)}
|
|
2150
|
+
</div>
|
|
2151
|
+
</div>
|
|
2152
|
+
)}
|
|
2153
|
+
<div className="input-row">
|
|
2154
|
+
<div className="mention-autocomplete-wrapper" ref={mentionContainerRef}>
|
|
2155
|
+
<textarea
|
|
2156
|
+
ref={textareaRef}
|
|
2157
|
+
className="input-field input-textarea"
|
|
2158
|
+
placeholder={queuedMessage ? agentContext.getUiCopy('inputPlaceholderQueued') : agentContext.getMessage('placeholderActive')}
|
|
2159
|
+
value={inputValue}
|
|
2160
|
+
onChange={handleInputChange}
|
|
2161
|
+
onKeyDown={handleKeyDown}
|
|
2162
|
+
onClick={handleInputClick}
|
|
2163
|
+
onKeyUp={handleInputKeyUp}
|
|
2164
|
+
rows={1}
|
|
2165
|
+
/>
|
|
2166
|
+
{renderMentionDropdown()}
|
|
2167
|
+
</div>
|
|
2168
|
+
<div className="input-actions">
|
|
2169
|
+
<ModelDropdown
|
|
2170
|
+
models={availableModels}
|
|
2171
|
+
selectedModel={selectedModel}
|
|
2172
|
+
onModelChange={onModelChange}
|
|
2173
|
+
/>
|
|
2174
|
+
<button
|
|
2175
|
+
className={`voice-input-btn ${voiceInput.state}`}
|
|
2176
|
+
onClick={voiceInput.toggleRecording}
|
|
2177
|
+
disabled={voiceInput.state === 'processing'}
|
|
2178
|
+
title={
|
|
2179
|
+
voiceInput.state === 'idle' ? 'Start voice input' :
|
|
2180
|
+
voiceInput.state === 'recording' ? 'Stop recording' :
|
|
2181
|
+
'Processing...'
|
|
2182
|
+
}
|
|
2183
|
+
>
|
|
2184
|
+
{voiceInput.state === 'processing' ? (
|
|
2185
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="voice-processing-spin">
|
|
2186
|
+
<circle cx="12" cy="12" r="10" />
|
|
2187
|
+
<path d="M12 6v6l4 2" />
|
|
2188
|
+
</svg>
|
|
2189
|
+
) : voiceInput.state === 'recording' ? (
|
|
2190
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
2191
|
+
<rect x="6" y="6" width="12" height="12" rx="2" />
|
|
2192
|
+
</svg>
|
|
2193
|
+
) : (
|
|
2194
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2195
|
+
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
|
|
2196
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
|
2197
|
+
<line x1="12" y1="19" x2="12" y2="23" />
|
|
2198
|
+
<line x1="8" y1="23" x2="16" y2="23" />
|
|
2199
|
+
</svg>
|
|
2200
|
+
)}
|
|
2201
|
+
{voiceInput.state === 'recording' && (
|
|
2202
|
+
<span className="voice-recording-indicator" style={{ width: `${voiceInput.audioLevel}%` }} />
|
|
2203
|
+
)}
|
|
2204
|
+
</button>
|
|
2205
|
+
<button
|
|
2206
|
+
className="lets-go-btn lets-go-btn-sm"
|
|
2207
|
+
onClick={handleSend}
|
|
2208
|
+
disabled={!inputValue.trim()}
|
|
2209
|
+
title="Send message"
|
|
2210
|
+
>
|
|
2211
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2212
|
+
<path d="M12 19V5M5 12l7-7 7 7" />
|
|
2213
|
+
</svg>
|
|
2214
|
+
</button>
|
|
2215
|
+
{task.status === 'executing' && onStopTask && (
|
|
2216
|
+
<button
|
|
2217
|
+
className="stop-btn-simple"
|
|
2218
|
+
onClick={onStopTask}
|
|
2219
|
+
title="Stop task"
|
|
2220
|
+
>
|
|
2221
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2222
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
2223
|
+
</svg>
|
|
2224
|
+
</button>
|
|
2225
|
+
)}
|
|
2226
|
+
</div>
|
|
2227
|
+
</div>
|
|
2228
|
+
<div className="input-below-actions">
|
|
2229
|
+
<button
|
|
2230
|
+
className={`shell-toggle ${shellEnabled ? 'enabled' : ''}`}
|
|
2231
|
+
onClick={handleShellToggle}
|
|
2232
|
+
title={shellEnabled ? 'Shell commands enabled - click to disable' : 'Shell commands disabled - click to enable'}
|
|
2233
|
+
>
|
|
2234
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2235
|
+
<path d="M4 17l6-6-6-6M12 19h8" />
|
|
2236
|
+
</svg>
|
|
2237
|
+
<span>Shell {shellEnabled ? 'ON' : 'OFF'}</span>
|
|
2238
|
+
</button>
|
|
2239
|
+
</div>
|
|
2240
|
+
</div>
|
|
2241
|
+
<div className="footer-disclaimer">
|
|
2242
|
+
{agentContext.getMessage('disclaimer')}
|
|
2243
|
+
</div>
|
|
2244
|
+
</div>
|
|
2245
|
+
|
|
2246
|
+
{pendingApproval && (
|
|
2247
|
+
<ApprovalDialog
|
|
2248
|
+
approval={pendingApproval}
|
|
2249
|
+
onApprove={() => handleApprovalResponse(true)}
|
|
2250
|
+
onDeny={() => handleApprovalResponse(false)}
|
|
2251
|
+
/>
|
|
2252
|
+
)}
|
|
2253
|
+
|
|
2254
|
+
{selectedSkillForParams && (
|
|
2255
|
+
<SkillParameterModal
|
|
2256
|
+
skill={selectedSkillForParams}
|
|
2257
|
+
onSubmit={handleSkillParamSubmit}
|
|
2258
|
+
onCancel={handleSkillParamCancel}
|
|
2259
|
+
/>
|
|
2260
|
+
)}
|
|
2261
|
+
|
|
2262
|
+
{/* File Viewer Modal - Task View */}
|
|
2263
|
+
{viewerFilePath && workspace?.path && (
|
|
2264
|
+
<FileViewer
|
|
2265
|
+
filePath={viewerFilePath}
|
|
2266
|
+
workspacePath={workspace.path}
|
|
2267
|
+
onClose={() => setViewerFilePath(null)}
|
|
2268
|
+
/>
|
|
2269
|
+
)}
|
|
2270
|
+
</div>
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
/**
|
|
2275
|
+
* Truncate long text for display, with expand option handled via CSS
|
|
2276
|
+
*/
|
|
2277
|
+
function truncateForDisplay(text: string, maxLength: number = 2000): string {
|
|
2278
|
+
if (!text || text.length <= maxLength) return text;
|
|
2279
|
+
return text.slice(0, maxLength) + '\n\n... [content truncated for display]';
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
function renderEventTitle(
|
|
2283
|
+
event: TaskEvent,
|
|
2284
|
+
workspacePath?: string,
|
|
2285
|
+
onOpenViewer?: (path: string) => void,
|
|
2286
|
+
agentCtx?: AgentContext
|
|
2287
|
+
): React.ReactNode {
|
|
2288
|
+
// Build message context for personalized messages
|
|
2289
|
+
const msgCtx = agentCtx ? {
|
|
2290
|
+
agentName: agentCtx.agentName,
|
|
2291
|
+
userName: agentCtx.userName,
|
|
2292
|
+
personality: agentCtx.personality,
|
|
2293
|
+
persona: agentCtx.persona,
|
|
2294
|
+
emojiUsage: agentCtx.emojiUsage,
|
|
2295
|
+
quirks: agentCtx.quirks,
|
|
2296
|
+
} : {
|
|
2297
|
+
agentName: 'CoWork',
|
|
2298
|
+
userName: undefined,
|
|
2299
|
+
personality: 'professional' as const,
|
|
2300
|
+
persona: undefined,
|
|
2301
|
+
emojiUsage: 'minimal' as const,
|
|
2302
|
+
quirks: DEFAULT_QUIRKS,
|
|
2303
|
+
};
|
|
2304
|
+
|
|
2305
|
+
switch (event.type) {
|
|
2306
|
+
case 'task_created':
|
|
2307
|
+
return getMessage('taskStart', msgCtx);
|
|
2308
|
+
case 'task_completed':
|
|
2309
|
+
return getMessage('taskComplete', msgCtx);
|
|
2310
|
+
case 'plan_created':
|
|
2311
|
+
return getMessage('planCreated', msgCtx);
|
|
2312
|
+
case 'step_started':
|
|
2313
|
+
return getMessage('stepStarted', msgCtx, event.payload.step?.description || 'Getting started...');
|
|
2314
|
+
case 'step_completed':
|
|
2315
|
+
return getMessage('stepCompleted', msgCtx, event.payload.step?.description || event.payload.message);
|
|
2316
|
+
case 'tool_call':
|
|
2317
|
+
return `Using: ${event.payload.tool}`;
|
|
2318
|
+
case 'tool_result': {
|
|
2319
|
+
const result = event.payload.result;
|
|
2320
|
+
const success = result?.success !== false && !result?.error;
|
|
2321
|
+
const status = success ? 'done' : 'issue';
|
|
2322
|
+
|
|
2323
|
+
// Extract useful info from result to show inline
|
|
2324
|
+
let detail = '';
|
|
2325
|
+
if (result) {
|
|
2326
|
+
if (!success && result.error) {
|
|
2327
|
+
// Show error message for failed tools
|
|
2328
|
+
const errorMsg = typeof result.error === 'string' ? result.error : 'Unknown error';
|
|
2329
|
+
detail = `: ${errorMsg.slice(0, 60)}${errorMsg.length > 60 ? '...' : ''}`;
|
|
2330
|
+
} else if (result.path) {
|
|
2331
|
+
detail = ` → ${result.path}`;
|
|
2332
|
+
} else if (result.content && typeof result.content === 'string') {
|
|
2333
|
+
const lines = result.content.split('\n').length;
|
|
2334
|
+
detail = ` → ${lines} lines`;
|
|
2335
|
+
} else if (result.size !== undefined) {
|
|
2336
|
+
detail = ` → ${result.size} bytes`;
|
|
2337
|
+
} else if (result.files) {
|
|
2338
|
+
detail = ` → ${result.files.length} items`;
|
|
2339
|
+
} else if (result.matches) {
|
|
2340
|
+
detail = ` → ${result.matches.length} matches`;
|
|
2341
|
+
} else if (result.exitCode !== undefined) {
|
|
2342
|
+
detail = result.exitCode === 0 ? '' : ` → exit ${result.exitCode}`;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
return `${event.payload.tool} ${status}${detail}`;
|
|
2346
|
+
}
|
|
2347
|
+
case 'assistant_message':
|
|
2348
|
+
return msgCtx.agentName;
|
|
2349
|
+
case 'file_created':
|
|
2350
|
+
return (
|
|
2351
|
+
<span>
|
|
2352
|
+
Created: <ClickableFilePath path={event.payload.path} workspacePath={workspacePath} onOpenViewer={onOpenViewer} />
|
|
2353
|
+
</span>
|
|
2354
|
+
);
|
|
2355
|
+
case 'file_modified':
|
|
2356
|
+
return (
|
|
2357
|
+
<span>
|
|
2358
|
+
Updated: <ClickableFilePath path={event.payload.path || event.payload.from} workspacePath={workspacePath} onOpenViewer={onOpenViewer} />
|
|
2359
|
+
</span>
|
|
2360
|
+
);
|
|
2361
|
+
case 'file_deleted':
|
|
2362
|
+
return `Removed: ${event.payload.path}`;
|
|
2363
|
+
case 'error':
|
|
2364
|
+
return getMessage('error', msgCtx);
|
|
2365
|
+
case 'approval_requested':
|
|
2366
|
+
return `${getMessage('approval', msgCtx)} ${event.payload.approval?.description}`;
|
|
2367
|
+
case 'log':
|
|
2368
|
+
return event.payload.message;
|
|
2369
|
+
// Goal Mode verification events
|
|
2370
|
+
case 'verification_started':
|
|
2371
|
+
return getMessage('verifying', msgCtx);
|
|
2372
|
+
case 'verification_passed':
|
|
2373
|
+
return `${getMessage('verifyPassed', msgCtx)} (attempt ${event.payload.attempt})`;
|
|
2374
|
+
case 'verification_failed':
|
|
2375
|
+
return `${getMessage('verifyFailed', msgCtx)} (attempt ${event.payload.attempt}/${event.payload.maxAttempts})`;
|
|
2376
|
+
case 'retry_started':
|
|
2377
|
+
return getMessage('retrying', msgCtx, String(event.payload.attempt));
|
|
2378
|
+
default:
|
|
2379
|
+
return event.type;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function renderEventDetails(event: TaskEvent, voiceEnabled: boolean) {
|
|
2384
|
+
switch (event.type) {
|
|
2385
|
+
case 'plan_created':
|
|
2386
|
+
return (
|
|
2387
|
+
<div className="event-details">
|
|
2388
|
+
<div style={{ marginBottom: 8, fontWeight: 500 }}>{event.payload.plan?.description}</div>
|
|
2389
|
+
{event.payload.plan?.steps && (
|
|
2390
|
+
<ul style={{ margin: 0, paddingLeft: 18 }}>
|
|
2391
|
+
{event.payload.plan.steps.map((step: any, i: number) => (
|
|
2392
|
+
<li key={i} style={{ marginBottom: 4 }}>{step.description}</li>
|
|
2393
|
+
))}
|
|
2394
|
+
</ul>
|
|
2395
|
+
)}
|
|
2396
|
+
</div>
|
|
2397
|
+
);
|
|
2398
|
+
case 'tool_call':
|
|
2399
|
+
return (
|
|
2400
|
+
<div className="event-details event-details-scrollable">
|
|
2401
|
+
<pre>{truncateForDisplay(JSON.stringify(event.payload.input, null, 2))}</pre>
|
|
2402
|
+
</div>
|
|
2403
|
+
);
|
|
2404
|
+
case 'tool_result':
|
|
2405
|
+
return (
|
|
2406
|
+
<div className="event-details event-details-scrollable">
|
|
2407
|
+
<pre>{truncateForDisplay(JSON.stringify(event.payload.result, null, 2))}</pre>
|
|
2408
|
+
</div>
|
|
2409
|
+
);
|
|
2410
|
+
case 'assistant_message':
|
|
2411
|
+
return (
|
|
2412
|
+
<div className="event-details assistant-message event-details-scrollable">
|
|
2413
|
+
<div className="markdown-content">
|
|
2414
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
|
2415
|
+
{event.payload.message.replace(/\[\[speak\]\]([\s\S]*?)\[\[\/speak\]\]/gi, '$1')}
|
|
2416
|
+
</ReactMarkdown>
|
|
2417
|
+
</div>
|
|
2418
|
+
<div className="message-actions">
|
|
2419
|
+
<MessageCopyButton text={event.payload.message} />
|
|
2420
|
+
<MessageSpeakButton text={event.payload.message} voiceEnabled={voiceEnabled} />
|
|
2421
|
+
</div>
|
|
2422
|
+
</div>
|
|
2423
|
+
);
|
|
2424
|
+
case 'error':
|
|
2425
|
+
return (
|
|
2426
|
+
<div className="event-details" style={{ background: 'rgba(239, 68, 68, 0.1)', borderColor: 'rgba(239, 68, 68, 0.2)' }}>
|
|
2427
|
+
{event.payload.error || event.payload.message}
|
|
2428
|
+
</div>
|
|
2429
|
+
);
|
|
2430
|
+
default:
|
|
2431
|
+
return null;
|
|
2432
|
+
}
|
|
2433
|
+
}
|