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,66 @@
1
+ import { create } from 'zustand';
2
+ import { DEFAULT_SHORTCUTS } from '@/lib/keyboard/defaults';
3
+ import type { KeyCombo, ShortcutDefinition } from '@/lib/keyboard/types';
4
+
5
+ const STORAGE_KEY = 'sandcastle:keybinds';
6
+
7
+ interface KeybindStore {
8
+ overrides: Record<string, KeyCombo | null>;
9
+ definitions: ShortcutDefinition[];
10
+ /** When true, the global listener ignores all keypresses (e.g. during rebinding) */
11
+ suspended: boolean;
12
+
13
+ getBinding: (id: string) => KeyCombo | null;
14
+ setBinding: (id: string, combo: KeyCombo | null) => void;
15
+ resetBinding: (id: string) => void;
16
+ resetAll: () => void;
17
+ setSuspended: (value: boolean) => void;
18
+ }
19
+
20
+ function loadOverrides(): Record<string, KeyCombo | null> {
21
+ if (typeof window === 'undefined') return {};
22
+ try {
23
+ const raw = localStorage.getItem(STORAGE_KEY);
24
+ if (raw) return JSON.parse(raw);
25
+ } catch {}
26
+ return {};
27
+ }
28
+
29
+ function saveOverrides(overrides: Record<string, KeyCombo | null>) {
30
+ try {
31
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(overrides));
32
+ } catch {}
33
+ }
34
+
35
+ export const useKeybindStore = create<KeybindStore>((set, get) => ({
36
+ overrides: loadOverrides(),
37
+ definitions: DEFAULT_SHORTCUTS,
38
+ suspended: false,
39
+
40
+ setSuspended: (value) => set({ suspended: value }),
41
+
42
+ getBinding: (id) => {
43
+ const { overrides, definitions } = get();
44
+ if (id in overrides) return overrides[id];
45
+ const def = definitions.find((d) => d.id === id);
46
+ return def?.defaultBinding ?? null;
47
+ },
48
+
49
+ setBinding: (id, combo) => {
50
+ const next = { ...get().overrides, [id]: combo };
51
+ set({ overrides: next });
52
+ saveOverrides(next);
53
+ },
54
+
55
+ resetBinding: (id) => {
56
+ const next = { ...get().overrides };
57
+ delete next[id];
58
+ set({ overrides: next });
59
+ saveOverrides(next);
60
+ },
61
+
62
+ resetAll: () => {
63
+ set({ overrides: {} });
64
+ saveOverrides({});
65
+ },
66
+ }));
@@ -0,0 +1,271 @@
1
+ import { create } from 'zustand';
2
+
3
+ export type NotificationUrgency = 'low' | 'normal' | 'critical';
4
+ export type DesktopNotifMode = 'background' | 'always' | 'off';
5
+
6
+ export interface DesktopNotification {
7
+ id: number;
8
+ replacesId: number;
9
+ appName: string;
10
+ summary: string;
11
+ body: string;
12
+ icon: string | null;
13
+ actions: string[];
14
+ expires: number;
15
+ timestamp: number;
16
+ read: boolean;
17
+ urgency: NotificationUrgency;
18
+ category: string | null;
19
+ transient: boolean;
20
+ workspaceId: string | null;
21
+ }
22
+
23
+ export interface TrayIcon {
24
+ wid: number;
25
+ title: string;
26
+ icon: string | null;
27
+ }
28
+
29
+ interface NotificationStore {
30
+ notifications: DesktopNotification[];
31
+ history: DesktopNotification[];
32
+ trayIcons: Map<number, TrayIcon>;
33
+ centerOpen: boolean;
34
+ unreadCount: number;
35
+
36
+ // Preferences
37
+ doNotDisturb: boolean;
38
+ desktopNotifMode: DesktopNotifMode;
39
+ browserPermission: NotificationPermission;
40
+
41
+ setDoNotDisturb: (value: boolean) => void;
42
+ setDesktopNotifMode: (mode: DesktopNotifMode) => void;
43
+ requestBrowserPermission: () => Promise<NotificationPermission>;
44
+ syncBrowserPermission: () => void;
45
+
46
+ addNotification: (
47
+ notification: Omit<
48
+ DesktopNotification,
49
+ 'timestamp' | 'read' | 'appName' | 'urgency' | 'category' | 'transient' | 'workspaceId'
50
+ > &
51
+ Partial<Pick<DesktopNotification, 'appName' | 'urgency' | 'category' | 'transient' | 'workspaceId'>>
52
+ ) => void;
53
+ dismissNotification: (id: number) => void;
54
+ hideNotification: (id: number) => void;
55
+ clearHistory: () => void;
56
+ markAllRead: () => void;
57
+ toggleCenter: () => void;
58
+ closeCenter: () => void;
59
+
60
+ addTrayIcon: (icon: TrayIcon) => void;
61
+ removeTrayIcon: (wid: number) => void;
62
+ updateTrayIcon: (wid: number, updates: Partial<TrayIcon>) => void;
63
+ }
64
+
65
+ const MAX_VISIBLE = 5;
66
+ const MAX_HISTORY = 50;
67
+
68
+ const PREFS_KEY = 'sandcastle_notification_prefs';
69
+
70
+ function loadPrefs(): {
71
+ doNotDisturb: boolean;
72
+ desktopNotifMode: DesktopNotifMode;
73
+ } {
74
+ if (typeof window === 'undefined') return { doNotDisturb: false, desktopNotifMode: 'background' };
75
+ try {
76
+ const raw = localStorage.getItem(PREFS_KEY);
77
+ if (raw) return JSON.parse(raw);
78
+ } catch {}
79
+ return { doNotDisturb: false, desktopNotifMode: 'background' };
80
+ }
81
+
82
+ function savePrefs(prefs: { doNotDisturb: boolean; desktopNotifMode: DesktopNotifMode }) {
83
+ try {
84
+ localStorage.setItem(PREFS_KEY, JSON.stringify(prefs));
85
+ } catch {}
86
+ }
87
+
88
+ function getBrowserPermission(): NotificationPermission {
89
+ if (typeof window === 'undefined' || !('Notification' in window)) return 'denied';
90
+ return Notification.permission;
91
+ }
92
+
93
+ function sendBrowserNotification(notif: DesktopNotification) {
94
+ if (typeof window === 'undefined' || !('Notification' in window) || Notification.permission !== 'granted') return;
95
+
96
+ const n = new Notification(notif.summary, {
97
+ body: notif.body || undefined,
98
+ icon: notif.icon || undefined,
99
+ tag: String(notif.id),
100
+ silent: notif.urgency === 'low',
101
+ });
102
+
103
+ n.onclick = () => {
104
+ window.focus();
105
+ n.close();
106
+ };
107
+
108
+ if (notif.expires > 0) {
109
+ setTimeout(() => n.close(), notif.expires);
110
+ } else if (notif.urgency !== 'critical') {
111
+ setTimeout(() => n.close(), 6000);
112
+ }
113
+ }
114
+
115
+ const initialPrefs = loadPrefs();
116
+
117
+ export const useNotificationStore = create<NotificationStore>()((set, get) => ({
118
+ notifications: [],
119
+ history: [],
120
+ trayIcons: new Map(),
121
+ centerOpen: false,
122
+ unreadCount: 0,
123
+
124
+ doNotDisturb: initialPrefs.doNotDisturb,
125
+ desktopNotifMode: initialPrefs.desktopNotifMode,
126
+ browserPermission: getBrowserPermission(),
127
+
128
+ setDoNotDisturb: (value) => {
129
+ set({ doNotDisturb: value });
130
+ savePrefs({ doNotDisturb: value, desktopNotifMode: get().desktopNotifMode });
131
+ },
132
+
133
+ setDesktopNotifMode: (mode) => {
134
+ set({ desktopNotifMode: mode });
135
+ savePrefs({ doNotDisturb: get().doNotDisturb, desktopNotifMode: mode });
136
+ },
137
+
138
+ requestBrowserPermission: async () => {
139
+ if (typeof window === 'undefined' || !('Notification' in window)) return 'denied';
140
+ const result = await Notification.requestPermission();
141
+ set({ browserPermission: result });
142
+ return result;
143
+ },
144
+
145
+ syncBrowserPermission: () => {
146
+ set({ browserPermission: getBrowserPermission() });
147
+ },
148
+
149
+ addNotification: (notification) => {
150
+ const full: DesktopNotification = {
151
+ ...notification,
152
+ appName: notification.appName ?? '',
153
+ urgency: notification.urgency ?? 'normal',
154
+ category: notification.category ?? null,
155
+ transient: notification.transient ?? false,
156
+ workspaceId: notification.workspaceId ?? null,
157
+ timestamp: Date.now(),
158
+ read: false,
159
+ };
160
+
161
+ const { doNotDisturb, desktopNotifMode, browserPermission } = get();
162
+
163
+ // Always add to history regardless of DND
164
+ set((state) => {
165
+ const history = full.transient ? state.history : [full, ...state.history].slice(0, MAX_HISTORY);
166
+
167
+ // In DND mode, skip visible toasts (still record in history)
168
+ if (doNotDisturb && full.urgency !== 'critical') {
169
+ return {
170
+ history,
171
+ unreadCount: full.transient ? state.unreadCount : state.unreadCount + 1,
172
+ };
173
+ }
174
+
175
+ let notifications = [...state.notifications];
176
+
177
+ if (notification.replacesId) {
178
+ notifications = notifications.filter((n) => n.id !== notification.replacesId);
179
+ }
180
+
181
+ notifications = [full, ...notifications].slice(0, MAX_VISIBLE);
182
+
183
+ return {
184
+ notifications,
185
+ history,
186
+ unreadCount: full.transient ? state.unreadCount : state.unreadCount + 1,
187
+ };
188
+ });
189
+
190
+ // Bridge to browser desktop notifications
191
+ if (browserPermission === 'granted' && desktopNotifMode !== 'off' && !full.transient) {
192
+ const shouldSend = desktopNotifMode === 'always' || (desktopNotifMode === 'background' && document.hidden);
193
+ if (shouldSend) {
194
+ sendBrowserNotification(full);
195
+ }
196
+ }
197
+
198
+ const isCritical = full.urgency === 'critical';
199
+ if (full.expires > 0) {
200
+ setTimeout(() => {
201
+ get().hideNotification(full.id);
202
+ }, full.expires);
203
+ } else if (!isCritical) {
204
+ setTimeout(() => {
205
+ get().hideNotification(full.id);
206
+ }, 6000);
207
+ }
208
+ },
209
+
210
+ dismissNotification: (id) => {
211
+ set((state) => ({
212
+ notifications: state.notifications.filter((n) => n.id !== id),
213
+ }));
214
+ },
215
+
216
+ hideNotification: (id) => {
217
+ set((state) => ({
218
+ notifications: state.notifications.filter((n) => n.id !== id),
219
+ }));
220
+ },
221
+
222
+ clearHistory: () => {
223
+ set({ history: [], unreadCount: 0 });
224
+ },
225
+
226
+ markAllRead: () => {
227
+ set((state) => ({
228
+ history: state.history.map((n) => ({ ...n, read: true })),
229
+ unreadCount: 0,
230
+ }));
231
+ },
232
+
233
+ toggleCenter: () => {
234
+ const opening = !get().centerOpen;
235
+ set({ centerOpen: opening });
236
+ if (opening) {
237
+ get().markAllRead();
238
+ }
239
+ },
240
+
241
+ closeCenter: () => {
242
+ set({ centerOpen: false });
243
+ },
244
+
245
+ addTrayIcon: (icon) => {
246
+ set((state) => {
247
+ const next = new Map(state.trayIcons);
248
+ next.set(icon.wid, icon);
249
+ return { trayIcons: next };
250
+ });
251
+ },
252
+
253
+ removeTrayIcon: (wid) => {
254
+ set((state) => {
255
+ const next = new Map(state.trayIcons);
256
+ next.delete(wid);
257
+ return { trayIcons: next };
258
+ });
259
+ },
260
+
261
+ updateTrayIcon: (wid, updates) => {
262
+ set((state) => {
263
+ const next = new Map(state.trayIcons);
264
+ const existing = next.get(wid);
265
+ if (existing) {
266
+ next.set(wid, { ...existing, ...updates });
267
+ }
268
+ return { trayIcons: next };
269
+ });
270
+ },
271
+ }));
@@ -0,0 +1,25 @@
1
+ import { create } from 'zustand';
2
+ import { persist } from 'zustand/middleware';
3
+
4
+ type ThemeMode = 'light' | 'dark' | 'system';
5
+
6
+ interface ThemeStore {
7
+ mode: ThemeMode;
8
+ setMode: (mode: ThemeMode) => void;
9
+ reduceMotion: boolean;
10
+ setReduceMotion: (value: boolean) => void;
11
+ }
12
+
13
+ export const useThemeStore = create<ThemeStore>()(
14
+ persist(
15
+ (set) => ({
16
+ mode: 'dark' as ThemeMode,
17
+ setMode: (mode) => set({ mode }),
18
+ reduceMotion: false,
19
+ setReduceMotion: (reduceMotion) => set({ reduceMotion }),
20
+ }),
21
+ {
22
+ name: 'khal-theme',
23
+ }
24
+ )
25
+ );
@@ -0,0 +1,294 @@
1
+ import { v4 as uuid } from 'uuid';
2
+ import { create } from 'zustand';
3
+ import { persist } from 'zustand/middleware';
4
+ import type { SnapZone, WindowPosition, WindowSize, WindowState } from '@/types/window';
5
+
6
+ interface WindowStore {
7
+ windowsByWorkspace: Record<string, WindowState[]>;
8
+ activeWorkspaceId: string | null;
9
+ nextZIndex: number;
10
+
11
+ getWindows: () => WindowState[];
12
+ setActiveWorkspace: (id: string | null) => void;
13
+ loadWindowState: (workspaceId: string, windows: WindowState[]) => void;
14
+
15
+ openWindow: (opts: {
16
+ title: string;
17
+ appId: string;
18
+ id?: string;
19
+ width?: number;
20
+ height?: number;
21
+ x?: number;
22
+ y?: number;
23
+ meta?: Record<string, unknown>;
24
+ }) => string;
25
+ closeWindow: (id: string) => void;
26
+ removeWindowImmediate: (id: string) => void;
27
+ focusWindow: (id: string) => void;
28
+ minimizeWindow: (id: string) => void;
29
+ maximizeWindow: (id: string) => void;
30
+ restoreWindow: (id: string) => void;
31
+ moveWindow: (id: string, position: WindowPosition) => void;
32
+ resizeWindow: (id: string, size: WindowSize) => void;
33
+ snapWindow: (id: string, zone: SnapZone) => void;
34
+ unsnapWindow: (id: string) => void;
35
+ setWindowTitle: (id: string, title: string) => void;
36
+ updateWindowMeta: (id: string, meta: Record<string, unknown>) => void;
37
+ getTopmostWindow: () => WindowState | undefined;
38
+ getSerializableState: (workspaceId?: string) => WindowState[];
39
+ }
40
+
41
+ const DEFAULT_WIDTH = 800;
42
+ const DEFAULT_HEIGHT = 600;
43
+ const CASCADE_OFFSET = 30;
44
+
45
+ function currentWindows(state: {
46
+ windowsByWorkspace: Record<string, WindowState[]>;
47
+ activeWorkspaceId: string | null;
48
+ }): WindowState[] {
49
+ if (!state.activeWorkspaceId) return [];
50
+ return state.windowsByWorkspace[state.activeWorkspaceId] || [];
51
+ }
52
+
53
+ function updateCurrentWindows(
54
+ state: { windowsByWorkspace: Record<string, WindowState[]>; activeWorkspaceId: string | null },
55
+ updater: (windows: WindowState[]) => WindowState[]
56
+ ) {
57
+ if (!state.activeWorkspaceId) return state.windowsByWorkspace;
58
+ const current = state.windowsByWorkspace[state.activeWorkspaceId] || [];
59
+ return {
60
+ ...state.windowsByWorkspace,
61
+ [state.activeWorkspaceId]: updater(current),
62
+ };
63
+ }
64
+
65
+ export const useWindowStore = create<WindowStore>()(
66
+ persist(
67
+ (set, get) => ({
68
+ windowsByWorkspace: {},
69
+ activeWorkspaceId: null,
70
+ nextZIndex: 1,
71
+
72
+ getWindows: () => currentWindows(get()),
73
+
74
+ setActiveWorkspace: (id) => {
75
+ set({ activeWorkspaceId: id });
76
+ },
77
+
78
+ loadWindowState: (workspaceId, windows) => {
79
+ set((state) => ({
80
+ windowsByWorkspace: {
81
+ ...state.windowsByWorkspace,
82
+ [workspaceId]: windows,
83
+ },
84
+ nextZIndex: Math.max(state.nextZIndex, ...windows.map((w) => w.zIndex + 1), 1),
85
+ }));
86
+ },
87
+
88
+ openWindow: (opts) => {
89
+ const id = opts.id ?? uuid();
90
+ set((state) => {
91
+ const windows = currentWindows(state);
92
+ const cascade = windows.length * CASCADE_OFFSET;
93
+ const z = state.nextZIndex;
94
+ const win: WindowState = {
95
+ id,
96
+ title: opts.title,
97
+ appId: opts.appId,
98
+ position: {
99
+ x: opts.x ?? 100 + (cascade % 300),
100
+ y: opts.y ?? 100 + (cascade % 200),
101
+ },
102
+ size: {
103
+ width: opts.width ?? DEFAULT_WIDTH,
104
+ height: opts.height ?? DEFAULT_HEIGHT,
105
+ },
106
+ minimized: false,
107
+ maximized: false,
108
+ focused: true,
109
+ zIndex: z,
110
+ ...(opts.meta && { meta: opts.meta }),
111
+ };
112
+
113
+ return {
114
+ windowsByWorkspace: updateCurrentWindows(state, (ws) => [
115
+ ...ws.map((w) => ({ ...w, focused: false })),
116
+ win,
117
+ ]),
118
+ nextZIndex: z + 1,
119
+ };
120
+ });
121
+ return id;
122
+ },
123
+
124
+ closeWindow: (id) => {
125
+ // First, set closing flag so components can react
126
+ set((state) => ({
127
+ windowsByWorkspace: updateCurrentWindows(state, (ws) =>
128
+ ws.map((w) => (w.id === id ? { ...w, closing: true } : w))
129
+ ),
130
+ }));
131
+ // Then remove on next tick to give components time to see the flag
132
+ queueMicrotask(() => {
133
+ set((state) => ({
134
+ windowsByWorkspace: updateCurrentWindows(state, (ws) => ws.filter((w) => w.id !== id)),
135
+ }));
136
+ });
137
+ },
138
+
139
+ removeWindowImmediate: (id) => {
140
+ set((state) => ({
141
+ windowsByWorkspace: updateCurrentWindows(state, (ws) => ws.filter((w) => w.id !== id)),
142
+ }));
143
+ },
144
+
145
+ focusWindow: (id) => {
146
+ set((state) => {
147
+ const z = state.nextZIndex;
148
+ return {
149
+ windowsByWorkspace: updateCurrentWindows(state, (ws) =>
150
+ ws.map((w) =>
151
+ w.id === id ? { ...w, focused: true, zIndex: z, minimized: false } : { ...w, focused: false }
152
+ )
153
+ ),
154
+ nextZIndex: z + 1,
155
+ };
156
+ });
157
+ },
158
+
159
+ minimizeWindow: (id) => {
160
+ set((state) => ({
161
+ windowsByWorkspace: updateCurrentWindows(state, (ws) =>
162
+ ws.map((w) => (w.id === id ? { ...w, minimized: true, focused: false } : w))
163
+ ),
164
+ }));
165
+ },
166
+
167
+ maximizeWindow: (id) => {
168
+ set((state) => ({
169
+ windowsByWorkspace: updateCurrentWindows(state, (ws) =>
170
+ ws.map((w) => (w.id === id ? { ...w, maximized: true } : w))
171
+ ),
172
+ }));
173
+ },
174
+
175
+ restoreWindow: (id) => {
176
+ set((state) => ({
177
+ windowsByWorkspace: updateCurrentWindows(state, (ws) =>
178
+ ws.map((w) => (w.id === id ? { ...w, maximized: false, minimized: false } : w))
179
+ ),
180
+ }));
181
+ },
182
+
183
+ moveWindow: (id, position) => {
184
+ set((state) => ({
185
+ windowsByWorkspace: updateCurrentWindows(state, (ws) =>
186
+ ws.map((w) => (w.id === id ? { ...w, position } : w))
187
+ ),
188
+ }));
189
+ },
190
+
191
+ resizeWindow: (id, size) => {
192
+ set((state) => ({
193
+ windowsByWorkspace: updateCurrentWindows(state, (ws) => ws.map((w) => (w.id === id ? { ...w, size } : w))),
194
+ }));
195
+ },
196
+
197
+ snapWindow: (id, zone) => {
198
+ if (!zone) return;
199
+ const TASKBAR_HEIGHT = 44;
200
+ const vw = globalThis.innerWidth ?? 1280;
201
+ const vh = (globalThis.innerHeight ?? 800) - TASKBAR_HEIGHT;
202
+ const halfW = Math.round(vw / 2);
203
+ const halfH = Math.round(vh / 2);
204
+
205
+ const geometry: Record<NonNullable<SnapZone>, { x: number; y: number; w: number; h: number }> = {
206
+ left: { x: 0, y: 0, w: halfW, h: vh },
207
+ right: { x: halfW, y: 0, w: vw - halfW, h: vh },
208
+ top: { x: 0, y: 0, w: vw, h: vh },
209
+ 'top-left': { x: 0, y: 0, w: halfW, h: halfH },
210
+ 'top-right': { x: halfW, y: 0, w: vw - halfW, h: halfH },
211
+ 'bottom-left': { x: 0, y: halfH, w: halfW, h: vh - halfH },
212
+ 'bottom-right': { x: halfW, y: halfH, w: vw - halfW, h: vh - halfH },
213
+ };
214
+
215
+ const g = geometry[zone];
216
+ set((state) => ({
217
+ windowsByWorkspace: updateCurrentWindows(state, (ws) =>
218
+ ws.map((w) => {
219
+ if (w.id !== id) return w;
220
+ return {
221
+ ...w,
222
+ snapped: zone,
223
+ preSnapPosition: w.snapped ? w.preSnapPosition : w.position,
224
+ preSnapSize: w.snapped ? w.preSnapSize : w.size,
225
+ position: { x: g.x, y: g.y },
226
+ size: { width: g.w, height: g.h },
227
+ maximized: false,
228
+ };
229
+ })
230
+ ),
231
+ }));
232
+ },
233
+
234
+ unsnapWindow: (id) => {
235
+ set((state) => ({
236
+ windowsByWorkspace: updateCurrentWindows(state, (ws) =>
237
+ ws.map((w) => {
238
+ if (w.id !== id || !w.snapped) return w;
239
+ return {
240
+ ...w,
241
+ snapped: null,
242
+ position: w.preSnapPosition ?? w.position,
243
+ size: w.preSnapSize ?? w.size,
244
+ preSnapPosition: undefined,
245
+ preSnapSize: undefined,
246
+ };
247
+ })
248
+ ),
249
+ }));
250
+ },
251
+
252
+ setWindowTitle: (id, title) => {
253
+ set((state) => ({
254
+ windowsByWorkspace: updateCurrentWindows(state, (ws) => ws.map((w) => (w.id === id ? { ...w, title } : w))),
255
+ }));
256
+ },
257
+
258
+ updateWindowMeta: (id, meta) => {
259
+ set((state) => ({
260
+ windowsByWorkspace: updateCurrentWindows(state, (ws) =>
261
+ ws.map((w) => (w.id === id ? { ...w, meta: { ...w.meta, ...meta } } : w))
262
+ ),
263
+ }));
264
+ },
265
+
266
+ getTopmostWindow: () => {
267
+ const windows = currentWindows(get());
268
+ const visible = windows.filter((w) => !w.minimized);
269
+ if (visible.length === 0) return undefined;
270
+ return visible.reduce((a, b) => (a.zIndex > b.zIndex ? a : b));
271
+ },
272
+
273
+ getSerializableState: (workspaceId) => {
274
+ const id = workspaceId || get().activeWorkspaceId;
275
+ if (!id) return [];
276
+ return get().windowsByWorkspace[id] || [];
277
+ },
278
+ }),
279
+ {
280
+ name: 'khal-windows',
281
+ version: 1,
282
+ partialize: (state) => ({
283
+ windowsByWorkspace: Object.fromEntries(
284
+ Object.entries(state.windowsByWorkspace).map(([workspace, windows]) => [
285
+ workspace,
286
+ windows.filter((w) => !w.closing).map(({ closing: _, ...rest }) => rest),
287
+ ])
288
+ ),
289
+ activeWorkspaceId: state.activeWorkspaceId,
290
+ nextZIndex: state.nextZIndex,
291
+ }),
292
+ }
293
+ )
294
+ );