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