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,112 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import type { SplitNode } from '../types';
5
+ import { SplitDragHandle } from './SplitDragHandle';
6
+ import { TerminalPane } from './TerminalPane';
7
+
8
+ interface SplitPaneRendererProps {
9
+ node: SplitNode;
10
+ focusedPaneId: string;
11
+ onPaneFocus: (paneId: string) => void;
12
+ onSessionIdChange: (paneId: string, sessionId: string) => void;
13
+ onKeyboardShortcut?: (event: KeyboardEvent) => boolean;
14
+ onRatioChange?: (nodeId: string, ratio: number) => void;
15
+ onCwdChange?: (paneId: string, cwd: string) => void;
16
+ onLastCommandChange?: (paneId: string, command: string) => void;
17
+ }
18
+
19
+ /**
20
+ * Recursive renderer for split pane tree.
21
+ * Branch nodes render two children with a draggable divider.
22
+ * Leaf nodes render a TerminalPane component.
23
+ */
24
+ export function SplitPaneRenderer({
25
+ node,
26
+ focusedPaneId,
27
+ onPaneFocus,
28
+ onSessionIdChange,
29
+ onKeyboardShortcut,
30
+ onRatioChange,
31
+ onCwdChange,
32
+ onLastCommandChange,
33
+ }: SplitPaneRendererProps) {
34
+ const [localRatio, setLocalRatio] = useState(0.5);
35
+
36
+ if (node.type === 'leaf') {
37
+ return (
38
+ <TerminalPane
39
+ paneId={node.id}
40
+ ptySessionId={node.ptySessionId}
41
+ isFocused={node.id === focusedPaneId}
42
+ onFocus={() => onPaneFocus(node.id)}
43
+ onSessionIdChange={(sessionId) => onSessionIdChange(node.id, sessionId)}
44
+ onKeyboardShortcut={onKeyboardShortcut}
45
+ onCwdChange={onCwdChange ? (cwd) => onCwdChange(node.id, cwd) : undefined}
46
+ onLastCommandChange={onLastCommandChange ? (cmd) => onLastCommandChange(node.id, cmd) : undefined}
47
+ />
48
+ );
49
+ }
50
+
51
+ // Branch node: render two children with divider
52
+ const ratio = node.ratio ?? localRatio;
53
+ const isVertical = node.direction === 'vertical';
54
+
55
+ const handleRatioChange = (newRatio: number) => {
56
+ setLocalRatio(newRatio);
57
+ if (onRatioChange) {
58
+ onRatioChange(node.id, newRatio);
59
+ }
60
+ };
61
+
62
+ return (
63
+ <div
64
+ className={`
65
+ split-container
66
+ flex ${isVertical ? 'flex-row' : 'flex-col'}
67
+ h-full w-full
68
+ `}
69
+ >
70
+ {/* First child */}
71
+ <div
72
+ style={{
73
+ flex: `0 0 ${ratio * 100}%`,
74
+ overflow: 'hidden',
75
+ }}
76
+ >
77
+ <SplitPaneRenderer
78
+ node={node.children[0]}
79
+ focusedPaneId={focusedPaneId}
80
+ onPaneFocus={onPaneFocus}
81
+ onSessionIdChange={onSessionIdChange}
82
+ onKeyboardShortcut={onKeyboardShortcut}
83
+ onRatioChange={onRatioChange}
84
+ onCwdChange={onCwdChange}
85
+ onLastCommandChange={onLastCommandChange}
86
+ />
87
+ </div>
88
+
89
+ {/* Drag handle */}
90
+ <SplitDragHandle direction={node.direction} onRatioChange={handleRatioChange} />
91
+
92
+ {/* Second child */}
93
+ <div
94
+ style={{
95
+ flex: 1,
96
+ overflow: 'hidden',
97
+ }}
98
+ >
99
+ <SplitPaneRenderer
100
+ node={node.children[1]}
101
+ focusedPaneId={focusedPaneId}
102
+ onPaneFocus={onPaneFocus}
103
+ onSessionIdChange={onSessionIdChange}
104
+ onKeyboardShortcut={onKeyboardShortcut}
105
+ onRatioChange={onRatioChange}
106
+ onCwdChange={onCwdChange}
107
+ onLastCommandChange={onLastCommandChange}
108
+ />
109
+ </div>
110
+ </div>
111
+ );
112
+ }
@@ -0,0 +1,476 @@
1
+ 'use client';
2
+
3
+ import type { FitAddon } from '@xterm/addon-fit';
4
+ import type { WebglAddon } from '@xterm/addon-webgl';
5
+ import type { Terminal } from '@xterm/xterm';
6
+ import { useEffect, useRef } from 'react';
7
+ import { useNats } from '@/lib/hooks/use-nats';
8
+ import { SUBJECTS } from '@/lib/subjects';
9
+
10
+ /** Resolve a CSS custom property from :root to its computed value. */
11
+ function resolveVar(name: string, fallback: string): string {
12
+ if (typeof document === 'undefined') return fallback;
13
+ const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
14
+ return v || fallback;
15
+ }
16
+
17
+ /** Build xterm color theme from current CSS custom properties. */
18
+ function buildTerminalTheme(focused: boolean) {
19
+ const bg = resolveVar('--khal-terminal-bg', '#0a0a0a');
20
+ const fg = resolveVar('--khal-text-primary', '#e0e0e0');
21
+ const muted = resolveVar('--khal-text-muted', '#808080');
22
+ const accent = resolveVar('--khal-accent-primary', '#39ff14');
23
+ return {
24
+ background: bg,
25
+ foreground: focused ? fg : muted,
26
+ cursor: focused ? accent : muted,
27
+ cursorAccent: bg,
28
+ selectionBackground: `${accent}33`, // accent with ~20% alpha
29
+ selectionForeground: fg,
30
+ // ANSI palette from CSS vars
31
+ black: resolveVar('--khal-ansi-black', '#0a0a0a'),
32
+ red: resolveVar('--khal-ansi-red', '#ff5555'),
33
+ green: resolveVar('--khal-ansi-green', '#39ff14'),
34
+ yellow: resolveVar('--khal-ansi-yellow', '#f1fa8c'),
35
+ blue: resolveVar('--khal-ansi-blue', '#6272a4'),
36
+ magenta: resolveVar('--khal-ansi-magenta', '#ff79c6'),
37
+ cyan: resolveVar('--khal-ansi-cyan', '#8be9fd'),
38
+ white: resolveVar('--khal-ansi-white', '#e0e0e0'),
39
+ brightBlack: resolveVar('--khal-ansi-bright-black', '#555555'),
40
+ brightRed: resolveVar('--khal-ansi-bright-red', '#ff6e6e'),
41
+ brightGreen: resolveVar('--khal-ansi-bright-green', '#69ff69'),
42
+ brightYellow: resolveVar('--khal-ansi-bright-yellow', '#ffffa5'),
43
+ brightBlue: resolveVar('--khal-ansi-bright-blue', '#d6acff'),
44
+ brightMagenta: resolveVar('--khal-ansi-bright-magenta', '#ff92df'),
45
+ brightCyan: resolveVar('--khal-ansi-bright-cyan', '#a4ffff'),
46
+ brightWhite: resolveVar('--khal-ansi-bright-white', '#ffffff'),
47
+ };
48
+ }
49
+
50
+ function decodeBase64(b64: string): Uint8Array {
51
+ const binary = atob(b64);
52
+ const bytes = new Uint8Array(binary.length);
53
+ for (let i = 0; i < binary.length; i++) {
54
+ bytes[i] = binary.charCodeAt(i);
55
+ }
56
+ return bytes;
57
+ }
58
+
59
+ interface TerminalPaneProps {
60
+ paneId: string;
61
+ ptySessionId: string | null;
62
+ isFocused: boolean;
63
+ onFocus: () => void;
64
+ onSessionIdChange: (sessionId: string) => void;
65
+ onKeyboardShortcut?: (event: KeyboardEvent) => boolean; // Return false to prevent xterm processing
66
+ onCwdChange?: (cwd: string) => void; // OSC 7 CWD updates
67
+ onLastCommandChange?: (command: string) => void; // Track last command entered
68
+ }
69
+
70
+ /**
71
+ * Single terminal pane (leaf in split tree).
72
+ * Manages one xterm.js instance connected to one PTY session via NATS.
73
+ */
74
+ export function TerminalPane({
75
+ paneId,
76
+ ptySessionId,
77
+ isFocused,
78
+ onFocus,
79
+ onSessionIdChange,
80
+ onKeyboardShortcut,
81
+ onCwdChange,
82
+ onLastCommandChange,
83
+ }: TerminalPaneProps) {
84
+ const containerRef = useRef<HTMLDivElement>(null);
85
+ const terminalRef = useRef<Terminal | null>(null);
86
+ const fitAddonRef = useRef<FitAddon | null>(null);
87
+ const webglAddonRef = useRef<WebglAddon | null>(null);
88
+ const sessionIdRef = useRef<string | null>(null);
89
+ const unsubsRef = useRef<Array<() => void>>([]);
90
+ const bufferReplayedRef = useRef(false);
91
+ const inputBufferRef = useRef<string>(''); // Track input between Enter keystrokes
92
+ // Dedup + debounce resize to avoid rapid SIGWINCH bursts (each causes bash prompt redraw)
93
+ const lastSentDimsRef = useRef('');
94
+ const resizeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
95
+ // Settle period: suppress all resize→PTY events for 500ms after creation.
96
+ // DOM layout can reflow multiple times as the window manager positions/sizes the
97
+ // window, each triggering fit()→onResize. Without suppression, each gets through
98
+ // the 150ms debounce if reflows are spread across multiple debounce windows,
99
+ // resulting in 2-3 SIGWINCHs → duplicate bash prompts on the same line.
100
+ const settleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
101
+
102
+ // Latest-ref pattern: callbacks in refs prevent the main useEffect from re-running
103
+ // when parent re-renders with new function references
104
+ const onSessionIdChangeRef = useRef(onSessionIdChange);
105
+ onSessionIdChangeRef.current = onSessionIdChange;
106
+ const onKeyboardShortcutRef = useRef(onKeyboardShortcut);
107
+ onKeyboardShortcutRef.current = onKeyboardShortcut;
108
+ const onCwdChangeRef = useRef(onCwdChange);
109
+ onCwdChangeRef.current = onCwdChange;
110
+ const onLastCommandChangeRef = useRef(onLastCommandChange);
111
+ onLastCommandChangeRef.current = onLastCommandChange;
112
+ // Capture initial ptySessionId -- don't re-run effect when our own onSessionIdChange updates it
113
+ const ptySessionIdRef = useRef(ptySessionId);
114
+
115
+ const { subscribe, publish, request, orgId } = useNats();
116
+
117
+ // Setup terminal instance
118
+ useEffect(() => {
119
+ // Local cancelled flag -- each effect invocation gets its own closure.
120
+ // In React StrictMode (mount -> cleanup -> remount), the first invocation's
121
+ // cancelled=true prevents its async work from proceeding, while the second
122
+ // invocation's cancelled=false allows it to run cleanly.
123
+ let cancelled = false;
124
+
125
+ if (!containerRef.current || !orgId) return;
126
+
127
+ (async () => {
128
+ const [xtermMod, fitMod] = await Promise.all([import('@xterm/xterm'), import('@xterm/addon-fit')]);
129
+
130
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
131
+ // @ts-ignore
132
+ await import('@xterm/xterm/css/xterm.css');
133
+
134
+ if (cancelled || !containerRef.current) return;
135
+
136
+ const terminal = new xtermMod.Terminal({
137
+ cursorBlink: false,
138
+ cursorStyle: 'block',
139
+ fontSize: 14,
140
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Menlo', monospace",
141
+ lineHeight: 1.2,
142
+ scrollback: 5000,
143
+ allowProposedApi: true,
144
+ theme: buildTerminalTheme(isFocused),
145
+ });
146
+
147
+ terminalRef.current = terminal;
148
+
149
+ const fitAddon = new fitMod.FitAddon();
150
+ fitAddonRef.current = fitAddon;
151
+ terminal.loadAddon(fitAddon);
152
+
153
+ // Attach custom key event handler
154
+ terminal.attachCustomKeyEventHandler((event) => {
155
+ return onKeyboardShortcutRef.current?.(event) ?? true;
156
+ });
157
+
158
+ terminal.open(containerRef.current);
159
+
160
+ // Register OSC 7 handler for CWD tracking (shell integration)
161
+ // OSC 7 format: ESC ] 7 ; file://host/path BEL
162
+ terminal.parser.registerOscHandler(7, (data) => {
163
+ try {
164
+ const url = new URL(data);
165
+ if (url.protocol === 'file:' && url.pathname) {
166
+ const cwd = decodeURIComponent(url.pathname);
167
+ onCwdChangeRef.current?.(cwd);
168
+ }
169
+ } catch {
170
+ // Invalid OSC 7 format, ignore
171
+ }
172
+ return true;
173
+ });
174
+
175
+ // Fit synchronously so proposeDimensions returns the ACTUAL fitted
176
+ // size. If we defer to rAF, the PTY is created with pre-fit dims
177
+ // and the subsequent fit triggers a resize → SIGWINCH → extra prompt.
178
+ try {
179
+ fitAddon.fit();
180
+ } catch {
181
+ // fit() can throw if container has zero dimensions
182
+ }
183
+
184
+ // Get terminal dimensions for PTY creation (after fit — matches reality)
185
+ const dims = fitAddon.proposeDimensions();
186
+ const cols = dims?.cols ?? 80;
187
+ const rows = dims?.rows ?? 24;
188
+
189
+ // Guard: StrictMode cleanup may have fired during the sync calls above
190
+ if (cancelled) return;
191
+
192
+ // Create or reattach PTY session via NATS
193
+ const response = (await request(SUBJECTS.pty.create(orgId), {
194
+ sessionId: ptySessionIdRef.current || undefined,
195
+ cols,
196
+ rows,
197
+ })) as { sessionId: string; created: boolean };
198
+
199
+ if (cancelled) return;
200
+
201
+ const resolvedSessionId = response.sessionId;
202
+ sessionIdRef.current = resolvedSessionId;
203
+ // Record dims sent to PTY so we skip redundant resizes (avoids extra SIGWINCH)
204
+ lastSentDimsRef.current = `${cols}x${rows}`;
205
+
206
+ // Start settle period: suppress resize→PTY for 500ms while DOM settles.
207
+ // After 500ms, sync final dimensions if they've drifted.
208
+ settleTimerRef.current = setTimeout(() => {
209
+ settleTimerRef.current = null;
210
+ if (cancelled || !sessionIdRef.current) return;
211
+ const settled = fitAddonRef.current?.proposeDimensions();
212
+ if (!settled) return;
213
+ const finalKey = `${settled.cols}x${settled.rows}`;
214
+ if (lastSentDimsRef.current === finalKey) return;
215
+ lastSentDimsRef.current = finalKey;
216
+ publish(SUBJECTS.pty.resize(orgId, sessionIdRef.current), {
217
+ sessionId: sessionIdRef.current,
218
+ cols: settled.cols,
219
+ rows: settled.rows,
220
+ });
221
+ }, 500);
222
+
223
+ if (response.created) {
224
+ onSessionIdChangeRef.current(resolvedSessionId);
225
+ }
226
+
227
+ // Subscribe to data (live output from PTY)
228
+ const unsubData = subscribe(SUBJECTS.pty.data(orgId, resolvedSessionId), (msg: unknown) => {
229
+ if (cancelled) return;
230
+ const { data } = msg as { sessionId: string; data: string };
231
+ const bytes = decodeBase64(data);
232
+ terminal.write(bytes);
233
+
234
+ // For new sessions, load WebGL after first data
235
+ if (response.created && !bufferReplayedRef.current) {
236
+ bufferReplayedRef.current = true;
237
+ (async () => {
238
+ try {
239
+ const webglMod = await import('@xterm/addon-webgl');
240
+ if (cancelled || !terminalRef.current) return;
241
+ const webglAddon = new webglMod.WebglAddon();
242
+ webglAddon.onContextLoss(() => {
243
+ webglAddon.dispose();
244
+ });
245
+ terminal.loadAddon(webglAddon);
246
+ webglAddonRef.current = webglAddon;
247
+ } catch {
248
+ // WebGL not available
249
+ }
250
+ })();
251
+ }
252
+ });
253
+ unsubsRef.current.push(unsubData);
254
+
255
+ // Buffer replay subscriptions — only for reattach.
256
+ // For new sessions, live data arrives via pty.data. Buffer replay is
257
+ // a broadcast (pty.buffer.{sessionId}), so if another tab reattaches
258
+ // and triggers replay, ALL subscribers receive it. Skipping these subs
259
+ // for new sessions prevents the creator from re-displaying already-visible
260
+ // output when other tabs replay.
261
+ if (!response.created) {
262
+ const unsubBuffer = subscribe(SUBJECTS.pty.buffer(orgId, resolvedSessionId), (msg: unknown) => {
263
+ if (cancelled) return;
264
+ const { data } = msg as { sessionId: string; data: string };
265
+ const bytes = decodeBase64(data);
266
+ terminal.write(bytes);
267
+ });
268
+ unsubsRef.current.push(unsubBuffer);
269
+
270
+ const unsubBufferEnd = subscribe(SUBJECTS.pty.bufferEnd(orgId, resolvedSessionId), (msg: unknown) => {
271
+ if (cancelled) return;
272
+ const { error } = (msg as { error?: string }) || {};
273
+ if (error) {
274
+ terminal.write(`\r\n\x1b[31m[Buffer replay denied: ${error}]\x1b[0m\r\n`);
275
+ }
276
+ bufferReplayedRef.current = true;
277
+
278
+ // Sync dimensions after replay — debounced to collapse with
279
+ // any ResizeObserver-triggered resizes during DOM settle
280
+ const dimsKey = `${cols}x${rows}`;
281
+ if (lastSentDimsRef.current !== dimsKey) {
282
+ if (resizeTimerRef.current) clearTimeout(resizeTimerRef.current);
283
+ resizeTimerRef.current = setTimeout(() => {
284
+ if (cancelled || !sessionIdRef.current) return;
285
+ lastSentDimsRef.current = dimsKey;
286
+ publish(SUBJECTS.pty.resize(orgId, resolvedSessionId), {
287
+ sessionId: resolvedSessionId,
288
+ cols,
289
+ rows,
290
+ });
291
+ }, 150);
292
+ }
293
+
294
+ // Load WebGL addon after buffer replay
295
+ (async () => {
296
+ try {
297
+ const webglMod = await import('@xterm/addon-webgl');
298
+ if (cancelled || !terminalRef.current) return;
299
+ const webglAddon = new webglMod.WebglAddon();
300
+ webglAddon.onContextLoss(() => {
301
+ webglAddon.dispose();
302
+ });
303
+ terminal.loadAddon(webglAddon);
304
+ webglAddonRef.current = webglAddon;
305
+ } catch {
306
+ // WebGL not available
307
+ }
308
+ })();
309
+ });
310
+ unsubsRef.current.push(unsubBufferEnd);
311
+
312
+ // Request buffer replay now that subscriptions are in place
313
+ publish(SUBJECTS.pty.replay(orgId, resolvedSessionId), {
314
+ sessionId: resolvedSessionId,
315
+ });
316
+ }
317
+
318
+ // Subscribe to exit (always — both new and reattached sessions)
319
+ const unsubExit = subscribe(SUBJECTS.pty.exit(orgId, resolvedSessionId), (msg: unknown) => {
320
+ if (cancelled) return;
321
+ const { code } = msg as { sessionId: string; code: number; signal?: string };
322
+ terminal.write(`\r\n\x1b[33m[Process exited with code ${code ?? 0}]\x1b[0m\r\n`);
323
+ });
324
+ unsubsRef.current.push(unsubExit);
325
+
326
+ // Terminal input -> NATS
327
+ terminal.onData((data) => {
328
+ if (cancelled) return;
329
+ publish(SUBJECTS.pty.input(orgId, resolvedSessionId), {
330
+ sessionId: resolvedSessionId,
331
+ data,
332
+ });
333
+
334
+ // Track input for last command detection
335
+ if (data === '\r') {
336
+ const command = inputBufferRef.current.trim();
337
+ if (command && onLastCommandChangeRef.current) {
338
+ onLastCommandChangeRef.current(command);
339
+ }
340
+ inputBufferRef.current = '';
341
+ } else if (data === '\x7f' || data === '\x08') {
342
+ inputBufferRef.current = inputBufferRef.current.slice(0, -1);
343
+ } else if (data === '\x03') {
344
+ inputBufferRef.current = '';
345
+ } else if (data === '\x15') {
346
+ inputBufferRef.current = '';
347
+ } else if (data.charCodeAt(0) >= 32) {
348
+ inputBufferRef.current += data;
349
+ }
350
+ });
351
+
352
+ terminal.onBinary((data) => {
353
+ if (cancelled) return;
354
+ publish(SUBJECTS.pty.input(orgId, resolvedSessionId), {
355
+ sessionId: resolvedSessionId,
356
+ data,
357
+ });
358
+ });
359
+
360
+ terminal.onResize(({ cols, rows }) => {
361
+ if (cancelled) return;
362
+ if (!sessionIdRef.current) return;
363
+ // During settle period, skip — the settle timer will sync at the end
364
+ if (settleTimerRef.current) return;
365
+ const dimsKey = `${cols}x${rows}`;
366
+ if (lastSentDimsRef.current === dimsKey) return;
367
+ // Debounce: DOM reflow can trigger several rapid resizes as
368
+ // the window layout settles; each sends SIGWINCH → bash redraws
369
+ // the prompt. Collapse them into a single resize after 150ms.
370
+ if (resizeTimerRef.current) clearTimeout(resizeTimerRef.current);
371
+ resizeTimerRef.current = setTimeout(() => {
372
+ if (cancelled || !sessionIdRef.current) return;
373
+ lastSentDimsRef.current = dimsKey;
374
+ publish(SUBJECTS.pty.resize(orgId, sessionIdRef.current), {
375
+ sessionId: sessionIdRef.current,
376
+ cols,
377
+ rows,
378
+ });
379
+ }, 150);
380
+ });
381
+ })();
382
+
383
+ return () => {
384
+ cancelled = true;
385
+
386
+ // Unsubscribe all NATS subscriptions
387
+ for (const unsub of unsubsRef.current) {
388
+ unsub();
389
+ }
390
+ unsubsRef.current = [];
391
+ sessionIdRef.current = null;
392
+
393
+ if (webglAddonRef.current) {
394
+ try {
395
+ webglAddonRef.current.dispose();
396
+ } catch {
397
+ // ignore
398
+ }
399
+ webglAddonRef.current = null;
400
+ }
401
+
402
+ if (terminalRef.current) {
403
+ terminalRef.current.dispose();
404
+ terminalRef.current = null;
405
+ }
406
+
407
+ fitAddonRef.current = null;
408
+ bufferReplayedRef.current = false;
409
+ lastSentDimsRef.current = '';
410
+ if (resizeTimerRef.current) {
411
+ clearTimeout(resizeTimerRef.current);
412
+ resizeTimerRef.current = null;
413
+ }
414
+ if (settleTimerRef.current) {
415
+ clearTimeout(settleTimerRef.current);
416
+ settleTimerRef.current = null;
417
+ }
418
+ };
419
+ // eslint-disable-next-line react-hooks/exhaustive-deps
420
+ }, [paneId, subscribe, publish, request, orgId]);
421
+
422
+ // Update theme and DOM focus when focus changes
423
+ useEffect(() => {
424
+ const terminal = terminalRef.current;
425
+ if (!terminal) return;
426
+
427
+ terminal.options.cursorBlink = false;
428
+ terminal.options.theme = {
429
+ ...terminal.options.theme,
430
+ ...buildTerminalTheme(isFocused),
431
+ };
432
+
433
+ if (isFocused) {
434
+ terminal.focus();
435
+ } else {
436
+ terminal.blur();
437
+ }
438
+ }, [isFocused]);
439
+
440
+ // Trigger fit when pane is resized
441
+ useEffect(() => {
442
+ const handleResize = () => {
443
+ if (fitAddonRef.current) {
444
+ requestAnimationFrame(() => {
445
+ try {
446
+ fitAddonRef.current?.fit();
447
+ } catch {
448
+ // ignore
449
+ }
450
+ });
451
+ }
452
+ };
453
+
454
+ const observer = new ResizeObserver(handleResize);
455
+ if (containerRef.current) {
456
+ observer.observe(containerRef.current);
457
+ }
458
+
459
+ return () => {
460
+ observer.disconnect();
461
+ };
462
+ }, []);
463
+
464
+ return (
465
+ <div
466
+ ref={containerRef}
467
+ className="h-full w-full"
468
+ style={{
469
+ padding: '4px',
470
+ opacity: isFocused ? 1 : 0.7,
471
+ transition: 'opacity 0.2s',
472
+ }}
473
+ onClick={onFocus}
474
+ />
475
+ );
476
+ }