khal-os 1.260324.2

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 (408) hide show
  1. package/.env.example +23 -0
  2. package/.genie/mailbox/cli-sent.jsonl +3 -0
  3. package/.genie/mailbox/ds1-wave2-engineer-1.json +15 -0
  4. package/.genie/mailbox/ds1-wave2-engineer-2.json +15 -0
  5. package/.genie/mailbox/ds1-wave2-engineer-3.json +15 -0
  6. package/.genie/state/os-observability.json +39 -0
  7. package/.genie/state/tmux-control-mode-terminal.json +28 -0
  8. package/.genie/wishes/genieos-one-theme/WISH.md +417 -0
  9. package/.genie/wishes/workos-prod-rbac/WISH.md +345 -0
  10. package/.github/workflows/ci.yml +39 -0
  11. package/.github/workflows/release.yml +78 -0
  12. package/.github/workflows/version.yml +122 -0
  13. package/.husky/pre-commit +1 -0
  14. package/.pnpm-approve-builds.json +1 -0
  15. package/CLAUDE.md +117 -0
  16. package/LICENSE +21 -0
  17. package/README.md +38 -0
  18. package/biome.json +124 -0
  19. package/bun.lock +1249 -0
  20. package/docs/workos-setup.md +116 -0
  21. package/ecosystem.config.cjs +26 -0
  22. package/instrumentation.ts +8 -0
  23. package/knip.json +35 -0
  24. package/nats.conf +7 -0
  25. package/next.config.ts +25 -0
  26. package/package.json +78 -0
  27. package/packages/dev3000-app/components.ts +12 -0
  28. package/packages/dev3000-app/manifest.ts +19 -0
  29. package/packages/dev3000-app/package.json +23 -0
  30. package/packages/dev3000-app/views/dev3000/Dev3000App.tsx +758 -0
  31. package/packages/dev3000-app/views/dev3000/ErrorsPanel.tsx +160 -0
  32. package/packages/dev3000-app/views/dev3000/dev3000-context.tsx +21 -0
  33. package/packages/dev3000-app/views/dev3000/index.ts +4 -0
  34. package/packages/dev3000-app/views/dev3000/schema.ts +55 -0
  35. package/packages/dev3000-app/views/dev3000/service/index.ts +358 -0
  36. package/packages/dev3000-app/views/dev3000/service/runtime +1 -0
  37. package/packages/dev3000-app/views/dev3000/subjects.ts +9 -0
  38. package/packages/dev3000-app/views/dev3000/types.ts +77 -0
  39. package/packages/files-app/components.ts +12 -0
  40. package/packages/files-app/manifest.ts +19 -0
  41. package/packages/files-app/package.json +23 -0
  42. package/packages/files-app/views/files/ContextMenu.tsx +151 -0
  43. package/packages/files-app/views/files/DeleteConfirmDialog.tsx +39 -0
  44. package/packages/files-app/views/files/FileItem.tsx +128 -0
  45. package/packages/files-app/views/files/FilesApp.tsx +509 -0
  46. package/packages/files-app/views/files/FilesListView.tsx +201 -0
  47. package/packages/files-app/views/files/FilesToolbar.tsx +117 -0
  48. package/packages/files-app/views/files/GridView.tsx +90 -0
  49. package/packages/files-app/views/files/InlineInput.tsx +131 -0
  50. package/packages/files-app/views/files/UploadOverlay.tsx +61 -0
  51. package/packages/files-app/views/files/schema.ts +49 -0
  52. package/packages/files-app/views/files/service/index.ts +184 -0
  53. package/packages/files-app/views/files/service/runtime +1 -0
  54. package/packages/files-app/views/files/use-files.ts +201 -0
  55. package/packages/files-app/views/files/use-upload.ts +105 -0
  56. package/packages/genie-app/components.ts +12 -0
  57. package/packages/genie-app/lib/subjects.ts +87 -0
  58. package/packages/genie-app/manifest.ts +19 -0
  59. package/packages/genie-app/package.json +29 -0
  60. package/packages/genie-app/views/genie/service/agent-lifecycle.ts +136 -0
  61. package/packages/genie-app/views/genie/service/cli.ts +114 -0
  62. package/packages/genie-app/views/genie/service/comms.ts +141 -0
  63. package/packages/genie-app/views/genie/service/directory.ts +167 -0
  64. package/packages/genie-app/views/genie/service/index.ts +219 -0
  65. package/packages/genie-app/views/genie/service/system.ts +123 -0
  66. package/packages/genie-app/views/genie/service/teams.ts +191 -0
  67. package/packages/genie-app/views/genie/service/terminal-proxy.ts +184 -0
  68. package/packages/genie-app/views/genie/service/tmux-control.ts +318 -0
  69. package/packages/genie-app/views/genie/service/wishes.ts +270 -0
  70. package/packages/genie-app/views/genie/ui/GenieApp.tsx +5 -0
  71. package/packages/genie-app/views/genie/ui/PaneCard.tsx +307 -0
  72. package/packages/genie-app/views/genie/ui/Sidebar.tsx +212 -0
  73. package/packages/genie-app/views/genie/ui/TabBar.tsx +70 -0
  74. package/packages/genie-app/views/genie/ui/WorkspaceCanvas.tsx +343 -0
  75. package/packages/genie-app/views/genie/ui/XTermPane.tsx +306 -0
  76. package/packages/genie-app/views/genie/ui/hooks/useNatsAction.ts +54 -0
  77. package/packages/genie-app/views/genie/ui/hooks/useNatsRequest.ts +68 -0
  78. package/packages/genie-app/views/genie/ui/panels/AgentsPanel.tsx +399 -0
  79. package/packages/genie-app/views/genie/ui/panels/ChatPanel.tsx +351 -0
  80. package/packages/genie-app/views/genie/ui/panels/SystemPanel.tsx +195 -0
  81. package/packages/genie-app/views/genie/ui/panels/TeamsPanel.tsx +560 -0
  82. package/packages/genie-app/views/genie/ui/panels/WishesPanel.tsx +424 -0
  83. package/packages/nats-viewer-app/components.ts +12 -0
  84. package/packages/nats-viewer-app/manifest.ts +18 -0
  85. package/packages/nats-viewer-app/package.json +14 -0
  86. package/packages/nats-viewer-app/views/nats-viewer/ActiveSubs.tsx +34 -0
  87. package/packages/nats-viewer-app/views/nats-viewer/MessageLog.tsx +247 -0
  88. package/packages/nats-viewer-app/views/nats-viewer/NatsViewer.tsx +209 -0
  89. package/packages/nats-viewer-app/views/nats-viewer/PublishPanel.tsx +111 -0
  90. package/packages/nats-viewer-app/views/nats-viewer/RequestPanel.tsx +165 -0
  91. package/packages/nats-viewer-app/views/nats-viewer/Sidebar.tsx +59 -0
  92. package/packages/nats-viewer-app/views/nats-viewer/SubjectCatalog.tsx +63 -0
  93. package/packages/nats-viewer-app/views/nats-viewer/SubscribeInput.tsx +59 -0
  94. package/packages/nats-viewer-app/views/nats-viewer/index.ts +5 -0
  95. package/packages/nats-viewer-app/views/nats-viewer/nats-viewer-context.tsx +31 -0
  96. package/packages/nats-viewer-app/views/nats-viewer/types.ts +7 -0
  97. package/packages/nats-viewer-app/views/nats-viewer/use-message-buffer.ts +55 -0
  98. package/packages/os-cli/package.json +18 -0
  99. package/packages/os-cli/src/commands/events.ts +176 -0
  100. package/packages/os-cli/src/commands/logs.ts +96 -0
  101. package/packages/os-cli/src/commands/status.ts +53 -0
  102. package/packages/os-cli/src/commands/traces.ts +115 -0
  103. package/packages/os-cli/src/index.ts +15 -0
  104. package/packages/os-cli/src/lib/formatter.ts +123 -0
  105. package/packages/os-cli/src/lib/nats.ts +16 -0
  106. package/packages/os-cli/src/lib/trace-tree.ts +144 -0
  107. package/packages/os-cli/tsconfig.json +12 -0
  108. package/packages/os-sdk/package.json +27 -0
  109. package/packages/os-sdk/src/api/handler.ts +67 -0
  110. package/packages/os-sdk/src/config.ts +68 -0
  111. package/packages/os-sdk/src/db/factory.test.ts +42 -0
  112. package/packages/os-sdk/src/db/factory.ts +72 -0
  113. package/packages/os-sdk/src/db/migrate.ts +140 -0
  114. package/packages/os-sdk/src/db/provision.ts +44 -0
  115. package/packages/os-sdk/src/index.ts +36 -0
  116. package/packages/os-sdk/src/service/console-intercept.ts +60 -0
  117. package/packages/os-sdk/src/service/logger.ts +88 -0
  118. package/packages/os-sdk/src/service/o11y-streams.ts +88 -0
  119. package/packages/os-sdk/src/service/runtime.ts +259 -0
  120. package/packages/os-sdk/src/service/trace.ts +71 -0
  121. package/packages/os-sdk/tsconfig.json +16 -0
  122. package/packages/os-ui/package.json +13 -0
  123. package/packages/os-ui/src/index.ts +29 -0
  124. package/packages/os-ui/src/server.ts +4 -0
  125. package/packages/os-ui/tsconfig.json +19 -0
  126. package/packages/settings-app/components.ts +12 -0
  127. package/packages/settings-app/manifest.ts +18 -0
  128. package/packages/settings-app/package.json +14 -0
  129. package/packages/settings-app/views/settings/Settings.tsx +492 -0
  130. package/packages/terminal-app/components.ts +12 -0
  131. package/packages/terminal-app/manifest.ts +20 -0
  132. package/packages/terminal-app/package.json +23 -0
  133. package/packages/terminal-app/views/terminal/schema.ts +82 -0
  134. package/packages/terminal-app/views/terminal/service/index.ts +133 -0
  135. package/packages/terminal-app/views/terminal/service/runtime +1 -0
  136. package/packages/terminal-app/views/terminal/service/session.ts +290 -0
  137. package/packages/terminal-app/views/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
  138. package/packages/terminal-app/views/terminal/types.ts +26 -0
  139. package/packages/terminal-app/views/terminal/ui/MultiTerminalApp.tsx +615 -0
  140. package/packages/terminal-app/views/terminal/ui/SplitDragHandle.tsx +91 -0
  141. package/packages/terminal-app/views/terminal/ui/SplitPaneRenderer.tsx +112 -0
  142. package/packages/terminal-app/views/terminal/ui/TerminalPane.tsx +478 -0
  143. package/packages/terminal-app/views/terminal/ui/TerminalTabBar.tsx +131 -0
  144. package/pnpm-workspace.yaml +9 -0
  145. package/postcss.config.mjs +7 -0
  146. package/public/file.svg +1 -0
  147. package/public/globe.svg +1 -0
  148. package/public/icons/code-server.svg +6 -0
  149. package/public/icons/default.svg +5 -0
  150. package/public/icons/dusk/1password.svg +1 -0
  151. package/public/icons/dusk/activity_monitor.svg +1 -0
  152. package/public/icons/dusk/app_store.svg +1 -0
  153. package/public/icons/dusk/atom.svg +1 -0
  154. package/public/icons/dusk/brave.svg +1 -0
  155. package/public/icons/dusk/calculator.svg +1 -0
  156. package/public/icons/dusk/calendar.svg +1 -0
  157. package/public/icons/dusk/chrome.svg +1 -0
  158. package/public/icons/dusk/chrome2.svg +1 -0
  159. package/public/icons/dusk/dashboard.svg +13 -0
  160. package/public/icons/dusk/discord.svg +1 -0
  161. package/public/icons/dusk/dropbox.svg +1 -0
  162. package/public/icons/dusk/electron.svg +1 -0
  163. package/public/icons/dusk/figma.svg +1 -0
  164. package/public/icons/dusk/finder.svg +1 -0
  165. package/public/icons/dusk/finder2.svg +1 -0
  166. package/public/icons/dusk/finder3.svg +1 -0
  167. package/public/icons/dusk/firefox.svg +1 -0
  168. package/public/icons/dusk/framer.svg +1 -0
  169. package/public/icons/dusk/gimp.svg +1 -0
  170. package/public/icons/dusk/github_desktop.svg +1 -0
  171. package/public/icons/dusk/hyper.svg +1 -0
  172. package/public/icons/dusk/hyper3.svg +1 -0
  173. package/public/icons/dusk/intellij.svg +1 -0
  174. package/public/icons/dusk/iterm2.svg +1 -0
  175. package/public/icons/dusk/itunes.svg +1 -0
  176. package/public/icons/dusk/mail.svg +1 -0
  177. package/public/icons/dusk/messenger.svg +1 -0
  178. package/public/icons/dusk/mongodb.svg +1 -0
  179. package/public/icons/dusk/notes.svg +1 -0
  180. package/public/icons/dusk/notion.svg +1 -0
  181. package/public/icons/dusk/obs.svg +1 -0
  182. package/public/icons/dusk/pages.svg +1 -0
  183. package/public/icons/dusk/photos.svg +1 -0
  184. package/public/icons/dusk/postman.svg +1 -0
  185. package/public/icons/dusk/preview.svg +1 -0
  186. package/public/icons/dusk/reminders.svg +1 -0
  187. package/public/icons/dusk/safari.svg +1 -0
  188. package/public/icons/dusk/sequel_pro.svg +1 -0
  189. package/public/icons/dusk/sketch.svg +1 -0
  190. package/public/icons/dusk/skype.svg +1 -0
  191. package/public/icons/dusk/slack.svg +1 -0
  192. package/public/icons/dusk/slack2.svg +1 -0
  193. package/public/icons/dusk/spotify.svg +1 -0
  194. package/public/icons/dusk/steam.svg +1 -0
  195. package/public/icons/dusk/system_preferences.svg +1 -0
  196. package/public/icons/dusk/tableplus.svg +1 -0
  197. package/public/icons/dusk/teams.svg +1 -0
  198. package/public/icons/dusk/telegram.svg +1 -0
  199. package/public/icons/dusk/terminal.svg +1 -0
  200. package/public/icons/dusk/todoist.svg +1 -0
  201. package/public/icons/dusk/trash.svg +1 -0
  202. package/public/icons/dusk/trello.svg +1 -0
  203. package/public/icons/dusk/vivaldi.svg +1 -0
  204. package/public/icons/dusk/vlc.svg +1 -0
  205. package/public/icons/dusk/vscode.svg +1 -0
  206. package/public/icons/dusk/whatsapp.svg +1 -0
  207. package/public/icons/dusk/xeyes.svg +1 -0
  208. package/public/icons/dusk/zoom.svg +1 -0
  209. package/public/icons/files.svg +5 -0
  210. package/public/icons/pwa/icon-192.png +0 -0
  211. package/public/icons/pwa/icon-512.png +0 -0
  212. package/public/icons/settings.svg +14 -0
  213. package/public/icons/terminal.svg +5 -0
  214. package/public/icons/text-editor.svg +7 -0
  215. package/public/manifest.json +38 -0
  216. package/public/next.svg +1 -0
  217. package/public/sw.js +41 -0
  218. package/public/vercel.svg +1 -0
  219. package/public/wallpapers/default.svg +10 -0
  220. package/public/window.svg +1 -0
  221. package/scripts/generate-pwa-icons.mjs +33 -0
  222. package/scripts/install-nats.sh +37 -0
  223. package/sentry.client.config.ts +21 -0
  224. package/sentry.edge.config.ts +12 -0
  225. package/sentry.server.config.ts +12 -0
  226. package/src/app/api/files/download/route.ts +81 -0
  227. package/src/app/api/files/download-zip/route.ts +102 -0
  228. package/src/app/api/files/upload/route.ts +58 -0
  229. package/src/app/api/webhooks/workos/route.ts +98 -0
  230. package/src/app/auth/callback/route.ts +16 -0
  231. package/src/app/auth/logout/route.ts +15 -0
  232. package/src/app/desktop/desktop-shell.tsx +110 -0
  233. package/src/app/desktop/layout.tsx +8 -0
  234. package/src/app/desktop/page.tsx +24 -0
  235. package/src/app/favicon.ico +0 -0
  236. package/src/app/globals.css +7 -0
  237. package/src/app/layout.tsx +64 -0
  238. package/src/app/offline/page.tsx +83 -0
  239. package/src/app/page.tsx +5 -0
  240. package/src/app/standalone/[appId]/page.tsx +28 -0
  241. package/src/app/standalone/layout.tsx +10 -0
  242. package/src/components/app-icon.tsx +55 -0
  243. package/src/components/apps/_echo/schema.ts +14 -0
  244. package/src/components/apps/_echo/service/index.ts +42 -0
  245. package/src/components/apps/app-manifest.ts +97 -0
  246. package/src/components/apps/app-registry.ts +55 -0
  247. package/src/components/apps/dev3000/Dev3000App.tsx +224 -0
  248. package/src/components/apps/dev3000/ErrorsPanel.tsx +160 -0
  249. package/src/components/apps/dev3000/Sidebar.tsx +41 -0
  250. package/src/components/apps/dev3000/TimelineLog.tsx +173 -0
  251. package/src/components/apps/dev3000/dev3000-context.tsx +29 -0
  252. package/src/components/apps/dev3000/index.ts +4 -0
  253. package/src/components/apps/dev3000/schema.ts +48 -0
  254. package/src/components/apps/dev3000/service/index.ts +520 -0
  255. package/src/components/apps/dev3000/service/runtime +1 -0
  256. package/src/components/apps/dev3000/types.ts +15 -0
  257. package/src/components/apps/dev3000/use-message-buffer.ts +46 -0
  258. package/src/components/apps/files/ContextMenu.tsx +151 -0
  259. package/src/components/apps/files/DeleteConfirmDialog.tsx +78 -0
  260. package/src/components/apps/files/FileItem.tsx +128 -0
  261. package/src/components/apps/files/FilesApp.tsx +509 -0
  262. package/src/components/apps/files/FilesListView.tsx +201 -0
  263. package/src/components/apps/files/FilesToolbar.tsx +117 -0
  264. package/src/components/apps/files/GridView.tsx +90 -0
  265. package/src/components/apps/files/InlineInput.tsx +131 -0
  266. package/src/components/apps/files/UploadOverlay.tsx +61 -0
  267. package/src/components/apps/files/schema.ts +49 -0
  268. package/src/components/apps/files/service/index.ts +227 -0
  269. package/src/components/apps/files/service/runtime +1 -0
  270. package/src/components/apps/files/use-files.ts +201 -0
  271. package/src/components/apps/files/use-upload.ts +105 -0
  272. package/src/components/apps/nats-viewer/ActiveSubs.tsx +34 -0
  273. package/src/components/apps/nats-viewer/MessageLog.tsx +247 -0
  274. package/src/components/apps/nats-viewer/NatsViewer.tsx +209 -0
  275. package/src/components/apps/nats-viewer/PublishPanel.tsx +113 -0
  276. package/src/components/apps/nats-viewer/RequestPanel.tsx +167 -0
  277. package/src/components/apps/nats-viewer/Sidebar.tsx +62 -0
  278. package/src/components/apps/nats-viewer/SubjectCatalog.tsx +64 -0
  279. package/src/components/apps/nats-viewer/SubscribeInput.tsx +59 -0
  280. package/src/components/apps/nats-viewer/index.ts +5 -0
  281. package/src/components/apps/nats-viewer/nats-viewer-context.tsx +31 -0
  282. package/src/components/apps/nats-viewer/types.ts +7 -0
  283. package/src/components/apps/nats-viewer/use-message-buffer.ts +55 -0
  284. package/src/components/apps/settings/Settings.tsx +492 -0
  285. package/src/components/apps/terminal/schema.ts +82 -0
  286. package/src/components/apps/terminal/service/index.ts +189 -0
  287. package/src/components/apps/terminal/service/runtime +1 -0
  288. package/src/components/apps/terminal/service/session.ts +296 -0
  289. package/src/components/apps/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
  290. package/src/components/apps/terminal/types.ts +26 -0
  291. package/src/components/apps/terminal/ui/MultiTerminalApp.tsx +617 -0
  292. package/src/components/apps/terminal/ui/SplitDragHandle.tsx +91 -0
  293. package/src/components/apps/terminal/ui/SplitPaneRenderer.tsx +112 -0
  294. package/src/components/apps/terminal/ui/TerminalPane.tsx +476 -0
  295. package/src/components/apps/terminal/ui/TerminalTabBar.tsx +131 -0
  296. package/src/components/desktop/AnimatedBackground.tsx +69 -0
  297. package/src/components/desktop/Desktop.tsx +79 -0
  298. package/src/components/desktop/DesktopBackground.tsx +16 -0
  299. package/src/components/desktop/DesktopIcon.tsx +49 -0
  300. package/src/components/desktop/ShortcutViewer.tsx +136 -0
  301. package/src/components/desktop/WindowRenderer.tsx +34 -0
  302. package/src/components/desktop/WindowSwitcher.tsx +42 -0
  303. package/src/components/notifications/NotificationCenter.tsx +153 -0
  304. package/src/components/notifications/NotificationToasts.tsx +66 -0
  305. package/src/components/notifications/OrphanSessionToast.tsx +293 -0
  306. package/src/components/os-primitives/collapsible-sidebar.tsx +226 -0
  307. package/src/components/os-primitives/dialog.tsx +76 -0
  308. package/src/components/os-primitives/empty-state.tsx +43 -0
  309. package/src/components/os-primitives/index.ts +21 -0
  310. package/src/components/os-primitives/list-view.tsx +155 -0
  311. package/src/components/os-primitives/property-panel.tsx +108 -0
  312. package/src/components/os-primitives/section-header.tsx +19 -0
  313. package/src/components/os-primitives/sidebar-nav.tsx +110 -0
  314. package/src/components/os-primitives/split-pane.tsx +146 -0
  315. package/src/components/os-primitives/status-badge.tsx +10 -0
  316. package/src/components/os-primitives/status-bar.tsx +100 -0
  317. package/src/components/os-primitives/toolbar.tsx +152 -0
  318. package/src/components/taskbar/AppLauncher.tsx +114 -0
  319. package/src/components/taskbar/RunningApps.tsx +71 -0
  320. package/src/components/taskbar/SystemTray.tsx +134 -0
  321. package/src/components/taskbar/Taskbar.tsx +45 -0
  322. package/src/components/taskbar/UserMenu.tsx +138 -0
  323. package/src/components/taskbar/WorkspaceSwitcher.tsx +9 -0
  324. package/src/components/ui/ContextMenu.tsx +130 -0
  325. package/src/components/ui/badge.tsx +39 -0
  326. package/src/components/ui/button.tsx +102 -0
  327. package/src/components/ui/command.tsx +165 -0
  328. package/src/components/ui/dropdown-menu.tsx +233 -0
  329. package/src/components/ui/input.tsx +48 -0
  330. package/src/components/ui/note.tsx +55 -0
  331. package/src/components/ui/separator.tsx +25 -0
  332. package/src/components/ui/spinner.tsx +42 -0
  333. package/src/components/ui/switch.tsx +36 -0
  334. package/src/components/ui/theme-provider.tsx +24 -0
  335. package/src/components/ui/theme-switcher.tsx +51 -0
  336. package/src/components/ui/tooltip.tsx +62 -0
  337. package/src/components/window/MobileWindowStack.tsx +218 -0
  338. package/src/components/window/SnapPreview.tsx +37 -0
  339. package/src/components/window/StandaloneFrame.tsx +170 -0
  340. package/src/components/window/Window.tsx +423 -0
  341. package/src/components/window/WindowContent.tsx +14 -0
  342. package/src/components/window/WindowControlsOverlay.tsx +89 -0
  343. package/src/components/window/WindowFrame.tsx +124 -0
  344. package/src/lib/auth/index.ts +27 -0
  345. package/src/lib/auth/roles.ts +50 -0
  346. package/src/lib/auth/types.ts +32 -0
  347. package/src/lib/auth/use-auth.ts +53 -0
  348. package/src/lib/auth/webhook-handler.ts +87 -0
  349. package/src/lib/auth/workos.ts +67 -0
  350. package/src/lib/constants.ts +1 -0
  351. package/src/lib/desktop/dedup.ts +57 -0
  352. package/src/lib/desktop/schema.ts +55 -0
  353. package/src/lib/files/filename-validation.ts +41 -0
  354. package/src/lib/files/safe-path.ts +49 -0
  355. package/src/lib/hooks/use-desktop-nats.ts +438 -0
  356. package/src/lib/hooks/use-is-mobile.ts +23 -0
  357. package/src/lib/hooks/use-launch-app.ts +79 -0
  358. package/src/lib/hooks/use-nats-notifications.ts +84 -0
  359. package/src/lib/hooks/use-nats.ts +60 -0
  360. package/src/lib/hooks/use-visual-viewport.ts +72 -0
  361. package/src/lib/icons/resolve-window-icon.ts +10 -0
  362. package/src/lib/keyboard/defaults.ts +146 -0
  363. package/src/lib/keyboard/types.ts +52 -0
  364. package/src/lib/keyboard/use-global-keybinds.ts +231 -0
  365. package/src/lib/nats-client.ts +255 -0
  366. package/src/lib/nats.ts +35 -0
  367. package/src/lib/notifications/schema.ts +12 -0
  368. package/src/lib/service-loader.ts +171 -0
  369. package/src/lib/subjects.ts +64 -0
  370. package/src/lib/utils.ts +6 -0
  371. package/src/lib/ws-bridge.ts +288 -0
  372. package/src/lib/ws-protocol.ts +53 -0
  373. package/src/lib/ws-server.ts +167 -0
  374. package/src/middleware.ts +57 -0
  375. package/src/stores/desktop-store.ts +112 -0
  376. package/src/stores/keybind-store.ts +66 -0
  377. package/src/stores/notification-store.ts +271 -0
  378. package/src/stores/theme-store.ts +25 -0
  379. package/src/stores/window-store.ts +294 -0
  380. package/src/theme/animations.css +68 -0
  381. package/src/theme/base.css +123 -0
  382. package/src/theme/controls.css +35 -0
  383. package/src/theme/design-tokens.css +276 -0
  384. package/src/theme/index.css +23 -0
  385. package/src/theme/menus.css +45 -0
  386. package/src/theme/status.css +41 -0
  387. package/src/theme/surfaces.css +94 -0
  388. package/src/theme/tailwind-map.css +138 -0
  389. package/src/theme/taskbar.css +25 -0
  390. package/src/theme/terminal.css +55 -0
  391. package/src/theme/typography.css +26 -0
  392. package/src/theme/utilities.css +156 -0
  393. package/src/theme/window.css +103 -0
  394. package/src/types/desktop-entry.ts +12 -0
  395. package/src/types/use-descendants.d.ts +13 -0
  396. package/src/types/window.ts +28 -0
  397. package/src/types.d.ts +9 -0
  398. package/tauri/Cargo.lock +5464 -0
  399. package/tauri/Cargo.toml +19 -0
  400. package/tauri/build.rs +3 -0
  401. package/tauri/capabilities/default.json +36 -0
  402. package/tauri/icons/128x128.png +0 -0
  403. package/tauri/icons/128x128@2x.png +0 -0
  404. package/tauri/icons/32x32.png +0 -0
  405. package/tauri/icons/icon.png +0 -0
  406. package/tauri/src/main.rs +396 -0
  407. package/tauri/tauri.conf.json +23 -0
  408. package/tsconfig.json +43 -0
@@ -0,0 +1,758 @@
1
+ 'use client';
2
+
3
+ import {
4
+ AlertTriangle,
5
+ Camera,
6
+ CheckCircle,
7
+ ChevronDown,
8
+ ChevronRight,
9
+ LayoutDashboard,
10
+ Loader2,
11
+ Monitor,
12
+ Play,
13
+ Square,
14
+ X,
15
+ XCircle,
16
+ } from 'lucide-react';
17
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
18
+ import { SidebarNav, SplitPane, StatusBar } from '@/components/os-primitives';
19
+ import { Badge } from '@/components/ui/badge';
20
+ import { Button } from '@/components/ui/button';
21
+ import { Input } from '@/components/ui/input';
22
+ import { useNats } from '@/lib/hooks/use-nats';
23
+ import type { Dev3000ContextValue } from './dev3000-context';
24
+ import { Dev3000Context } from './dev3000-context';
25
+ import { ErrorsPanel } from './ErrorsPanel';
26
+ import { DEV3000_SUBJECTS } from './subjects';
27
+ import type { D3kQaReport, D3kSessionEvent, D3kSessionState, D3kTab } from './types';
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Session Status Panel
31
+ // ---------------------------------------------------------------------------
32
+
33
+ function SessionStatusPanel({
34
+ sessionInfo,
35
+ sessionActive,
36
+ onStartSession,
37
+ onStopSession,
38
+ isCommandRunning,
39
+ }: {
40
+ sessionInfo: D3kSessionState | null;
41
+ sessionActive: boolean;
42
+ onStartSession: () => void;
43
+ onStopSession: () => void;
44
+ isCommandRunning: boolean;
45
+ }) {
46
+ return (
47
+ <div className="rounded-lg border border-gray-alpha-200 bg-background-100 p-4">
48
+ <div className="mb-3 flex items-center gap-2">
49
+ <Monitor className="h-4 w-4 text-gray-700" />
50
+ <h3 className="text-label-13 font-medium text-gray-1000">Session</h3>
51
+ <Badge variant={sessionActive ? 'green' : 'gray'} size="sm" className="ml-auto">
52
+ {sessionActive ? 'Active' : 'Inactive'}
53
+ </Badge>
54
+ </div>
55
+
56
+ {sessionActive && sessionInfo && (
57
+ <div className="mb-3 flex flex-col gap-1 rounded border border-gray-alpha-100 bg-gray-alpha-50 px-3 py-2">
58
+ <div className="flex gap-3 font-mono text-[11px] text-gray-800">
59
+ <span className="text-gray-600">PID</span>
60
+ <span>{sessionInfo.pid}</span>
61
+ </div>
62
+ {sessionInfo.port && (
63
+ <div className="flex gap-3 font-mono text-[11px] text-gray-800">
64
+ <span className="text-gray-600">PORT</span>
65
+ <span>{sessionInfo.port}</span>
66
+ </div>
67
+ )}
68
+ {sessionInfo.startedAt && (
69
+ <div className="flex gap-3 font-mono text-[11px] text-gray-800">
70
+ <span className="text-gray-600">SINCE</span>
71
+ <span>{new Date(sessionInfo.startedAt).toLocaleTimeString()}</span>
72
+ </div>
73
+ )}
74
+ </div>
75
+ )}
76
+
77
+ <div className="flex gap-2">
78
+ <Button
79
+ size="small"
80
+ variant="secondary"
81
+ onClick={onStartSession}
82
+ disabled={sessionActive || isCommandRunning}
83
+ prefix={<Play className="h-3 w-3" />}
84
+ >
85
+ Start Session
86
+ </Button>
87
+ <Button
88
+ size="small"
89
+ variant="secondary"
90
+ onClick={onStopSession}
91
+ disabled={!sessionActive || isCommandRunning}
92
+ prefix={<Square className="h-3 w-3" />}
93
+ >
94
+ Stop Session
95
+ </Button>
96
+ </div>
97
+ </div>
98
+ );
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // QA Runner Panel
103
+ // ---------------------------------------------------------------------------
104
+
105
+ function QaRunnerPanel({ onRunQa, isRunning }: { onRunQa: (routes: string) => void; isRunning: boolean }) {
106
+ const [routes, setRoutes] = useState('/desktop');
107
+
108
+ return (
109
+ <div className="rounded-lg border border-gray-alpha-200 bg-background-100 p-4">
110
+ <div className="mb-3 flex items-center gap-2">
111
+ <Play className="h-4 w-4 text-gray-700" />
112
+ <h3 className="text-label-13 font-medium text-gray-1000">Run QA</h3>
113
+ </div>
114
+ <div className="flex gap-2">
115
+ <Input
116
+ size="small"
117
+ placeholder="Routes (comma-separated)"
118
+ value={routes}
119
+ onChange={(e) => setRoutes(e.target.value)}
120
+ onKeyDown={(e) => {
121
+ if (e.key === 'Enter' && !isRunning) onRunQa(routes);
122
+ }}
123
+ aria-label="Routes to test"
124
+ className="flex-1"
125
+ />
126
+ <Button
127
+ size="small"
128
+ variant="secondary"
129
+ onClick={() => onRunQa(routes)}
130
+ disabled={isRunning || !routes.trim()}
131
+ loading={isRunning}
132
+ >
133
+ Run QA
134
+ </Button>
135
+ </div>
136
+ </div>
137
+ );
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // DOM Checks Section
142
+ // ---------------------------------------------------------------------------
143
+
144
+ function DomChecks({ checks }: { checks: D3kQaReport['dom_checks'] }) {
145
+ const [expanded, setExpanded] = useState(false);
146
+ const failed = checks.filter((c) => !c.pass);
147
+
148
+ return (
149
+ <div>
150
+ <button
151
+ type="button"
152
+ className="flex w-full items-center gap-2 py-1 text-left text-[12px] font-medium text-gray-900 hover:text-gray-1000"
153
+ onClick={() => setExpanded(!expanded)}
154
+ >
155
+ {expanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
156
+ DOM Checks
157
+ <span className="ml-auto font-mono text-[11px] text-gray-600">
158
+ {checks.filter((c) => c.pass).length}/{checks.length} passed
159
+ </span>
160
+ {failed.length > 0 && (
161
+ <Badge variant="red" size="sm">
162
+ {failed.length} failed
163
+ </Badge>
164
+ )}
165
+ </button>
166
+ {expanded && (
167
+ <div className="mt-1 flex flex-col gap-1 pl-4">
168
+ {checks.map((check, i) => (
169
+ <div key={`${check.route}-${check.selector}-${i}`} className="flex items-center gap-2 text-[11px]">
170
+ {check.pass ? (
171
+ <CheckCircle className="h-3 w-3 shrink-0 text-green-600" />
172
+ ) : (
173
+ <XCircle className="h-3 w-3 shrink-0 text-red-600" />
174
+ )}
175
+ <span className="font-mono text-gray-700">{check.selector}</span>
176
+ <span className="text-gray-600">on</span>
177
+ <span className="font-mono text-gray-800">{check.route}</span>
178
+ </div>
179
+ ))}
180
+ </div>
181
+ )}
182
+ </div>
183
+ );
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Errors Section
188
+ // ---------------------------------------------------------------------------
189
+
190
+ function ErrorsSection({ report }: { report: D3kQaReport }) {
191
+ const [expanded, setExpanded] = useState(false);
192
+ const jsErrors = report.errors.js_exceptions as unknown[];
193
+ const consoleErrors = report.errors.console_errors as unknown[];
194
+ const totalErrors = jsErrors.length + consoleErrors.length;
195
+
196
+ return (
197
+ <div>
198
+ <button
199
+ type="button"
200
+ className="flex w-full items-center gap-2 py-1 text-left text-[12px] font-medium text-gray-900 hover:text-gray-1000"
201
+ onClick={() => setExpanded(!expanded)}
202
+ >
203
+ {expanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
204
+ Errors
205
+ {totalErrors > 0 ? (
206
+ <Badge variant="red" size="sm" className="ml-auto">
207
+ {totalErrors}
208
+ </Badge>
209
+ ) : (
210
+ <span className="ml-auto text-[11px] text-green-600">Clean</span>
211
+ )}
212
+ </button>
213
+ {expanded && totalErrors === 0 && <p className="pl-4 text-[11px] text-gray-600">No errors detected.</p>}
214
+ {expanded && totalErrors > 0 && (
215
+ <div className="mt-1 flex flex-col gap-1 pl-4">
216
+ {jsErrors.map((e) => {
217
+ const text = typeof e === 'object' ? JSON.stringify(e) : String(e);
218
+ return (
219
+ <div
220
+ key={`js-${text.slice(0, 80)}`}
221
+ className="rounded border border-red-100 bg-red-50 px-2 py-1 font-mono text-[10px] text-red-700"
222
+ >
223
+ {text}
224
+ </div>
225
+ );
226
+ })}
227
+ {consoleErrors.map((e) => {
228
+ const text = typeof e === 'object' ? JSON.stringify(e) : String(e);
229
+ return (
230
+ <div
231
+ key={`ce-${text.slice(0, 80)}`}
232
+ className="rounded border border-amber-100 bg-amber-50 px-2 py-1 font-mono text-[10px] text-amber-700"
233
+ >
234
+ {text}
235
+ </div>
236
+ );
237
+ })}
238
+ </div>
239
+ )}
240
+ </div>
241
+ );
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Network Section
246
+ // ---------------------------------------------------------------------------
247
+
248
+ function NetworkSection({ report }: { report: D3kQaReport }) {
249
+ const [expanded, setExpanded] = useState(false);
250
+ const failedCount = report.network.failed_requests;
251
+
252
+ return (
253
+ <div>
254
+ <button
255
+ type="button"
256
+ className="flex w-full items-center gap-2 py-1 text-left text-[12px] font-medium text-gray-900 hover:text-gray-1000"
257
+ onClick={() => setExpanded(!expanded)}
258
+ >
259
+ {expanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
260
+ Network
261
+ {failedCount > 0 ? (
262
+ <Badge variant="red" size="sm" className="ml-auto">
263
+ {failedCount} failed
264
+ </Badge>
265
+ ) : (
266
+ <span className="ml-auto text-[11px] text-gray-600">{report.network.total_requests} requests</span>
267
+ )}
268
+ </button>
269
+ {expanded && (
270
+ <div className="mt-1 pl-4">
271
+ <div className="flex flex-col gap-1">
272
+ {Object.entries(report.network.by_route).map(([route, stats]) => (
273
+ <div key={route} className="flex items-center gap-2 text-[11px]">
274
+ <span className="font-mono text-gray-800">{route}</span>
275
+ <span className="text-gray-600">{stats.total} req</span>
276
+ {stats.failed > 0 && (
277
+ <Badge variant="red" size="sm">
278
+ {stats.failed} failed
279
+ </Badge>
280
+ )}
281
+ </div>
282
+ ))}
283
+ </div>
284
+ </div>
285
+ )}
286
+ </div>
287
+ );
288
+ }
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // Screenshot Modal
292
+ // ---------------------------------------------------------------------------
293
+
294
+ function ScreenshotModal({ src, label, onClose }: { src: string; label: string; onClose: () => void }) {
295
+ // Close on Escape key
296
+ useEffect(() => {
297
+ const handler = (e: KeyboardEvent) => {
298
+ if (e.key === 'Escape') onClose();
299
+ };
300
+ window.addEventListener('keydown', handler);
301
+ return () => window.removeEventListener('keydown', handler);
302
+ }, [onClose]);
303
+
304
+ return (
305
+ <div
306
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/70"
307
+ onClick={onClose}
308
+ onKeyDown={(e) => {
309
+ if (e.key === 'Enter' || e.key === ' ') onClose();
310
+ }}
311
+ role="dialog"
312
+ aria-modal="true"
313
+ aria-label={`Screenshot: ${label}`}
314
+ >
315
+ <div
316
+ className="relative max-h-[90vh] max-w-[90vw] overflow-auto rounded-lg bg-gray-950 p-1 shadow-2xl"
317
+ onClick={(e) => e.stopPropagation()}
318
+ onKeyDown={(e) => e.stopPropagation()}
319
+ >
320
+ <button
321
+ type="button"
322
+ className="absolute right-2 top-2 z-10 rounded bg-black/60 p-1 text-white hover:bg-black/80"
323
+ onClick={onClose}
324
+ aria-label="Close screenshot"
325
+ >
326
+ <X className="h-4 w-4" />
327
+ </button>
328
+ <div className="mb-1 px-2 pt-1 font-mono text-[11px] text-gray-400">{label}</div>
329
+ <img src={src} alt={label} className="max-h-[80vh] max-w-full rounded object-contain" />
330
+ </div>
331
+ </div>
332
+ );
333
+ }
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // Screenshots Section
337
+ // ---------------------------------------------------------------------------
338
+
339
+ function ScreenshotsSection({ screenshots }: { screenshots: D3kQaReport['screenshots'] }) {
340
+ const [expanded, setExpanded] = useState(true);
341
+ const [modalSrc, setModalSrc] = useState<string | null>(null);
342
+ const [modalLabel, setModalLabel] = useState('');
343
+
344
+ if (screenshots.length === 0) return null;
345
+
346
+ const openModal = (file: string, label: string) => {
347
+ setModalSrc(`/api/dev3000/screenshot?file=${encodeURIComponent(file)}`);
348
+ setModalLabel(label);
349
+ };
350
+
351
+ const closeModal = () => setModalSrc(null);
352
+
353
+ return (
354
+ <>
355
+ {modalSrc && <ScreenshotModal src={modalSrc} label={modalLabel} onClose={closeModal} />}
356
+ <div>
357
+ <button
358
+ type="button"
359
+ className="flex w-full items-center gap-2 py-1 text-left text-[12px] font-medium text-gray-900 hover:text-gray-1000"
360
+ onClick={() => setExpanded(!expanded)}
361
+ >
362
+ {expanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
363
+ <Camera className="h-3 w-3" />
364
+ Screenshots
365
+ <span className="ml-auto font-mono text-[11px] text-gray-600">{screenshots.length}</span>
366
+ </button>
367
+ {expanded && (
368
+ <div className="mt-1 grid grid-cols-2 gap-2 pl-4">
369
+ {screenshots.map((shot) => (
370
+ <button
371
+ key={`${shot.route}-${shot.file}`}
372
+ type="button"
373
+ className="group flex flex-col overflow-hidden rounded border border-gray-alpha-100 bg-gray-alpha-50 text-left hover:border-gray-alpha-300"
374
+ onClick={() => openModal(shot.file, `${shot.route} (${shot.viewport})`)}
375
+ >
376
+ <div className="relative overflow-hidden bg-gray-200">
377
+ <img
378
+ src={`/api/dev3000/screenshot?file=${encodeURIComponent(shot.file)}`}
379
+ alt={shot.route}
380
+ className="h-20 w-full object-cover object-top transition-opacity group-hover:opacity-80"
381
+ loading="lazy"
382
+ />
383
+ </div>
384
+ <div className="px-1.5 py-1">
385
+ <div className="truncate font-mono text-[10px] text-gray-800">{shot.route}</div>
386
+ <div className="text-[9px] text-gray-500">{shot.viewport}</div>
387
+ </div>
388
+ </button>
389
+ ))}
390
+ </div>
391
+ )}
392
+ </div>
393
+ </>
394
+ );
395
+ }
396
+
397
+ // ---------------------------------------------------------------------------
398
+ // QA Report Viewer
399
+ // ---------------------------------------------------------------------------
400
+
401
+ function QaReportViewer({ report }: { report: D3kQaReport }) {
402
+ const passed = report.summary.pass;
403
+
404
+ return (
405
+ <div className="rounded-lg border border-gray-alpha-200 bg-background-100 p-4">
406
+ <div className="mb-3 flex items-center gap-2">
407
+ {passed ? (
408
+ <CheckCircle className="h-4 w-4 text-green-600" />
409
+ ) : (
410
+ <AlertTriangle className="h-4 w-4 text-red-500" />
411
+ )}
412
+ <h3 className="text-label-13 font-medium text-gray-1000">Last QA Report</h3>
413
+ <Badge variant={passed ? 'green' : 'red'} size="sm" className="ml-auto">
414
+ {passed ? 'PASS' : 'FAIL'}
415
+ </Badge>
416
+ </div>
417
+
418
+ {/* Summary stats */}
419
+ <div className="mb-3 grid grid-cols-3 gap-2">
420
+ <div className="rounded border border-gray-alpha-100 bg-gray-alpha-50 px-2 py-1.5 text-center">
421
+ <div className="font-mono text-[16px] font-semibold text-gray-1000">{report.summary.routes_passed}</div>
422
+ <div className="text-[10px] text-gray-600">routes passed</div>
423
+ </div>
424
+ <div className="rounded border border-gray-alpha-100 bg-gray-alpha-50 px-2 py-1.5 text-center">
425
+ <div
426
+ className={`font-mono text-[16px] font-semibold ${report.summary.total_errors > 0 ? 'text-red-600' : 'text-gray-1000'}`}
427
+ >
428
+ {report.summary.total_errors}
429
+ </div>
430
+ <div className="text-[10px] text-gray-600">errors</div>
431
+ </div>
432
+ <div className="rounded border border-gray-alpha-100 bg-gray-alpha-50 px-2 py-1.5 text-center">
433
+ <div className="font-mono text-[16px] font-semibold text-gray-1000">{report.summary.screenshots_taken}</div>
434
+ <div className="text-[10px] text-gray-600">screenshots</div>
435
+ </div>
436
+ </div>
437
+
438
+ {/* Routes tested */}
439
+ <div className="mb-2 flex flex-wrap gap-1">
440
+ {report.routes_tested.map((r) => (
441
+ <span key={r} className="rounded bg-gray-alpha-100 px-1.5 py-0.5 font-mono text-[10px] text-gray-800">
442
+ {r}
443
+ </span>
444
+ ))}
445
+ </div>
446
+
447
+ {/* Expandable sections */}
448
+ <div className="flex flex-col gap-0.5 border-t border-gray-alpha-100 pt-2">
449
+ <ScreenshotsSection screenshots={report.screenshots} />
450
+ <DomChecks checks={report.dom_checks} />
451
+ <ErrorsSection report={report} />
452
+ <NetworkSection report={report} />
453
+ </div>
454
+
455
+ {/* Timestamp */}
456
+ <div className="mt-2 text-right text-[10px] text-gray-600">
457
+ {new Date(report.timestamp).toLocaleString()}
458
+ {' — '}
459
+ {(report.duration_ms / 1000).toFixed(1)}s
460
+ </div>
461
+ </div>
462
+ );
463
+ }
464
+
465
+ // ---------------------------------------------------------------------------
466
+ // Dashboard Tab
467
+ // ---------------------------------------------------------------------------
468
+
469
+ function DashboardTab({
470
+ sessionActive,
471
+ sessionInfo,
472
+ lastReport,
473
+ onStartSession,
474
+ onStopSession,
475
+ onRunQa,
476
+ isCommandRunning,
477
+ commandStatus,
478
+ }: {
479
+ sessionActive: boolean;
480
+ sessionInfo: D3kSessionState | null;
481
+ lastReport: D3kQaReport | null;
482
+ onStartSession: () => void;
483
+ onStopSession: () => void;
484
+ onRunQa: (routes: string) => void;
485
+ isCommandRunning: boolean;
486
+ commandStatus: string;
487
+ }) {
488
+ return (
489
+ <div className="flex h-full flex-col gap-4 overflow-auto p-4">
490
+ <SessionStatusPanel
491
+ sessionInfo={sessionInfo}
492
+ sessionActive={sessionActive}
493
+ onStartSession={onStartSession}
494
+ onStopSession={onStopSession}
495
+ isCommandRunning={isCommandRunning}
496
+ />
497
+
498
+ <QaRunnerPanel onRunQa={onRunQa} isRunning={isCommandRunning} />
499
+
500
+ {isCommandRunning && (
501
+ <div className="flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2">
502
+ <Loader2 className="h-4 w-4 animate-spin text-blue-600" />
503
+ <span className="text-[12px] text-blue-800">{commandStatus}</span>
504
+ </div>
505
+ )}
506
+
507
+ {lastReport && <QaReportViewer report={lastReport} />}
508
+
509
+ {!lastReport && !isCommandRunning && (
510
+ <div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-gray-alpha-300 py-8 text-center">
511
+ <AlertTriangle className="mb-2 h-6 w-6 text-gray-500" />
512
+ <p className="text-[13px] font-medium text-gray-800">No QA report yet</p>
513
+ <p className="text-[11px] text-gray-600">Start a session and run QA to see results.</p>
514
+ </div>
515
+ )}
516
+ </div>
517
+ );
518
+ }
519
+
520
+ // ---------------------------------------------------------------------------
521
+ // Main App
522
+ // ---------------------------------------------------------------------------
523
+
524
+ export function Dev3000App(_props: { windowId: string; meta?: Record<string, unknown> }) {
525
+ const [activeTab, setActiveTab] = useState<D3kTab>('dashboard');
526
+ const [sessionActive, setSessionActive] = useState(false);
527
+ const [sessionInfo, setSessionInfo] = useState<D3kSessionState | null>(null);
528
+ const [lastReport, setLastReport] = useState<D3kQaReport | null>(null);
529
+ const [isCommandRunning, setIsCommandRunning] = useState(false);
530
+ const [commandStatus, setCommandStatus] = useState('');
531
+
532
+ const { connected, subscribe, request } = useNats();
533
+
534
+ // Stable refs
535
+ const sessionActiveRef = useRef(sessionActive);
536
+ sessionActiveRef.current = sessionActive;
537
+
538
+ // Subscribe to session events
539
+ useEffect(() => {
540
+ if (!connected) return;
541
+
542
+ // Request current status on connect
543
+ const timer = setTimeout(() => {
544
+ request(DEV3000_SUBJECTS.status(), {}, 5_000)
545
+ .then((result) => {
546
+ const status = result as {
547
+ sessionActive?: boolean;
548
+ pid?: number;
549
+ port?: number;
550
+ wsEndpoint?: string;
551
+ startedAt?: string;
552
+ };
553
+ if (status.sessionActive !== undefined) {
554
+ setSessionActive(status.sessionActive);
555
+ if (status.sessionActive) {
556
+ setSessionInfo({
557
+ active: true,
558
+ pid: status.pid,
559
+ port: status.port,
560
+ wsEndpoint: status.wsEndpoint,
561
+ startedAt: status.startedAt,
562
+ });
563
+ }
564
+ }
565
+ })
566
+ .catch(() => {
567
+ // service may not be running yet
568
+ });
569
+ }, 100);
570
+
571
+ // Subscribe to session events
572
+ const unsubSession = subscribe(DEV3000_SUBJECTS.session(), (data: unknown) => {
573
+ const event = data as D3kSessionEvent;
574
+ if (event.type === 'started') {
575
+ setSessionActive(true);
576
+ setSessionInfo({
577
+ active: true,
578
+ pid: event.pid,
579
+ wsEndpoint: event.wsEndpoint,
580
+ port: event.port,
581
+ startedAt: event.timestamp,
582
+ });
583
+ } else if (event.type === 'stopped') {
584
+ setSessionActive(false);
585
+ setSessionInfo(null);
586
+ }
587
+ });
588
+
589
+ // Subscribe to QA reports
590
+ const unsubQa = subscribe(DEV3000_SUBJECTS.qaReport(), (data: unknown) => {
591
+ try {
592
+ const report = data as D3kQaReport;
593
+ if (report.summary) {
594
+ setLastReport(report);
595
+ setIsCommandRunning(false);
596
+ setCommandStatus('');
597
+ }
598
+ } catch {
599
+ // ignore malformed
600
+ }
601
+ });
602
+
603
+ return () => {
604
+ clearTimeout(timer);
605
+ unsubSession();
606
+ unsubQa();
607
+ };
608
+ }, [connected, subscribe, request]);
609
+
610
+ // Run CLI command via NATS
611
+ const runCommand = useCallback(
612
+ async (cmd: string, args: string[]) => {
613
+ try {
614
+ const timeout = cmd === 'qa' ? 35_000 : 20_000;
615
+ const result = (await request(DEV3000_SUBJECTS.cmd(), { command: cmd, args }, timeout)) as {
616
+ ok: boolean;
617
+ output?: string;
618
+ error?: string;
619
+ };
620
+ return result;
621
+ } catch (err) {
622
+ return { ok: false, error: (err as Error).message };
623
+ }
624
+ },
625
+ [request]
626
+ );
627
+
628
+ const handleStartSession = useCallback(async () => {
629
+ setIsCommandRunning(true);
630
+ setCommandStatus('Starting Chrome session...');
631
+ const result = await runCommand('session', ['start']);
632
+ if (!result.ok) {
633
+ setCommandStatus(`Error: ${result.error}`);
634
+ setIsCommandRunning(false);
635
+ } else {
636
+ // Session file watcher will fire and update state via NATS
637
+ setCommandStatus('Session starting...');
638
+ // Auto-clear after 3s if no event received
639
+ setTimeout(() => {
640
+ setIsCommandRunning(false);
641
+ setCommandStatus('');
642
+ }, 3_000);
643
+ }
644
+ }, [runCommand]);
645
+
646
+ const handleStopSession = useCallback(async () => {
647
+ setIsCommandRunning(true);
648
+ setCommandStatus('Stopping session...');
649
+ const result = await runCommand('session', ['stop']);
650
+ if (!result.ok) {
651
+ setCommandStatus(`Error: ${result.error}`);
652
+ }
653
+ setIsCommandRunning(false);
654
+ setCommandStatus('');
655
+ }, [runCommand]);
656
+
657
+ const handleRunQa = useCallback(
658
+ async (routes: string) => {
659
+ setIsCommandRunning(true);
660
+ setCommandStatus(`Running QA on ${routes}...`);
661
+ const routeArgs = ['--routes', routes.trim()];
662
+ const result = await runCommand('qa', routeArgs);
663
+ if (!result.ok) {
664
+ setCommandStatus(`QA error: ${result.error}`);
665
+ setIsCommandRunning(false);
666
+ return;
667
+ }
668
+ // Try parsing result directly (in case NATS event was missed)
669
+ if (result.output) {
670
+ try {
671
+ const report = JSON.parse(result.output) as D3kQaReport;
672
+ if (report.summary) {
673
+ setLastReport(report);
674
+ }
675
+ } catch {
676
+ // ignore — will be set via NATS subscription
677
+ }
678
+ }
679
+ setIsCommandRunning(false);
680
+ setCommandStatus('');
681
+ },
682
+ [runCommand]
683
+ );
684
+
685
+ const ctxValue = useMemo<Dev3000ContextValue>(
686
+ () => ({
687
+ sessionActive,
688
+ sessionInfo,
689
+ lastReport,
690
+ activeTab,
691
+ setActiveTab,
692
+ runCommand,
693
+ }),
694
+ [sessionActive, sessionInfo, lastReport, activeTab, runCommand]
695
+ );
696
+
697
+ return (
698
+ <Dev3000Context.Provider value={ctxValue}>
699
+ <div className="flex h-full flex-col bg-background-100">
700
+ <div className="flex-1 overflow-hidden">
701
+ <SplitPane defaultSize={160} min={130} max={220} collapseBelow={400}>
702
+ {/* Sidebar */}
703
+ <SplitPane.Panel className="bg-gray-alpha-50">
704
+ <SidebarNav label="dev3000" title="dev3000">
705
+ <SidebarNav.Item
706
+ active={activeTab === 'dashboard'}
707
+ onClick={() => setActiveTab('dashboard')}
708
+ icon={<LayoutDashboard />}
709
+ >
710
+ Dashboard
711
+ </SidebarNav.Item>
712
+ <SidebarNav.Item
713
+ active={activeTab === 'errors'}
714
+ onClick={() => setActiveTab('errors')}
715
+ icon={<AlertTriangle />}
716
+ >
717
+ Errors
718
+ </SidebarNav.Item>
719
+ </SidebarNav>
720
+ </SplitPane.Panel>
721
+
722
+ {/* Main */}
723
+ <SplitPane.Panel>
724
+ <div className="h-full overflow-hidden">
725
+ {activeTab === 'dashboard' && (
726
+ <DashboardTab
727
+ sessionActive={sessionActive}
728
+ sessionInfo={sessionInfo}
729
+ lastReport={lastReport}
730
+ onStartSession={handleStartSession}
731
+ onStopSession={handleStopSession}
732
+ onRunQa={handleRunQa}
733
+ isCommandRunning={isCommandRunning}
734
+ commandStatus={commandStatus}
735
+ />
736
+ )}
737
+ {activeTab === 'errors' && <ErrorsPanel />}
738
+ </div>
739
+ </SplitPane.Panel>
740
+ </SplitPane>
741
+ </div>
742
+
743
+ {/* Status bar */}
744
+ <StatusBar>
745
+ <StatusBar.Item>dev3000</StatusBar.Item>
746
+ <StatusBar.Separator />
747
+ <StatusBar.Item variant={sessionActive ? 'success' : 'default'}>
748
+ {sessionActive ? 'session active' : 'no session'}
749
+ </StatusBar.Item>
750
+ <StatusBar.Spacer />
751
+ <StatusBar.Item variant={connected ? 'success' : 'default'}>
752
+ {connected ? 'connected' : 'disconnected'}
753
+ </StatusBar.Item>
754
+ </StatusBar>
755
+ </div>
756
+ </Dev3000Context.Provider>
757
+ );
758
+ }