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,1189 @@
|
|
|
1
|
+
import { useRef, useEffect, useState, useCallback, useMemo, memo } from 'react';
|
|
2
|
+
import type { CanvasSession } from '../../shared/types';
|
|
3
|
+
import { useAgentContext } from '../hooks/useAgentContext';
|
|
4
|
+
|
|
5
|
+
interface CanvasPreviewProps {
|
|
6
|
+
session: CanvasSession;
|
|
7
|
+
onClose?: () => void;
|
|
8
|
+
forceSnapshot?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SnapshotHistoryEntry {
|
|
12
|
+
imageData: string;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
dimensions: { width: number; height: number };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ConsoleLogEntry {
|
|
18
|
+
type: 'log' | 'warn' | 'error' | 'info';
|
|
19
|
+
message: string;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Refresh rate options
|
|
24
|
+
type RefreshRate = 1000 | 2000 | 5000 | 0; // 0 = manual only
|
|
25
|
+
const REFRESH_RATE_OPTIONS: { value: RefreshRate; label: string }[] = [
|
|
26
|
+
{ value: 1000, label: '1s' },
|
|
27
|
+
{ value: 2000, label: '2s' },
|
|
28
|
+
{ value: 5000, label: '5s' },
|
|
29
|
+
{ value: 0, label: 'Manual' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Number of times to retry initial snapshot before showing error
|
|
33
|
+
const MAX_INITIAL_RETRIES = 3;
|
|
34
|
+
const RETRY_DELAY_MS = 500;
|
|
35
|
+
// Timeout for snapshot requests (ms)
|
|
36
|
+
const SNAPSHOT_TIMEOUT_MS = 10000;
|
|
37
|
+
// Debounce delay for rapid snapshot requests (ms)
|
|
38
|
+
const DEBOUNCE_DELAY_MS = 300;
|
|
39
|
+
// Maximum number of snapshots to keep in history
|
|
40
|
+
const MAX_HISTORY_SIZE = 20;
|
|
41
|
+
// Minimum height for the preview
|
|
42
|
+
const MIN_PREVIEW_HEIGHT = 188;
|
|
43
|
+
// Maximum height for the preview
|
|
44
|
+
const MAX_PREVIEW_HEIGHT = 2500;
|
|
45
|
+
// Default preview height (taller for better interactive mode experience)
|
|
46
|
+
const DEFAULT_PREVIEW_HEIGHT = 600;
|
|
47
|
+
|
|
48
|
+
// Helper to create a timeout promise
|
|
49
|
+
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage: string): Promise<T> {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const timeoutId = setTimeout(() => {
|
|
52
|
+
reject(new Error(errorMessage));
|
|
53
|
+
}, timeoutMs);
|
|
54
|
+
|
|
55
|
+
promise
|
|
56
|
+
.then((result) => {
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
resolve(result);
|
|
59
|
+
})
|
|
60
|
+
.catch((err) => {
|
|
61
|
+
clearTimeout(timeoutId);
|
|
62
|
+
reject(err);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Simple hash function for change detection
|
|
68
|
+
function simpleHash(str: string): string {
|
|
69
|
+
let hash = 0;
|
|
70
|
+
for (let i = 0; i < str.length; i++) {
|
|
71
|
+
const char = str.charCodeAt(i);
|
|
72
|
+
hash = ((hash << 5) - hash) + char;
|
|
73
|
+
hash = hash & hash;
|
|
74
|
+
}
|
|
75
|
+
return hash.toString(36);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Memoized image component to prevent re-renders when only image changes
|
|
79
|
+
interface CanvasImageProps {
|
|
80
|
+
src: string;
|
|
81
|
+
dimensions: { width: number; height: number };
|
|
82
|
+
isPaused: boolean;
|
|
83
|
+
isLoading: boolean;
|
|
84
|
+
historyIndex: number;
|
|
85
|
+
historyTimestamp?: number;
|
|
86
|
+
onOpenWindow: () => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const CanvasImage = memo(function CanvasImage({
|
|
90
|
+
src,
|
|
91
|
+
dimensions,
|
|
92
|
+
isPaused,
|
|
93
|
+
isLoading,
|
|
94
|
+
historyIndex,
|
|
95
|
+
historyTimestamp,
|
|
96
|
+
onOpenWindow,
|
|
97
|
+
}: CanvasImageProps) {
|
|
98
|
+
const formatTime = (timestamp: number) => {
|
|
99
|
+
const date = new Date(timestamp);
|
|
100
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div
|
|
105
|
+
className="canvas-preview-image-wrapper"
|
|
106
|
+
onClick={onOpenWindow}
|
|
107
|
+
title="Click to open in window (O)"
|
|
108
|
+
>
|
|
109
|
+
<img
|
|
110
|
+
src={src}
|
|
111
|
+
alt="Canvas Preview"
|
|
112
|
+
className="canvas-preview-image"
|
|
113
|
+
/>
|
|
114
|
+
{dimensions.width > 0 && (
|
|
115
|
+
<div className="canvas-preview-dimensions">
|
|
116
|
+
{dimensions.width} x {dimensions.height}
|
|
117
|
+
{isPaused && historyIndex < 0 && <span className="canvas-paused-indicator"> • Paused</span>}
|
|
118
|
+
{historyIndex >= 0 && historyTimestamp && (
|
|
119
|
+
<span className="canvas-history-time"> • {formatTime(historyTimestamp)}</span>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
{isLoading && historyIndex < 0 && (
|
|
124
|
+
<div className="canvas-preview-updating">
|
|
125
|
+
<svg className="spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
126
|
+
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
|
127
|
+
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
|
|
128
|
+
</svg>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export function CanvasPreview({ session, onClose, forceSnapshot = false }: CanvasPreviewProps) {
|
|
136
|
+
const isBrowserCanvas = session.mode === 'browser';
|
|
137
|
+
const agentContext = useAgentContext();
|
|
138
|
+
const [imageData, setImageData] = useState<string | null>(null);
|
|
139
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
140
|
+
const [error, setError] = useState<string | null>(null);
|
|
141
|
+
const [errorDetails, setErrorDetails] = useState<string | null>(null);
|
|
142
|
+
const [isMinimized, setIsMinimized] = useState(false);
|
|
143
|
+
const [isPaused, setIsPaused] = useState(isBrowserCanvas ? false : true);
|
|
144
|
+
const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 });
|
|
145
|
+
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
|
146
|
+
const [copyFeedback, setCopyFeedback] = useState<string | null>(null);
|
|
147
|
+
const [sessionStatus, setSessionStatus] = useState(session.status);
|
|
148
|
+
|
|
149
|
+
// New feature states
|
|
150
|
+
const [refreshRate, setRefreshRate] = useState<RefreshRate>(forceSnapshot ? 0 : 2000);
|
|
151
|
+
const [showRefreshMenu, setShowRefreshMenu] = useState(false);
|
|
152
|
+
const [previewHeight, setPreviewHeight] = useState(DEFAULT_PREVIEW_HEIGHT);
|
|
153
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
154
|
+
const [snapshotHistory, setSnapshotHistory] = useState<SnapshotHistoryEntry[]>([]);
|
|
155
|
+
const [historyIndex, setHistoryIndex] = useState(-1); // -1 means live view
|
|
156
|
+
const [showHistory, setShowHistory] = useState(false);
|
|
157
|
+
const [consoleLogs, setConsoleLogs] = useState<ConsoleLogEntry[]>([]);
|
|
158
|
+
const [showConsole, setShowConsole] = useState(false);
|
|
159
|
+
const [showExportMenu, setShowExportMenu] = useState(false);
|
|
160
|
+
const [isInteractiveMode, setIsInteractiveMode] = useState(!isBrowserCanvas && !forceSnapshot);
|
|
161
|
+
|
|
162
|
+
const refreshIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
163
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
164
|
+
const retryCountRef = useRef(0);
|
|
165
|
+
const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
166
|
+
const mountedRef = useRef(true);
|
|
167
|
+
const snapshotInProgressRef = useRef(false);
|
|
168
|
+
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
169
|
+
const lastSnapshotTimeRef = useRef(0);
|
|
170
|
+
const lastImageHashRef = useRef<string | null>(null);
|
|
171
|
+
const resizeStartYRef = useRef(0);
|
|
172
|
+
const resizeStartHeightRef = useRef(0);
|
|
173
|
+
|
|
174
|
+
// Update local status when prop changes
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
setSessionStatus(session.status);
|
|
177
|
+
}, [session.status]);
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (isBrowserCanvas) {
|
|
181
|
+
setIsInteractiveMode(false);
|
|
182
|
+
setIsPaused(false);
|
|
183
|
+
}
|
|
184
|
+
}, [isBrowserCanvas]);
|
|
185
|
+
|
|
186
|
+
// Force snapshot mode for archived/previous canvases
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!forceSnapshot) return;
|
|
189
|
+
setIsInteractiveMode(false);
|
|
190
|
+
setIsPaused(true);
|
|
191
|
+
setRefreshRate(0);
|
|
192
|
+
}, [forceSnapshot]);
|
|
193
|
+
|
|
194
|
+
// Add snapshot to history
|
|
195
|
+
const addToHistory = useCallback((newImageData: string, dimensions: { width: number; height: number }) => {
|
|
196
|
+
setSnapshotHistory(prev => {
|
|
197
|
+
const newEntry: SnapshotHistoryEntry = {
|
|
198
|
+
imageData: newImageData,
|
|
199
|
+
timestamp: Date.now(),
|
|
200
|
+
dimensions,
|
|
201
|
+
};
|
|
202
|
+
const updated = [...prev, newEntry];
|
|
203
|
+
// Keep only the last MAX_HISTORY_SIZE entries
|
|
204
|
+
if (updated.length > MAX_HISTORY_SIZE) {
|
|
205
|
+
return updated.slice(-MAX_HISTORY_SIZE);
|
|
206
|
+
}
|
|
207
|
+
return updated;
|
|
208
|
+
});
|
|
209
|
+
}, []);
|
|
210
|
+
|
|
211
|
+
// Take a snapshot of the canvas with timeout and debouncing
|
|
212
|
+
const takeSnapshot = useCallback(async (isRetry = false, isManual = false) => {
|
|
213
|
+
if (!mountedRef.current) return;
|
|
214
|
+
|
|
215
|
+
// Check if session is closed
|
|
216
|
+
if (sessionStatus === 'closed') {
|
|
217
|
+
setError('Canvas session closed');
|
|
218
|
+
setErrorDetails('The canvas session has been terminated');
|
|
219
|
+
setInitialLoadComplete(true);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Prevent overlapping snapshot requests
|
|
224
|
+
if (snapshotInProgressRef.current && !isRetry) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// For automatic refreshes, enforce minimum interval based on refresh rate
|
|
229
|
+
const effectiveMinInterval = refreshRate > 0 ? refreshRate : 2000;
|
|
230
|
+
if (!isManual && !isRetry) {
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
const timeSinceLastSnapshot = now - lastSnapshotTimeRef.current;
|
|
233
|
+
if (timeSinceLastSnapshot < effectiveMinInterval) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
snapshotInProgressRef.current = true;
|
|
240
|
+
|
|
241
|
+
if (!isRetry) {
|
|
242
|
+
setIsLoading(true);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Wrap snapshot call with timeout
|
|
246
|
+
const snapshot = await withTimeout(
|
|
247
|
+
window.electronAPI.canvasSnapshot(session.id),
|
|
248
|
+
SNAPSHOT_TIMEOUT_MS,
|
|
249
|
+
'Snapshot request timed out'
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (!mountedRef.current) return;
|
|
253
|
+
|
|
254
|
+
if (snapshot && snapshot.imageBase64) {
|
|
255
|
+
const newImageData = `data:image/png;base64,${snapshot.imageBase64}`;
|
|
256
|
+
const newHash = simpleHash(snapshot.imageBase64);
|
|
257
|
+
|
|
258
|
+
// Smart change detection - only update if content changed
|
|
259
|
+
const hasChanged = lastImageHashRef.current !== newHash;
|
|
260
|
+
|
|
261
|
+
if (hasChanged || isManual) {
|
|
262
|
+
lastImageHashRef.current = newHash;
|
|
263
|
+
|
|
264
|
+
// Directly update image data without clearing first to avoid flicker
|
|
265
|
+
// React will batch these updates efficiently
|
|
266
|
+
setImageData(newImageData);
|
|
267
|
+
setImageDimensions({ width: snapshot.width, height: snapshot.height });
|
|
268
|
+
setError(null);
|
|
269
|
+
setErrorDetails(null);
|
|
270
|
+
setInitialLoadComplete(true);
|
|
271
|
+
lastSnapshotTimeRef.current = Date.now();
|
|
272
|
+
|
|
273
|
+
// Add to history (only when content changed)
|
|
274
|
+
if (hasChanged) {
|
|
275
|
+
addToHistory(newImageData, { width: snapshot.width, height: snapshot.height });
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
// Content didn't change, just update timestamp
|
|
279
|
+
lastSnapshotTimeRef.current = Date.now();
|
|
280
|
+
setIsLoading(false);
|
|
281
|
+
}
|
|
282
|
+
retryCountRef.current = 0;
|
|
283
|
+
} else {
|
|
284
|
+
throw new Error('Empty snapshot received');
|
|
285
|
+
}
|
|
286
|
+
} catch (err) {
|
|
287
|
+
if (!mountedRef.current) return;
|
|
288
|
+
|
|
289
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
290
|
+
console.error('Failed to take canvas snapshot:', errorMessage);
|
|
291
|
+
|
|
292
|
+
// If we haven't successfully loaded yet, retry a few times
|
|
293
|
+
if (!initialLoadComplete && retryCountRef.current < MAX_INITIAL_RETRIES) {
|
|
294
|
+
retryCountRef.current++;
|
|
295
|
+
if (retryTimeoutRef.current) {
|
|
296
|
+
clearTimeout(retryTimeoutRef.current);
|
|
297
|
+
}
|
|
298
|
+
retryTimeoutRef.current = setTimeout(() => {
|
|
299
|
+
if (mountedRef.current) {
|
|
300
|
+
takeSnapshot(true, isManual);
|
|
301
|
+
}
|
|
302
|
+
}, RETRY_DELAY_MS);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Parse error for better user feedback
|
|
307
|
+
let userError = 'Failed to capture canvas';
|
|
308
|
+
let details = errorMessage;
|
|
309
|
+
|
|
310
|
+
if (errorMessage.includes('not found') || errorMessage.includes('not open')) {
|
|
311
|
+
userError = 'Canvas window not available';
|
|
312
|
+
details = 'The canvas window may have been closed or not yet created';
|
|
313
|
+
} else if (errorMessage.includes('timeout') || errorMessage.includes('timed out')) {
|
|
314
|
+
userError = 'Snapshot timed out';
|
|
315
|
+
details = 'The canvas took too long to respond. Try refreshing.';
|
|
316
|
+
} else if (errorMessage.includes('destroyed')) {
|
|
317
|
+
userError = 'Canvas window destroyed';
|
|
318
|
+
details = 'The canvas window has been closed';
|
|
319
|
+
setSessionStatus('closed');
|
|
320
|
+
} else if (errorMessage.includes('closed')) {
|
|
321
|
+
userError = 'Canvas session closed';
|
|
322
|
+
details = 'The canvas session is no longer available';
|
|
323
|
+
setSessionStatus('closed');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (initialLoadComplete || retryCountRef.current >= MAX_INITIAL_RETRIES) {
|
|
327
|
+
setError(userError);
|
|
328
|
+
setErrorDetails(details);
|
|
329
|
+
setInitialLoadComplete(true);
|
|
330
|
+
}
|
|
331
|
+
} finally {
|
|
332
|
+
if (mountedRef.current) {
|
|
333
|
+
setIsLoading(false);
|
|
334
|
+
snapshotInProgressRef.current = false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}, [session.id, sessionStatus, initialLoadComplete, refreshRate, addToHistory]);
|
|
338
|
+
|
|
339
|
+
// Debounced version of takeSnapshot for manual refreshes
|
|
340
|
+
const debouncedTakeSnapshot = useCallback((isManual = true) => {
|
|
341
|
+
if (debounceTimerRef.current) {
|
|
342
|
+
clearTimeout(debounceTimerRef.current);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
346
|
+
if (mountedRef.current) {
|
|
347
|
+
takeSnapshot(false, isManual);
|
|
348
|
+
}
|
|
349
|
+
}, DEBOUNCE_DELAY_MS);
|
|
350
|
+
}, [takeSnapshot]);
|
|
351
|
+
|
|
352
|
+
// Track mounted state and cleanup all timers on unmount
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
mountedRef.current = true;
|
|
355
|
+
return () => {
|
|
356
|
+
mountedRef.current = false;
|
|
357
|
+
if (refreshIntervalRef.current) {
|
|
358
|
+
clearInterval(refreshIntervalRef.current);
|
|
359
|
+
refreshIntervalRef.current = null;
|
|
360
|
+
}
|
|
361
|
+
if (retryTimeoutRef.current) {
|
|
362
|
+
clearTimeout(retryTimeoutRef.current);
|
|
363
|
+
retryTimeoutRef.current = null;
|
|
364
|
+
}
|
|
365
|
+
if (debounceTimerRef.current) {
|
|
366
|
+
clearTimeout(debounceTimerRef.current);
|
|
367
|
+
debounceTimerRef.current = null;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}, []);
|
|
371
|
+
|
|
372
|
+
// Listen for canvas session events from main process
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
const unsubscribe = window.electronAPI.onCanvasEvent((event) => {
|
|
375
|
+
if (event.sessionId !== session.id || !mountedRef.current) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
switch (event.type) {
|
|
380
|
+
case 'session_closed':
|
|
381
|
+
setSessionStatus('closed');
|
|
382
|
+
setError('Canvas session closed');
|
|
383
|
+
setErrorDetails('The canvas session has been terminated');
|
|
384
|
+
if (refreshIntervalRef.current) {
|
|
385
|
+
clearInterval(refreshIntervalRef.current);
|
|
386
|
+
refreshIntervalRef.current = null;
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
|
|
390
|
+
case 'content_pushed':
|
|
391
|
+
if (!isPaused && !isMinimized) {
|
|
392
|
+
setTimeout(() => {
|
|
393
|
+
if (mountedRef.current && !snapshotInProgressRef.current) {
|
|
394
|
+
takeSnapshot(false, false);
|
|
395
|
+
}
|
|
396
|
+
}, 500);
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
399
|
+
|
|
400
|
+
case 'session_updated':
|
|
401
|
+
if (event.session && event.session.status !== sessionStatus) {
|
|
402
|
+
setSessionStatus(event.session.status);
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
return () => {
|
|
409
|
+
unsubscribe();
|
|
410
|
+
};
|
|
411
|
+
}, [session.id, isPaused, isMinimized, takeSnapshot, sessionStatus]);
|
|
412
|
+
|
|
413
|
+
// Initial snapshot and periodic refresh
|
|
414
|
+
useEffect(() => {
|
|
415
|
+
takeSnapshot(false, false);
|
|
416
|
+
|
|
417
|
+
// Refresh snapshot based on refresh rate when not minimized, not paused, and session is active
|
|
418
|
+
if (!isMinimized && !isPaused && sessionStatus === 'active' && refreshRate > 0) {
|
|
419
|
+
refreshIntervalRef.current = setInterval(() => {
|
|
420
|
+
if (mountedRef.current && !snapshotInProgressRef.current) {
|
|
421
|
+
takeSnapshot(false, false);
|
|
422
|
+
}
|
|
423
|
+
}, refreshRate);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return () => {
|
|
427
|
+
if (refreshIntervalRef.current) {
|
|
428
|
+
clearInterval(refreshIntervalRef.current);
|
|
429
|
+
refreshIntervalRef.current = null;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}, [takeSnapshot, isMinimized, isPaused, sessionStatus, refreshRate]);
|
|
433
|
+
|
|
434
|
+
// Resize handlers
|
|
435
|
+
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
|
436
|
+
e.preventDefault();
|
|
437
|
+
setIsResizing(true);
|
|
438
|
+
resizeStartYRef.current = e.clientY;
|
|
439
|
+
resizeStartHeightRef.current = previewHeight;
|
|
440
|
+
}, [previewHeight]);
|
|
441
|
+
|
|
442
|
+
useEffect(() => {
|
|
443
|
+
if (!isResizing) return;
|
|
444
|
+
|
|
445
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
446
|
+
const deltaY = e.clientY - resizeStartYRef.current;
|
|
447
|
+
const newHeight = Math.min(
|
|
448
|
+
MAX_PREVIEW_HEIGHT,
|
|
449
|
+
Math.max(MIN_PREVIEW_HEIGHT, resizeStartHeightRef.current + deltaY)
|
|
450
|
+
);
|
|
451
|
+
setPreviewHeight(newHeight);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const handleMouseUp = () => {
|
|
455
|
+
setIsResizing(false);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
459
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
460
|
+
|
|
461
|
+
return () => {
|
|
462
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
463
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
464
|
+
};
|
|
465
|
+
}, [isResizing]);
|
|
466
|
+
|
|
467
|
+
// Open the canvas in its own window
|
|
468
|
+
const handleOpenWindow = useCallback(async () => {
|
|
469
|
+
try {
|
|
470
|
+
await window.electronAPI.canvasShow(session.id);
|
|
471
|
+
} catch (err) {
|
|
472
|
+
console.error('Failed to show canvas window:', err);
|
|
473
|
+
}
|
|
474
|
+
}, [session.id]);
|
|
475
|
+
|
|
476
|
+
// Close the canvas session
|
|
477
|
+
const handleClose = useCallback(async () => {
|
|
478
|
+
try {
|
|
479
|
+
await window.electronAPI.canvasClose(session.id);
|
|
480
|
+
onClose?.();
|
|
481
|
+
} catch (err) {
|
|
482
|
+
console.error('Failed to close canvas:', err);
|
|
483
|
+
}
|
|
484
|
+
}, [session.id, onClose]);
|
|
485
|
+
|
|
486
|
+
// Toggle minimize state
|
|
487
|
+
const handleMinimize = useCallback(() => {
|
|
488
|
+
setIsMinimized(prev => !prev);
|
|
489
|
+
}, []);
|
|
490
|
+
|
|
491
|
+
// Toggle pause state
|
|
492
|
+
const handleTogglePause = useCallback(() => {
|
|
493
|
+
setIsPaused(prev => !prev);
|
|
494
|
+
}, []);
|
|
495
|
+
|
|
496
|
+
// Refresh the snapshot manually (debounced)
|
|
497
|
+
const handleRefresh = useCallback(() => {
|
|
498
|
+
debouncedTakeSnapshot(true);
|
|
499
|
+
}, [debouncedTakeSnapshot]);
|
|
500
|
+
|
|
501
|
+
// Copy snapshot to clipboard
|
|
502
|
+
const handleCopyToClipboard = useCallback(async () => {
|
|
503
|
+
const currentImage = historyIndex >= 0 ? snapshotHistory[historyIndex]?.imageData : imageData;
|
|
504
|
+
if (!currentImage) return;
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const response = await fetch(currentImage);
|
|
508
|
+
const blob = await response.blob();
|
|
509
|
+
|
|
510
|
+
await navigator.clipboard.write([
|
|
511
|
+
new ClipboardItem({ 'image/png': blob })
|
|
512
|
+
]);
|
|
513
|
+
|
|
514
|
+
setCopyFeedback('Copied!');
|
|
515
|
+
setTimeout(() => setCopyFeedback(null), 2000);
|
|
516
|
+
} catch (err) {
|
|
517
|
+
console.error('Failed to copy to clipboard:', err);
|
|
518
|
+
setCopyFeedback('Failed to copy');
|
|
519
|
+
setTimeout(() => setCopyFeedback(null), 2000);
|
|
520
|
+
}
|
|
521
|
+
}, [imageData, historyIndex, snapshotHistory]);
|
|
522
|
+
|
|
523
|
+
// Save snapshot as PNG
|
|
524
|
+
const handleSaveSnapshot = useCallback(() => {
|
|
525
|
+
const currentImage = historyIndex >= 0 ? snapshotHistory[historyIndex]?.imageData : imageData;
|
|
526
|
+
if (!currentImage) return;
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const link = document.createElement('a');
|
|
530
|
+
link.download = `canvas-${session.id.slice(0, 8)}-${Date.now()}.png`;
|
|
531
|
+
link.href = currentImage;
|
|
532
|
+
document.body.appendChild(link);
|
|
533
|
+
link.click();
|
|
534
|
+
document.body.removeChild(link);
|
|
535
|
+
|
|
536
|
+
setCopyFeedback('Saved!');
|
|
537
|
+
setTimeout(() => setCopyFeedback(null), 2000);
|
|
538
|
+
} catch (err) {
|
|
539
|
+
console.error('Failed to save snapshot:', err);
|
|
540
|
+
setCopyFeedback('Failed to save');
|
|
541
|
+
setTimeout(() => setCopyFeedback(null), 2000);
|
|
542
|
+
}
|
|
543
|
+
}, [imageData, session.id, historyIndex, snapshotHistory]);
|
|
544
|
+
|
|
545
|
+
// Handle refresh rate change
|
|
546
|
+
const handleRefreshRateChange = useCallback((rate: RefreshRate) => {
|
|
547
|
+
setRefreshRate(rate);
|
|
548
|
+
setShowRefreshMenu(false);
|
|
549
|
+
// If switching to manual, pause auto-refresh
|
|
550
|
+
if (rate === 0) {
|
|
551
|
+
setIsPaused(true);
|
|
552
|
+
} else {
|
|
553
|
+
setIsPaused(false);
|
|
554
|
+
}
|
|
555
|
+
}, []);
|
|
556
|
+
|
|
557
|
+
// Export as standalone HTML file
|
|
558
|
+
const handleExportHTML = useCallback(async () => {
|
|
559
|
+
setShowExportMenu(false);
|
|
560
|
+
try {
|
|
561
|
+
const result = await window.electronAPI.canvasExportHTML(session.id);
|
|
562
|
+
// Create and download the file
|
|
563
|
+
const blob = new Blob([result.content], { type: 'text/html' });
|
|
564
|
+
const url = URL.createObjectURL(blob);
|
|
565
|
+
const link = document.createElement('a');
|
|
566
|
+
link.href = url;
|
|
567
|
+
link.download = result.filename;
|
|
568
|
+
document.body.appendChild(link);
|
|
569
|
+
link.click();
|
|
570
|
+
document.body.removeChild(link);
|
|
571
|
+
URL.revokeObjectURL(url);
|
|
572
|
+
|
|
573
|
+
setCopyFeedback('Exported!');
|
|
574
|
+
setTimeout(() => setCopyFeedback(null), 2000);
|
|
575
|
+
} catch (err) {
|
|
576
|
+
console.error('Failed to export HTML:', err);
|
|
577
|
+
setCopyFeedback('Export failed');
|
|
578
|
+
setTimeout(() => setCopyFeedback(null), 2000);
|
|
579
|
+
}
|
|
580
|
+
}, [session.id]);
|
|
581
|
+
|
|
582
|
+
// Open canvas in system browser
|
|
583
|
+
const handleOpenInBrowser = useCallback(async () => {
|
|
584
|
+
setShowExportMenu(false);
|
|
585
|
+
try {
|
|
586
|
+
await window.electronAPI.canvasOpenInBrowser(session.id);
|
|
587
|
+
setCopyFeedback('Opened in browser');
|
|
588
|
+
setTimeout(() => setCopyFeedback(null), 2000);
|
|
589
|
+
} catch (err) {
|
|
590
|
+
console.error('Failed to open in browser:', err);
|
|
591
|
+
setCopyFeedback('Failed to open');
|
|
592
|
+
setTimeout(() => setCopyFeedback(null), 2000);
|
|
593
|
+
}
|
|
594
|
+
}, [session.id]);
|
|
595
|
+
|
|
596
|
+
// Open session folder in Finder
|
|
597
|
+
const handleOpenFolder = useCallback(async () => {
|
|
598
|
+
setShowExportMenu(false);
|
|
599
|
+
try {
|
|
600
|
+
const sessionDir = await window.electronAPI.canvasGetSessionDir(session.id);
|
|
601
|
+
if (sessionDir) {
|
|
602
|
+
await window.electronAPI.showInFinder(sessionDir);
|
|
603
|
+
setCopyFeedback('Opened folder');
|
|
604
|
+
setTimeout(() => setCopyFeedback(null), 2000);
|
|
605
|
+
}
|
|
606
|
+
} catch (err) {
|
|
607
|
+
console.error('Failed to open folder:', err);
|
|
608
|
+
setCopyFeedback('Failed to open');
|
|
609
|
+
setTimeout(() => setCopyFeedback(null), 2000);
|
|
610
|
+
}
|
|
611
|
+
}, [session.id]);
|
|
612
|
+
|
|
613
|
+
// Navigate history
|
|
614
|
+
const handleHistoryChange = useCallback((index: number) => {
|
|
615
|
+
setHistoryIndex(index);
|
|
616
|
+
}, []);
|
|
617
|
+
|
|
618
|
+
// Go to live view
|
|
619
|
+
const handleGoLive = useCallback(() => {
|
|
620
|
+
setHistoryIndex(-1);
|
|
621
|
+
}, []);
|
|
622
|
+
|
|
623
|
+
// TODO: Implement console capture from canvas window via IPC
|
|
624
|
+
// The addConsoleLog function can be added when canvas console forwarding is implemented
|
|
625
|
+
// Example usage: addConsoleLog('log', 'Message from canvas');
|
|
626
|
+
|
|
627
|
+
// Clear console logs
|
|
628
|
+
const handleClearConsole = useCallback(() => {
|
|
629
|
+
setConsoleLogs([]);
|
|
630
|
+
}, []);
|
|
631
|
+
|
|
632
|
+
// Toggle interactive mode
|
|
633
|
+
const handleToggleInteractiveMode = useCallback(() => {
|
|
634
|
+
if (isBrowserCanvas) {
|
|
635
|
+
setIsInteractiveMode(false);
|
|
636
|
+
setIsPaused(false);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
setIsInteractiveMode(prev => !prev);
|
|
640
|
+
// Toggle pause state based on mode
|
|
641
|
+
if (!isInteractiveMode) {
|
|
642
|
+
// Switching to interactive mode - pause snapshots to save resources
|
|
643
|
+
setIsPaused(true);
|
|
644
|
+
} else {
|
|
645
|
+
// Switching to snapshot mode - resume snapshots
|
|
646
|
+
setIsPaused(false);
|
|
647
|
+
}
|
|
648
|
+
}, [isInteractiveMode, isBrowserCanvas]);
|
|
649
|
+
|
|
650
|
+
// Keyboard shortcuts
|
|
651
|
+
useEffect(() => {
|
|
652
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
653
|
+
if (!containerRef.current?.contains(document.activeElement) &&
|
|
654
|
+
document.activeElement !== containerRef.current) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
switch (e.key.toLowerCase()) {
|
|
663
|
+
case 'r':
|
|
664
|
+
if (!e.metaKey && !e.ctrlKey) {
|
|
665
|
+
e.preventDefault();
|
|
666
|
+
debouncedTakeSnapshot(true);
|
|
667
|
+
}
|
|
668
|
+
break;
|
|
669
|
+
case 'm':
|
|
670
|
+
if (!e.metaKey && !e.ctrlKey) {
|
|
671
|
+
e.preventDefault();
|
|
672
|
+
setIsMinimized(prev => !prev);
|
|
673
|
+
}
|
|
674
|
+
break;
|
|
675
|
+
case 'o':
|
|
676
|
+
if (!e.metaKey && !e.ctrlKey) {
|
|
677
|
+
e.preventDefault();
|
|
678
|
+
handleOpenWindow();
|
|
679
|
+
}
|
|
680
|
+
break;
|
|
681
|
+
case 'p':
|
|
682
|
+
if (!e.metaKey && !e.ctrlKey) {
|
|
683
|
+
e.preventDefault();
|
|
684
|
+
setIsPaused(prev => !prev);
|
|
685
|
+
}
|
|
686
|
+
break;
|
|
687
|
+
case 'c':
|
|
688
|
+
if (!e.metaKey && !e.ctrlKey) {
|
|
689
|
+
e.preventDefault();
|
|
690
|
+
handleCopyToClipboard();
|
|
691
|
+
}
|
|
692
|
+
break;
|
|
693
|
+
case 's':
|
|
694
|
+
if (!e.metaKey && !e.ctrlKey) {
|
|
695
|
+
e.preventDefault();
|
|
696
|
+
handleSaveSnapshot();
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
case 'h':
|
|
700
|
+
if (!e.metaKey && !e.ctrlKey) {
|
|
701
|
+
e.preventDefault();
|
|
702
|
+
setShowHistory(prev => !prev);
|
|
703
|
+
}
|
|
704
|
+
break;
|
|
705
|
+
case 'l':
|
|
706
|
+
if (!e.metaKey && !e.ctrlKey) {
|
|
707
|
+
e.preventDefault();
|
|
708
|
+
setShowConsole(prev => !prev);
|
|
709
|
+
}
|
|
710
|
+
break;
|
|
711
|
+
case 'e':
|
|
712
|
+
if (!e.metaKey && !e.ctrlKey) {
|
|
713
|
+
e.preventDefault();
|
|
714
|
+
setShowExportMenu(prev => !prev);
|
|
715
|
+
}
|
|
716
|
+
break;
|
|
717
|
+
case 'b':
|
|
718
|
+
if (!e.metaKey && !e.ctrlKey) {
|
|
719
|
+
e.preventDefault();
|
|
720
|
+
handleOpenInBrowser();
|
|
721
|
+
}
|
|
722
|
+
break;
|
|
723
|
+
case 'i':
|
|
724
|
+
if (!e.metaKey && !e.ctrlKey) {
|
|
725
|
+
e.preventDefault();
|
|
726
|
+
handleToggleInteractiveMode();
|
|
727
|
+
}
|
|
728
|
+
break;
|
|
729
|
+
case 'arrowleft':
|
|
730
|
+
if (showHistory && historyIndex < snapshotHistory.length - 1) {
|
|
731
|
+
e.preventDefault();
|
|
732
|
+
setHistoryIndex(prev => prev === -1 ? snapshotHistory.length - 1 : Math.min(prev + 1, snapshotHistory.length - 1));
|
|
733
|
+
}
|
|
734
|
+
break;
|
|
735
|
+
case 'arrowright':
|
|
736
|
+
if (showHistory && historyIndex >= 0) {
|
|
737
|
+
e.preventDefault();
|
|
738
|
+
setHistoryIndex(prev => prev - 1);
|
|
739
|
+
}
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
745
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
746
|
+
}, [debouncedTakeSnapshot, handleCopyToClipboard, handleSaveSnapshot, handleOpenWindow, handleOpenInBrowser, handleToggleInteractiveMode, showHistory, historyIndex, snapshotHistory.length]);
|
|
747
|
+
|
|
748
|
+
// Get status indicator
|
|
749
|
+
const getStatusIndicator = () => {
|
|
750
|
+
if (historyIndex >= 0) {
|
|
751
|
+
return <span className="canvas-status history">History</span>;
|
|
752
|
+
}
|
|
753
|
+
if (isPaused && sessionStatus === 'active') {
|
|
754
|
+
return <span className="canvas-status paused">Paused</span>;
|
|
755
|
+
}
|
|
756
|
+
switch (sessionStatus) {
|
|
757
|
+
case 'active':
|
|
758
|
+
return <span className="canvas-status active">Live</span>;
|
|
759
|
+
case 'paused':
|
|
760
|
+
return <span className="canvas-status paused">Paused</span>;
|
|
761
|
+
case 'closed':
|
|
762
|
+
return <span className="canvas-status closed">Closed</span>;
|
|
763
|
+
default:
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// Get current display image (live or from history)
|
|
769
|
+
const currentDisplayImage = useMemo(() => {
|
|
770
|
+
if (historyIndex >= 0 && snapshotHistory[historyIndex]) {
|
|
771
|
+
return snapshotHistory[historyIndex].imageData;
|
|
772
|
+
}
|
|
773
|
+
return imageData;
|
|
774
|
+
}, [historyIndex, snapshotHistory, imageData]);
|
|
775
|
+
|
|
776
|
+
const currentDisplayDimensions = useMemo(() => {
|
|
777
|
+
if (historyIndex >= 0 && snapshotHistory[historyIndex]) {
|
|
778
|
+
return snapshotHistory[historyIndex].dimensions;
|
|
779
|
+
}
|
|
780
|
+
return imageDimensions;
|
|
781
|
+
}, [historyIndex, snapshotHistory, imageDimensions]);
|
|
782
|
+
|
|
783
|
+
// Format timestamp for history
|
|
784
|
+
const formatHistoryTime = (timestamp: number) => {
|
|
785
|
+
const date = new Date(timestamp);
|
|
786
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
// Loading skeleton component
|
|
790
|
+
const LoadingSkeleton = () => (
|
|
791
|
+
<div className="canvas-preview-skeleton">
|
|
792
|
+
<div className="skeleton-header">
|
|
793
|
+
<div className="skeleton-title"></div>
|
|
794
|
+
<div className="skeleton-actions">
|
|
795
|
+
<div className="skeleton-btn"></div>
|
|
796
|
+
<div className="skeleton-btn"></div>
|
|
797
|
+
<div className="skeleton-btn"></div>
|
|
798
|
+
</div>
|
|
799
|
+
</div>
|
|
800
|
+
<div className="skeleton-content">
|
|
801
|
+
<div className="skeleton-image"></div>
|
|
802
|
+
</div>
|
|
803
|
+
</div>
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
// Show skeleton during initial load
|
|
807
|
+
if (!initialLoadComplete && isLoading) {
|
|
808
|
+
return <LoadingSkeleton />;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Don't render if no content and no error
|
|
812
|
+
if (!initialLoadComplete || (!imageData && !error)) {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return (
|
|
817
|
+
<div
|
|
818
|
+
className={`canvas-preview-container ${isMinimized ? 'minimized' : ''} ${isResizing ? 'resizing' : ''}`}
|
|
819
|
+
ref={containerRef}
|
|
820
|
+
tabIndex={0}
|
|
821
|
+
style={!isMinimized ? { '--preview-height': `${previewHeight}px` } as React.CSSProperties : undefined}
|
|
822
|
+
>
|
|
823
|
+
<div className="canvas-preview-header">
|
|
824
|
+
<div className="canvas-preview-title">
|
|
825
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
826
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
827
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
828
|
+
<polyline points="21 15 16 10 5 21" />
|
|
829
|
+
</svg>
|
|
830
|
+
<span className="canvas-title-text">{session.title || 'Live Canvas'}</span>
|
|
831
|
+
</div>
|
|
832
|
+
<div className="canvas-preview-actions">
|
|
833
|
+
{getStatusIndicator()}
|
|
834
|
+
{copyFeedback && (
|
|
835
|
+
<span className="canvas-copy-feedback">{copyFeedback}</span>
|
|
836
|
+
)}
|
|
837
|
+
{!isMinimized && currentDisplayImage && (
|
|
838
|
+
<>
|
|
839
|
+
{/* Copy to clipboard */}
|
|
840
|
+
<button
|
|
841
|
+
className="canvas-action-btn"
|
|
842
|
+
onClick={handleCopyToClipboard}
|
|
843
|
+
title="Copy to clipboard (C)"
|
|
844
|
+
>
|
|
845
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
846
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
847
|
+
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
|
848
|
+
</svg>
|
|
849
|
+
</button>
|
|
850
|
+
{/* Save as PNG */}
|
|
851
|
+
<button
|
|
852
|
+
className="canvas-action-btn"
|
|
853
|
+
onClick={handleSaveSnapshot}
|
|
854
|
+
title="Save as PNG (S)"
|
|
855
|
+
>
|
|
856
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
857
|
+
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
|
858
|
+
<polyline points="7 10 12 15 17 10" />
|
|
859
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
860
|
+
</svg>
|
|
861
|
+
</button>
|
|
862
|
+
{/* History toggle */}
|
|
863
|
+
<button
|
|
864
|
+
className={`canvas-action-btn ${showHistory ? 'active' : ''}`}
|
|
865
|
+
onClick={() => setShowHistory(prev => !prev)}
|
|
866
|
+
title={`${showHistory ? 'Hide' : 'Show'} history (H)`}
|
|
867
|
+
>
|
|
868
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
869
|
+
<circle cx="12" cy="12" r="10" />
|
|
870
|
+
<polyline points="12 6 12 12 16 14" />
|
|
871
|
+
</svg>
|
|
872
|
+
</button>
|
|
873
|
+
{/* Console toggle */}
|
|
874
|
+
<button
|
|
875
|
+
className={`canvas-action-btn ${showConsole ? 'active' : ''}`}
|
|
876
|
+
onClick={() => setShowConsole(prev => !prev)}
|
|
877
|
+
title={`${showConsole ? 'Hide' : 'Show'} console (L)`}
|
|
878
|
+
>
|
|
879
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
880
|
+
<polyline points="4 17 10 11 4 5" />
|
|
881
|
+
<line x1="12" y1="19" x2="20" y2="19" />
|
|
882
|
+
</svg>
|
|
883
|
+
</button>
|
|
884
|
+
{/* Interactive mode toggle */}
|
|
885
|
+
<button
|
|
886
|
+
className={`canvas-action-btn ${isInteractiveMode ? 'active' : ''} ${(isBrowserCanvas || forceSnapshot) ? 'disabled' : ''}`}
|
|
887
|
+
onClick={handleToggleInteractiveMode}
|
|
888
|
+
disabled={isBrowserCanvas || forceSnapshot}
|
|
889
|
+
title={isBrowserCanvas
|
|
890
|
+
? 'Interactive preview unavailable for browser pages. Use Open in window.'
|
|
891
|
+
: (forceSnapshot ? 'Snapshot locked for previous canvases' : (isInteractiveMode ? 'Switch to snapshot mode (I)' : 'Switch to interactive mode (I)'))}
|
|
892
|
+
>
|
|
893
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
894
|
+
<path d="M5 3l14 9-7 2-4 6-3-17z" />
|
|
895
|
+
</svg>
|
|
896
|
+
</button>
|
|
897
|
+
{/* Export menu */}
|
|
898
|
+
<div className="canvas-export-menu-container">
|
|
899
|
+
<button
|
|
900
|
+
className={`canvas-action-btn ${showExportMenu ? 'active' : ''}`}
|
|
901
|
+
onClick={() => setShowExportMenu(prev => !prev)}
|
|
902
|
+
title="Export options (E)"
|
|
903
|
+
>
|
|
904
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
905
|
+
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
|
906
|
+
<polyline points="17 8 12 3 7 8" />
|
|
907
|
+
<line x1="12" y1="3" x2="12" y2="15" />
|
|
908
|
+
</svg>
|
|
909
|
+
</button>
|
|
910
|
+
{showExportMenu && (
|
|
911
|
+
<div className="canvas-export-menu">
|
|
912
|
+
<button className="export-menu-item" onClick={handleExportHTML}>
|
|
913
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
914
|
+
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
|
915
|
+
<polyline points="14 2 14 8 20 8" />
|
|
916
|
+
</svg>
|
|
917
|
+
Export HTML
|
|
918
|
+
</button>
|
|
919
|
+
<button className="export-menu-item" onClick={handleOpenInBrowser}>
|
|
920
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
921
|
+
<circle cx="12" cy="12" r="10" />
|
|
922
|
+
<line x1="2" y1="12" x2="22" y2="12" />
|
|
923
|
+
<path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" />
|
|
924
|
+
</svg>
|
|
925
|
+
Open in Browser (B)
|
|
926
|
+
</button>
|
|
927
|
+
<button className="export-menu-item" onClick={handleOpenFolder}>
|
|
928
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
929
|
+
<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z" />
|
|
930
|
+
</svg>
|
|
931
|
+
Show in Finder
|
|
932
|
+
</button>
|
|
933
|
+
</div>
|
|
934
|
+
)}
|
|
935
|
+
</div>
|
|
936
|
+
</>
|
|
937
|
+
)}
|
|
938
|
+
{!isMinimized && sessionStatus === 'active' && (
|
|
939
|
+
<>
|
|
940
|
+
{/* Refresh rate selector */}
|
|
941
|
+
<div className="canvas-refresh-rate-container">
|
|
942
|
+
<button
|
|
943
|
+
className="canvas-action-btn"
|
|
944
|
+
onClick={() => setShowRefreshMenu(prev => !prev)}
|
|
945
|
+
title="Refresh rate"
|
|
946
|
+
>
|
|
947
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
948
|
+
<circle cx="12" cy="12" r="3" />
|
|
949
|
+
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
|
950
|
+
</svg>
|
|
951
|
+
<span className="refresh-rate-label">{refreshRate === 0 ? 'M' : `${refreshRate / 1000}s`}</span>
|
|
952
|
+
</button>
|
|
953
|
+
{showRefreshMenu && (
|
|
954
|
+
<div className="canvas-refresh-menu">
|
|
955
|
+
{REFRESH_RATE_OPTIONS.map(option => (
|
|
956
|
+
<button
|
|
957
|
+
key={option.value}
|
|
958
|
+
className={`refresh-menu-item ${refreshRate === option.value ? 'active' : ''}`}
|
|
959
|
+
onClick={() => handleRefreshRateChange(option.value)}
|
|
960
|
+
>
|
|
961
|
+
{option.label}
|
|
962
|
+
</button>
|
|
963
|
+
))}
|
|
964
|
+
</div>
|
|
965
|
+
)}
|
|
966
|
+
</div>
|
|
967
|
+
{/* Pause/Resume */}
|
|
968
|
+
<button
|
|
969
|
+
className={`canvas-action-btn ${isPaused ? 'paused' : ''}`}
|
|
970
|
+
onClick={handleTogglePause}
|
|
971
|
+
title={isPaused ? 'Resume auto-refresh (P)' : 'Pause auto-refresh (P)'}
|
|
972
|
+
>
|
|
973
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
974
|
+
{isPaused ? (
|
|
975
|
+
<polygon points="5 3 19 12 5 21 5 3" />
|
|
976
|
+
) : (
|
|
977
|
+
<>
|
|
978
|
+
<rect x="6" y="4" width="4" height="16" />
|
|
979
|
+
<rect x="14" y="4" width="4" height="16" />
|
|
980
|
+
</>
|
|
981
|
+
)}
|
|
982
|
+
</svg>
|
|
983
|
+
</button>
|
|
984
|
+
{/* Refresh */}
|
|
985
|
+
<button
|
|
986
|
+
className="canvas-action-btn"
|
|
987
|
+
onClick={handleRefresh}
|
|
988
|
+
title="Refresh snapshot (R)"
|
|
989
|
+
>
|
|
990
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
991
|
+
<polyline points="23 4 23 10 17 10" />
|
|
992
|
+
<polyline points="1 20 1 14 7 14" />
|
|
993
|
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
|
994
|
+
</svg>
|
|
995
|
+
</button>
|
|
996
|
+
</>
|
|
997
|
+
)}
|
|
998
|
+
{/* Go live button (when viewing history) */}
|
|
999
|
+
{historyIndex >= 0 && (
|
|
1000
|
+
<button
|
|
1001
|
+
className="canvas-action-btn go-live"
|
|
1002
|
+
onClick={handleGoLive}
|
|
1003
|
+
title="Return to live view"
|
|
1004
|
+
>
|
|
1005
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1006
|
+
<circle cx="12" cy="12" r="10" />
|
|
1007
|
+
<circle cx="12" cy="12" r="3" fill="currentColor" />
|
|
1008
|
+
</svg>
|
|
1009
|
+
<span>Live</span>
|
|
1010
|
+
</button>
|
|
1011
|
+
)}
|
|
1012
|
+
{/* Open in window */}
|
|
1013
|
+
<button
|
|
1014
|
+
className="canvas-action-btn"
|
|
1015
|
+
onClick={handleOpenWindow}
|
|
1016
|
+
title="Open in window (O)"
|
|
1017
|
+
>
|
|
1018
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1019
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
1020
|
+
<polyline points="15 3 21 3 21 9" />
|
|
1021
|
+
<line x1="10" y1="14" x2="21" y2="3" />
|
|
1022
|
+
</svg>
|
|
1023
|
+
</button>
|
|
1024
|
+
{/* Minimize */}
|
|
1025
|
+
<button
|
|
1026
|
+
className="canvas-action-btn"
|
|
1027
|
+
onClick={handleMinimize}
|
|
1028
|
+
title={isMinimized ? 'Expand (M)' : 'Minimize (M)'}
|
|
1029
|
+
>
|
|
1030
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1031
|
+
{isMinimized ? (
|
|
1032
|
+
<polyline points="15 3 21 3 21 9" />
|
|
1033
|
+
) : (
|
|
1034
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
1035
|
+
)}
|
|
1036
|
+
</svg>
|
|
1037
|
+
</button>
|
|
1038
|
+
{/* Close */}
|
|
1039
|
+
<button
|
|
1040
|
+
className="canvas-close-btn"
|
|
1041
|
+
onClick={handleClose}
|
|
1042
|
+
title="Close canvas"
|
|
1043
|
+
>
|
|
1044
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1045
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
1046
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
1047
|
+
</svg>
|
|
1048
|
+
</button>
|
|
1049
|
+
</div>
|
|
1050
|
+
</div>
|
|
1051
|
+
{!isMinimized && (
|
|
1052
|
+
<>
|
|
1053
|
+
<div className="canvas-preview-content">
|
|
1054
|
+
{/* Loading/error only show in snapshot mode */}
|
|
1055
|
+
{!isInteractiveMode && isLoading && !currentDisplayImage && (
|
|
1056
|
+
<div className="canvas-preview-loading">
|
|
1057
|
+
<svg className="spinner" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1058
|
+
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
|
1059
|
+
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
|
|
1060
|
+
</svg>
|
|
1061
|
+
<span>{agentContext.getUiCopy('canvasLoading')}</span>
|
|
1062
|
+
</div>
|
|
1063
|
+
)}
|
|
1064
|
+
{!isInteractiveMode && error && !currentDisplayImage && (
|
|
1065
|
+
<div className="canvas-preview-error">
|
|
1066
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1067
|
+
<circle cx="12" cy="12" r="10" />
|
|
1068
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
1069
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
1070
|
+
</svg>
|
|
1071
|
+
<span className="canvas-error-title">{error}</span>
|
|
1072
|
+
{errorDetails && (
|
|
1073
|
+
<span className="canvas-error-details">{errorDetails}</span>
|
|
1074
|
+
)}
|
|
1075
|
+
<button className="canvas-retry-btn" onClick={handleRefresh}>
|
|
1076
|
+
Try Again
|
|
1077
|
+
</button>
|
|
1078
|
+
</div>
|
|
1079
|
+
)}
|
|
1080
|
+
{/* Interactive mode: show webview for full interactivity */}
|
|
1081
|
+
{isInteractiveMode && (
|
|
1082
|
+
<div className="canvas-interactive-wrapper" style={{ height: previewHeight - 48 }}>
|
|
1083
|
+
<webview
|
|
1084
|
+
src={`canvas://${session.id}/index.html`}
|
|
1085
|
+
className="canvas-interactive-iframe"
|
|
1086
|
+
style={{ width: '100%', height: '100%' }}
|
|
1087
|
+
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
|
1088
|
+
// @ts-expect-error - webview attributes not typed in React
|
|
1089
|
+
allowpopups="true"
|
|
1090
|
+
webpreferences="contextIsolation=yes, nodeIntegration=no"
|
|
1091
|
+
/>
|
|
1092
|
+
<div className="canvas-interactive-indicator">
|
|
1093
|
+
<span>Interactive Mode</span>
|
|
1094
|
+
<span className="canvas-interactive-hint">Press I to switch to snapshot mode • Drag bottom edge to resize</span>
|
|
1095
|
+
</div>
|
|
1096
|
+
</div>
|
|
1097
|
+
)}
|
|
1098
|
+
{/* Snapshot mode: show image */}
|
|
1099
|
+
{!isInteractiveMode && currentDisplayImage && (
|
|
1100
|
+
<CanvasImage
|
|
1101
|
+
src={currentDisplayImage}
|
|
1102
|
+
dimensions={currentDisplayDimensions}
|
|
1103
|
+
isPaused={isPaused}
|
|
1104
|
+
isLoading={isLoading}
|
|
1105
|
+
historyIndex={historyIndex}
|
|
1106
|
+
historyTimestamp={historyIndex >= 0 ? snapshotHistory[historyIndex]?.timestamp : undefined}
|
|
1107
|
+
onOpenWindow={handleOpenWindow}
|
|
1108
|
+
/>
|
|
1109
|
+
)}
|
|
1110
|
+
</div>
|
|
1111
|
+
|
|
1112
|
+
{/* History timeline */}
|
|
1113
|
+
{showHistory && snapshotHistory.length > 0 && (
|
|
1114
|
+
<div className="canvas-history-panel">
|
|
1115
|
+
<div className="canvas-history-header">
|
|
1116
|
+
<span>Snapshot History ({snapshotHistory.length})</span>
|
|
1117
|
+
<button
|
|
1118
|
+
className={`canvas-history-live-btn ${historyIndex < 0 ? 'active' : ''}`}
|
|
1119
|
+
onClick={handleGoLive}
|
|
1120
|
+
>
|
|
1121
|
+
Live
|
|
1122
|
+
</button>
|
|
1123
|
+
</div>
|
|
1124
|
+
<div className="canvas-history-slider">
|
|
1125
|
+
<input
|
|
1126
|
+
type="range"
|
|
1127
|
+
min={-1}
|
|
1128
|
+
max={snapshotHistory.length - 1}
|
|
1129
|
+
value={historyIndex}
|
|
1130
|
+
onChange={(e) => handleHistoryChange(parseInt(e.target.value))}
|
|
1131
|
+
className="history-slider"
|
|
1132
|
+
/>
|
|
1133
|
+
</div>
|
|
1134
|
+
<div className="canvas-history-thumbnails">
|
|
1135
|
+
{snapshotHistory.slice(-10).map((entry, idx) => {
|
|
1136
|
+
const actualIndex = snapshotHistory.length - 10 + idx;
|
|
1137
|
+
if (actualIndex < 0) return null;
|
|
1138
|
+
return (
|
|
1139
|
+
<button
|
|
1140
|
+
key={entry.timestamp}
|
|
1141
|
+
className={`history-thumbnail ${historyIndex === actualIndex ? 'active' : ''}`}
|
|
1142
|
+
onClick={() => handleHistoryChange(actualIndex)}
|
|
1143
|
+
title={formatHistoryTime(entry.timestamp)}
|
|
1144
|
+
>
|
|
1145
|
+
<img src={entry.imageData} alt={`Snapshot ${actualIndex + 1}`} />
|
|
1146
|
+
</button>
|
|
1147
|
+
);
|
|
1148
|
+
})}
|
|
1149
|
+
</div>
|
|
1150
|
+
</div>
|
|
1151
|
+
)}
|
|
1152
|
+
|
|
1153
|
+
{/* Console log viewer */}
|
|
1154
|
+
{showConsole && (
|
|
1155
|
+
<div className="canvas-console-panel">
|
|
1156
|
+
<div className="canvas-console-header">
|
|
1157
|
+
<span>Console</span>
|
|
1158
|
+
<button className="canvas-console-clear" onClick={handleClearConsole}>
|
|
1159
|
+
Clear
|
|
1160
|
+
</button>
|
|
1161
|
+
</div>
|
|
1162
|
+
<div className="canvas-console-logs">
|
|
1163
|
+
{consoleLogs.length === 0 ? (
|
|
1164
|
+
<div className="canvas-console-empty">No console output</div>
|
|
1165
|
+
) : (
|
|
1166
|
+
consoleLogs.map((log, idx) => (
|
|
1167
|
+
<div key={idx} className={`console-log console-${log.type}`}>
|
|
1168
|
+
<span className="console-time">{formatHistoryTime(log.timestamp)}</span>
|
|
1169
|
+
<span className="console-message">{log.message}</span>
|
|
1170
|
+
</div>
|
|
1171
|
+
))
|
|
1172
|
+
)}
|
|
1173
|
+
</div>
|
|
1174
|
+
</div>
|
|
1175
|
+
)}
|
|
1176
|
+
|
|
1177
|
+
{/* Resize handle */}
|
|
1178
|
+
<div
|
|
1179
|
+
className="canvas-resize-handle"
|
|
1180
|
+
onMouseDown={handleResizeStart}
|
|
1181
|
+
title="Drag to resize"
|
|
1182
|
+
>
|
|
1183
|
+
<div className="resize-handle-bar"></div>
|
|
1184
|
+
</div>
|
|
1185
|
+
</>
|
|
1186
|
+
)}
|
|
1187
|
+
</div>
|
|
1188
|
+
);
|
|
1189
|
+
}
|