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,31 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext } from 'react';
4
+ import type { LogEntry } from './types';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Context — shared state for sidebar panels and the main log area
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export interface NatsViewerContextValue {
11
+ subscriptions: Set<string>;
12
+ addSubscription: (subject: string) => void;
13
+ removeSubscription: (subject: string) => void;
14
+ buffer: {
15
+ entries: LogEntry[];
16
+ push: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
17
+ clear: () => void;
18
+ };
19
+ filter: string;
20
+ setFilter: (f: string) => void;
21
+ paused: boolean;
22
+ setPaused: (p: boolean) => void;
23
+ }
24
+
25
+ export const NatsViewerContext = createContext<NatsViewerContextValue | null>(null);
26
+
27
+ export function useNatsViewer(): NatsViewerContextValue {
28
+ const ctx = useContext(NatsViewerContext);
29
+ if (!ctx) throw new Error('useNatsViewer must be used within <NatsViewer>');
30
+ return ctx;
31
+ }
@@ -0,0 +1,7 @@
1
+ export interface LogEntry {
2
+ id: string;
3
+ timestamp: number;
4
+ subject: string;
5
+ payload: unknown;
6
+ direction: 'in' | 'out'; // 'in' = received, 'out' = published by user
7
+ }
@@ -0,0 +1,55 @@
1
+ import { useCallback, useReducer } from 'react';
2
+ import type { LogEntry } from './types';
3
+
4
+ const DEFAULT_CAPACITY = 1000;
5
+
6
+ type Action =
7
+ | { type: 'push'; entry: Omit<LogEntry, 'id' | 'timestamp'> }
8
+ | { type: 'clear' }
9
+ | { type: 'set-capacity'; capacity: number };
10
+
11
+ interface State {
12
+ entries: LogEntry[];
13
+ capacity: number;
14
+ }
15
+
16
+ function reducer(state: State, action: Action): State {
17
+ switch (action.type) {
18
+ case 'push': {
19
+ const entry: LogEntry = {
20
+ ...action.entry,
21
+ id: crypto.randomUUID(),
22
+ timestamp: Date.now(),
23
+ };
24
+ const next = [...state.entries, entry];
25
+ // Drop oldest entries when over capacity
26
+ if (next.length > state.capacity) {
27
+ return { ...state, entries: next.slice(next.length - state.capacity) };
28
+ }
29
+ return { ...state, entries: next };
30
+ }
31
+ case 'clear':
32
+ return { ...state, entries: [] };
33
+ case 'set-capacity': {
34
+ const entries =
35
+ state.entries.length > action.capacity
36
+ ? state.entries.slice(state.entries.length - action.capacity)
37
+ : state.entries;
38
+ return { capacity: action.capacity, entries };
39
+ }
40
+ }
41
+ }
42
+
43
+ export function useMessageBuffer(capacity = DEFAULT_CAPACITY) {
44
+ const [state, dispatch] = useReducer(reducer, { entries: [], capacity });
45
+
46
+ const push = useCallback((entry: Omit<LogEntry, 'id' | 'timestamp'>) => {
47
+ dispatch({ type: 'push', entry });
48
+ }, []);
49
+
50
+ const clear = useCallback(() => {
51
+ dispatch({ type: 'clear' });
52
+ }, []);
53
+
54
+ return { entries: state.entries, push, clear };
55
+ }
@@ -0,0 +1,492 @@
1
+ 'use client';
2
+
3
+ import { Bell, Command, Info, Monitor, Radio } from 'lucide-react';
4
+ import { useEffect, useState } from 'react';
5
+ import { EmptyState, PropertyPanel, SectionHeader, SidebarNav, SplitPane, StatusBar } from '@/components/os-primitives';
6
+ import { Button } from '@/components/ui/button';
7
+ import { Input } from '@/components/ui/input';
8
+ import { Note } from '@/components/ui/note';
9
+ import { Separator } from '@/components/ui/separator';
10
+ import { Toggle } from '@/components/ui/switch';
11
+ import { ThemeSwitcher } from '@/components/ui/theme-switcher';
12
+ import { useNats } from '@/lib/hooks/use-nats';
13
+ import type { KeyCombo, ModifierKey, ShortcutCategory } from '@/lib/keyboard/types';
14
+ import { comboToSymbols } from '@/lib/keyboard/types';
15
+ import { SUBJECTS } from '@/lib/subjects';
16
+ import { useKeybindStore } from '@/stores/keybind-store';
17
+ import type { DesktopNotifMode } from '@/stores/notification-store';
18
+ import { useNotificationStore } from '@/stores/notification-store';
19
+ import { useThemeStore } from '@/stores/theme-store';
20
+
21
+ const IS_DEV = process.env.NODE_ENV === 'development';
22
+
23
+ type SettingsTab = 'appearance' | 'notifications' | 'keyboard' | 'about' | 'nats';
24
+
25
+ const NAV_ITEMS: Array<{ id: SettingsTab; label: string; icon: React.ComponentType; devOnly?: boolean }> = [
26
+ { id: 'appearance', label: 'Appearance', icon: Monitor },
27
+ { id: 'notifications', label: 'Notifications', icon: Bell },
28
+ { id: 'keyboard', label: 'Keyboard Shortcuts', icon: Command },
29
+ { id: 'about', label: 'About', icon: Info },
30
+ { id: 'nats', label: 'NATS Echo Test', icon: Radio, devOnly: true },
31
+ ];
32
+
33
+ export function Settings(_props: { windowId: string; meta?: Record<string, unknown> }) {
34
+ const [tab, setTab] = useState<SettingsTab>('appearance');
35
+
36
+ return (
37
+ <div className="flex h-full flex-col bg-background-100">
38
+ <div className="flex-1 overflow-hidden">
39
+ <SplitPane defaultSize={160} min={130} max={220} collapseBelow={400}>
40
+ <SplitPane.Panel className="bg-gray-alpha-50">
41
+ <SidebarNav label="Settings" title="Settings">
42
+ {NAV_ITEMS.filter((item) => !item.devOnly || IS_DEV).map((item) => {
43
+ const Icon = item.icon;
44
+ return (
45
+ <SidebarNav.Item
46
+ key={item.id}
47
+ active={tab === item.id}
48
+ onClick={() => setTab(item.id)}
49
+ icon={<Icon />}
50
+ >
51
+ {item.label}
52
+ </SidebarNav.Item>
53
+ );
54
+ })}
55
+ </SidebarNav>
56
+ </SplitPane.Panel>
57
+ <SplitPane.Panel className="overflow-auto p-6">
58
+ {tab === 'appearance' && <AppearanceTab />}
59
+ {tab === 'notifications' && <NotificationsTab />}
60
+ {tab === 'keyboard' && <KeyboardShortcutsTab />}
61
+ {tab === 'about' && <AboutTab />}
62
+ {tab === 'nats' && <NatsEchoTab />}
63
+ </SplitPane.Panel>
64
+ </SplitPane>
65
+ </div>
66
+ <StatusBar>
67
+ <StatusBar.Item>Khal</StatusBar.Item>
68
+ <StatusBar.Spacer />
69
+ <StatusBar.Item variant="success">local</StatusBar.Item>
70
+ </StatusBar>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Appearance Tab
77
+ // ---------------------------------------------------------------------------
78
+
79
+ function ReduceMotionToggle() {
80
+ const reduceMotion = useThemeStore((s) => s.reduceMotion);
81
+ const setReduceMotion = useThemeStore((s) => s.setReduceMotion);
82
+
83
+ return (
84
+ <div className="flex items-center justify-between rounded-lg border border-gray-alpha-200 bg-background-100 px-4 py-3">
85
+ <div>
86
+ <p className="text-copy-13 font-medium text-gray-1000">Reduce motion</p>
87
+ <p className="text-copy-12 text-gray-800">
88
+ {reduceMotion ? 'Animations are disabled' : 'Animations are enabled'}
89
+ </p>
90
+ </div>
91
+ <Toggle checked={reduceMotion} onChange={() => setReduceMotion(!reduceMotion)} />
92
+ </div>
93
+ );
94
+ }
95
+
96
+ function AppearanceTab() {
97
+ return (
98
+ <div className="flex max-w-2xl flex-col gap-8 text-gray-1000">
99
+ <section>
100
+ <SectionHeader title="Mode" description="Choose light, dark, or system." />
101
+ <ThemeSwitcher />
102
+ </section>
103
+
104
+ <Separator />
105
+
106
+ <section>
107
+ <SectionHeader title="Motion" description="Control animation preferences." />
108
+ <ReduceMotionToggle />
109
+ </section>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Notifications Tab
116
+ // ---------------------------------------------------------------------------
117
+
118
+ const NOTIF_MODE_OPTIONS: Array<{
119
+ value: DesktopNotifMode;
120
+ label: string;
121
+ description: string;
122
+ }> = [
123
+ { value: 'background', label: 'When in background', description: 'Only show when the tab is not visible' },
124
+ { value: 'always', label: 'Always', description: 'Show for every notification' },
125
+ { value: 'off', label: 'Off', description: 'Never send browser notifications' },
126
+ ];
127
+
128
+ function NotificationsTab() {
129
+ const doNotDisturb = useNotificationStore((s) => s.doNotDisturb);
130
+ const setDoNotDisturb = useNotificationStore((s) => s.setDoNotDisturb);
131
+ const desktopNotifMode = useNotificationStore((s) => s.desktopNotifMode);
132
+ const setDesktopNotifMode = useNotificationStore((s) => s.setDesktopNotifMode);
133
+ const browserPermission = useNotificationStore((s) => s.browserPermission);
134
+ const requestBrowserPermission = useNotificationStore((s) => s.requestBrowserPermission);
135
+ const syncBrowserPermission = useNotificationStore((s) => s.syncBrowserPermission);
136
+ const history = useNotificationStore((s) => s.history);
137
+ const clearHistory = useNotificationStore((s) => s.clearHistory);
138
+
139
+ useEffect(() => {
140
+ syncBrowserPermission();
141
+ }, [syncBrowserPermission]);
142
+
143
+ const needsPermission = browserPermission !== 'granted' && desktopNotifMode !== 'off';
144
+
145
+ return (
146
+ <div className="flex max-w-2xl flex-col gap-8 text-gray-1000">
147
+ <section>
148
+ <SectionHeader title="Do Not Disturb" description="Suppress in-app toast notifications." />
149
+ <div className="flex items-center justify-between rounded-lg border border-gray-alpha-200 bg-background-100 px-4 py-3">
150
+ <div>
151
+ <p className="text-copy-13 font-medium text-gray-1000">Do Not Disturb</p>
152
+ <p className="text-copy-12 text-gray-800">
153
+ {doNotDisturb ? 'Toasts are hidden' : 'Toasts are shown normally'}
154
+ </p>
155
+ </div>
156
+ <Toggle checked={doNotDisturb} onChange={() => setDoNotDisturb(!doNotDisturb)} />
157
+ </div>
158
+ </section>
159
+
160
+ <Separator />
161
+
162
+ <section>
163
+ <SectionHeader title="Desktop Notifications" description="Bridge notifications to your OS." />
164
+ <div className="flex flex-col gap-3">
165
+ {NOTIF_MODE_OPTIONS.map((opt) => (
166
+ <button
167
+ key={opt.value}
168
+ onClick={() => setDesktopNotifMode(opt.value)}
169
+ className={`flex items-center gap-3 rounded-lg border px-4 py-3 text-left transition-colors ${
170
+ desktopNotifMode === opt.value
171
+ ? 'border-blue-400 bg-blue-100'
172
+ : 'border-gray-alpha-200 bg-background-100 hover:border-gray-alpha-300'
173
+ }`}
174
+ >
175
+ <div
176
+ className={`h-3 w-3 shrink-0 rounded-full border-2 ${desktopNotifMode === opt.value ? 'border-blue-600 bg-blue-600' : 'border-gray-alpha-400'}`}
177
+ />
178
+ <div>
179
+ <p className="text-copy-13 font-medium text-gray-1000">{opt.label}</p>
180
+ <p className="text-copy-12 text-gray-800">{opt.description}</p>
181
+ </div>
182
+ </button>
183
+ ))}
184
+ {needsPermission && (
185
+ <Note type="warning" size="small">
186
+ <div className="flex items-center justify-between gap-4">
187
+ <span>{browserPermission === 'denied' ? 'Blocked by browser.' : 'Permission required.'}</span>
188
+ {browserPermission !== 'denied' && (
189
+ <Button size="small" variant="secondary" onClick={() => requestBrowserPermission()}>
190
+ Allow
191
+ </Button>
192
+ )}
193
+ </div>
194
+ </Note>
195
+ )}
196
+ </div>
197
+ </section>
198
+
199
+ <Separator />
200
+
201
+ <section>
202
+ <SectionHeader
203
+ title="Notification History"
204
+ description={`${history.length} notification${history.length !== 1 ? 's' : ''}`}
205
+ >
206
+ {history.length > 0 && (
207
+ <Button size="small" variant="secondary" onClick={clearHistory}>
208
+ Clear History
209
+ </Button>
210
+ )}
211
+ </SectionHeader>
212
+ </section>
213
+ </div>
214
+ );
215
+ }
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // About Tab
219
+ // ---------------------------------------------------------------------------
220
+
221
+ function AboutTab() {
222
+ return (
223
+ <div className="flex max-w-2xl flex-col gap-8 text-gray-1000">
224
+ <section>
225
+ <SectionHeader title="Khal" description="Desktop-in-browser OS shell." />
226
+ <PropertyPanel className="rounded-lg border border-gray-alpha-200">
227
+ <PropertyPanel.Section>
228
+ <PropertyPanel.Row label="Version">v2-dev</PropertyPanel.Row>
229
+ <PropertyPanel.Row label="Framework">Next.js</PropertyPanel.Row>
230
+ <PropertyPanel.Row label="Runtime">
231
+ {typeof navigator !== 'undefined' ? navigator.userAgent.split(' ').pop() : '--'}
232
+ </PropertyPanel.Row>
233
+ </PropertyPanel.Section>
234
+ </PropertyPanel>
235
+ </section>
236
+ </div>
237
+ );
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // NATS Echo Test Tab (dev only)
242
+ // ---------------------------------------------------------------------------
243
+
244
+ function NatsEchoTab() {
245
+ const { connected, request, orgId } = useNats();
246
+ const [message, setMessage] = useState('');
247
+ const [response, setResponse] = useState<string | null>(null);
248
+ const [loading, setLoading] = useState(false);
249
+ const [error, setError] = useState<string | null>(null);
250
+
251
+ const handleSendEcho = async () => {
252
+ if (!message.trim() || !orgId) return;
253
+ setLoading(true);
254
+ setError(null);
255
+ setResponse(null);
256
+ try {
257
+ const reply = await request(SUBJECTS.echo(orgId), { message });
258
+ setResponse(JSON.stringify(reply, null, 2));
259
+ } catch (err) {
260
+ setError(err instanceof Error ? err.message : String(err));
261
+ } finally {
262
+ setLoading(false);
263
+ }
264
+ };
265
+
266
+ return (
267
+ <div className="flex max-w-2xl flex-col gap-8 text-gray-1000">
268
+ <section>
269
+ <SectionHeader title="NATS Echo Test" description="Send a message to the echo service and see the response.">
270
+ <span
271
+ className={`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-copy-12 font-medium ${
272
+ connected ? 'bg-green-100 text-green-900' : 'bg-red-100 text-red-900'
273
+ }`}
274
+ >
275
+ <span className={`h-1.5 w-1.5 rounded-full ${connected ? 'bg-green-600' : 'bg-red-600'}`} />
276
+ {connected ? 'Connected' : 'Disconnected'}
277
+ </span>
278
+ </SectionHeader>
279
+
280
+ <div className="flex flex-col gap-4">
281
+ <div className="flex gap-2">
282
+ <Input
283
+ size="small"
284
+ placeholder="Type a message..."
285
+ value={message}
286
+ onChange={(e) => setMessage(e.target.value)}
287
+ onKeyDown={(e) => {
288
+ if (e.key === 'Enter') handleSendEcho();
289
+ }}
290
+ aria-label="Echo message"
291
+ />
292
+ <Button size="small" variant="secondary" onClick={handleSendEcho} disabled={loading || !connected}>
293
+ {loading ? 'Sending...' : 'Send Echo'}
294
+ </Button>
295
+ </div>
296
+
297
+ {error && (
298
+ <Note type="error" size="small">
299
+ {error}
300
+ </Note>
301
+ )}
302
+
303
+ {response && (
304
+ <div className="rounded-lg border border-gray-alpha-200 bg-background-100 p-4">
305
+ <p className="mb-2 text-copy-12 font-medium text-gray-800">Response:</p>
306
+ <pre className="overflow-auto whitespace-pre-wrap break-all font-mono text-copy-13 text-gray-1000">
307
+ {response}
308
+ </pre>
309
+ </div>
310
+ )}
311
+ </div>
312
+ </section>
313
+ </div>
314
+ );
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Keyboard Shortcuts Tab
319
+ // ---------------------------------------------------------------------------
320
+
321
+ const CATEGORY_LABELS: Record<ShortcutCategory, string> = {
322
+ window: 'Window Management',
323
+ workspace: 'Workspaces',
324
+ launcher: 'App Launcher',
325
+ terminal: 'Terminal',
326
+ system: 'System',
327
+ };
328
+
329
+ const CATEGORY_ORDER: ShortcutCategory[] = ['window', 'launcher', 'terminal', 'system'];
330
+
331
+ function ShortcutRecorder({
332
+ value,
333
+ onChange,
334
+ onReset,
335
+ isDefault,
336
+ }: {
337
+ value: KeyCombo | null;
338
+ onChange: (combo: KeyCombo) => void;
339
+ onReset: () => void;
340
+ isDefault: boolean;
341
+ }) {
342
+ const [recording, setRecording] = useState(false);
343
+ const setSuspended = useKeybindStore((s) => s.setSuspended);
344
+
345
+ useEffect(() => {
346
+ if (!recording) {
347
+ setSuspended(false);
348
+ return;
349
+ }
350
+ setSuspended(true);
351
+
352
+ const handler = (e: KeyboardEvent) => {
353
+ e.preventDefault();
354
+ e.stopPropagation();
355
+ if (e.key === 'Escape') {
356
+ setRecording(false);
357
+ return;
358
+ }
359
+ if (['Meta', 'Control', 'Alt', 'Shift'].includes(e.key)) return;
360
+ const modifiers: ModifierKey[] = [];
361
+ if (e.metaKey) modifiers.push('meta');
362
+ if (e.ctrlKey) modifiers.push('ctrl');
363
+ if (e.altKey) modifiers.push('alt');
364
+ if (e.shiftKey) modifiers.push('shift');
365
+ onChange({ key: e.key, modifiers });
366
+ setRecording(false);
367
+ };
368
+ const handleBlur = () => setRecording(false);
369
+ window.addEventListener('keydown', handler, { capture: true });
370
+ window.addEventListener('blur', handleBlur);
371
+ return () => {
372
+ window.removeEventListener('keydown', handler, { capture: true });
373
+ window.removeEventListener('blur', handleBlur);
374
+ setSuspended(false);
375
+ };
376
+ }, [recording, onChange, setSuspended]);
377
+
378
+ if (recording) {
379
+ return (
380
+ <div className="flex items-center gap-2">
381
+ <span className="animate-pulse rounded border border-blue-400 bg-blue-100 px-2 py-0.5 text-copy-13 text-blue-900">
382
+ Press a key combo...
383
+ </span>
384
+ <button className="text-copy-12 text-gray-700 hover:text-gray-1000" onClick={() => setRecording(false)}>
385
+ Cancel
386
+ </button>
387
+ </div>
388
+ );
389
+ }
390
+
391
+ return (
392
+ <div className="flex items-center gap-2">
393
+ <button
394
+ className="rounded border border-gray-alpha-300 bg-background-100 px-2 py-0.5 font-mono text-copy-13 text-gray-1000 transition-colors hover:border-gray-alpha-400 hover:bg-gray-alpha-100"
395
+ onClick={() => setRecording(true)}
396
+ title="Click to rebind"
397
+ >
398
+ {value ? comboToSymbols(value) : <span className="text-gray-600">Disabled</span>}
399
+ </button>
400
+ {!isDefault && (
401
+ <button className="text-copy-12 text-gray-700 hover:text-gray-1000" onClick={onReset} title="Reset to default">
402
+ Reset
403
+ </button>
404
+ )}
405
+ </div>
406
+ );
407
+ }
408
+
409
+ function KeyboardShortcutsTab() {
410
+ const definitions = useKeybindStore((s) => s.definitions);
411
+ const overrides = useKeybindStore((s) => s.overrides);
412
+ const getBinding = useKeybindStore((s) => s.getBinding);
413
+ const setBinding = useKeybindStore((s) => s.setBinding);
414
+ const resetBinding = useKeybindStore((s) => s.resetBinding);
415
+ const resetAll = useKeybindStore((s) => s.resetAll);
416
+ const [search, setSearch] = useState('');
417
+
418
+ const hasOverrides = Object.keys(overrides).length > 0;
419
+ const filtered = search.trim()
420
+ ? definitions.filter(
421
+ (d) =>
422
+ d.label.toLowerCase().includes(search.toLowerCase()) ||
423
+ d.description.toLowerCase().includes(search.toLowerCase())
424
+ )
425
+ : definitions;
426
+
427
+ const grouped = CATEGORY_ORDER.map((cat) => ({
428
+ category: cat,
429
+ label: CATEGORY_LABELS[cat],
430
+ shortcuts: filtered.filter((d) => d.category === cat),
431
+ })).filter((g) => g.shortcuts.length > 0);
432
+
433
+ return (
434
+ <div className="flex max-w-2xl flex-col gap-6 text-gray-1000">
435
+ <section>
436
+ <SectionHeader title="Keyboard Shortcuts" description="Customize keybindings. Click a shortcut to rebind it.">
437
+ {hasOverrides && (
438
+ <Button
439
+ size="small"
440
+ variant="secondary"
441
+ onClick={() => {
442
+ if (globalThis.confirm('Reset all shortcuts to defaults?')) resetAll();
443
+ }}
444
+ >
445
+ Reset All
446
+ </Button>
447
+ )}
448
+ </SectionHeader>
449
+ <Input
450
+ size="small"
451
+ placeholder="Search shortcuts..."
452
+ value={search}
453
+ onChange={(e) => setSearch(e.target.value)}
454
+ aria-label="Search keyboard shortcuts"
455
+ />
456
+ </section>
457
+
458
+ {grouped.map((group) => (
459
+ <section key={group.category}>
460
+ <h3 className="mb-2 text-copy-13 font-medium text-gray-900">{group.label}</h3>
461
+ <div className="rounded-lg border border-gray-alpha-200 bg-background-100">
462
+ {group.shortcuts.map((def, i) => {
463
+ const binding = getBinding(def.id);
464
+ const isDefault = !(def.id in overrides);
465
+ return (
466
+ <div
467
+ key={def.id}
468
+ className={`flex items-center justify-between px-4 py-2.5 ${i > 0 ? 'border-t border-gray-alpha-100' : ''}`}
469
+ >
470
+ <div className="min-w-0 flex-1">
471
+ <p className="text-copy-13 text-gray-1000">{def.label}</p>
472
+ <p className="text-copy-12 text-gray-700">{def.description}</p>
473
+ </div>
474
+ <ShortcutRecorder
475
+ value={binding}
476
+ onChange={(combo) => setBinding(def.id, combo)}
477
+ onReset={() => resetBinding(def.id)}
478
+ isDefault={isDefault}
479
+ />
480
+ </div>
481
+ );
482
+ })}
483
+ </div>
484
+ </section>
485
+ ))}
486
+
487
+ {grouped.length === 0 && search && (
488
+ <EmptyState title="No matching shortcuts" description={`No shortcuts match "${search}".`} compact />
489
+ )}
490
+ </div>
491
+ );
492
+ }
@@ -0,0 +1,82 @@
1
+ import { type Static, Type } from '@sinclair/typebox';
2
+
3
+ // Note: userId is NOT included in request schemas.
4
+ // The WS bridge injects `_authUserId` from the authenticated session
5
+ // into all outbound NATS messages. Services read that field instead.
6
+
7
+ export const PtyCreateRequest = Type.Object({
8
+ sessionId: Type.Optional(Type.String()),
9
+ cols: Type.Optional(Type.Number()),
10
+ rows: Type.Optional(Type.Number()),
11
+ });
12
+ export type PtyCreateRequest = Static<typeof PtyCreateRequest>;
13
+
14
+ export const PtyCreateResponse = Type.Object({
15
+ sessionId: Type.String(),
16
+ created: Type.Boolean(),
17
+ });
18
+ export type PtyCreateResponse = Static<typeof PtyCreateResponse>;
19
+
20
+ export const PtyListRequest = Type.Object({});
21
+ export type PtyListRequest = Static<typeof PtyListRequest>;
22
+
23
+ export const PtyDestroyRequest = Type.Object({
24
+ sessionId: Type.String(),
25
+ });
26
+ export type PtyDestroyRequest = Static<typeof PtyDestroyRequest>;
27
+
28
+ export const PtyResizeRequest = Type.Object({
29
+ sessionId: Type.String(),
30
+ cols: Type.Number(),
31
+ rows: Type.Number(),
32
+ });
33
+ export type PtyResizeRequest = Static<typeof PtyResizeRequest>;
34
+
35
+ export const PtyReplayRequest = Type.Object({
36
+ sessionId: Type.String(),
37
+ });
38
+ export type PtyReplayRequest = Static<typeof PtyReplayRequest>;
39
+
40
+ export const PtyListResponse = Type.Object({
41
+ sessions: Type.Array(
42
+ Type.Object({
43
+ sessionId: Type.String(),
44
+ createdAt: Type.Number(),
45
+ lastActivity: Type.Number(),
46
+ bufferBytes: Type.Number(),
47
+ connected: Type.Boolean(),
48
+ })
49
+ ),
50
+ });
51
+ export type PtyListResponse = Static<typeof PtyListResponse>;
52
+
53
+ export const PtyDataMessage = Type.Object({
54
+ sessionId: Type.String(),
55
+ data: Type.String(), // base64 encoded
56
+ });
57
+ export type PtyDataMessage = Static<typeof PtyDataMessage>;
58
+
59
+ export const PtyInputMessage = Type.Object({
60
+ sessionId: Type.String(),
61
+ data: Type.String(), // raw text
62
+ });
63
+ export type PtyInputMessage = Static<typeof PtyInputMessage>;
64
+
65
+ export const PtyExitMessage = Type.Object({
66
+ sessionId: Type.String(),
67
+ code: Type.Number(),
68
+ signal: Type.Optional(Type.Number()),
69
+ });
70
+ export type PtyExitMessage = Static<typeof PtyExitMessage>;
71
+
72
+ export const PtyBufferMessage = Type.Object({
73
+ sessionId: Type.String(),
74
+ data: Type.String(), // base64 encoded chunk
75
+ });
76
+ export type PtyBufferMessage = Static<typeof PtyBufferMessage>;
77
+
78
+ export const PtyBufferEndMessage = Type.Object({
79
+ sessionId: Type.String(),
80
+ error: Type.Optional(Type.String()),
81
+ });
82
+ export type PtyBufferEndMessage = Static<typeof PtyBufferEndMessage>;