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.
Files changed (526) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1638 -0
  3. package/bin/cowork.js +42 -0
  4. package/build/entitlements.mac.plist +16 -0
  5. package/build/icon.icns +0 -0
  6. package/build/icon.png +0 -0
  7. package/dist/electron/electron/activity/ActivityRepository.js +190 -0
  8. package/dist/electron/electron/agent/browser/browser-service.js +639 -0
  9. package/dist/electron/electron/agent/context-manager.js +225 -0
  10. package/dist/electron/electron/agent/custom-skill-loader.js +566 -0
  11. package/dist/electron/electron/agent/daemon.js +975 -0
  12. package/dist/electron/electron/agent/executor.js +3561 -0
  13. package/dist/electron/electron/agent/llm/anthropic-provider.js +155 -0
  14. package/dist/electron/electron/agent/llm/bedrock-provider.js +202 -0
  15. package/dist/electron/electron/agent/llm/gemini-provider.js +375 -0
  16. package/dist/electron/electron/agent/llm/index.js +34 -0
  17. package/dist/electron/electron/agent/llm/ollama-provider.js +263 -0
  18. package/dist/electron/electron/agent/llm/openai-oauth.js +101 -0
  19. package/dist/electron/electron/agent/llm/openai-provider.js +657 -0
  20. package/dist/electron/electron/agent/llm/openrouter-provider.js +232 -0
  21. package/dist/electron/electron/agent/llm/pricing.js +160 -0
  22. package/dist/electron/electron/agent/llm/provider-factory.js +880 -0
  23. package/dist/electron/electron/agent/llm/types.js +178 -0
  24. package/dist/electron/electron/agent/queue-manager.js +378 -0
  25. package/dist/electron/electron/agent/sandbox/docker-sandbox.js +402 -0
  26. package/dist/electron/electron/agent/sandbox/macos-sandbox.js +407 -0
  27. package/dist/electron/electron/agent/sandbox/runner.js +410 -0
  28. package/dist/electron/electron/agent/sandbox/sandbox-factory.js +228 -0
  29. package/dist/electron/electron/agent/sandbox/security-utils.js +258 -0
  30. package/dist/electron/electron/agent/search/brave-provider.js +119 -0
  31. package/dist/electron/electron/agent/search/google-provider.js +100 -0
  32. package/dist/electron/electron/agent/search/index.js +28 -0
  33. package/dist/electron/electron/agent/search/provider-factory.js +395 -0
  34. package/dist/electron/electron/agent/search/serpapi-provider.js +112 -0
  35. package/dist/electron/electron/agent/search/tavily-provider.js +90 -0
  36. package/dist/electron/electron/agent/search/types.js +40 -0
  37. package/dist/electron/electron/agent/security/index.js +12 -0
  38. package/dist/electron/electron/agent/security/input-sanitizer.js +303 -0
  39. package/dist/electron/electron/agent/security/output-filter.js +217 -0
  40. package/dist/electron/electron/agent/skill-eligibility.js +281 -0
  41. package/dist/electron/electron/agent/skill-registry.js +396 -0
  42. package/dist/electron/electron/agent/skills/document.js +878 -0
  43. package/dist/electron/electron/agent/skills/image-generator.js +225 -0
  44. package/dist/electron/electron/agent/skills/organizer.js +141 -0
  45. package/dist/electron/electron/agent/skills/presentation.js +367 -0
  46. package/dist/electron/electron/agent/skills/spreadsheet.js +165 -0
  47. package/dist/electron/electron/agent/tools/browser-tools.js +523 -0
  48. package/dist/electron/electron/agent/tools/builtin-settings.js +384 -0
  49. package/dist/electron/electron/agent/tools/canvas-tools.js +530 -0
  50. package/dist/electron/electron/agent/tools/cron-tools.js +577 -0
  51. package/dist/electron/electron/agent/tools/edit-tools.js +194 -0
  52. package/dist/electron/electron/agent/tools/file-tools.js +719 -0
  53. package/dist/electron/electron/agent/tools/glob-tools.js +283 -0
  54. package/dist/electron/electron/agent/tools/grep-tools.js +387 -0
  55. package/dist/electron/electron/agent/tools/image-tools.js +111 -0
  56. package/dist/electron/electron/agent/tools/mention-tools.js +282 -0
  57. package/dist/electron/electron/agent/tools/node-tools.js +476 -0
  58. package/dist/electron/electron/agent/tools/registry.js +2719 -0
  59. package/dist/electron/electron/agent/tools/search-tools.js +91 -0
  60. package/dist/electron/electron/agent/tools/shell-tools.js +574 -0
  61. package/dist/electron/electron/agent/tools/skill-tools.js +274 -0
  62. package/dist/electron/electron/agent/tools/system-tools.js +578 -0
  63. package/dist/electron/electron/agent/tools/web-fetch-tools.js +444 -0
  64. package/dist/electron/electron/agent/tools/x-tools.js +264 -0
  65. package/dist/electron/electron/agents/AgentRoleRepository.js +420 -0
  66. package/dist/electron/electron/agents/HeartbeatService.js +356 -0
  67. package/dist/electron/electron/agents/MentionRepository.js +197 -0
  68. package/dist/electron/electron/agents/TaskSubscriptionRepository.js +168 -0
  69. package/dist/electron/electron/agents/WorkingStateRepository.js +229 -0
  70. package/dist/electron/electron/canvas/canvas-manager.js +714 -0
  71. package/dist/electron/electron/canvas/canvas-preload.js +53 -0
  72. package/dist/electron/electron/canvas/canvas-protocol.js +195 -0
  73. package/dist/electron/electron/canvas/canvas-store.js +174 -0
  74. package/dist/electron/electron/canvas/index.js +13 -0
  75. package/dist/electron/electron/control-plane/client.js +364 -0
  76. package/dist/electron/electron/control-plane/handlers.js +572 -0
  77. package/dist/electron/electron/control-plane/index.js +41 -0
  78. package/dist/electron/electron/control-plane/node-manager.js +264 -0
  79. package/dist/electron/electron/control-plane/protocol.js +194 -0
  80. package/dist/electron/electron/control-plane/remote-client.js +437 -0
  81. package/dist/electron/electron/control-plane/server.js +640 -0
  82. package/dist/electron/electron/control-plane/settings.js +369 -0
  83. package/dist/electron/electron/control-plane/ssh-tunnel.js +549 -0
  84. package/dist/electron/electron/cron/index.js +30 -0
  85. package/dist/electron/electron/cron/schedule.js +190 -0
  86. package/dist/electron/electron/cron/service.js +614 -0
  87. package/dist/electron/electron/cron/store.js +155 -0
  88. package/dist/electron/electron/cron/types.js +82 -0
  89. package/dist/electron/electron/cron/webhook.js +258 -0
  90. package/dist/electron/electron/database/SecureSettingsRepository.js +444 -0
  91. package/dist/electron/electron/database/TaskLabelRepository.js +120 -0
  92. package/dist/electron/electron/database/repositories.js +1781 -0
  93. package/dist/electron/electron/database/schema.js +978 -0
  94. package/dist/electron/electron/extensions/index.js +33 -0
  95. package/dist/electron/electron/extensions/loader.js +313 -0
  96. package/dist/electron/electron/extensions/registry.js +485 -0
  97. package/dist/electron/electron/extensions/types.js +11 -0
  98. package/dist/electron/electron/gateway/channel-registry.js +1102 -0
  99. package/dist/electron/electron/gateway/channels/bluebubbles-client.js +479 -0
  100. package/dist/electron/electron/gateway/channels/bluebubbles.js +432 -0
  101. package/dist/electron/electron/gateway/channels/discord.js +975 -0
  102. package/dist/electron/electron/gateway/channels/email-client.js +593 -0
  103. package/dist/electron/electron/gateway/channels/email.js +443 -0
  104. package/dist/electron/electron/gateway/channels/google-chat.js +631 -0
  105. package/dist/electron/electron/gateway/channels/imessage-client.js +363 -0
  106. package/dist/electron/electron/gateway/channels/imessage.js +465 -0
  107. package/dist/electron/electron/gateway/channels/index.js +36 -0
  108. package/dist/electron/electron/gateway/channels/line-client.js +470 -0
  109. package/dist/electron/electron/gateway/channels/line.js +479 -0
  110. package/dist/electron/electron/gateway/channels/matrix-client.js +432 -0
  111. package/dist/electron/electron/gateway/channels/matrix.js +592 -0
  112. package/dist/electron/electron/gateway/channels/mattermost-client.js +394 -0
  113. package/dist/electron/electron/gateway/channels/mattermost.js +496 -0
  114. package/dist/electron/electron/gateway/channels/signal-client.js +500 -0
  115. package/dist/electron/electron/gateway/channels/signal.js +582 -0
  116. package/dist/electron/electron/gateway/channels/slack.js +415 -0
  117. package/dist/electron/electron/gateway/channels/teams.js +596 -0
  118. package/dist/electron/electron/gateway/channels/telegram.js +1390 -0
  119. package/dist/electron/electron/gateway/channels/twitch-client.js +502 -0
  120. package/dist/electron/electron/gateway/channels/twitch.js +396 -0
  121. package/dist/electron/electron/gateway/channels/types.js +8 -0
  122. package/dist/electron/electron/gateway/channels/whatsapp.js +953 -0
  123. package/dist/electron/electron/gateway/context-policy.js +268 -0
  124. package/dist/electron/electron/gateway/index.js +1063 -0
  125. package/dist/electron/electron/gateway/infrastructure.js +496 -0
  126. package/dist/electron/electron/gateway/router.js +2700 -0
  127. package/dist/electron/electron/gateway/security.js +375 -0
  128. package/dist/electron/electron/gateway/session.js +115 -0
  129. package/dist/electron/electron/gateway/tunnel.js +503 -0
  130. package/dist/electron/electron/guardrails/guardrail-manager.js +348 -0
  131. package/dist/electron/electron/hooks/gmail-watcher.js +300 -0
  132. package/dist/electron/electron/hooks/index.js +46 -0
  133. package/dist/electron/electron/hooks/mappings.js +381 -0
  134. package/dist/electron/electron/hooks/server.js +480 -0
  135. package/dist/electron/electron/hooks/settings.js +447 -0
  136. package/dist/electron/electron/hooks/types.js +41 -0
  137. package/dist/electron/electron/ipc/canvas-handlers.js +158 -0
  138. package/dist/electron/electron/ipc/handlers.js +3138 -0
  139. package/dist/electron/electron/ipc/mission-control-handlers.js +141 -0
  140. package/dist/electron/electron/main.js +448 -0
  141. package/dist/electron/electron/mcp/client/MCPClientManager.js +330 -0
  142. package/dist/electron/electron/mcp/client/MCPServerConnection.js +437 -0
  143. package/dist/electron/electron/mcp/client/transports/SSETransport.js +304 -0
  144. package/dist/electron/electron/mcp/client/transports/StdioTransport.js +307 -0
  145. package/dist/electron/electron/mcp/client/transports/WebSocketTransport.js +329 -0
  146. package/dist/electron/electron/mcp/host/MCPHostServer.js +354 -0
  147. package/dist/electron/electron/mcp/host/ToolAdapter.js +100 -0
  148. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +497 -0
  149. package/dist/electron/electron/mcp/settings.js +446 -0
  150. package/dist/electron/electron/mcp/types.js +59 -0
  151. package/dist/electron/electron/memory/MemoryService.js +435 -0
  152. package/dist/electron/electron/notifications/index.js +17 -0
  153. package/dist/electron/electron/notifications/service.js +118 -0
  154. package/dist/electron/electron/notifications/store.js +144 -0
  155. package/dist/electron/electron/preload.js +842 -0
  156. package/dist/electron/electron/reports/StandupReportService.js +272 -0
  157. package/dist/electron/electron/security/concurrency.js +293 -0
  158. package/dist/electron/electron/security/index.js +15 -0
  159. package/dist/electron/electron/security/policy-manager.js +435 -0
  160. package/dist/electron/electron/settings/appearance-manager.js +193 -0
  161. package/dist/electron/electron/settings/personality-manager.js +724 -0
  162. package/dist/electron/electron/settings/x-manager.js +58 -0
  163. package/dist/electron/electron/tailscale/exposure.js +188 -0
  164. package/dist/electron/electron/tailscale/index.js +28 -0
  165. package/dist/electron/electron/tailscale/settings.js +205 -0
  166. package/dist/electron/electron/tailscale/tailscale.js +355 -0
  167. package/dist/electron/electron/tray/QuickInputWindow.js +568 -0
  168. package/dist/electron/electron/tray/TrayManager.js +895 -0
  169. package/dist/electron/electron/tray/index.js +9 -0
  170. package/dist/electron/electron/updater/index.js +6 -0
  171. package/dist/electron/electron/updater/update-manager.js +418 -0
  172. package/dist/electron/electron/utils/env-migration.js +209 -0
  173. package/dist/electron/electron/utils/process.js +102 -0
  174. package/dist/electron/electron/utils/rate-limiter.js +104 -0
  175. package/dist/electron/electron/utils/validation.js +419 -0
  176. package/dist/electron/electron/utils/x-cli.js +177 -0
  177. package/dist/electron/electron/voice/VoiceService.js +507 -0
  178. package/dist/electron/electron/voice/index.js +14 -0
  179. package/dist/electron/electron/voice/voice-settings-manager.js +359 -0
  180. package/dist/electron/shared/channelMessages.js +170 -0
  181. package/dist/electron/shared/types.js +1185 -0
  182. package/package.json +159 -0
  183. package/resources/skills/1password.json +10 -0
  184. package/resources/skills/add-documentation.json +31 -0
  185. package/resources/skills/analyze-csv.json +17 -0
  186. package/resources/skills/apple-notes.json +10 -0
  187. package/resources/skills/apple-reminders.json +10 -0
  188. package/resources/skills/auto-commenter.json +10 -0
  189. package/resources/skills/bear-notes.json +10 -0
  190. package/resources/skills/bird.json +35 -0
  191. package/resources/skills/blogwatcher.json +10 -0
  192. package/resources/skills/blucli.json +10 -0
  193. package/resources/skills/bluebubbles.json +10 -0
  194. package/resources/skills/camsnap.json +10 -0
  195. package/resources/skills/clean-imports.json +18 -0
  196. package/resources/skills/code-review.json +18 -0
  197. package/resources/skills/coding-agent.json +10 -0
  198. package/resources/skills/compare-files.json +23 -0
  199. package/resources/skills/convert-code.json +34 -0
  200. package/resources/skills/create-changelog.json +24 -0
  201. package/resources/skills/debug-error.json +17 -0
  202. package/resources/skills/dependency-check.json +10 -0
  203. package/resources/skills/discord.json +10 -0
  204. package/resources/skills/eightctl.json +10 -0
  205. package/resources/skills/explain-code.json +29 -0
  206. package/resources/skills/extract-todos.json +18 -0
  207. package/resources/skills/food-order.json +10 -0
  208. package/resources/skills/gemini.json +10 -0
  209. package/resources/skills/generate-readme.json +10 -0
  210. package/resources/skills/gifgrep.json +10 -0
  211. package/resources/skills/git-commit.json +10 -0
  212. package/resources/skills/github.json +10 -0
  213. package/resources/skills/gog.json +10 -0
  214. package/resources/skills/goplaces.json +10 -0
  215. package/resources/skills/himalaya.json +10 -0
  216. package/resources/skills/imsg.json +10 -0
  217. package/resources/skills/karpathy-guidelines.json +12 -0
  218. package/resources/skills/last30days.json +26 -0
  219. package/resources/skills/local-places.json +10 -0
  220. package/resources/skills/mcporter.json +10 -0
  221. package/resources/skills/model-usage.json +10 -0
  222. package/resources/skills/nano-banana-pro.json +10 -0
  223. package/resources/skills/nano-pdf.json +10 -0
  224. package/resources/skills/notion.json +10 -0
  225. package/resources/skills/obsidian.json +10 -0
  226. package/resources/skills/openai-image-gen.json +10 -0
  227. package/resources/skills/openai-whisper-api.json +10 -0
  228. package/resources/skills/openai-whisper.json +10 -0
  229. package/resources/skills/openhue.json +10 -0
  230. package/resources/skills/oracle.json +10 -0
  231. package/resources/skills/ordercli.json +10 -0
  232. package/resources/skills/peekaboo.json +10 -0
  233. package/resources/skills/project-structure.json +10 -0
  234. package/resources/skills/proofread.json +17 -0
  235. package/resources/skills/refactor-code.json +31 -0
  236. package/resources/skills/rename-symbol.json +23 -0
  237. package/resources/skills/sag.json +10 -0
  238. package/resources/skills/security-audit.json +18 -0
  239. package/resources/skills/session-logs.json +10 -0
  240. package/resources/skills/sherpa-onnx-tts.json +10 -0
  241. package/resources/skills/skill-creator.json +15 -0
  242. package/resources/skills/skill-hub.json +29 -0
  243. package/resources/skills/slack.json +10 -0
  244. package/resources/skills/songsee.json +10 -0
  245. package/resources/skills/sonoscli.json +10 -0
  246. package/resources/skills/spotify-player.json +10 -0
  247. package/resources/skills/startup-cfo.json +55 -0
  248. package/resources/skills/summarize-folder.json +18 -0
  249. package/resources/skills/summarize.json +10 -0
  250. package/resources/skills/things-mac.json +10 -0
  251. package/resources/skills/tmux.json +10 -0
  252. package/resources/skills/translate.json +36 -0
  253. package/resources/skills/trello.json +10 -0
  254. package/resources/skills/video-frames.json +10 -0
  255. package/resources/skills/voice-call.json +10 -0
  256. package/resources/skills/wacli.json +10 -0
  257. package/resources/skills/weather.json +10 -0
  258. package/resources/skills/write-tests.json +31 -0
  259. package/src/electron/activity/ActivityRepository.ts +238 -0
  260. package/src/electron/agent/browser/browser-service.ts +721 -0
  261. package/src/electron/agent/context-manager.ts +257 -0
  262. package/src/electron/agent/custom-skill-loader.ts +634 -0
  263. package/src/electron/agent/daemon.ts +1097 -0
  264. package/src/electron/agent/executor.ts +4017 -0
  265. package/src/electron/agent/llm/anthropic-provider.ts +175 -0
  266. package/src/electron/agent/llm/bedrock-provider.ts +236 -0
  267. package/src/electron/agent/llm/gemini-provider.ts +422 -0
  268. package/src/electron/agent/llm/index.ts +9 -0
  269. package/src/electron/agent/llm/ollama-provider.ts +347 -0
  270. package/src/electron/agent/llm/openai-oauth.ts +127 -0
  271. package/src/electron/agent/llm/openai-provider.ts +686 -0
  272. package/src/electron/agent/llm/openrouter-provider.ts +273 -0
  273. package/src/electron/agent/llm/pricing.ts +180 -0
  274. package/src/electron/agent/llm/provider-factory.ts +971 -0
  275. package/src/electron/agent/llm/types.ts +291 -0
  276. package/src/electron/agent/queue-manager.ts +408 -0
  277. package/src/electron/agent/sandbox/docker-sandbox.ts +453 -0
  278. package/src/electron/agent/sandbox/macos-sandbox.ts +426 -0
  279. package/src/electron/agent/sandbox/runner.ts +453 -0
  280. package/src/electron/agent/sandbox/sandbox-factory.ts +337 -0
  281. package/src/electron/agent/sandbox/security-utils.ts +251 -0
  282. package/src/electron/agent/search/brave-provider.ts +141 -0
  283. package/src/electron/agent/search/google-provider.ts +131 -0
  284. package/src/electron/agent/search/index.ts +6 -0
  285. package/src/electron/agent/search/provider-factory.ts +450 -0
  286. package/src/electron/agent/search/serpapi-provider.ts +138 -0
  287. package/src/electron/agent/search/tavily-provider.ts +108 -0
  288. package/src/electron/agent/search/types.ts +118 -0
  289. package/src/electron/agent/security/index.ts +20 -0
  290. package/src/electron/agent/security/input-sanitizer.ts +380 -0
  291. package/src/electron/agent/security/output-filter.ts +259 -0
  292. package/src/electron/agent/skill-eligibility.ts +334 -0
  293. package/src/electron/agent/skill-registry.ts +457 -0
  294. package/src/electron/agent/skills/document.ts +1070 -0
  295. package/src/electron/agent/skills/image-generator.ts +272 -0
  296. package/src/electron/agent/skills/organizer.ts +131 -0
  297. package/src/electron/agent/skills/presentation.ts +418 -0
  298. package/src/electron/agent/skills/spreadsheet.ts +166 -0
  299. package/src/electron/agent/tools/browser-tools.ts +546 -0
  300. package/src/electron/agent/tools/builtin-settings.ts +422 -0
  301. package/src/electron/agent/tools/canvas-tools.ts +572 -0
  302. package/src/electron/agent/tools/cron-tools.ts +723 -0
  303. package/src/electron/agent/tools/edit-tools.ts +196 -0
  304. package/src/electron/agent/tools/file-tools.ts +811 -0
  305. package/src/electron/agent/tools/glob-tools.ts +303 -0
  306. package/src/electron/agent/tools/grep-tools.ts +432 -0
  307. package/src/electron/agent/tools/image-tools.ts +126 -0
  308. package/src/electron/agent/tools/mention-tools.ts +371 -0
  309. package/src/electron/agent/tools/node-tools.ts +550 -0
  310. package/src/electron/agent/tools/registry.ts +3052 -0
  311. package/src/electron/agent/tools/search-tools.ts +111 -0
  312. package/src/electron/agent/tools/shell-tools.ts +651 -0
  313. package/src/electron/agent/tools/skill-tools.ts +340 -0
  314. package/src/electron/agent/tools/system-tools.ts +665 -0
  315. package/src/electron/agent/tools/web-fetch-tools.ts +528 -0
  316. package/src/electron/agent/tools/x-tools.ts +267 -0
  317. package/src/electron/agents/AgentRoleRepository.ts +557 -0
  318. package/src/electron/agents/HeartbeatService.ts +469 -0
  319. package/src/electron/agents/MentionRepository.ts +242 -0
  320. package/src/electron/agents/TaskSubscriptionRepository.ts +231 -0
  321. package/src/electron/agents/WorkingStateRepository.ts +278 -0
  322. package/src/electron/canvas/canvas-manager.ts +818 -0
  323. package/src/electron/canvas/canvas-preload.ts +102 -0
  324. package/src/electron/canvas/canvas-protocol.ts +174 -0
  325. package/src/electron/canvas/canvas-store.ts +200 -0
  326. package/src/electron/canvas/index.ts +8 -0
  327. package/src/electron/control-plane/client.ts +527 -0
  328. package/src/electron/control-plane/handlers.ts +723 -0
  329. package/src/electron/control-plane/index.ts +51 -0
  330. package/src/electron/control-plane/node-manager.ts +322 -0
  331. package/src/electron/control-plane/protocol.ts +269 -0
  332. package/src/electron/control-plane/remote-client.ts +517 -0
  333. package/src/electron/control-plane/server.ts +853 -0
  334. package/src/electron/control-plane/settings.ts +401 -0
  335. package/src/electron/control-plane/ssh-tunnel.ts +624 -0
  336. package/src/electron/cron/index.ts +9 -0
  337. package/src/electron/cron/schedule.ts +217 -0
  338. package/src/electron/cron/service.ts +743 -0
  339. package/src/electron/cron/store.ts +165 -0
  340. package/src/electron/cron/types.ts +291 -0
  341. package/src/electron/cron/webhook.ts +303 -0
  342. package/src/electron/database/SecureSettingsRepository.ts +514 -0
  343. package/src/electron/database/TaskLabelRepository.ts +148 -0
  344. package/src/electron/database/repositories.ts +2397 -0
  345. package/src/electron/database/schema.ts +1017 -0
  346. package/src/electron/extensions/index.ts +18 -0
  347. package/src/electron/extensions/loader.ts +336 -0
  348. package/src/electron/extensions/registry.ts +546 -0
  349. package/src/electron/extensions/types.ts +372 -0
  350. package/src/electron/gateway/channel-registry.ts +1267 -0
  351. package/src/electron/gateway/channels/bluebubbles-client.ts +641 -0
  352. package/src/electron/gateway/channels/bluebubbles.ts +509 -0
  353. package/src/electron/gateway/channels/discord.ts +1150 -0
  354. package/src/electron/gateway/channels/email-client.ts +708 -0
  355. package/src/electron/gateway/channels/email.ts +516 -0
  356. package/src/electron/gateway/channels/google-chat.ts +760 -0
  357. package/src/electron/gateway/channels/imessage-client.ts +473 -0
  358. package/src/electron/gateway/channels/imessage.ts +520 -0
  359. package/src/electron/gateway/channels/index.ts +21 -0
  360. package/src/electron/gateway/channels/line-client.ts +598 -0
  361. package/src/electron/gateway/channels/line.ts +559 -0
  362. package/src/electron/gateway/channels/matrix-client.ts +632 -0
  363. package/src/electron/gateway/channels/matrix.ts +655 -0
  364. package/src/electron/gateway/channels/mattermost-client.ts +526 -0
  365. package/src/electron/gateway/channels/mattermost.ts +550 -0
  366. package/src/electron/gateway/channels/signal-client.ts +722 -0
  367. package/src/electron/gateway/channels/signal.ts +666 -0
  368. package/src/electron/gateway/channels/slack.ts +458 -0
  369. package/src/electron/gateway/channels/teams.ts +681 -0
  370. package/src/electron/gateway/channels/telegram.ts +1727 -0
  371. package/src/electron/gateway/channels/twitch-client.ts +665 -0
  372. package/src/electron/gateway/channels/twitch.ts +468 -0
  373. package/src/electron/gateway/channels/types.ts +1002 -0
  374. package/src/electron/gateway/channels/whatsapp.ts +1101 -0
  375. package/src/electron/gateway/context-policy.ts +382 -0
  376. package/src/electron/gateway/index.ts +1274 -0
  377. package/src/electron/gateway/infrastructure.ts +645 -0
  378. package/src/electron/gateway/router.ts +3206 -0
  379. package/src/electron/gateway/security.ts +422 -0
  380. package/src/electron/gateway/session.ts +144 -0
  381. package/src/electron/gateway/tunnel.ts +626 -0
  382. package/src/electron/guardrails/guardrail-manager.ts +380 -0
  383. package/src/electron/hooks/gmail-watcher.ts +355 -0
  384. package/src/electron/hooks/index.ts +30 -0
  385. package/src/electron/hooks/mappings.ts +404 -0
  386. package/src/electron/hooks/server.ts +574 -0
  387. package/src/electron/hooks/settings.ts +466 -0
  388. package/src/electron/hooks/types.ts +245 -0
  389. package/src/electron/ipc/canvas-handlers.ts +223 -0
  390. package/src/electron/ipc/handlers.ts +3661 -0
  391. package/src/electron/ipc/mission-control-handlers.ts +182 -0
  392. package/src/electron/main.ts +496 -0
  393. package/src/electron/mcp/client/MCPClientManager.ts +406 -0
  394. package/src/electron/mcp/client/MCPServerConnection.ts +514 -0
  395. package/src/electron/mcp/client/transports/SSETransport.ts +360 -0
  396. package/src/electron/mcp/client/transports/StdioTransport.ts +355 -0
  397. package/src/electron/mcp/client/transports/WebSocketTransport.ts +384 -0
  398. package/src/electron/mcp/host/MCPHostServer.ts +388 -0
  399. package/src/electron/mcp/host/ToolAdapter.ts +140 -0
  400. package/src/electron/mcp/registry/MCPRegistryManager.ts +565 -0
  401. package/src/electron/mcp/settings.ts +468 -0
  402. package/src/electron/mcp/types.ts +371 -0
  403. package/src/electron/memory/MemoryService.ts +523 -0
  404. package/src/electron/notifications/index.ts +16 -0
  405. package/src/electron/notifications/service.ts +161 -0
  406. package/src/electron/notifications/store.ts +163 -0
  407. package/src/electron/preload.ts +2845 -0
  408. package/src/electron/reports/StandupReportService.ts +356 -0
  409. package/src/electron/security/concurrency.ts +333 -0
  410. package/src/electron/security/index.ts +17 -0
  411. package/src/electron/security/policy-manager.ts +539 -0
  412. package/src/electron/settings/appearance-manager.ts +182 -0
  413. package/src/electron/settings/personality-manager.ts +800 -0
  414. package/src/electron/settings/x-manager.ts +62 -0
  415. package/src/electron/tailscale/exposure.ts +262 -0
  416. package/src/electron/tailscale/index.ts +34 -0
  417. package/src/electron/tailscale/settings.ts +218 -0
  418. package/src/electron/tailscale/tailscale.ts +379 -0
  419. package/src/electron/tray/QuickInputWindow.ts +609 -0
  420. package/src/electron/tray/TrayManager.ts +1005 -0
  421. package/src/electron/tray/index.ts +6 -0
  422. package/src/electron/updater/index.ts +1 -0
  423. package/src/electron/updater/update-manager.ts +447 -0
  424. package/src/electron/utils/env-migration.ts +203 -0
  425. package/src/electron/utils/process.ts +124 -0
  426. package/src/electron/utils/rate-limiter.ts +130 -0
  427. package/src/electron/utils/validation.ts +493 -0
  428. package/src/electron/utils/x-cli.ts +198 -0
  429. package/src/electron/voice/VoiceService.ts +583 -0
  430. package/src/electron/voice/index.ts +9 -0
  431. package/src/electron/voice/voice-settings-manager.ts +403 -0
  432. package/src/renderer/App.tsx +775 -0
  433. package/src/renderer/components/ActivityFeed.tsx +407 -0
  434. package/src/renderer/components/ActivityFeedItem.tsx +285 -0
  435. package/src/renderer/components/AgentRoleCard.tsx +343 -0
  436. package/src/renderer/components/AgentRoleEditor.tsx +805 -0
  437. package/src/renderer/components/AgentSquadSettings.tsx +295 -0
  438. package/src/renderer/components/AgentWorkingStatePanel.tsx +411 -0
  439. package/src/renderer/components/AppearanceSettings.tsx +122 -0
  440. package/src/renderer/components/ApprovalDialog.tsx +100 -0
  441. package/src/renderer/components/BlueBubblesSettings.tsx +505 -0
  442. package/src/renderer/components/BuiltinToolsSettings.tsx +307 -0
  443. package/src/renderer/components/CanvasPreview.tsx +1189 -0
  444. package/src/renderer/components/CommandOutput.tsx +202 -0
  445. package/src/renderer/components/ContextPolicySettings.tsx +523 -0
  446. package/src/renderer/components/ControlPlaneSettings.tsx +1134 -0
  447. package/src/renderer/components/DisclaimerModal.tsx +124 -0
  448. package/src/renderer/components/DiscordSettings.tsx +436 -0
  449. package/src/renderer/components/EmailSettings.tsx +606 -0
  450. package/src/renderer/components/ExtensionsSettings.tsx +542 -0
  451. package/src/renderer/components/FileViewer.tsx +224 -0
  452. package/src/renderer/components/GoogleChatSettings.tsx +535 -0
  453. package/src/renderer/components/GuardrailSettings.tsx +487 -0
  454. package/src/renderer/components/HooksSettings.tsx +581 -0
  455. package/src/renderer/components/ImessageSettings.tsx +484 -0
  456. package/src/renderer/components/LineSettings.tsx +483 -0
  457. package/src/renderer/components/MCPRegistryBrowser.tsx +386 -0
  458. package/src/renderer/components/MCPSettings.tsx +943 -0
  459. package/src/renderer/components/MainContent.tsx +2433 -0
  460. package/src/renderer/components/MatrixSettings.tsx +510 -0
  461. package/src/renderer/components/MattermostSettings.tsx +473 -0
  462. package/src/renderer/components/MemorySettings.tsx +247 -0
  463. package/src/renderer/components/MentionBadge.tsx +87 -0
  464. package/src/renderer/components/MentionInput.tsx +409 -0
  465. package/src/renderer/components/MentionList.tsx +476 -0
  466. package/src/renderer/components/MissionControlPanel.tsx +1995 -0
  467. package/src/renderer/components/NodesSettings.tsx +316 -0
  468. package/src/renderer/components/NotificationPanel.tsx +481 -0
  469. package/src/renderer/components/Onboarding/AwakeningOrb.tsx +44 -0
  470. package/src/renderer/components/Onboarding/Onboarding.tsx +443 -0
  471. package/src/renderer/components/Onboarding/TypewriterText.tsx +102 -0
  472. package/src/renderer/components/Onboarding/index.ts +3 -0
  473. package/src/renderer/components/OnboardingModal.tsx +698 -0
  474. package/src/renderer/components/PairingCodeDisplay.tsx +324 -0
  475. package/src/renderer/components/PersonalitySettings.tsx +597 -0
  476. package/src/renderer/components/QueueSettings.tsx +119 -0
  477. package/src/renderer/components/QuickTaskFAB.tsx +71 -0
  478. package/src/renderer/components/RightPanel.tsx +413 -0
  479. package/src/renderer/components/ScheduledTasksSettings.tsx +1328 -0
  480. package/src/renderer/components/SearchSettings.tsx +328 -0
  481. package/src/renderer/components/Settings.tsx +1504 -0
  482. package/src/renderer/components/Sidebar.tsx +344 -0
  483. package/src/renderer/components/SignalSettings.tsx +673 -0
  484. package/src/renderer/components/SkillHubBrowser.tsx +458 -0
  485. package/src/renderer/components/SkillParameterModal.tsx +185 -0
  486. package/src/renderer/components/SkillsSettings.tsx +451 -0
  487. package/src/renderer/components/SlackSettings.tsx +442 -0
  488. package/src/renderer/components/StandupReportViewer.tsx +614 -0
  489. package/src/renderer/components/TaskBoard.tsx +498 -0
  490. package/src/renderer/components/TaskBoardCard.tsx +357 -0
  491. package/src/renderer/components/TaskBoardColumn.tsx +211 -0
  492. package/src/renderer/components/TaskLabelManager.tsx +472 -0
  493. package/src/renderer/components/TaskQueuePanel.tsx +144 -0
  494. package/src/renderer/components/TaskQuickActions.tsx +492 -0
  495. package/src/renderer/components/TaskTimeline.tsx +216 -0
  496. package/src/renderer/components/TaskView.tsx +162 -0
  497. package/src/renderer/components/TeamsSettings.tsx +518 -0
  498. package/src/renderer/components/TelegramSettings.tsx +421 -0
  499. package/src/renderer/components/Toast.tsx +76 -0
  500. package/src/renderer/components/TraySettings.tsx +189 -0
  501. package/src/renderer/components/TwitchSettings.tsx +511 -0
  502. package/src/renderer/components/UpdateSettings.tsx +295 -0
  503. package/src/renderer/components/VoiceIndicator.tsx +270 -0
  504. package/src/renderer/components/VoiceSettings.tsx +867 -0
  505. package/src/renderer/components/WhatsAppSettings.tsx +721 -0
  506. package/src/renderer/components/WorkingStateEditor.tsx +309 -0
  507. package/src/renderer/components/WorkingStateHistory.tsx +481 -0
  508. package/src/renderer/components/WorkspaceSelector.tsx +150 -0
  509. package/src/renderer/components/XSettings.tsx +311 -0
  510. package/src/renderer/global.d.ts +9 -0
  511. package/src/renderer/hooks/useAgentContext.ts +153 -0
  512. package/src/renderer/hooks/useOnboardingFlow.ts +548 -0
  513. package/src/renderer/hooks/useVoiceInput.ts +268 -0
  514. package/src/renderer/index.html +12 -0
  515. package/src/renderer/main.tsx +10 -0
  516. package/src/renderer/public/cowork-os-logo.png +0 -0
  517. package/src/renderer/quick-input.html +164 -0
  518. package/src/renderer/styles/index.css +14504 -0
  519. package/src/renderer/utils/agentMessages.ts +749 -0
  520. package/src/renderer/utils/voice-directives.ts +169 -0
  521. package/src/shared/channelMessages.ts +213 -0
  522. package/src/shared/types.ts +3608 -0
  523. package/tsconfig.electron.json +26 -0
  524. package/tsconfig.json +26 -0
  525. package/tsconfig.node.json +10 -0
  526. 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
+ }