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,247 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { EmptyState } from '@/components/os-primitives/empty-state';
5
+ import type { LogEntry } from './types';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Hash-to-color for subject segments
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const SUBJECT_COLORS = [
12
+ 'text-blue-400',
13
+ 'text-green-400',
14
+ 'text-amber-400',
15
+ 'text-purple-400',
16
+ 'text-pink-400',
17
+ 'text-cyan-400',
18
+ 'text-orange-400',
19
+ 'text-teal-400',
20
+ 'text-red-400',
21
+ 'text-indigo-400',
22
+ ];
23
+
24
+ function hashColor(segment: string): string {
25
+ let hash = 0;
26
+ for (let i = 0; i < segment.length; i++) {
27
+ hash = (hash * 31 + segment.charCodeAt(i)) | 0;
28
+ }
29
+ return SUBJECT_COLORS[Math.abs(hash) % SUBJECT_COLORS.length];
30
+ }
31
+
32
+ function subjectColor(subject: string): string {
33
+ // Color by first segment after "os."
34
+ const parts = subject.split('.');
35
+ const key = parts.length > 1 ? parts[1] : parts[0];
36
+ return hashColor(key);
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Time formatting
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function formatTimestamp(ts: number): string {
44
+ const d = new Date(ts);
45
+ const h = String(d.getHours()).padStart(2, '0');
46
+ const m = String(d.getMinutes()).padStart(2, '0');
47
+ const s = String(d.getSeconds()).padStart(2, '0');
48
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
49
+ return `${h}:${m}:${s}.${ms}`;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Base64 detection and decoding
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const BASE64_RE = /^[A-Za-z0-9+/]{4,}={0,2}$/;
57
+
58
+ function isBase64(value: string): boolean {
59
+ if (value.length < 4) return false;
60
+ return BASE64_RE.test(value.trim());
61
+ }
62
+
63
+ function tryDecodeBase64(value: string): string | null {
64
+ try {
65
+ const decoded = atob(value.trim());
66
+ // Check if the decoded result is valid UTF-8 text (printable)
67
+ const isPrintable = /^[\x20-\x7E\t\n\r]+$/.test(decoded);
68
+ return isPrintable ? decoded : null;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ function decodeBase64InPayload(payload: unknown): unknown {
75
+ if (typeof payload === 'string' && isBase64(payload)) {
76
+ const decoded = tryDecodeBase64(payload);
77
+ return decoded !== null ? decoded : payload;
78
+ }
79
+ if (Array.isArray(payload)) {
80
+ return payload.map(decodeBase64InPayload);
81
+ }
82
+ if (payload !== null && typeof payload === 'object') {
83
+ const result: Record<string, unknown> = {};
84
+ for (const [key, value] of Object.entries(payload)) {
85
+ result[key] = decodeBase64InPayload(value);
86
+ }
87
+ return result;
88
+ }
89
+ return payload;
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Payload truncation
94
+ // ---------------------------------------------------------------------------
95
+
96
+ function truncatePayload(payload: unknown, maxLen = 120): string {
97
+ try {
98
+ const str = JSON.stringify(payload);
99
+ return str.length > maxLen ? `${str.slice(0, maxLen)}...` : str;
100
+ } catch {
101
+ return String(payload);
102
+ }
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // MessageRow
107
+ // ---------------------------------------------------------------------------
108
+
109
+ function MessageRow({ entry, index }: { entry: LogEntry; index: number }) {
110
+ const [expanded, setExpanded] = useState(false);
111
+ const [decodeBase64, setDecodeBase64] = useState(false);
112
+
113
+ const displayPayload = decodeBase64 ? decodeBase64InPayload(entry.payload) : entry.payload;
114
+
115
+ return (
116
+ <div>
117
+ <button
118
+ type="button"
119
+ onClick={() => setExpanded(!expanded)}
120
+ 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 ${
121
+ index % 2 === 0 ? 'bg-transparent' : 'bg-gray-alpha-50'
122
+ }`}
123
+ >
124
+ {/* Timestamp */}
125
+ <span className="shrink-0 font-mono text-gray-700">{formatTimestamp(entry.timestamp)}</span>
126
+
127
+ {/* Direction arrow */}
128
+ <span className={`shrink-0 font-mono ${entry.direction === 'in' ? 'text-green-500' : 'text-blue-500'}`}>
129
+ {entry.direction === 'in' ? '\u2190' : '\u2192'}
130
+ </span>
131
+
132
+ {/* Subject */}
133
+ <span className={`shrink-0 font-mono font-medium ${subjectColor(entry.subject)}`}>{entry.subject}</span>
134
+
135
+ {/* Payload preview */}
136
+ <span className="min-w-0 truncate font-mono text-gray-600">{truncatePayload(entry.payload)}</span>
137
+ </button>
138
+
139
+ {/* Expanded detail */}
140
+ {expanded && (
141
+ <div className="mx-2 mb-1 rounded border border-gray-alpha-200 bg-gray-alpha-50 p-2">
142
+ <div className="mb-1 flex items-center gap-2">
143
+ <button
144
+ type="button"
145
+ onClick={() => setDecodeBase64(!decodeBase64)}
146
+ className={`rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors ${
147
+ decodeBase64 ? 'bg-blue-600 text-white' : 'bg-gray-alpha-200 text-gray-800 hover:bg-gray-alpha-300'
148
+ }`}
149
+ >
150
+ Decode Base64
151
+ </button>
152
+ </div>
153
+ <pre className="overflow-auto whitespace-pre-wrap break-all font-mono text-[11px] leading-4 text-gray-1000">
154
+ {JSON.stringify(displayPayload, null, 2)}
155
+ </pre>
156
+ </div>
157
+ )}
158
+ </div>
159
+ );
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // MessageLog
164
+ // ---------------------------------------------------------------------------
165
+
166
+ interface MessageLogProps {
167
+ entries: LogEntry[];
168
+ filter: string;
169
+ paused: boolean;
170
+ }
171
+
172
+ export function MessageLog({ entries, filter, paused }: MessageLogProps) {
173
+ const containerRef = useRef<HTMLDivElement>(null);
174
+ const wasAtBottomRef = useRef(true);
175
+ const prevEntryCountRef = useRef(0);
176
+
177
+ // Filter entries
178
+ const filtered = useMemo(() => {
179
+ if (!filter) return entries;
180
+ const lower = filter.toLowerCase();
181
+ return entries.filter((e) => {
182
+ if (e.subject.toLowerCase().includes(lower)) return true;
183
+ try {
184
+ const payloadStr = JSON.stringify(e.payload).toLowerCase();
185
+ return payloadStr.includes(lower);
186
+ } catch {
187
+ return false;
188
+ }
189
+ });
190
+ }, [entries, filter]);
191
+
192
+ // Track scroll position to know if we're at the bottom
193
+ const handleScroll = () => {
194
+ const el = containerRef.current;
195
+ if (!el) return;
196
+ const threshold = 30;
197
+ wasAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
198
+ };
199
+
200
+ // Auto-scroll when new entries arrive and not paused
201
+ useEffect(() => {
202
+ const el = containerRef.current;
203
+ if (!el) return;
204
+
205
+ const hasNewEntries = filtered.length > prevEntryCountRef.current;
206
+ prevEntryCountRef.current = filtered.length;
207
+
208
+ if (!paused && hasNewEntries && wasAtBottomRef.current) {
209
+ el.scrollTop = el.scrollHeight;
210
+ }
211
+ }, [filtered.length, paused]);
212
+
213
+ // When unpausing, scroll to bottom
214
+ useEffect(() => {
215
+ if (!paused) {
216
+ const el = containerRef.current;
217
+ if (el) {
218
+ el.scrollTop = el.scrollHeight;
219
+ wasAtBottomRef.current = true;
220
+ }
221
+ }
222
+ }, [paused]);
223
+
224
+ if (filtered.length === 0 && !filter) {
225
+ return (
226
+ <div className="flex h-full items-center justify-center">
227
+ <EmptyState title="No messages yet" description="Subscribe to a NATS subject to start seeing messages." />
228
+ </div>
229
+ );
230
+ }
231
+
232
+ if (filtered.length === 0 && filter) {
233
+ return (
234
+ <div className="flex h-full items-center justify-center">
235
+ <EmptyState title="No matching messages" description={`No entries match "${filter}".`} />
236
+ </div>
237
+ );
238
+ }
239
+
240
+ return (
241
+ <div ref={containerRef} className="h-full overflow-auto" onScroll={handleScroll}>
242
+ {filtered.map((entry, i) => (
243
+ <MessageRow key={entry.id} entry={entry} index={i} />
244
+ ))}
245
+ </div>
246
+ );
247
+ }
@@ -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,111 @@
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
+ function buildQuickPickSubjects(orgId: string): string[] {
13
+ return [SUBJECTS.echo(orgId), SUBJECTS.pty.create(orgId), SUBJECTS.pty.list(orgId), SUBJECTS.notify.broadcast(orgId)];
14
+ }
15
+
16
+ interface PublishPanelProps {
17
+ /** Optional callback to push an entry to the message buffer. */
18
+ onPublish?: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
19
+ }
20
+
21
+ export function PublishPanel({ onPublish }: PublishPanelProps) {
22
+ const { connected, publish } = useNats();
23
+ const auth = useKhalAuth();
24
+ const orgId = auth?.orgId ?? 'default';
25
+ const quickPicks = useMemo(() => buildQuickPickSubjects(orgId), [orgId]);
26
+
27
+ const [subject, setSubject] = useState('');
28
+ const [payload, setPayload] = useState('');
29
+ const [error, setError] = useState<string | null>(null);
30
+ const [sent, setSent] = useState(false);
31
+ const sentTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
32
+
33
+ const handleSend = useCallback(() => {
34
+ setError(null);
35
+
36
+ let parsed: unknown;
37
+ if (payload.trim()) {
38
+ try {
39
+ parsed = JSON.parse(payload);
40
+ } catch (e) {
41
+ setError(`Invalid JSON: ${(e as Error).message}`);
42
+ return;
43
+ }
44
+ }
45
+
46
+ publish(subject, parsed);
47
+
48
+ // Push to buffer as outgoing message
49
+ onPublish?.({ subject, payload: parsed, direction: 'out' });
50
+
51
+ // Show "Sent!" indicator
52
+ setSent(true);
53
+ if (sentTimer.current) clearTimeout(sentTimer.current);
54
+ sentTimer.current = setTimeout(() => setSent(false), 1000);
55
+ }, [subject, payload, publish, onPublish]);
56
+
57
+ const canSend = subject.trim().length > 0 && connected;
58
+
59
+ return (
60
+ <div className="flex flex-col gap-2">
61
+ {/* Subject input */}
62
+ <Input
63
+ size="small"
64
+ placeholder={SUBJECTS.echo(orgId)}
65
+ value={subject}
66
+ onChange={(e) => {
67
+ setSubject(e.target.value);
68
+ setError(null);
69
+ }}
70
+ />
71
+
72
+ {/* Quick-pick subjects */}
73
+ <div className="flex flex-wrap gap-1">
74
+ {quickPicks.map((s) => (
75
+ <button
76
+ key={s}
77
+ type="button"
78
+ 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"
79
+ onClick={() => setSubject(s)}
80
+ >
81
+ {s}
82
+ </button>
83
+ ))}
84
+ </div>
85
+
86
+ {/* Payload textarea */}
87
+ <textarea
88
+ 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"
89
+ rows={4}
90
+ placeholder='{"key": "value"}'
91
+ value={payload}
92
+ onChange={(e) => {
93
+ setPayload(e.target.value);
94
+ setError(null);
95
+ }}
96
+ />
97
+
98
+ {/* Error message */}
99
+ {error && <p className="text-[11px] text-red-600">{error}</p>}
100
+
101
+ {/* Send button + Sent indicator */}
102
+ <div className="flex items-center gap-2">
103
+ <Button size="small" disabled={!canSend} onClick={handleSend} prefix={<Send className="h-3 w-3" />}>
104
+ Send
105
+ </Button>
106
+ {sent && <span className="text-[11px] font-medium text-green-600 animate-pulse">Sent!</span>}
107
+ {!connected && <span className="text-[11px] text-gray-600">Not connected</span>}
108
+ </div>
109
+ </div>
110
+ );
111
+ }