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,209 @@
1
+ 'use client';
2
+
3
+ import { Filter, Inbox, Pause, Play, Radio, Send, Trash2 } from 'lucide-react';
4
+ import { useCallback, useMemo, useRef, useState } from 'react';
5
+ import { SectionHeader, 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 { MessageLog } from './MessageLog';
9
+ import type { NatsViewerContextValue } from './nats-viewer-context';
10
+ import { NatsViewerContext } from './nats-viewer-context';
11
+ import { PublishPanel } from './PublishPanel';
12
+ import { RequestPanel } from './RequestPanel';
13
+ import { Sidebar } from './Sidebar';
14
+ import { useMessageBuffer } from './use-message-buffer';
15
+
16
+ export type { NatsViewerContextValue } from './nats-viewer-context';
17
+ export { useNatsViewer } from './nats-viewer-context';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // NatsViewer component
21
+ // ---------------------------------------------------------------------------
22
+
23
+ type SidebarSection = 'subjects' | 'publish' | 'request';
24
+
25
+ export function NatsViewer(_props: { windowId: string; meta?: Record<string, unknown> }) {
26
+ const [activeSection, setActiveSection] = useState<SidebarSection>('subjects');
27
+ const [paused, setPaused] = useState(false);
28
+ const [filter, setFilter] = useState('');
29
+
30
+ const { connected, subscribe } = useNats();
31
+ const buffer = useMessageBuffer();
32
+
33
+ // Track active subscriptions: subject -> unsub function
34
+ const unsubMapRef = useRef<Map<string, () => void>>(new Map());
35
+ const [subscriptions, setSubscriptions] = useState<Set<string>>(new Set());
36
+
37
+ // Stable ref for buffer.push so the subscribe callback never goes stale
38
+ const pushRef = useRef(buffer.push);
39
+ pushRef.current = buffer.push;
40
+
41
+ const addSubscription = useCallback(
42
+ (subject: string) => {
43
+ if (unsubMapRef.current.has(subject)) return;
44
+
45
+ const unsub = subscribe(subject, (data: unknown, actualSubject: string) => {
46
+ pushRef.current({ subject: actualSubject, payload: data, direction: 'in' });
47
+ });
48
+
49
+ unsubMapRef.current.set(subject, unsub);
50
+ setSubscriptions((prev) => new Set(prev).add(subject));
51
+ },
52
+ [subscribe]
53
+ );
54
+
55
+ const removeSubscription = useCallback((subject: string) => {
56
+ const unsub = unsubMapRef.current.get(subject);
57
+ if (unsub) {
58
+ unsub();
59
+ unsubMapRef.current.delete(subject);
60
+ }
61
+ setSubscriptions((prev) => {
62
+ const next = new Set(prev);
63
+ next.delete(subject);
64
+ return next;
65
+ });
66
+ }, []);
67
+
68
+ // Filtered count for toolbar display
69
+ const filteredEntries = useMemo(() => {
70
+ if (!filter) return buffer.entries;
71
+ const lower = filter.toLowerCase();
72
+ return buffer.entries.filter((e) => {
73
+ if (e.subject.toLowerCase().includes(lower)) return true;
74
+ try {
75
+ return JSON.stringify(e.payload).toLowerCase().includes(lower);
76
+ } catch {
77
+ return false;
78
+ }
79
+ });
80
+ }, [buffer.entries, filter]);
81
+
82
+ const messageCountLabel = filter
83
+ ? `${filteredEntries.length}/${buffer.entries.length} messages`
84
+ : `${buffer.entries.length} messages`;
85
+
86
+ // Context value
87
+ const ctxValue = useMemo<NatsViewerContextValue>(
88
+ () => ({
89
+ subscriptions,
90
+ addSubscription,
91
+ removeSubscription,
92
+ buffer,
93
+ filter,
94
+ setFilter,
95
+ paused,
96
+ setPaused,
97
+ }),
98
+ [subscriptions, addSubscription, removeSubscription, buffer, filter, paused]
99
+ );
100
+
101
+ return (
102
+ <NatsViewerContext.Provider value={ctxValue}>
103
+ <div className="flex h-full flex-col bg-background-100">
104
+ <div className="flex-1 overflow-hidden">
105
+ <SplitPane defaultSize={250} min={180} max={360} collapseBelow={500}>
106
+ {/* ---- Sidebar ---- */}
107
+ <SplitPane.Panel className="bg-gray-alpha-50">
108
+ <SidebarNav label="NATS Viewer" title="NATS Viewer">
109
+ <SidebarNav.Group title="Subscriptions">
110
+ <SidebarNav.Item
111
+ active={activeSection === 'subjects'}
112
+ onClick={() => setActiveSection('subjects')}
113
+ icon={<Radio />}
114
+ >
115
+ Subjects
116
+ </SidebarNav.Item>
117
+ </SidebarNav.Group>
118
+
119
+ <SidebarNav.Group title="Tools">
120
+ <SidebarNav.Item
121
+ active={activeSection === 'publish'}
122
+ onClick={() => setActiveSection('publish')}
123
+ icon={<Send />}
124
+ >
125
+ Publish
126
+ </SidebarNav.Item>
127
+ <SidebarNav.Item
128
+ active={activeSection === 'request'}
129
+ onClick={() => setActiveSection('request')}
130
+ icon={<Inbox />}
131
+ >
132
+ Request
133
+ </SidebarNav.Item>
134
+ </SidebarNav.Group>
135
+ </SidebarNav>
136
+
137
+ {/* Sidebar detail panel */}
138
+ <div className="flex-1 overflow-y-auto border-t border-gray-alpha-200 p-3">
139
+ {activeSection === 'subjects' && <Sidebar />}
140
+ {activeSection === 'publish' && (
141
+ <div className="flex flex-col gap-2">
142
+ <SectionHeader title="Publish" description="Send a message to a subject." />
143
+ <PublishPanel onPublish={buffer.push} />
144
+ </div>
145
+ )}
146
+ {activeSection === 'request' && (
147
+ <div className="flex flex-col gap-2">
148
+ <SectionHeader title="Request" description="Send a request and view the reply." />
149
+ <RequestPanel onMessage={buffer.push} />
150
+ </div>
151
+ )}
152
+ </div>
153
+ </SplitPane.Panel>
154
+
155
+ {/* ---- Main log area ---- */}
156
+ <SplitPane.Panel>
157
+ <div className="flex h-full flex-col">
158
+ {/* Toolbar */}
159
+ <Toolbar>
160
+ <Toolbar.Group>
161
+ <Toolbar.Button
162
+ tooltip={paused ? 'Resume' : 'Pause'}
163
+ onClick={() => setPaused(!paused)}
164
+ active={paused}
165
+ >
166
+ {paused ? <Play /> : <Pause />}
167
+ </Toolbar.Button>
168
+ <Toolbar.Button tooltip="Clear log" onClick={() => buffer.clear()}>
169
+ <Trash2 />
170
+ </Toolbar.Button>
171
+ </Toolbar.Group>
172
+ <Toolbar.Separator />
173
+ <Toolbar.Group>
174
+ <Toolbar.Button tooltip="Filter">
175
+ <Filter />
176
+ </Toolbar.Button>
177
+ </Toolbar.Group>
178
+ <Toolbar.Input
179
+ placeholder="Filter by subject or payload..."
180
+ value={filter}
181
+ onChange={(e) => setFilter(e.target.value)}
182
+ />
183
+ <Toolbar.Spacer />
184
+ <Toolbar.Text>{messageCountLabel}</Toolbar.Text>
185
+ </Toolbar>
186
+
187
+ {/* Message log */}
188
+ <div className="flex-1 overflow-hidden">
189
+ <MessageLog entries={buffer.entries} filter={filter} paused={paused} />
190
+ </div>
191
+ </div>
192
+ </SplitPane.Panel>
193
+ </SplitPane>
194
+ </div>
195
+
196
+ {/* Status bar */}
197
+ <StatusBar>
198
+ <StatusBar.Item>NATS Viewer</StatusBar.Item>
199
+ <StatusBar.Separator />
200
+ <StatusBar.Item>{subscriptions.size} subscription(s)</StatusBar.Item>
201
+ <StatusBar.Spacer />
202
+ <StatusBar.Item variant={connected ? 'success' : 'default'}>
203
+ {connected ? 'connected' : 'disconnected'}
204
+ </StatusBar.Item>
205
+ </StatusBar>
206
+ </div>
207
+ </NatsViewerContext.Provider>
208
+ );
209
+ }
@@ -0,0 +1,113 @@
1
+ 'use client';
2
+
3
+ import { Send } from 'lucide-react';
4
+ import { useCallback, useMemo, useRef, useState } from 'react';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+ import { useKhalAuth } from '@/lib/auth/use-auth';
8
+ import { useNats } from '@/lib/hooks/use-nats';
9
+ import { SUBJECTS } from '@/lib/subjects';
10
+ import type { LogEntry } from './types';
11
+
12
+ /** Build quick-pick subjects using the current orgId. */
13
+ function buildQuickPickSubjects(orgId: string): string[] {
14
+ if (!orgId) return [];
15
+ return [SUBJECTS.echo(orgId), SUBJECTS.pty.create(orgId), SUBJECTS.pty.list(orgId), SUBJECTS.notify.broadcast(orgId)];
16
+ }
17
+
18
+ interface PublishPanelProps {
19
+ /** Optional callback to push an entry to the message buffer. */
20
+ onPublish?: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
21
+ }
22
+
23
+ export function PublishPanel({ onPublish }: PublishPanelProps) {
24
+ const { connected, publish } = useNats();
25
+ const auth = useKhalAuth();
26
+ const orgId = auth?.orgId ?? '';
27
+ const quickPicks = useMemo(() => buildQuickPickSubjects(orgId), [orgId]);
28
+
29
+ const [subject, setSubject] = useState('');
30
+ const [payload, setPayload] = useState('');
31
+ const [error, setError] = useState<string | null>(null);
32
+ const [sent, setSent] = useState(false);
33
+ const sentTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
34
+
35
+ const handleSend = useCallback(() => {
36
+ setError(null);
37
+
38
+ let parsed: unknown;
39
+ if (payload.trim()) {
40
+ try {
41
+ parsed = JSON.parse(payload);
42
+ } catch (e) {
43
+ setError(`Invalid JSON: ${(e as Error).message}`);
44
+ return;
45
+ }
46
+ }
47
+
48
+ publish(subject, parsed);
49
+
50
+ // Push to buffer as outgoing message
51
+ onPublish?.({ subject, payload: parsed, direction: 'out' });
52
+
53
+ // Show "Sent!" indicator
54
+ setSent(true);
55
+ if (sentTimer.current) clearTimeout(sentTimer.current);
56
+ sentTimer.current = setTimeout(() => setSent(false), 1000);
57
+ }, [subject, payload, publish, onPublish]);
58
+
59
+ const canSend = subject.trim().length > 0 && connected;
60
+
61
+ return (
62
+ <div className="flex flex-col gap-2">
63
+ {/* Subject input */}
64
+ <Input
65
+ size="small"
66
+ placeholder={orgId ? SUBJECTS.echo(orgId) : 'khal.<orgId>.echo'}
67
+ value={subject}
68
+ onChange={(e) => {
69
+ setSubject(e.target.value);
70
+ setError(null);
71
+ }}
72
+ />
73
+
74
+ {/* Quick-pick subjects */}
75
+ <div className="flex flex-wrap gap-1">
76
+ {quickPicks.map((s) => (
77
+ <button
78
+ key={s}
79
+ type="button"
80
+ className="rounded border border-gray-alpha-300 px-1.5 py-0.5 text-[10px] font-mono text-gray-800 hover:bg-gray-alpha-100 transition-colors"
81
+ onClick={() => setSubject(s)}
82
+ >
83
+ {s}
84
+ </button>
85
+ ))}
86
+ </div>
87
+
88
+ {/* Payload textarea */}
89
+ <textarea
90
+ className="w-full rounded-md border border-gray-alpha-400 bg-background-100 px-2 py-1.5 font-mono text-xs text-gray-1000 placeholder:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:ring-offset-1 resize-none"
91
+ rows={4}
92
+ placeholder='{"key": "value"}'
93
+ value={payload}
94
+ onChange={(e) => {
95
+ setPayload(e.target.value);
96
+ setError(null);
97
+ }}
98
+ />
99
+
100
+ {/* Error message */}
101
+ {error && <p className="text-[11px] text-red-600">{error}</p>}
102
+
103
+ {/* Send button + Sent indicator */}
104
+ <div className="flex items-center gap-2">
105
+ <Button size="small" disabled={!canSend} onClick={handleSend} prefix={<Send className="h-3 w-3" />}>
106
+ Send
107
+ </Button>
108
+ {sent && <span className="text-[11px] font-medium text-green-600 animate-pulse">Sent!</span>}
109
+ {!connected && <span className="text-[11px] text-gray-600">Not connected</span>}
110
+ </div>
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,167 @@
1
+ 'use client';
2
+
3
+ import { Inbox } from 'lucide-react';
4
+ import { useCallback, useMemo, useState } from 'react';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Spinner } from '@/components/ui/spinner';
8
+ import { useKhalAuth } from '@/lib/auth/use-auth';
9
+ import { useNats } from '@/lib/hooks/use-nats';
10
+ import { SUBJECTS } from '@/lib/subjects';
11
+ import type { LogEntry } from './types';
12
+
13
+ /** Build quick-pick subjects using the current orgId. */
14
+ function buildQuickPickSubjects(orgId: string): string[] {
15
+ if (!orgId) return [];
16
+ return [SUBJECTS.echo(orgId), SUBJECTS.pty.create(orgId), SUBJECTS.pty.list(orgId)];
17
+ }
18
+
19
+ type RequestState =
20
+ | { status: 'idle' }
21
+ | { status: 'loading' }
22
+ | { status: 'success'; data: unknown }
23
+ | { status: 'error'; message: string };
24
+
25
+ interface RequestPanelProps {
26
+ /** Optional callback to push an entry to the message buffer. */
27
+ onMessage?: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
28
+ }
29
+
30
+ export function RequestPanel({ onMessage }: RequestPanelProps) {
31
+ const { connected, request } = useNats();
32
+ const auth = useKhalAuth();
33
+ const orgId = auth?.orgId ?? '';
34
+ const quickPicks = useMemo(() => buildQuickPickSubjects(orgId), [orgId]);
35
+
36
+ const [subject, setSubject] = useState('');
37
+ const [payload, setPayload] = useState('');
38
+ const [timeout, setTimeout_] = useState(5000);
39
+ const [jsonError, setJsonError] = useState<string | null>(null);
40
+ const [reqState, setReqState] = useState<RequestState>({ status: 'idle' });
41
+
42
+ const handleSend = useCallback(async () => {
43
+ setJsonError(null);
44
+
45
+ let parsed: unknown;
46
+ if (payload.trim()) {
47
+ try {
48
+ parsed = JSON.parse(payload);
49
+ } catch (e) {
50
+ setJsonError(`Invalid JSON: ${(e as Error).message}`);
51
+ return;
52
+ }
53
+ }
54
+
55
+ // Clear previous response and start loading
56
+ setReqState({ status: 'loading' });
57
+
58
+ // Push outgoing request to buffer
59
+ onMessage?.({ subject, payload: parsed, direction: 'out' });
60
+
61
+ try {
62
+ const response = await request(subject, parsed, timeout);
63
+ setReqState({ status: 'success', data: response });
64
+
65
+ // Push response to buffer
66
+ onMessage?.({ subject: `${subject} (reply)`, payload: response, direction: 'in' });
67
+ } catch (e) {
68
+ const message = e instanceof Error ? e.message : String(e);
69
+ setReqState({ status: 'error', message });
70
+ }
71
+ }, [subject, payload, timeout, request, onMessage]);
72
+
73
+ const canSend = subject.trim().length > 0 && connected && reqState.status !== 'loading';
74
+
75
+ return (
76
+ <div className="flex flex-col gap-2">
77
+ {/* Subject input */}
78
+ <Input
79
+ size="small"
80
+ placeholder={orgId ? SUBJECTS.echo(orgId) : 'khal.<orgId>.echo'}
81
+ value={subject}
82
+ onChange={(e) => {
83
+ setSubject(e.target.value);
84
+ setJsonError(null);
85
+ }}
86
+ />
87
+
88
+ {/* Quick-pick subjects */}
89
+ <div className="flex flex-wrap gap-1">
90
+ {quickPicks.map((s) => (
91
+ <button
92
+ key={s}
93
+ type="button"
94
+ className="rounded border border-gray-alpha-300 px-1.5 py-0.5 text-[10px] font-mono text-gray-800 hover:bg-gray-alpha-100 transition-colors"
95
+ onClick={() => setSubject(s)}
96
+ >
97
+ {s}
98
+ </button>
99
+ ))}
100
+ </div>
101
+
102
+ {/* Payload textarea */}
103
+ <textarea
104
+ className="w-full rounded-md border border-gray-alpha-400 bg-background-100 px-2 py-1.5 font-mono text-xs text-gray-1000 placeholder:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:ring-offset-1 resize-none"
105
+ rows={4}
106
+ placeholder="{}"
107
+ value={payload}
108
+ onChange={(e) => {
109
+ setPayload(e.target.value);
110
+ setJsonError(null);
111
+ }}
112
+ />
113
+
114
+ {/* Timeout input */}
115
+ <div className="flex items-center gap-2">
116
+ <label className="text-[11px] text-gray-800 shrink-0">Timeout</label>
117
+ <Input
118
+ size="small"
119
+ type="number"
120
+ className="w-20 text-xs"
121
+ value={timeout}
122
+ onChange={(e) => setTimeout_(Number(e.target.value) || 5000)}
123
+ />
124
+ <span className="text-[11px] text-gray-700">ms</span>
125
+ </div>
126
+
127
+ {/* JSON error */}
128
+ {jsonError && <p className="text-[11px] text-red-600">{jsonError}</p>}
129
+
130
+ {/* Send button */}
131
+ <div className="flex items-center gap-2">
132
+ <Button
133
+ size="small"
134
+ disabled={!canSend}
135
+ onClick={handleSend}
136
+ loading={reqState.status === 'loading'}
137
+ prefix={<Inbox className="h-3 w-3" />}
138
+ >
139
+ Send Request
140
+ </Button>
141
+ {!connected && <span className="text-[11px] text-gray-600">Not connected</span>}
142
+ </div>
143
+
144
+ {/* Response area */}
145
+ {reqState.status === 'loading' && (
146
+ <div className="flex items-center gap-2 rounded border border-gray-alpha-300 bg-gray-alpha-50 px-2 py-2">
147
+ <Spinner size="sm" />
148
+ <span className="text-[11px] text-gray-800">Waiting for reply...</span>
149
+ </div>
150
+ )}
151
+
152
+ {reqState.status === 'success' && (
153
+ <div className="rounded border border-gray-alpha-400 bg-gray-alpha-50 p-2 overflow-auto max-h-48">
154
+ <pre className="font-mono text-xs text-gray-1000 whitespace-pre-wrap break-all">
155
+ {typeof reqState.data === 'string' ? reqState.data : JSON.stringify(reqState.data, null, 2)}
156
+ </pre>
157
+ </div>
158
+ )}
159
+
160
+ {reqState.status === 'error' && (
161
+ <div className="rounded border border-red-300 bg-red-50 dark:bg-red-950/20 p-2">
162
+ <p className="font-mono text-[11px] text-red-600 break-all">{reqState.message}</p>
163
+ </div>
164
+ )}
165
+ </div>
166
+ );
167
+ }
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+
3
+ import { Radio } from 'lucide-react';
4
+ import { Separator } from '@/components/ui/separator';
5
+ import { useKhalAuth } from '@/lib/auth/use-auth';
6
+ import { ActiveSubs } from './ActiveSubs';
7
+ import { useNatsViewer } from './nats-viewer-context';
8
+ import { SubjectCatalog } from './SubjectCatalog';
9
+ import { SubscribeInput } from './SubscribeInput';
10
+
11
+ export function Sidebar() {
12
+ const { subscriptions, addSubscription, removeSubscription } = useNatsViewer();
13
+ const auth = useKhalAuth();
14
+ const orgId = auth?.orgId ?? '';
15
+ const catchAll = orgId ? `khal.${orgId}.>` : 'khal.>';
16
+ const catchAllActive = subscriptions.has(catchAll);
17
+
18
+ const toggleCatchAll = () => {
19
+ if (catchAllActive) {
20
+ removeSubscription(catchAll);
21
+ } else {
22
+ addSubscription(catchAll);
23
+ }
24
+ };
25
+
26
+ return (
27
+ <div className="flex flex-col gap-3 overflow-y-auto">
28
+ {/* Catch-all toggle */}
29
+ <button
30
+ onClick={toggleCatchAll}
31
+ className={`flex items-center gap-2 rounded-md px-2 py-1.5 text-xs font-medium transition-colors ${
32
+ catchAllActive
33
+ ? 'bg-green-500/15 text-green-700 hover:bg-green-500/25'
34
+ : 'bg-gray-alpha-100 text-gray-900 hover:bg-gray-alpha-200'
35
+ }`}
36
+ >
37
+ <Radio className="h-3.5 w-3.5" />
38
+ <span className="font-mono">{catchAll}</span>
39
+ <span className="ml-auto text-[11px]">{catchAllActive ? 'ON' : 'OFF'}</span>
40
+ </button>
41
+
42
+ {/* Custom subscribe input */}
43
+ <SubscribeInput />
44
+
45
+ <Separator />
46
+
47
+ {/* Known Subjects */}
48
+ <div>
49
+ <h3 className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-gray-700">Known Subjects</h3>
50
+ <SubjectCatalog />
51
+ </div>
52
+
53
+ <Separator />
54
+
55
+ {/* Active Subscriptions */}
56
+ <div>
57
+ <h3 className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-gray-700">Active Subscriptions</h3>
58
+ <ActiveSubs />
59
+ </div>
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { useKhalAuth } from '@/lib/auth/use-auth';
5
+ import { SUBJECTS } from '@/lib/subjects';
6
+ import { useNatsViewer } from './nats-viewer-context';
7
+
8
+ /**
9
+ * Build a list of known static subjects using the current orgId.
10
+ * Only includes subjects that don't require dynamic parameters
11
+ * beyond orgId (i.e., skips session-scoped subjects like pty.data).
12
+ */
13
+ function buildKnownSubjects(orgId: string, userId: string): string[] {
14
+ if (!orgId) return [];
15
+ const subjects = [
16
+ SUBJECTS.echo(orgId),
17
+ SUBJECTS.system.health(orgId),
18
+ SUBJECTS.pty.create(orgId),
19
+ SUBJECTS.pty.destroy(orgId),
20
+ SUBJECTS.pty.list(orgId),
21
+ SUBJECTS.fs.list(orgId),
22
+ SUBJECTS.fs.read(orgId),
23
+ SUBJECTS.fs.write(orgId),
24
+ SUBJECTS.fs.search(orgId),
25
+ SUBJECTS.notify.broadcast(orgId),
26
+ ];
27
+ if (userId) {
28
+ subjects.push(SUBJECTS.desktop.cmd.all(orgId, userId), SUBJECTS.desktop.event.all(orgId, userId));
29
+ }
30
+ return subjects.sort();
31
+ }
32
+
33
+ export function SubjectCatalog() {
34
+ const { subscriptions, addSubscription, removeSubscription } = useNatsViewer();
35
+ const auth = useKhalAuth();
36
+ const orgId = auth?.orgId ?? '';
37
+ const userId = auth?.userId ?? '';
38
+
39
+ const knownSubjects = useMemo(() => buildKnownSubjects(orgId, userId), [orgId, userId]);
40
+
41
+ return (
42
+ <div className="flex flex-col gap-0.5">
43
+ {knownSubjects.map((subject) => {
44
+ const active = subscriptions.has(subject);
45
+ return (
46
+ <button
47
+ key={subject}
48
+ onClick={() => (active ? removeSubscription(subject) : addSubscription(subject))}
49
+ className="group flex items-center gap-2 rounded px-1.5 py-0.5 text-left transition-colors hover:bg-gray-alpha-100"
50
+ >
51
+ <span
52
+ className={`inline-block h-2 w-2 shrink-0 rounded-full transition-colors ${
53
+ active ? 'bg-green-500' : 'bg-gray-400 group-hover:bg-gray-500'
54
+ }`}
55
+ />
56
+ <span className="min-w-0 truncate font-mono text-xs text-gray-900 group-hover:text-gray-1000">
57
+ {subject}
58
+ </span>
59
+ </button>
60
+ );
61
+ })}
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { Plus } from 'lucide-react';
4
+ import { useState } from 'react';
5
+ import { useNatsViewer } from './nats-viewer-context';
6
+
7
+ export function SubscribeInput() {
8
+ const { addSubscription } = useNatsViewer();
9
+ const [value, setValue] = useState('');
10
+ const [error, setError] = useState('');
11
+
12
+ const handleSubmit = () => {
13
+ const trimmed = value.trim();
14
+ if (!trimmed) return;
15
+
16
+ if (!trimmed.startsWith('khal.')) {
17
+ setError('Subject must start with "khal."');
18
+ return;
19
+ }
20
+
21
+ setError('');
22
+ addSubscription(trimmed);
23
+ setValue('');
24
+ };
25
+
26
+ const handleKeyDown = (e: React.KeyboardEvent) => {
27
+ if (e.key === 'Enter') {
28
+ e.preventDefault();
29
+ handleSubmit();
30
+ }
31
+ };
32
+
33
+ return (
34
+ <div className="flex flex-col gap-1">
35
+ <div className="flex items-center gap-1">
36
+ <input
37
+ type="text"
38
+ value={value}
39
+ onChange={(e) => {
40
+ setValue(e.target.value);
41
+ if (error) setError('');
42
+ }}
43
+ onKeyDown={handleKeyDown}
44
+ placeholder="khal.custom.subject"
45
+ className="h-7 flex-1 rounded border border-gray-alpha-400 bg-background-100 px-2 font-mono text-xs text-gray-1000 placeholder:text-gray-600 focus:outline-none focus:ring-1 focus:ring-blue-700"
46
+ />
47
+ <button
48
+ onClick={handleSubmit}
49
+ disabled={!value.trim()}
50
+ className="flex h-7 shrink-0 items-center gap-1 rounded border border-gray-alpha-400 bg-background-100 px-2 text-xs text-gray-900 transition-colors hover:bg-gray-alpha-100 hover:text-gray-1000 disabled:opacity-40 disabled:pointer-events-none"
51
+ >
52
+ <Plus className="h-3 w-3" />
53
+ Sub
54
+ </button>
55
+ </div>
56
+ {error && <p className="px-0.5 text-[11px] text-red-600">{error}</p>}
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,5 @@
1
+ export { MessageLog } from './MessageLog';
2
+ export type { NatsViewerContextValue } from './NatsViewer';
3
+ export { NatsViewer, useNatsViewer } from './NatsViewer';
4
+ export type { LogEntry } from './types';
5
+ export { useMessageBuffer } from './use-message-buffer';