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,224 @@
1
+ 'use client';
2
+
3
+ import { AlertTriangle, Clock, Filter, Pause, Play, Trash2 } from 'lucide-react';
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
+ import { SplitPane, StatusBar, Toolbar } from '@/components/os-primitives';
6
+ import { SidebarNav } from '@/components/os-primitives/sidebar-nav';
7
+ import { useNats } from '@/lib/hooks/use-nats';
8
+ import { SUBJECTS } from '@/lib/subjects';
9
+ import type { Dev3000ContextValue } from './dev3000-context';
10
+ import { Dev3000Context } from './dev3000-context';
11
+ import { ErrorsPanel } from './ErrorsPanel';
12
+ import { Sidebar } from './Sidebar';
13
+ import { TimelineLog } from './TimelineLog';
14
+ import type { D3kLogEntry, D3kSource, D3kTab } from './types';
15
+ import { useMessageBuffer } from './use-message-buffer';
16
+
17
+ export function Dev3000App(_props: { windowId: string; meta?: Record<string, unknown> }) {
18
+ const [activeTab, setActiveTab] = useState<D3kTab>('timeline');
19
+ const [paused, setPaused] = useState(false);
20
+ const [filter, setFilter] = useState('');
21
+ const [sourceFilter, setSourceFilter] = useState<D3kSource | 'ALL'>('ALL');
22
+
23
+ const { connected, subscribe, request, orgId } = useNats();
24
+ const buffer = useMessageBuffer();
25
+
26
+ // Stable ref for buffer.push so the subscribe callback never goes stale
27
+ const pushRef = useRef(buffer.push);
28
+ pushRef.current = buffer.push;
29
+ const pausedRef = useRef(paused);
30
+ pausedRef.current = paused;
31
+
32
+ const [d3kRunning, setD3kRunning] = useState(false);
33
+
34
+ // Subscribe to d3k.log, then request replay of buffered entries
35
+ useEffect(() => {
36
+ if (!orgId || !connected) return;
37
+
38
+ // 1. Subscribe first
39
+ const subject = SUBJECTS.d3k.log(orgId);
40
+ const unsub = subscribe(subject, (data: unknown) => {
41
+ if (pausedRef.current) return;
42
+ try {
43
+ const entry = data as D3kLogEntry;
44
+ if (entry.id && entry.message) {
45
+ pushRef.current(entry);
46
+ }
47
+ } catch {
48
+ // ignore malformed entries
49
+ }
50
+ });
51
+
52
+ // 2. Then request status — response includes buffered entries (targeted to this client only)
53
+ const timer = setTimeout(() => {
54
+ request(SUBJECTS.d3k.status(orgId), {}, 5_000)
55
+ .then((result) => {
56
+ const status = result as { d3kRunning?: boolean; entries?: D3kLogEntry[] };
57
+ if (status.d3kRunning !== undefined) setD3kRunning(status.d3kRunning);
58
+ if (status.entries && !pausedRef.current) {
59
+ for (const entry of status.entries) {
60
+ pushRef.current(entry);
61
+ }
62
+ }
63
+ })
64
+ .catch(() => {
65
+ // service may not be running yet
66
+ });
67
+ }, 100);
68
+
69
+ return () => {
70
+ unsub();
71
+ clearTimeout(timer);
72
+ };
73
+ }, [orgId, connected, subscribe, request]);
74
+
75
+ // Run d3k command via NATS request
76
+ const runCommand = useCallback(
77
+ async (command: string, args?: string[]) => {
78
+ if (!orgId) return { ok: false, error: 'Not connected' };
79
+ const subject = SUBJECTS.d3k.cmd(orgId);
80
+ try {
81
+ const result = (await request(subject, { command, args }, 20_000)) as {
82
+ ok: boolean;
83
+ output?: string;
84
+ error?: string;
85
+ };
86
+ return result;
87
+ } catch (err) {
88
+ return { ok: false, error: (err as Error).message };
89
+ }
90
+ },
91
+ [orgId, request]
92
+ );
93
+
94
+ // Filtered count
95
+ const filteredCount = useMemo(() => {
96
+ let result = buffer.entries;
97
+ if (sourceFilter !== 'ALL') {
98
+ result = result.filter((e) => e.source === sourceFilter);
99
+ }
100
+ if (filter) {
101
+ const lower = filter.toLowerCase();
102
+ result = result.filter((e) => e.message.toLowerCase().includes(lower));
103
+ }
104
+ return result.length;
105
+ }, [buffer.entries, filter, sourceFilter]);
106
+
107
+ const countLabel =
108
+ filter || sourceFilter !== 'ALL'
109
+ ? `${filteredCount}/${buffer.entries.length} entries`
110
+ : `${buffer.entries.length} entries`;
111
+
112
+ // Context value
113
+ const ctxValue = useMemo<Dev3000ContextValue>(
114
+ () => ({
115
+ buffer,
116
+ filter,
117
+ setFilter,
118
+ sourceFilter,
119
+ setSourceFilter,
120
+ paused,
121
+ setPaused,
122
+ activeTab,
123
+ setActiveTab,
124
+ runCommand,
125
+ }),
126
+ [buffer, filter, sourceFilter, paused, activeTab, runCommand]
127
+ );
128
+
129
+ return (
130
+ <Dev3000Context.Provider value={ctxValue}>
131
+ <div className="flex h-full flex-col bg-background-100">
132
+ <div className="flex-1 overflow-hidden">
133
+ <SplitPane defaultSize={200} min={160} max={300} collapseBelow={400}>
134
+ {/* ---- Sidebar ---- */}
135
+ <SplitPane.Panel className="bg-gray-alpha-50">
136
+ <SidebarNav label="dev3000" title="dev3000">
137
+ <SidebarNav.Group title="Views">
138
+ <SidebarNav.Item
139
+ active={activeTab === 'timeline'}
140
+ onClick={() => setActiveTab('timeline')}
141
+ icon={<Clock />}
142
+ >
143
+ Timeline
144
+ </SidebarNav.Item>
145
+ <SidebarNav.Item
146
+ active={activeTab === 'errors'}
147
+ onClick={() => setActiveTab('errors')}
148
+ icon={<AlertTriangle />}
149
+ >
150
+ Errors
151
+ </SidebarNav.Item>
152
+ </SidebarNav.Group>
153
+ </SidebarNav>
154
+
155
+ {/* Source filter panel */}
156
+ <div className="flex-1 overflow-y-auto border-t border-gray-alpha-200 p-3">
157
+ <Sidebar />
158
+ </div>
159
+ </SplitPane.Panel>
160
+
161
+ {/* ---- Main area ---- */}
162
+ <SplitPane.Panel>
163
+ <div className="flex h-full flex-col">
164
+ {/* Toolbar — only for timeline tab */}
165
+ {activeTab === 'timeline' && (
166
+ <Toolbar>
167
+ <Toolbar.Group>
168
+ <Toolbar.Button
169
+ tooltip={paused ? 'Resume' : 'Pause'}
170
+ onClick={() => setPaused(!paused)}
171
+ active={paused}
172
+ >
173
+ {paused ? <Play /> : <Pause />}
174
+ </Toolbar.Button>
175
+ <Toolbar.Button tooltip="Clear log" onClick={() => buffer.clear()}>
176
+ <Trash2 />
177
+ </Toolbar.Button>
178
+ </Toolbar.Group>
179
+ <Toolbar.Separator />
180
+ <Toolbar.Group>
181
+ <Toolbar.Button tooltip="Filter">
182
+ <Filter />
183
+ </Toolbar.Button>
184
+ </Toolbar.Group>
185
+ <Toolbar.Input
186
+ placeholder="Filter by message..."
187
+ value={filter}
188
+ onChange={(e) => setFilter(e.target.value)}
189
+ />
190
+ <Toolbar.Spacer />
191
+ <Toolbar.Text>{countLabel}</Toolbar.Text>
192
+ </Toolbar>
193
+ )}
194
+
195
+ {/* Content */}
196
+ <div className="flex-1 overflow-hidden">
197
+ {activeTab === 'timeline' && (
198
+ <TimelineLog entries={buffer.entries} filter={filter} sourceFilter={sourceFilter} paused={paused} />
199
+ )}
200
+ {activeTab === 'errors' && <ErrorsPanel />}
201
+ </div>
202
+ </div>
203
+ </SplitPane.Panel>
204
+ </SplitPane>
205
+ </div>
206
+
207
+ {/* Status bar */}
208
+ <StatusBar>
209
+ <StatusBar.Item>dev3000</StatusBar.Item>
210
+ <StatusBar.Separator />
211
+ <StatusBar.Item>{countLabel}</StatusBar.Item>
212
+ <StatusBar.Spacer />
213
+ <StatusBar.Item variant={d3kRunning ? 'success' : 'default'}>
214
+ {d3kRunning ? 'd3k running' : 'd3k stopped'}
215
+ </StatusBar.Item>
216
+ <StatusBar.Separator />
217
+ <StatusBar.Item variant={connected ? 'success' : 'default'}>
218
+ {connected ? 'connected' : 'disconnected'}
219
+ </StatusBar.Item>
220
+ </StatusBar>
221
+ </div>
222
+ </Dev3000Context.Provider>
223
+ );
224
+ }
@@ -0,0 +1,160 @@
1
+ 'use client';
2
+
3
+ import { AlertTriangle, CheckCircle, Loader2, RefreshCw } from 'lucide-react';
4
+ import { useCallback, useState } from 'react';
5
+ import { EmptyState } from '@/components/os-primitives/empty-state';
6
+ import { useDev3000 } from './dev3000-context';
7
+
8
+ type ErrorsState = 'idle' | 'loading' | 'success' | 'error';
9
+
10
+ interface D3kErrorsResponse {
11
+ total: number;
12
+ showing: number;
13
+ errors: string[];
14
+ }
15
+
16
+ interface ParsedError {
17
+ key: string;
18
+ time: string;
19
+ source: string;
20
+ message: string;
21
+ }
22
+
23
+ // Parse d3k log line: "[HH:mm:ss.SSS] [SOURCE] [TAG] message"
24
+ const LOG_LINE_RE = /^\[(\d{2}:\d{2}:\d{2}\.\d{3})]\s+\[(\w+)]\s+(.*)$/;
25
+
26
+ function parseErrors(raw: string): { errors: ParsedError[]; total: number } {
27
+ try {
28
+ const parsed: D3kErrorsResponse = JSON.parse(raw);
29
+ const errors = (parsed.errors || []).map((line, i) => {
30
+ const match = line.match(LOG_LINE_RE);
31
+ if (match) {
32
+ return { key: `${match[1]}-${i}`, time: match[1], source: match[2], message: match[3] };
33
+ }
34
+ return { key: `raw-${i}`, time: '', source: '', message: line };
35
+ });
36
+ return { errors, total: parsed.total || 0 };
37
+ } catch {
38
+ return { errors: [{ key: 'raw-0', time: '', source: '', message: raw }], total: 0 };
39
+ }
40
+ }
41
+
42
+ function ErrorRow({ error }: { error: ParsedError }) {
43
+ return (
44
+ <div className="px-3 py-2 text-[11px] transition-colors hover:bg-gray-alpha-50">
45
+ <div className="flex items-baseline gap-2">
46
+ {error.time && <span className="shrink-0 font-mono text-gray-600">{error.time}</span>}
47
+ {error.source && (
48
+ <span className="shrink-0 rounded bg-red-500/10 px-1 py-0.5 font-mono text-[9px] font-semibold text-red-600">
49
+ {error.source}
50
+ </span>
51
+ )}
52
+ </div>
53
+ <p className="mt-0.5 font-mono leading-4 text-gray-1000">{error.message}</p>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ export function ErrorsPanel() {
59
+ const { runCommand } = useDev3000();
60
+ const [state, setState] = useState<ErrorsState>('idle');
61
+ const [errors, setErrors] = useState<ParsedError[]>([]);
62
+ const [total, setTotal] = useState(0);
63
+ const [errorMsg, setErrorMsg] = useState('');
64
+
65
+ const fetchErrors = useCallback(async () => {
66
+ setState('loading');
67
+ setErrors([]);
68
+ setErrorMsg('');
69
+ const result = await runCommand('errors').catch((err) => ({
70
+ ok: false as const,
71
+ error: (err as Error).message,
72
+ output: undefined as string | undefined,
73
+ }));
74
+ if (result.ok && result.output) {
75
+ const parsed = parseErrors(result.output);
76
+ setErrors(parsed.errors);
77
+ setTotal(parsed.total);
78
+ setState('success');
79
+ } else if (result.ok) {
80
+ setErrors([]);
81
+ setState('success');
82
+ } else {
83
+ setState('error');
84
+ setErrorMsg(result.error || 'Failed to fetch errors.');
85
+ }
86
+ }, [runCommand]);
87
+
88
+ return (
89
+ <div className="flex h-full flex-col">
90
+ {/* Header with refresh */}
91
+ <div className="flex items-center gap-2 border-b border-gray-alpha-200 px-3 py-2">
92
+ <AlertTriangle className="h-4 w-4 text-red-500" />
93
+ <span className="text-label-13 font-medium text-gray-1000">Errors</span>
94
+ {state === 'success' && <span className="text-[11px] text-gray-600">{total} total</span>}
95
+ <button
96
+ type="button"
97
+ onClick={fetchErrors}
98
+ disabled={state === 'loading'}
99
+ className="ml-auto flex items-center gap-1 rounded px-2 py-1 text-[11px] font-medium text-gray-800 transition-colors hover:bg-gray-alpha-200 disabled:opacity-50"
100
+ >
101
+ {state === 'loading' ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
102
+ {state === 'loading' ? 'Fetching...' : 'Refresh'}
103
+ </button>
104
+ </div>
105
+
106
+ {/* Content */}
107
+ <div className="flex-1 overflow-auto">
108
+ {state === 'idle' && (
109
+ <div className="p-3">
110
+ <EmptyState
111
+ title="Click to fetch errors"
112
+ description="Run d3k errors to see recent browser and server errors."
113
+ action={
114
+ <button
115
+ type="button"
116
+ onClick={fetchErrors}
117
+ className="rounded bg-red-500 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-red-600"
118
+ >
119
+ Fetch Errors
120
+ </button>
121
+ }
122
+ />
123
+ </div>
124
+ )}
125
+
126
+ {state === 'loading' && (
127
+ <div className="flex h-32 items-center justify-center">
128
+ <Loader2 className="h-5 w-5 animate-spin text-gray-600" />
129
+ </div>
130
+ )}
131
+
132
+ {state === 'success' && errors.length === 0 && (
133
+ <div className="p-3">
134
+ <EmptyState
135
+ title="No errors"
136
+ description="No recent errors found in the d3k session."
137
+ icon={<CheckCircle className="h-8 w-8 text-green-500" />}
138
+ />
139
+ </div>
140
+ )}
141
+
142
+ {state === 'success' && errors.length > 0 && (
143
+ <div className="divide-y divide-gray-alpha-100">
144
+ {errors.map((error) => (
145
+ <ErrorRow key={error.key} error={error} />
146
+ ))}
147
+ </div>
148
+ )}
149
+
150
+ {state === 'error' && (
151
+ <div className="p-3">
152
+ <div className="rounded border border-red-200 bg-red-50 p-3">
153
+ <p className="font-mono text-[11px] text-red-600">{errorMsg}</p>
154
+ </div>
155
+ </div>
156
+ )}
157
+ </div>
158
+ </div>
159
+ );
160
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { useDev3000 } from './dev3000-context';
4
+ import type { D3kSource } from './types';
5
+
6
+ const SOURCES: Array<{ label: string; value: D3kSource | 'ALL' }> = [
7
+ { label: 'ALL', value: 'ALL' },
8
+ { label: 'SRV', value: 'SERVER' },
9
+ { label: 'BRW', value: 'BROWSER' },
10
+ { label: 'NET', value: 'NETWORK' },
11
+ { label: 'D3K', value: 'D3K' },
12
+ ];
13
+
14
+ export function Sidebar() {
15
+ const { sourceFilter, setSourceFilter } = useDev3000();
16
+
17
+ return (
18
+ <div className="flex flex-col gap-3 overflow-y-auto">
19
+ {/* Source filter */}
20
+ <div>
21
+ <h3 className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-gray-700">Source</h3>
22
+ <div className="flex flex-wrap gap-1.5">
23
+ {SOURCES.map(({ label, value }) => (
24
+ <button
25
+ key={value}
26
+ type="button"
27
+ onClick={() => setSourceFilter(value)}
28
+ className={`rounded px-2 py-1 text-[11px] font-medium transition-colors ${
29
+ sourceFilter === value
30
+ ? 'bg-blue-600 text-white'
31
+ : 'bg-gray-alpha-100 text-gray-900 hover:bg-gray-alpha-200'
32
+ }`}
33
+ >
34
+ {label}
35
+ </button>
36
+ ))}
37
+ </div>
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,173 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { EmptyState } from '@/components/os-primitives/empty-state';
5
+ import type { D3kLogEntry, D3kSource } from './types';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Source badge colors
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const SOURCE_STYLES: Record<D3kSource, string> = {
12
+ SERVER: 'bg-blue-500/15 text-blue-600',
13
+ BROWSER: 'bg-amber-500/15 text-amber-600',
14
+ NETWORK: 'bg-green-500/15 text-green-600',
15
+ D3K: 'bg-purple-500/15 text-purple-600',
16
+ UNKNOWN: 'bg-gray-500/15 text-gray-600',
17
+ };
18
+
19
+ const LEVEL_TEXT: Record<string, string> = {
20
+ error: 'text-red-500',
21
+ warn: 'text-amber-500',
22
+ info: 'text-gray-1000',
23
+ debug: 'text-gray-600',
24
+ };
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Time formatting
28
+ // ---------------------------------------------------------------------------
29
+
30
+ function formatTimestamp(ts: number): string {
31
+ const d = new Date(ts);
32
+ const h = String(d.getHours()).padStart(2, '0');
33
+ const m = String(d.getMinutes()).padStart(2, '0');
34
+ const s = String(d.getSeconds()).padStart(2, '0');
35
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
36
+ return `${h}:${m}:${s}.${ms}`;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // LogRow
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function LogRow({ entry, index }: { entry: D3kLogEntry; index: number }) {
44
+ const [expanded, setExpanded] = useState(false);
45
+
46
+ return (
47
+ <div>
48
+ <button
49
+ type="button"
50
+ onClick={() => setExpanded(!expanded)}
51
+ className={`flex w-full items-baseline gap-2 px-2 py-0.5 text-left text-[11px] leading-5 transition-colors hover:bg-gray-alpha-200 ${
52
+ index % 2 === 0 ? 'bg-transparent' : 'bg-gray-alpha-50'
53
+ }`}
54
+ >
55
+ {/* Timestamp */}
56
+ <span className="shrink-0 font-mono text-gray-700">{formatTimestamp(entry.timestamp)}</span>
57
+
58
+ {/* Source badge */}
59
+ <span
60
+ className={`shrink-0 rounded px-1 py-0.5 font-mono text-[9px] font-semibold ${SOURCE_STYLES[entry.source]}`}
61
+ >
62
+ {entry.source}
63
+ </span>
64
+
65
+ {/* Sub-tag badge */}
66
+ {entry.tag && (
67
+ <span className="shrink-0 rounded bg-gray-alpha-100 px-1 py-0.5 font-mono text-[9px] text-gray-700">
68
+ {entry.tag}
69
+ </span>
70
+ )}
71
+
72
+ {/* Message */}
73
+ <span className={`min-w-0 truncate font-mono ${LEVEL_TEXT[entry.level] || 'text-gray-1000'}`}>
74
+ {entry.message}
75
+ </span>
76
+ </button>
77
+
78
+ {/* Expanded detail */}
79
+ {expanded && (
80
+ <div className="mx-2 mb-1 rounded border border-gray-alpha-200 bg-gray-alpha-50 p-2">
81
+ <pre className="overflow-auto whitespace-pre-wrap break-all font-mono text-[11px] leading-4 text-gray-1000">
82
+ {entry.message}
83
+ </pre>
84
+ <div className="mt-1 flex gap-2 text-[10px] text-gray-600">
85
+ <span>Source: {entry.source}</span>
86
+ <span>Level: {entry.level}</span>
87
+ <span>ID: {entry.id}</span>
88
+ </div>
89
+ </div>
90
+ )}
91
+ </div>
92
+ );
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // TimelineLog
97
+ // ---------------------------------------------------------------------------
98
+
99
+ interface TimelineLogProps {
100
+ entries: D3kLogEntry[];
101
+ filter: string;
102
+ sourceFilter: D3kSource | 'ALL';
103
+ paused: boolean;
104
+ }
105
+
106
+ export function TimelineLog({ entries, filter, sourceFilter, paused }: TimelineLogProps) {
107
+ const containerRef = useRef<HTMLDivElement>(null);
108
+ const wasAtBottomRef = useRef(true);
109
+ const prevEntryCountRef = useRef(0);
110
+
111
+ const filtered = useMemo(() => {
112
+ let result = entries;
113
+ if (sourceFilter !== 'ALL') {
114
+ result = result.filter((e) => e.source === sourceFilter);
115
+ }
116
+ if (filter) {
117
+ const lower = filter.toLowerCase();
118
+ result = result.filter((e) => e.message.toLowerCase().includes(lower));
119
+ }
120
+ return result;
121
+ }, [entries, filter, sourceFilter]);
122
+
123
+ const handleScroll = () => {
124
+ const el = containerRef.current;
125
+ if (!el) return;
126
+ const threshold = 30;
127
+ wasAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
128
+ };
129
+
130
+ useEffect(() => {
131
+ const el = containerRef.current;
132
+ if (!el) return;
133
+ const hasNewEntries = filtered.length > prevEntryCountRef.current;
134
+ prevEntryCountRef.current = filtered.length;
135
+ if (!paused && hasNewEntries && wasAtBottomRef.current) {
136
+ el.scrollTop = el.scrollHeight;
137
+ }
138
+ }, [filtered.length, paused]);
139
+
140
+ useEffect(() => {
141
+ if (!paused) {
142
+ const el = containerRef.current;
143
+ if (el) {
144
+ el.scrollTop = el.scrollHeight;
145
+ wasAtBottomRef.current = true;
146
+ }
147
+ }
148
+ }, [paused]);
149
+
150
+ if (filtered.length === 0 && !filter && sourceFilter === 'ALL') {
151
+ return (
152
+ <div className="flex h-full items-center justify-center">
153
+ <EmptyState title="No log entries yet" description="Waiting for dev3000 to produce log output..." />
154
+ </div>
155
+ );
156
+ }
157
+
158
+ if (filtered.length === 0) {
159
+ return (
160
+ <div className="flex h-full items-center justify-center">
161
+ <EmptyState title="No matching entries" description="Try adjusting your filter or source selection." />
162
+ </div>
163
+ );
164
+ }
165
+
166
+ return (
167
+ <div ref={containerRef} className="h-full overflow-auto" onScroll={handleScroll}>
168
+ {filtered.map((entry, i) => (
169
+ <LogRow key={entry.id} entry={entry} index={i} />
170
+ ))}
171
+ </div>
172
+ );
173
+ }
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext } from 'react';
4
+ import type { D3kLogEntry, D3kSource, D3kTab } from './types';
5
+
6
+ export interface Dev3000ContextValue {
7
+ buffer: {
8
+ entries: D3kLogEntry[];
9
+ push: (entry: D3kLogEntry) => void;
10
+ clear: () => void;
11
+ };
12
+ filter: string;
13
+ setFilter: (f: string) => void;
14
+ sourceFilter: D3kSource | 'ALL';
15
+ setSourceFilter: (s: D3kSource | 'ALL') => void;
16
+ paused: boolean;
17
+ setPaused: (p: boolean) => void;
18
+ activeTab: D3kTab;
19
+ setActiveTab: (t: D3kTab) => void;
20
+ runCommand: (command: string, args?: string[]) => Promise<{ ok: boolean; output?: string; error?: string }>;
21
+ }
22
+
23
+ export const Dev3000Context = createContext<Dev3000ContextValue | null>(null);
24
+
25
+ export function useDev3000(): Dev3000ContextValue {
26
+ const ctx = useContext(Dev3000Context);
27
+ if (!ctx) throw new Error('useDev3000 must be used within <Dev3000App>');
28
+ return ctx;
29
+ }
@@ -0,0 +1,4 @@
1
+ export { Dev3000App } from './Dev3000App';
2
+ export type { Dev3000ContextValue } from './dev3000-context';
3
+ export { useDev3000 } from './dev3000-context';
4
+ export type { D3kLogEntry, D3kSource, D3kTab } from './types';
@@ -0,0 +1,48 @@
1
+ import { type Static, Type } from '@sinclair/typebox';
2
+
3
+ // --- Log entry published by the service ---
4
+
5
+ export const D3kLogEntrySchema = Type.Object({
6
+ id: Type.String(),
7
+ timestamp: Type.Number(),
8
+ source: Type.Union([
9
+ Type.Literal('SERVER'),
10
+ Type.Literal('BROWSER'),
11
+ Type.Literal('NETWORK'),
12
+ Type.Literal('D3K'),
13
+ Type.Literal('UNKNOWN'),
14
+ ]),
15
+ level: Type.Union([Type.Literal('info'), Type.Literal('warn'), Type.Literal('error'), Type.Literal('debug')]),
16
+ message: Type.String(),
17
+ tag: Type.Optional(Type.String()),
18
+ });
19
+ export type D3kLogEntrySchema = Static<typeof D3kLogEntrySchema>;
20
+
21
+ // --- Command request/response ---
22
+
23
+ export const D3kCmdRequest = Type.Object({
24
+ command: Type.String(), // e.g. 'errors', 'logs', 'fix'
25
+ args: Type.Optional(Type.Array(Type.String())),
26
+ });
27
+ export type D3kCmdRequest = Static<typeof D3kCmdRequest>;
28
+
29
+ export const D3kCmdResponse = Type.Object({
30
+ ok: Type.Boolean(),
31
+ output: Type.Optional(Type.String()),
32
+ error: Type.Optional(Type.String()),
33
+ });
34
+ export type D3kCmdResponse = Static<typeof D3kCmdResponse>;
35
+
36
+ // --- Status request/response ---
37
+
38
+ export const D3kStatusRequest = Type.Object({});
39
+ export type D3kStatusRequest = Static<typeof D3kStatusRequest>;
40
+
41
+ export const D3kStatusResponse = Type.Object({
42
+ d3kRunning: Type.Boolean(),
43
+ lineCount: Type.Number(),
44
+ version: Type.String(),
45
+ uptime: Type.Number(),
46
+ entries: Type.Optional(Type.Array(D3kLogEntrySchema)),
47
+ });
48
+ export type D3kStatusResponse = Static<typeof D3kStatusResponse>;