@nimbus-sh/worker 0.1.0

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/README.md +219 -0
  2. package/dist/_shared/bytes.d.ts +18 -0
  3. package/dist/_shared/bytes.d.ts.map +1 -0
  4. package/dist/_shared/bytes.js +17 -0
  5. package/dist/_shared/cache-stats.d.ts +146 -0
  6. package/dist/_shared/cache-stats.d.ts.map +1 -0
  7. package/dist/_shared/cache-stats.js +162 -0
  8. package/dist/_shared/exports-resolver.d.ts +76 -0
  9. package/dist/_shared/exports-resolver.d.ts.map +1 -0
  10. package/dist/_shared/exports-resolver.js +284 -0
  11. package/dist/_shared/install-phase.d.ts +47 -0
  12. package/dist/_shared/install-phase.d.ts.map +1 -0
  13. package/dist/_shared/install-phase.js +1 -0
  14. package/dist/_shared/real-node-imports.d.ts +38 -0
  15. package/dist/_shared/real-node-imports.d.ts.map +1 -0
  16. package/dist/_shared/real-node-imports.js +46 -0
  17. package/dist/_shared/retry.d.ts +93 -0
  18. package/dist/_shared/retry.d.ts.map +1 -0
  19. package/dist/_shared/retry.js +161 -0
  20. package/dist/_shared/session-id.d.ts +39 -0
  21. package/dist/_shared/session-id.d.ts.map +1 -0
  22. package/dist/_shared/session-id.js +127 -0
  23. package/dist/_shared/session-router.d.ts +104 -0
  24. package/dist/_shared/session-router.d.ts.map +1 -0
  25. package/dist/_shared/session-router.js +165 -0
  26. package/dist/_shared/w7-frame.d.ts +93 -0
  27. package/dist/_shared/w7-frame.d.ts.map +1 -0
  28. package/dist/_shared/w7-frame.js +402 -0
  29. package/dist/auth/index.d.ts +16 -0
  30. package/dist/auth/index.d.ts.map +1 -0
  31. package/dist/auth/index.js +15 -0
  32. package/dist/auth/middleware.d.ts +85 -0
  33. package/dist/auth/middleware.d.ts.map +1 -0
  34. package/dist/auth/middleware.js +150 -0
  35. package/dist/auth/token.d.ts +96 -0
  36. package/dist/auth/token.d.ts.map +1 -0
  37. package/dist/auth/token.js +226 -0
  38. package/dist/auth/types.d.ts +141 -0
  39. package/dist/auth/types.d.ts.map +1 -0
  40. package/dist/auth/types.js +114 -0
  41. package/dist/bindings/d1.d.ts +102 -0
  42. package/dist/bindings/d1.d.ts.map +1 -0
  43. package/dist/bindings/d1.js +549 -0
  44. package/dist/bindings/kv.d.ts +82 -0
  45. package/dist/bindings/kv.d.ts.map +1 -0
  46. package/dist/bindings/kv.js +290 -0
  47. package/dist/bindings/r2.d.ts +137 -0
  48. package/dist/bindings/r2.d.ts.map +1 -0
  49. package/dist/bindings/r2.js +379 -0
  50. package/dist/cirrus-npm-cjs.generated.d.ts +17 -0
  51. package/dist/cirrus-npm-cjs.generated.d.ts.map +1 -0
  52. package/dist/cirrus-npm-cjs.generated.js +41 -0
  53. package/dist/cirrus-plugin-react.generated.d.ts +34 -0
  54. package/dist/cirrus-plugin-react.generated.d.ts.map +1 -0
  55. package/dist/cirrus-plugin-react.generated.js +33 -0
  56. package/dist/constants.d.ts +34 -0
  57. package/dist/constants.d.ts.map +1 -0
  58. package/dist/constants.js +116 -0
  59. package/dist/esbuild-wasm-bundle.generated.d.ts +41 -0
  60. package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -0
  61. package/dist/esbuild-wasm-bundle.generated.js +40 -0
  62. package/dist/facets/cirrus-real.d.ts +147 -0
  63. package/dist/facets/cirrus-real.d.ts.map +1 -0
  64. package/dist/facets/cirrus-real.js +981 -0
  65. package/dist/facets/inner-do-registry.d.ts +41 -0
  66. package/dist/facets/inner-do-registry.d.ts.map +1 -0
  67. package/dist/facets/inner-do-registry.js +51 -0
  68. package/dist/facets/manager.d.ts +314 -0
  69. package/dist/facets/manager.d.ts.map +1 -0
  70. package/dist/facets/manager.js +2681 -0
  71. package/dist/facets/process.d.ts +318 -0
  72. package/dist/facets/process.d.ts.map +1 -0
  73. package/dist/facets/process.js +567 -0
  74. package/dist/facets/real-vite-fs-shim.d.ts +151 -0
  75. package/dist/facets/real-vite-fs-shim.d.ts.map +1 -0
  76. package/dist/facets/real-vite-fs-shim.js +1279 -0
  77. package/dist/facets/real-vite-hmr.d.ts +142 -0
  78. package/dist/facets/real-vite-hmr.d.ts.map +1 -0
  79. package/dist/facets/real-vite-hmr.js +476 -0
  80. package/dist/facets/vite-dev-server.d.ts +293 -0
  81. package/dist/facets/vite-dev-server.d.ts.map +1 -0
  82. package/dist/facets/vite-dev-server.js +2580 -0
  83. package/dist/facets/wasm-swap-registry.d.ts +261 -0
  84. package/dist/facets/wasm-swap-registry.d.ts.map +1 -0
  85. package/dist/facets/wasm-swap-registry.js +551 -0
  86. package/dist/facets/ws-terminal.d.ts +107 -0
  87. package/dist/facets/ws-terminal.d.ts.map +1 -0
  88. package/dist/facets/ws-terminal.js +207 -0
  89. package/dist/git/commands.d.ts +13 -0
  90. package/dist/git/commands.d.ts.map +1 -0
  91. package/dist/git/commands.js +633 -0
  92. package/dist/git/network-facet.d.ts +61 -0
  93. package/dist/git/network-facet.d.ts.map +1 -0
  94. package/dist/git/network-facet.js +700 -0
  95. package/dist/git-bundle.generated.d.ts +16 -0
  96. package/dist/git-bundle.generated.d.ts.map +1 -0
  97. package/dist/git-bundle.generated.js +15 -0
  98. package/dist/index.d.ts +41 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +73 -0
  101. package/dist/loaders/child-process/spawn-facet.d.ts +90 -0
  102. package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -0
  103. package/dist/loaders/child-process/spawn-facet.js +103 -0
  104. package/dist/loaders/child-process/spawn-pool.d.ts +110 -0
  105. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -0
  106. package/dist/loaders/child-process/spawn-pool.js +161 -0
  107. package/dist/loaders/fanout-pool.d.ts +195 -0
  108. package/dist/loaders/fanout-pool.d.ts.map +1 -0
  109. package/dist/loaders/fanout-pool.js +326 -0
  110. package/dist/loaders/generated-workers.d.ts +26 -0
  111. package/dist/loaders/generated-workers.d.ts.map +1 -0
  112. package/dist/loaders/generated-workers.js +25 -0
  113. package/dist/loaders/index.d.ts +15 -0
  114. package/dist/loaders/index.d.ts.map +1 -0
  115. package/dist/loaders/index.js +13 -0
  116. package/dist/loaders/loader-pool.d.ts +273 -0
  117. package/dist/loaders/loader-pool.d.ts.map +1 -0
  118. package/dist/loaders/loader-pool.js +624 -0
  119. package/dist/loaders/npm-resolve-preamble.d.ts +34 -0
  120. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -0
  121. package/dist/loaders/npm-resolve-preamble.js +237 -0
  122. package/dist/loaders/pre-bundle-preamble.d.ts +41 -0
  123. package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -0
  124. package/dist/loaders/pre-bundle-preamble.js +114 -0
  125. package/dist/loaders/vendor/codegen.d.ts +24 -0
  126. package/dist/loaders/vendor/codegen.d.ts.map +1 -0
  127. package/dist/loaders/vendor/codegen.js +44 -0
  128. package/dist/loaders/vendor/errors.d.ts +24 -0
  129. package/dist/loaders/vendor/errors.d.ts.map +1 -0
  130. package/dist/loaders/vendor/errors.js +46 -0
  131. package/dist/loaders/vendor/index.d.ts +16 -0
  132. package/dist/loaders/vendor/index.d.ts.map +1 -0
  133. package/dist/loaders/vendor/index.js +11 -0
  134. package/dist/loaders/vendor/pool.d.ts +88 -0
  135. package/dist/loaders/vendor/pool.d.ts.map +1 -0
  136. package/dist/loaders/vendor/pool.js +353 -0
  137. package/dist/loaders/vendor/primitives.d.ts +11 -0
  138. package/dist/loaders/vendor/primitives.d.ts.map +1 -0
  139. package/dist/loaders/vendor/primitives.js +33 -0
  140. package/dist/loaders/vendor/serialize.d.ts +3 -0
  141. package/dist/loaders/vendor/serialize.d.ts.map +1 -0
  142. package/dist/loaders/vendor/serialize.js +25 -0
  143. package/dist/loaders/vendor/types.d.ts +60 -0
  144. package/dist/loaders/vendor/types.d.ts.map +1 -0
  145. package/dist/loaders/vendor/types.js +4 -0
  146. package/dist/npm/cache.d.ts +136 -0
  147. package/dist/npm/cache.d.ts.map +1 -0
  148. package/dist/npm/cache.js +429 -0
  149. package/dist/npm/install-batch-facet.d.ts +129 -0
  150. package/dist/npm/install-batch-facet.d.ts.map +1 -0
  151. package/dist/npm/install-batch-facet.js +598 -0
  152. package/dist/npm/install-facet.d.ts +29 -0
  153. package/dist/npm/install-facet.d.ts.map +1 -0
  154. package/dist/npm/install-facet.js +9 -0
  155. package/dist/npm/installer.d.ts +239 -0
  156. package/dist/npm/installer.d.ts.map +1 -0
  157. package/dist/npm/installer.js +2022 -0
  158. package/dist/npm/npx-install.d.ts +74 -0
  159. package/dist/npm/npx-install.d.ts.map +1 -0
  160. package/dist/npm/npx-install.js +302 -0
  161. package/dist/npm/pre-bundle-facet.d.ts +168 -0
  162. package/dist/npm/pre-bundle-facet.d.ts.map +1 -0
  163. package/dist/npm/pre-bundle-facet.js +653 -0
  164. package/dist/npm/r2-cache.d.ts +286 -0
  165. package/dist/npm/r2-cache.d.ts.map +1 -0
  166. package/dist/npm/r2-cache.js +506 -0
  167. package/dist/npm/resolve-facet.d.ts +202 -0
  168. package/dist/npm/resolve-facet.d.ts.map +1 -0
  169. package/dist/npm/resolve-facet.js +833 -0
  170. package/dist/npm/resolve-one-facet.d.ts +204 -0
  171. package/dist/npm/resolve-one-facet.d.ts.map +1 -0
  172. package/dist/npm/resolve-one-facet.js +437 -0
  173. package/dist/npm/resolver.d.ts +109 -0
  174. package/dist/npm/resolver.d.ts.map +1 -0
  175. package/dist/npm/resolver.js +899 -0
  176. package/dist/npm/tarball-stream.d.ts +85 -0
  177. package/dist/npm/tarball-stream.d.ts.map +1 -0
  178. package/dist/npm/tarball-stream.js +205 -0
  179. package/dist/npm/tarball.d.ts +51 -0
  180. package/dist/npm/tarball.d.ts.map +1 -0
  181. package/dist/npm/tarball.js +185 -0
  182. package/dist/observability/diag-counters.d.ts +292 -0
  183. package/dist/observability/diag-counters.d.ts.map +1 -0
  184. package/dist/observability/diag-counters.js +302 -0
  185. package/dist/observability/heap-estimate.d.ts +166 -0
  186. package/dist/observability/heap-estimate.d.ts.map +1 -0
  187. package/dist/observability/heap-estimate.js +192 -0
  188. package/dist/observability/heavy-alloc-coord.d.ts +95 -0
  189. package/dist/observability/heavy-alloc-coord.d.ts.map +1 -0
  190. package/dist/observability/heavy-alloc-coord.js +149 -0
  191. package/dist/observability/oom-classify.d.ts +56 -0
  192. package/dist/observability/oom-classify.d.ts.map +1 -0
  193. package/dist/observability/oom-classify.js +118 -0
  194. package/dist/observability/oom-discriminator.d.ts +192 -0
  195. package/dist/observability/oom-discriminator.d.ts.map +1 -0
  196. package/dist/observability/oom-discriminator.js +233 -0
  197. package/dist/real-vite-bundle.generated.d.ts +46 -0
  198. package/dist/real-vite-bundle.generated.d.ts.map +1 -0
  199. package/dist/real-vite-bundle.generated.js +53 -0
  200. package/dist/replica/routing.d.ts +171 -0
  201. package/dist/replica/routing.d.ts.map +1 -0
  202. package/dist/replica/routing.js +288 -0
  203. package/dist/replica/suspension.d.ts +36 -0
  204. package/dist/replica/suspension.d.ts.map +1 -0
  205. package/dist/replica/suspension.js +49 -0
  206. package/dist/router/index.d.ts +156 -0
  207. package/dist/router/index.d.ts.map +1 -0
  208. package/dist/router/index.js +220 -0
  209. package/dist/runtime/assets-loader.d.ts +71 -0
  210. package/dist/runtime/assets-loader.d.ts.map +1 -0
  211. package/dist/runtime/assets-loader.js +128 -0
  212. package/dist/runtime/barrel-detect.d.ts +46 -0
  213. package/dist/runtime/barrel-detect.d.ts.map +1 -0
  214. package/dist/runtime/barrel-detect.js +73 -0
  215. package/dist/runtime/barrel-synthesizer.d.ts +181 -0
  216. package/dist/runtime/barrel-synthesizer.d.ts.map +1 -0
  217. package/dist/runtime/barrel-synthesizer.js +541 -0
  218. package/dist/runtime/bun-repl.d.ts +46 -0
  219. package/dist/runtime/bun-repl.d.ts.map +1 -0
  220. package/dist/runtime/bun-repl.js +326 -0
  221. package/dist/runtime/bun-runner.d.ts +66 -0
  222. package/dist/runtime/bun-runner.d.ts.map +1 -0
  223. package/dist/runtime/bun-runner.js +219 -0
  224. package/dist/runtime/clang-runner.d.ts +38 -0
  225. package/dist/runtime/clang-runner.d.ts.map +1 -0
  226. package/dist/runtime/clang-runner.js +1033 -0
  227. package/dist/runtime/comment-strip.d.ts +37 -0
  228. package/dist/runtime/comment-strip.d.ts.map +1 -0
  229. package/dist/runtime/comment-strip.js +74 -0
  230. package/dist/runtime/esbuild-service.d.ts +201 -0
  231. package/dist/runtime/esbuild-service.d.ts.map +1 -0
  232. package/dist/runtime/esbuild-service.js +1444 -0
  233. package/dist/runtime/esbuild-wasm-bytes.d.ts +87 -0
  234. package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -0
  235. package/dist/runtime/esbuild-wasm-bytes.js +131 -0
  236. package/dist/runtime/framework-detect.d.ts +71 -0
  237. package/dist/runtime/framework-detect.d.ts.map +1 -0
  238. package/dist/runtime/framework-detect.js +178 -0
  239. package/dist/runtime/long-running-handle.d.ts +133 -0
  240. package/dist/runtime/long-running-handle.d.ts.map +1 -0
  241. package/dist/runtime/long-running-handle.js +192 -0
  242. package/dist/runtime/node-repl.d.ts +36 -0
  243. package/dist/runtime/node-repl.d.ts.map +1 -0
  244. package/dist/runtime/node-repl.js +293 -0
  245. package/dist/runtime/node-runner.d.ts +111 -0
  246. package/dist/runtime/node-runner.d.ts.map +1 -0
  247. package/dist/runtime/node-runner.js +194 -0
  248. package/dist/runtime/node-shims.d.ts +24 -0
  249. package/dist/runtime/node-shims.d.ts.map +1 -0
  250. package/dist/runtime/node-shims.js +3019 -0
  251. package/dist/runtime/package-manager.d.ts +118 -0
  252. package/dist/runtime/package-manager.d.ts.map +1 -0
  253. package/dist/runtime/package-manager.js +420 -0
  254. package/dist/runtime/port-registry.d.ts +80 -0
  255. package/dist/runtime/port-registry.d.ts.map +1 -0
  256. package/dist/runtime/port-registry.js +166 -0
  257. package/dist/runtime/process-logs-api.d.ts +102 -0
  258. package/dist/runtime/process-logs-api.d.ts.map +1 -0
  259. package/dist/runtime/process-logs-api.js +253 -0
  260. package/dist/runtime/process-logs.d.ts +311 -0
  261. package/dist/runtime/process-logs.d.ts.map +1 -0
  262. package/dist/runtime/process-logs.js +656 -0
  263. package/dist/runtime/process-table.d.ts +65 -0
  264. package/dist/runtime/process-table.d.ts.map +1 -0
  265. package/dist/runtime/process-table.js +107 -0
  266. package/dist/runtime/project-detect.d.ts +21 -0
  267. package/dist/runtime/project-detect.d.ts.map +1 -0
  268. package/dist/runtime/project-detect.js +46 -0
  269. package/dist/runtime/python-repl.d.ts +66 -0
  270. package/dist/runtime/python-repl.d.ts.map +1 -0
  271. package/dist/runtime/python-repl.js +563 -0
  272. package/dist/runtime/python-runner.d.ts +50 -0
  273. package/dist/runtime/python-runner.d.ts.map +1 -0
  274. package/dist/runtime/python-runner.js +870 -0
  275. package/dist/runtime/repl-session.d.ts +160 -0
  276. package/dist/runtime/repl-session.d.ts.map +1 -0
  277. package/dist/runtime/repl-session.js +393 -0
  278. package/dist/runtime/require-resolver.d.ts +52 -0
  279. package/dist/runtime/require-resolver.d.ts.map +1 -0
  280. package/dist/runtime/require-resolver.js +690 -0
  281. package/dist/runtime/router-basename.d.ts +36 -0
  282. package/dist/runtime/router-basename.d.ts.map +1 -0
  283. package/dist/runtime/router-basename.js +310 -0
  284. package/dist/runtime/ruby-repl.d.ts +43 -0
  285. package/dist/runtime/ruby-repl.d.ts.map +1 -0
  286. package/dist/runtime/ruby-repl.js +229 -0
  287. package/dist/runtime/ruby-runner.d.ts +62 -0
  288. package/dist/runtime/ruby-runner.d.ts.map +1 -0
  289. package/dist/runtime/ruby-runner.js +737 -0
  290. package/dist/runtime/runtime-catalog.d.ts +99 -0
  291. package/dist/runtime/runtime-catalog.d.ts.map +1 -0
  292. package/dist/runtime/runtime-catalog.js +183 -0
  293. package/dist/runtime/runtime-registry.d.ts +136 -0
  294. package/dist/runtime/runtime-registry.d.ts.map +1 -0
  295. package/dist/runtime/runtime-registry.js +318 -0
  296. package/dist/runtime/streams.d.ts +13 -0
  297. package/dist/runtime/streams.d.ts.map +1 -0
  298. package/dist/runtime/streams.js +402 -0
  299. package/dist/runtime/wasi-instance.d.ts +190 -0
  300. package/dist/runtime/wasi-instance.d.ts.map +1 -0
  301. package/dist/runtime/wasi-instance.js +2044 -0
  302. package/dist/runtime/wasm-runner.d.ts +117 -0
  303. package/dist/runtime/wasm-runner.d.ts.map +1 -0
  304. package/dist/runtime/wasm-runner.js +705 -0
  305. package/dist/session/bindings.d.ts +168 -0
  306. package/dist/session/bindings.d.ts.map +1 -0
  307. package/dist/session/bindings.js +524 -0
  308. package/dist/session/ctx-exports.d.ts +16 -0
  309. package/dist/session/ctx-exports.d.ts.map +1 -0
  310. package/dist/session/ctx-exports.js +22 -0
  311. package/dist/session/diag.d.ts +62 -0
  312. package/dist/session/diag.d.ts.map +1 -0
  313. package/dist/session/diag.js +118 -0
  314. package/dist/session/fs-watch.d.ts +132 -0
  315. package/dist/session/fs-watch.d.ts.map +1 -0
  316. package/dist/session/fs-watch.js +255 -0
  317. package/dist/session/helpers.d.ts +150 -0
  318. package/dist/session/helpers.d.ts.map +1 -0
  319. package/dist/session/helpers.js +472 -0
  320. package/dist/session/hibernation.d.ts +145 -0
  321. package/dist/session/hibernation.d.ts.map +1 -0
  322. package/dist/session/hibernation.js +385 -0
  323. package/dist/session/init-phases.d.ts +87 -0
  324. package/dist/session/init-phases.d.ts.map +1 -0
  325. package/dist/session/init-phases.js +137 -0
  326. package/dist/session/init.d.ts +46 -0
  327. package/dist/session/init.d.ts.map +1 -0
  328. package/dist/session/init.js +3579 -0
  329. package/dist/session/keys.d.ts +56 -0
  330. package/dist/session/keys.d.ts.map +1 -0
  331. package/dist/session/keys.js +55 -0
  332. package/dist/session/nimbus-session.d.ts +434 -0
  333. package/dist/session/nimbus-session.d.ts.map +1 -0
  334. package/dist/session/nimbus-session.js +1084 -0
  335. package/dist/session/programmatic.d.ts +105 -0
  336. package/dist/session/programmatic.d.ts.map +1 -0
  337. package/dist/session/programmatic.js +326 -0
  338. package/dist/session/replica-routes.d.ts +68 -0
  339. package/dist/session/replica-routes.d.ts.map +1 -0
  340. package/dist/session/replica-routes.js +77 -0
  341. package/dist/session/routes.d.ts +29 -0
  342. package/dist/session/routes.d.ts.map +1 -0
  343. package/dist/session/routes.js +1331 -0
  344. package/dist/session/rpc.d.ts +323 -0
  345. package/dist/session/rpc.d.ts.map +1 -0
  346. package/dist/session/rpc.js +808 -0
  347. package/dist/session/state-store.d.ts +210 -0
  348. package/dist/session/state-store.d.ts.map +1 -0
  349. package/dist/session/state-store.js +459 -0
  350. package/dist/session/supervisor-rpc.d.ts +269 -0
  351. package/dist/session/supervisor-rpc.d.ts.map +1 -0
  352. package/dist/session/supervisor-rpc.js +437 -0
  353. package/dist/session/ws-hibernation-config.d.ts +64 -0
  354. package/dist/session/ws-hibernation-config.d.ts.map +1 -0
  355. package/dist/session/ws-hibernation-config.js +89 -0
  356. package/dist/session/ws.d.ts +95 -0
  357. package/dist/session/ws.d.ts.map +1 -0
  358. package/dist/session/ws.js +395 -0
  359. package/dist/shell/features.d.ts +410 -0
  360. package/dist/shell/features.d.ts.map +1 -0
  361. package/dist/shell/features.js +1885 -0
  362. package/dist/shell/unix-commands.d.ts +15 -0
  363. package/dist/shell/unix-commands.d.ts.map +1 -0
  364. package/dist/shell/unix-commands.js +3703 -0
  365. package/dist/stubs/lifo-ui.d.ts +10 -0
  366. package/dist/stubs/lifo-ui.d.ts.map +1 -0
  367. package/dist/stubs/lifo-ui.js +1 -0
  368. package/dist/tailwind-play.generated.d.ts +19 -0
  369. package/dist/tailwind-play.generated.d.ts.map +1 -0
  370. package/dist/tailwind-play.generated.js +18 -0
  371. package/dist/vfs/dev-provider.d.ts +64 -0
  372. package/dist/vfs/dev-provider.d.ts.map +1 -0
  373. package/dist/vfs/dev-provider.js +191 -0
  374. package/dist/vfs/events.d.ts +51 -0
  375. package/dist/vfs/events.d.ts.map +1 -0
  376. package/dist/vfs/events.js +127 -0
  377. package/dist/vfs/path.d.ts +30 -0
  378. package/dist/vfs/path.d.ts.map +1 -0
  379. package/dist/vfs/path.js +41 -0
  380. package/dist/vfs/seed-project.d.ts +62 -0
  381. package/dist/vfs/seed-project.d.ts.map +1 -0
  382. package/dist/vfs/seed-project.js +1045 -0
  383. package/dist/vfs/sqlite-vfs.d.ts +435 -0
  384. package/dist/vfs/sqlite-vfs.d.ts.map +1 -0
  385. package/dist/vfs/sqlite-vfs.js +1670 -0
  386. package/dist/vfs/symlink-registry.d.ts +64 -0
  387. package/dist/vfs/symlink-registry.d.ts.map +1 -0
  388. package/dist/vfs/symlink-registry.js +137 -0
  389. package/dist/wrangler/nimbus-wrangler.d.ts +157 -0
  390. package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -0
  391. package/dist/wrangler/nimbus-wrangler.js +931 -0
  392. package/package.json +77 -0
  393. package/public/_assets/cirrus-plugin-react.bundle.js +70740 -0
  394. package/public/_assets/esbuild-0.24.2.wasm +0 -0
  395. package/public/_assets/real-vite-bundle.js +101478 -0
  396. package/public/_assets/rollup.wasm +0 -0
  397. package/public/favicon.svg +8 -0
  398. package/public/index.html +565 -0
  399. package/public/s/index.html +2379 -0
  400. package/scripts/bundle-esbuild-wasm.mjs +244 -0
  401. package/scripts/bundle-facet-workers.mjs +137 -0
  402. package/scripts/bundle-git.mjs +118 -0
  403. package/scripts/bundle-npm-cjs.mjs +363 -0
  404. package/scripts/bundle-plugin-react.mjs +480 -0
  405. package/scripts/bundle-real-vite.mjs +959 -0
  406. package/scripts/bundle-runtime.mjs +600 -0
  407. package/scripts/bundle-tailwind-play.mjs +130 -0
  408. package/scripts/patch-lifo-ui.mjs +131 -0
@@ -0,0 +1,2379 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Nimbus — Cloud Dev Environment</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
8
+ <style>
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+ body { background: #0d1117; color: #c9d1d9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
11
+
12
+ /* Header */
13
+ .header { display: flex; align-items: center; justify-content: space-between; padding: 4px 12px; background: #161b22; border-bottom: 1px solid #30363d; flex-shrink: 0; min-height: 32px; }
14
+ .header h1 { font-size: 13px; font-weight: 600; color: #58a6ff; display: flex; gap: 8px; align-items: center; }
15
+ .header h1 span { color: #3fb950; font-weight: 400; font-family: ui-monospace, Menlo, monospace; font-size: 11px; cursor: pointer; padding: 1px 6px; border-radius: 3px; background: #161b22; border: 1px solid #30363d; transition: background 0.15s; }
16
+ .header h1 span:hover { background: #21262d; }
17
+ .header h1 span.copied { color: #3fb950; background: #0f2416; border-color: #238636; }
18
+ .status { display: flex; align-items: center; gap: 6px; font-size: 12px; }
19
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #f85149; transition: background 0.3s; }
20
+ .status-dot.connected { background: #3fb950; }
21
+ .status-dot.connecting { background: #d29922; animation: pulse 1s infinite; }
22
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
23
+
24
+ /* Toolbar */
25
+ .toolbar { display: flex; align-items: center; gap: 2px; padding: 2px 8px; background: #161b22; border-bottom: 1px solid #30363d; flex-shrink: 0; }
26
+ .toolbar button { background: #21262d; border: 1px solid #30363d; color: #c9d1d9; padding: 3px 10px; font-size: 11px; cursor: pointer; border-radius: 4px; }
27
+ .toolbar button:hover { background: #30363d; }
28
+ .toolbar button.active { background: #388bfd26; border-color: #58a6ff; color: #58a6ff; }
29
+ .toolbar .sep { width: 1px; height: 16px; background: #30363d; margin: 0 4px; }
30
+ .toolbar .vite-status { font-size: 11px; color: #8b949e; margin-left: auto; }
31
+ .toolbar .vite-status.running { color: #3fb950; }
32
+ /* Process count button. Sits next to the layout-mode buttons. Hidden
33
+ (display:none) until at least one process has been seen in this
34
+ session so it doesn't clutter a brand-new session's toolbar. */
35
+ .toolbar #btnProcs { display: none; gap: 6px; align-items: center; font-family: ui-monospace, Menlo, monospace; }
36
+ .toolbar #btnProcs.visible { display: inline-flex; }
37
+ .toolbar #btnProcs.has-running { border-color: #3fb950; color: #3fb950; }
38
+ .toolbar #btnProcs.active { background: #388bfd26; border-color: #58a6ff; color: #58a6ff; }
39
+ .toolbar #btnProcs .procs-dot { width: 6px; height: 6px; border-radius: 50%; background: #6e7681; display: inline-block; }
40
+ .toolbar #btnProcs.has-running .procs-dot { background: #3fb950; box-shadow: 0 0 4px #3fb950; animation: pulse 1.6s ease-in-out infinite; }
41
+
42
+ /* Main panel */
43
+ .main { flex: 1; display: flex; overflow: hidden; }
44
+
45
+ /* Terminal panel */
46
+ .panel-terminal { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
47
+ #terminal-container { flex: 1; padding: 4px; overflow: hidden; min-height: 0; }
48
+
49
+ /* Process log tabs panel — lives below the xterm inside .panel-terminal.
50
+ Hidden entirely when no tabs are open so the xterm fills the full panel. */
51
+ .logs-panel { display: none; flex-direction: column; border-top: 1px solid #30363d; background: #0d1117; flex-shrink: 0; max-height: 45%; min-height: 120px; }
52
+ .logs-panel.active { display: flex; }
53
+ .logs-tabbar { display: flex; align-items: stretch; background: #161b22; border-bottom: 1px solid #30363d; overflow-x: auto; scrollbar-width: thin; flex-shrink: 0; }
54
+ .logs-tabbar::-webkit-scrollbar { height: 4px; }
55
+ .logs-tabbar::-webkit-scrollbar-thumb { background: #30363d; }
56
+ .logs-tab { display: flex; align-items: center; gap: 6px; padding: 4px 8px 4px 10px; font-size: 11px; color: #8b949e; border-right: 1px solid #30363d; cursor: pointer; white-space: nowrap; background: #161b22; font-family: ui-monospace, Menlo, monospace; user-select: none; max-width: 240px; flex-shrink: 0; }
57
+ .logs-tab:hover { background: #1c2128; color: #c9d1d9; }
58
+ .logs-tab.active { background: #0d1117; color: #58a6ff; box-shadow: inset 0 -2px 0 #58a6ff; }
59
+ .logs-tab.exited { color: #6e7681; }
60
+ .logs-tab.exited.failed { color: #ff7b72; }
61
+ .logs-tab.exited.active { color: #ff9e80; }
62
+ .logs-tab .logs-tab-label { overflow: hidden; text-overflow: ellipsis; max-width: 180px; }
63
+ .logs-tab .logs-tab-dot { width: 6px; height: 6px; border-radius: 50%; background: #3fb950; flex-shrink: 0; box-shadow: 0 0 4px #3fb950; }
64
+ .logs-tab.exited .logs-tab-dot { background: #6e7681; box-shadow: none; }
65
+ .logs-tab.exited.failed .logs-tab-dot { background: #ff7b72; box-shadow: none; }
66
+ .logs-tab .logs-tab-close { color: #6e7681; font-size: 13px; line-height: 1; padding: 0 2px; border-radius: 2px; margin-left: 2px; }
67
+ .logs-tab .logs-tab-close:hover { color: #f85149; background: #30363d; }
68
+ /* Primitives wave (P11): per-tab Kill / Restart controls. Kept
69
+ small + monochrome so they don't compete with the close X for
70
+ attention. Hover state mirrors the close-X red. */
71
+ .logs-tab .logs-tab-action { color: #6e7681; font-size: 11px; line-height: 1; padding: 0 4px; border-radius: 2px; margin-left: 2px; cursor: pointer; user-select: none; }
72
+ .logs-tab .logs-tab-action:hover { color: #c9d1d9; background: #30363d; }
73
+ .logs-tab .logs-tab-kill:hover { color: #f85149; }
74
+ .logs-tab.exited .logs-tab-action { display: none; }
75
+ .logs-panel-body { flex: 1; overflow: auto; background: #0d1117; position: relative; min-height: 0; }
76
+ .logs-view { display: none; white-space: pre; font-family: 'JetBrains Mono', 'Fira Code', Menlo, monospace; font-size: 12px; line-height: 1.5; color: #c9d1d9; padding: 8px 10px; word-break: break-all; min-height: 100%; }
77
+ .logs-view.active { display: block; }
78
+ .logs-view .stderr { color: #ff7b72; }
79
+ .logs-view .binary { color: #8b949e; font-style: italic; }
80
+ .logs-view .banner { display: block; color: #d29922; background: #221a00; border-left: 2px solid #d29922; padding: 4px 8px; margin: 6px 0; font-family: inherit; }
81
+ .logs-view .banner.ok { color: #3fb950; background: #0f2416; border-left-color: #3fb950; }
82
+ .logs-view .banner.failed { color: #ff7b72; background: #2d0d0c; border-left-color: #ff7b72; }
83
+
84
+ /* Resize handle */
85
+ .resize-handle { width: 4px; background: #30363d; cursor: col-resize; flex-shrink: 0; }
86
+ .resize-handle:hover { background: #58a6ff; }
87
+ /* monaco-polish (2026-05-14): vertical resize handle for editor (top) ↔
88
+ terminal (bottom) inside the editor-stack column. Hidden outside
89
+ editor mode (rule under .main.editor below). */
90
+ .vresize-handle {
91
+ display: none; height: 4px; background: #30363d;
92
+ cursor: row-resize; flex-shrink: 0;
93
+ }
94
+ .vresize-handle:hover { background: #58a6ff; }
95
+
96
+ /* Preview panel */
97
+ .panel-preview { flex: 1; display: flex; flex-direction: column; background: #0d1117; overflow: hidden; min-width: 0; }
98
+ .panel-preview.hidden { display: none; }
99
+ .preview-bar { display: flex; align-items: center; gap: 6px; padding: 3px 8px; background: #161b22; border-bottom: 1px solid #30363d; flex-shrink: 0; }
100
+ .preview-bar .url-bar { flex: 1; background: #0d1117; border: 1px solid #30363d; color: #c9d1d9; padding: 2px 8px; font-size: 11px; border-radius: 4px; font-family: monospace; }
101
+ .preview-bar button { background: none; border: none; color: #8b949e; cursor: pointer; font-size: 14px; padding: 2px 4px; }
102
+ .preview-bar button:hover { color: #c9d1d9; }
103
+ #preview-frame { flex: 1; border: none; background: white; width: 100%; }
104
+
105
+ /* Footer */
106
+ .footer { padding: 3px 12px; background: #161b22; border-top: 1px solid #30363d; font-size: 11px; color: #8b949e; flex-shrink: 0; display: flex; justify-content: space-between; gap: 12px; }
107
+ .footer .stats { display: flex; gap: 10px; flex-wrap: wrap; }
108
+ .footer .stat-label { color: #484f58; }
109
+ .footer .stat-value { color: #c9d1d9; }
110
+ .footer .stat-value.good { color: #3fb950; }
111
+
112
+ /* Layout modes */
113
+ .main.terminal-only .panel-preview,
114
+ .main.terminal-only .panel-editor,
115
+ .main.terminal-only .resize-handle { display: none; }
116
+ .main.terminal-only .panel-terminal { flex: 1; }
117
+ .main.preview-only .panel-terminal,
118
+ .main.preview-only .panel-editor,
119
+ .main.preview-only .resize-handle { display: none; }
120
+ .main.preview-only .panel-preview { flex: 1; }
121
+
122
+ /* monaco-wave-a + Wave-B (2026-05-13/14):
123
+ Editor layout mode — 4-pane VSCode-style:
124
+ FILE TREE | editor-stack (editor TOP + terminal BOTTOM) | PREVIEW
125
+ Single mode: 'editor'. Dropped earlier 'editor-split' (editor+preview
126
+ without terminal) per user feedback "I don't need 'edit + preview'
127
+ at all". Renamed 'editor-split-with-term' → 'editor' for clarity.
128
+
129
+ Editor + terminal live INSIDE .panel-left-stack. In 1D modes
130
+ (terminal-only / split / preview-only) the stack is
131
+ display:contents (layout-transparent — children flex directly
132
+ into .main). In editor mode the stack flips to flex column so
133
+ editor (top) + terminal (bottom) stack vertically. */
134
+
135
+ /* Editor panel — hidden by default; shown by mode-specific rules. */
136
+ .panel-editor {
137
+ flex: 1; display: none; flex-direction: column;
138
+ background: #1e1e1e; /* VSCode dark editor background */
139
+ overflow: hidden; min-width: 0; min-height: 0;
140
+ }
141
+ .panel-editor.active { display: flex; }
142
+
143
+ /* Wrapper for the editor-stack column. display:contents in 1D
144
+ modes (transparent). flex column in editor mode. */
145
+ .panel-left-stack { display: contents; }
146
+
147
+ /* monaco-wave-b: File-tree sidebar. Lives as a FIRST child of
148
+ .main; visible only in 'editor' mode. ~250px default width,
149
+ resizable via .tree-resize-handle. */
150
+ .panel-tree {
151
+ display: none; flex-direction: column;
152
+ width: 250px; min-width: 160px; max-width: 600px;
153
+ background: #181818; /* slightly darker than editor bg */
154
+ border-right: 1px solid #30363d;
155
+ flex-shrink: 0;
156
+ overflow: hidden;
157
+ }
158
+ .tree-resize-handle {
159
+ display: none;
160
+ width: 4px; background: #30363d; cursor: col-resize; flex-shrink: 0;
161
+ }
162
+ .tree-resize-handle:hover { background: #58a6ff; }
163
+
164
+ .tree-toolbar {
165
+ display: flex; align-items: center; gap: 4px;
166
+ padding: 4px 6px;
167
+ background: #161b22;
168
+ border-bottom: 1px solid #30363d;
169
+ flex-shrink: 0;
170
+ min-height: 28px;
171
+ }
172
+ .tree-toolbar .tree-title {
173
+ flex: 1;
174
+ font-size: 11px; color: #8b949e; text-transform: uppercase;
175
+ letter-spacing: 0.5px; font-family: ui-monospace, Menlo, monospace;
176
+ padding-left: 4px;
177
+ }
178
+ .tree-toolbar button {
179
+ background: transparent; border: 1px solid transparent;
180
+ color: #8b949e; padding: 2px 6px; font-size: 14px;
181
+ cursor: pointer; border-radius: 3px; line-height: 1;
182
+ }
183
+ .tree-toolbar button:hover { background: #30363d; color: #c9d1d9; }
184
+
185
+ .tree-search-wrap {
186
+ padding: 4px 6px; background: #161b22;
187
+ border-bottom: 1px solid #30363d; flex-shrink: 0;
188
+ }
189
+ .tree-search {
190
+ width: 100%;
191
+ background: #0d1117; border: 1px solid #30363d; color: #c9d1d9;
192
+ padding: 3px 6px; font-size: 11px; border-radius: 3px;
193
+ font-family: ui-monospace, Menlo, monospace; outline: none;
194
+ }
195
+ .tree-search:focus { border-color: #58a6ff; }
196
+
197
+ .tree-body {
198
+ flex: 1; overflow: auto; padding: 4px 0;
199
+ font-family: ui-monospace, Menlo, monospace; font-size: 12px;
200
+ color: #c9d1d9;
201
+ }
202
+ .tree-body::-webkit-scrollbar { width: 8px; }
203
+ .tree-body::-webkit-scrollbar-thumb { background: #30363d; border-radius: 4px; }
204
+ .tree-body::-webkit-scrollbar-thumb:hover { background: #484f58; }
205
+
206
+ /* Tree nodes — each row has its own indent calculated via padding-left.
207
+ Hover + selected states match VSCode dark. */
208
+ .tree-node {
209
+ display: flex; align-items: center; gap: 4px;
210
+ padding: 2px 8px 2px 4px;
211
+ cursor: pointer; user-select: none;
212
+ white-space: nowrap; overflow: hidden;
213
+ line-height: 20px;
214
+ }
215
+ .tree-node:hover { background: #21262d; }
216
+ .tree-node.selected { background: #1f6feb44; color: #58a6ff; }
217
+ .tree-node.hidden { display: none; }
218
+ .tree-node .tree-chevron {
219
+ width: 12px; flex-shrink: 0; color: #6e7681;
220
+ font-size: 9px; text-align: center;
221
+ transition: transform 0.15s;
222
+ }
223
+ .tree-node.dir > .tree-chevron { color: #c9d1d9; }
224
+ .tree-node.dir.expanded > .tree-chevron { transform: rotate(90deg); }
225
+ .tree-node.file > .tree-chevron { visibility: hidden; }
226
+ .tree-node .tree-icon { width: 14px; flex-shrink: 0; text-align: center; }
227
+ .tree-node .tree-label { overflow: hidden; text-overflow: ellipsis; }
228
+ .tree-node.dirty .tree-label::after { content: ' ●'; color: #d29922; }
229
+ .tree-children { /* unused; tree is flat with calculated indent */ }
230
+ .tree-empty {
231
+ padding: 16px 12px; color: #6e7681; font-style: italic;
232
+ font-size: 11px; text-align: center;
233
+ }
234
+
235
+ /* Editor layout (single mode, monaco-wave-b): file tree | editor-stack | preview */
236
+ .main.editor .panel-tree { display: flex; }
237
+ .main.editor .tree-resize-handle { display: block; }
238
+ .main.editor .panel-left-stack {
239
+ display: flex; flex: 1; flex-direction: column;
240
+ min-width: 0; min-height: 0; overflow: hidden;
241
+ }
242
+ .main.editor .panel-editor {
243
+ display: flex; flex: 1 1 60%; min-height: 100px;
244
+ }
245
+ .main.editor .panel-terminal {
246
+ display: flex; flex: 0 0 40%; min-height: 80px;
247
+ }
248
+ .main.editor .vresize-handle { display: block; }
249
+ .main.editor .resize-handle { display: block; }
250
+ .main.editor .panel-preview { display: flex; flex: 1; min-width: 0; }
251
+
252
+ /* VSCode-like editor statusbar (bottom-of-editor bar with
253
+ line:col, language, encoding). */
254
+ .editor-statusbar {
255
+ display: flex; align-items: center; gap: 8px;
256
+ padding: 2px 12px;
257
+ background: #007acc; /* VSCode blue */
258
+ color: #ffffff;
259
+ font-family: ui-monospace, Menlo, monospace;
260
+ font-size: 11px;
261
+ flex-shrink: 0;
262
+ min-height: 20px;
263
+ }
264
+ .editor-statusbar .sep { color: rgba(255,255,255,0.5); }
265
+ .editor-bar { display: flex; align-items: center; gap: 6px; padding: 3px 8px; background: #161b22; border-bottom: 1px solid #30363d; flex-shrink: 0; font-size: 11px; color: #8b949e; min-height: 24px; }
266
+ .editor-bar .editor-tab { padding: 2px 8px; background: #21262d; border: 1px solid #30363d; border-radius: 3px; color: #c9d1d9; font-family: ui-monospace, Menlo, monospace; font-size: 11px; display: flex; align-items: center; gap: 6px; }
267
+ .editor-bar .editor-tab.dirty::after { content: '●'; color: #d29922; font-size: 10px; }
268
+ .editor-bar .editor-tab-empty { color: #6e7681; font-style: italic; }
269
+ .editor-bar .editor-hint { margin-left: auto; color: #6e7681; font-family: ui-monospace, Menlo, monospace; }
270
+ #monaco-container { flex: 1; min-height: 0; min-width: 0; }
271
+ .editor-placeholder { flex: 1; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 12px; color: #8b949e; font-family: ui-monospace, Menlo, monospace; font-size: 13px; padding: 24px; text-align: center; }
272
+ .editor-placeholder .key { background: #21262d; border: 1px solid #30363d; padding: 1px 6px; border-radius: 3px; color: #c9d1d9; font-size: 11px; }
273
+
274
+ /* Command palette (Ctrl+P) */
275
+ .palette-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.4); z-index: 1000; display: none; align-items: flex-start; justify-content: center; padding-top: 80px; }
276
+ .palette-overlay.active { display: flex; }
277
+ .palette-box { background: #161b22; border: 1px solid #30363d; border-radius: 6px; width: 520px; max-width: 90vw; max-height: 60vh; display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.5); }
278
+ .palette-input { background: #0d1117; border: none; border-bottom: 1px solid #30363d; color: #c9d1d9; padding: 10px 14px; font-size: 13px; outline: none; font-family: ui-monospace, Menlo, monospace; border-radius: 6px 6px 0 0; }
279
+ .palette-list { overflow-y: auto; max-height: 400px; }
280
+ .palette-item { padding: 6px 14px; color: #c9d1d9; font-family: ui-monospace, Menlo, monospace; font-size: 12px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
281
+ .palette-item.active { background: #1f6feb44; color: #58a6ff; }
282
+ .palette-item:hover { background: #21262d; }
283
+ .palette-empty { padding: 14px; color: #6e7681; font-style: italic; font-size: 12px; text-align: center; }
284
+ </style>
285
+ </head>
286
+ <body>
287
+ <div class="header">
288
+ <h1>
289
+ <a href="/" style="color:inherit;text-decoration:none">Nimbus</a>
290
+ <span id="sessionLabel" title="Session ID (shareable) — click to copy URL"></span>
291
+ </h1>
292
+ <div class="status">
293
+ <div class="status-dot" id="statusDot"></div>
294
+ <span class="status-text" id="statusText">Disconnected</span>
295
+ </div>
296
+ </div>
297
+ <div class="toolbar" id="toolbar">
298
+ <button id="btnTerminal" class="active" title="Terminal">Terminal</button>
299
+ <button id="btnPreview" title="Preview">Preview</button>
300
+ <button id="btnSplit" title="Split view">Split</button>
301
+ <!-- monaco-wave-b (2026-05-14): single canonical Editor mode.
302
+ Drops Edit+Preview (per user "I don't need edit+preview at all").
303
+ Layout: file tree | editor (top) + terminal (bottom) | preview.
304
+ Lazy-loads Monaco + the tree on first click. -->
305
+ <button id="btnEditor" title="Editor (file tree + Monaco + terminal + preview)">Editor</button>
306
+ <div class="sep"></div>
307
+ <button id="btnProcs" title="Toggle process logs panel">
308
+ <span class="procs-dot"></span>
309
+ <span>Processes</span>
310
+ <span id="procsCount" style="color:#8b949e">0</span>
311
+ </button>
312
+ <button id="btnRefresh" title="Reload preview">&#8635;</button>
313
+ <span class="vite-status" id="viteStatus"></span>
314
+ </div>
315
+ <div class="main" id="mainPanel">
316
+ <!-- monaco-wave-b (2026-05-14): file-tree sidebar. Visible ONLY
317
+ in editor mode (CSS toggles display:flex on .main.editor).
318
+ The .tree-resize-handle sits between tree and editor stack;
319
+ dragging it adjusts .panel-tree's width inline. -->
320
+ <div class="panel-tree" id="treePanel">
321
+ <div class="tree-toolbar">
322
+ <span class="tree-title">Explorer</span>
323
+ <button id="btnTreeNewFile" title="New File">+</button>
324
+ <button id="btnTreeNewDir" title="New Folder">📁</button>
325
+ <button id="btnTreeRefresh" title="Refresh tree (re-fetch root)">⟳</button>
326
+ </div>
327
+ <div class="tree-search-wrap">
328
+ <input id="treeSearch" class="tree-search" placeholder="Filter files…" autocomplete="off" spellcheck="false">
329
+ </div>
330
+ <div class="tree-body" id="treeBody"></div>
331
+ </div>
332
+ <div class="tree-resize-handle" id="treeResizeHandle"></div>
333
+ <!-- monaco-wave-a (2026-05-13, hotfix course-correct):
334
+ The .panel-left-stack wrapper is display:contents in 1D modes
335
+ (terminal-only, split, preview-only) — its children act as
336
+ direct flex children of .main. In editor mode (monaco-wave-b),
337
+ CSS flips it to display:flex; flex-direction:column so editor
338
+ and terminal stack vertically in the MIDDLE column. -->
339
+ <div class="panel-left-stack" id="leftStack">
340
+ <div class="panel-editor" id="editorPanel">
341
+ <div class="editor-bar">
342
+ <div class="editor-tab editor-tab-empty" id="editorTab">no file</div>
343
+ <span class="editor-hint">
344
+ <kbd>Ctrl+P</kbd> open file &nbsp;·&nbsp; <kbd>Ctrl+S</kbd> save
345
+ </span>
346
+ <span class="editor-status" id="editorStatus" style="margin-left:8px;color:#6e7681;font-family:ui-monospace,Menlo,monospace;font-size:10px"></span>
347
+ </div>
348
+ <div class="editor-placeholder" id="editorPlaceholder">
349
+ <div>Press <span class="key">Ctrl</span>+<span class="key">P</span> to open a file.</div>
350
+ <div style="font-size:11px;color:#6e7681">Monaco loads on demand.</div>
351
+ </div>
352
+ <div id="monaco-container" style="display:none"></div>
353
+ <div class="editor-statusbar" id="editorStatusBar">
354
+ <span id="editorStatusPos">—</span>
355
+ <span class="sep">·</span>
356
+ <span id="editorStatusLang">plaintext</span>
357
+ <span class="sep">·</span>
358
+ <span id="editorStatusEnc">UTF-8</span>
359
+ </div>
360
+ </div>
361
+ <!-- monaco-polish (2026-05-14): vertical resize handle between
362
+ editor (top) and terminal (bottom) inside the editor-stack
363
+ column. Visible only in editor mode (.main.editor). -->
364
+ <div class="vresize-handle" id="editorTerminalResizeHandle"></div>
365
+ <div class="panel-terminal">
366
+ <div id="terminal-container"></div>
367
+ <div class="logs-panel" id="logsPanel">
368
+ <div class="logs-tabbar" id="logsTabBar"></div>
369
+ <div class="logs-panel-body" id="logsPanelBody"></div>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ <div class="resize-handle" id="resizeHandle"></div>
374
+ <div class="panel-preview" id="previewPanel">
375
+ <div class="preview-bar">
376
+ <span style="color:#3fb950;font-size:12px">&#9679;</span>
377
+ <input class="url-bar" id="urlBar" value="" readonly>
378
+ <button id="btnReloadPreview" title="Reload">&#8635;</button>
379
+ </div>
380
+ <iframe id="preview-frame" src="about:blank"></iframe>
381
+ </div>
382
+ </div>
383
+ <!-- monaco-wave-a (2026-05-13): Ctrl+P command palette. Lazy-populated
384
+ fuzzy file finder; fs-list message sent on first open per
385
+ editor session. -->
386
+ <div class="palette-overlay" id="paletteOverlay">
387
+ <div class="palette-box">
388
+ <input class="palette-input" id="paletteInput" placeholder="Type a file name…" autocomplete="off" spellcheck="false">
389
+ <div class="palette-list" id="paletteList"></div>
390
+ </div>
391
+ </div>
392
+ <div class="footer">
393
+ <div class="stats" id="statsBar">
394
+ <span><span class="stat-label">FS:</span> <span class="stat-value" id="stFiles">-</span></span>
395
+ <span><span class="stat-label">Size:</span> <span class="stat-value" id="stSize">-</span></span>
396
+ <span><span class="stat-label">Cache:</span> <span class="stat-value" id="stCache">-</span></span>
397
+ <span><span class="stat-label">Hit:</span> <span class="stat-value good" id="stHitRate">-</span></span>
398
+ </div>
399
+ <span style="color:#484f58">DO + SQLite demand-paged | esbuild-wasm | vite dev server</span>
400
+ </div>
401
+
402
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
403
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
404
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.0/lib/addon-webgl.min.js"></script>
405
+ <script>
406
+ // ── Session-aware URL helpers ──
407
+ // This page is served at /s/<id>/ via the asset binding. All DO-bound
408
+ // paths (/ws, /api/*, /preview/*) live under the same session prefix.
409
+ // Compute once at load time — location.pathname won't change.
410
+ const SESSION_PREFIX = (function() {
411
+ const m = location.pathname.match(/^\/s\/([a-z]{3,14}-[a-z]{3,14}-\d{4})(?:\/|$)/);
412
+ return m ? '/s/' + m[1] : '';
413
+ })();
414
+ const WS_URL = (location.protocol === 'https:' ? 'wss:' : 'ws:') +
415
+ '//' + location.host + SESSION_PREFIX + '/ws';
416
+ const STATS_URL = SESSION_PREFIX + '/api/stats';
417
+ const VITE_URL = SESSION_PREFIX + '/preview/';
418
+ const WORKER_URL = SESSION_PREFIX + '/worker/';
419
+ // The iframe starts pointing at /preview/ which handles its own
420
+ // "no dev server running" placeholder. When the user spawns a
421
+ // wrangler dev (via `wrangler dev` in the terminal), stats polling
422
+ // below flips this to WORKER_URL automatically.
423
+ const PREVIEW_URL = VITE_URL;
424
+
425
+ // Populate the URL bar with the full session preview URL so users can
426
+ // copy-paste it (e.g. to share just the preview with a teammate).
427
+ // The value is kept in sync with the iframe by applyPreviewTarget().
428
+ document.getElementById('urlBar').value = PREVIEW_URL;
429
+
430
+ // Update document title to include the session ID — makes tabs easy to
431
+ // distinguish when a user has multiple Nimbus sessions open.
432
+ // Also populate the session-label chip in the header; clicking it copies
433
+ // the full session URL to the clipboard for easy sharing.
434
+ if (SESSION_PREFIX) {
435
+ const sessionId = SESSION_PREFIX.slice(3); // strip "/s/"
436
+ document.title = 'Nimbus — ' + sessionId;
437
+ const chip = document.getElementById('sessionLabel');
438
+ if (chip) {
439
+ chip.textContent = sessionId;
440
+ chip.addEventListener('click', async () => {
441
+ const shareUrl = location.origin + SESSION_PREFIX + '/';
442
+ try {
443
+ await navigator.clipboard.writeText(shareUrl);
444
+ const original = chip.textContent;
445
+ chip.textContent = 'copied!';
446
+ chip.classList.add('copied');
447
+ setTimeout(() => { chip.textContent = original; chip.classList.remove('copied'); }, 1200);
448
+ } catch { /* clipboard API unavailable — silently ignore */ }
449
+ });
450
+ }
451
+ }
452
+
453
+ // ── Terminal setup ──
454
+ const term = new Terminal({
455
+ cursorBlink: true, cursorStyle: 'bar', fontSize: 13,
456
+ fontFamily: "'JetBrains Mono', 'Fira Code', Menlo, monospace",
457
+ theme: {
458
+ background: '#0d1117', foreground: '#c9d1d9', cursor: '#58a6ff', selectionBackground: '#264f78',
459
+ black: '#484f58', red: '#ff7b72', green: '#3fb950', yellow: '#d29922',
460
+ blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39d353', white: '#c9d1d9',
461
+ brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341',
462
+ brightBlue: '#79c0ff', brightMagenta: '#d2a8ff', brightCyan: '#56d364', brightWhite: '#f0f6fc',
463
+ },
464
+ allowProposedApi: true,
465
+ // macOS Option key emits Meta-prefixed sequences (\x1b<letter>),
466
+ // matching Linux Alt behaviour. Without this, Option+B/F/D on
467
+ // Mac silently inserts unicode `∫ƒ∂` glyphs instead of triggering
468
+ // word-by-word readline edits. xterm.js default is false; bash /
469
+ // Terminal.app / iTerm2 default this to true via their own
470
+ // "Use Option as Meta" preference. We hard-set true so Mac users
471
+ // get Linux-parity readline keybindings out of the box.
472
+ macOptionIsMeta: true,
473
+ });
474
+ const fitAddon = new FitAddon.FitAddon();
475
+ term.loadAddon(fitAddon);
476
+ try { term.loadAddon(new WebglAddon.WebglAddon()); } catch (e) {}
477
+ term.open(document.getElementById('terminal-container'));
478
+ fitAddon.fit();
479
+
480
+ // ── State ──
481
+ const dot = document.getElementById('statusDot');
482
+ const stText = document.getElementById('statusText');
483
+ const previewFrame = document.getElementById('preview-frame');
484
+ const previewPanel = document.getElementById('previewPanel');
485
+ const mainPanel = document.getElementById('mainPanel');
486
+ const viteStatus = document.getElementById('viteStatus');
487
+ let ws = null, rd = 1000, statsTimer = null;
488
+ // monaco-wave-b (2026-05-14): single 'editor' mode replaces the
489
+ // Wave-A duo of 'editor-split' (dropped per user feedback) and
490
+ // 'editor-split-with-term' (renamed → 'editor'). Layout:
491
+ // file tree | editor (top) + terminal (bottom) | preview
492
+ // Old modes preserved verbatim: 'split', 'terminal-only', 'preview-only'.
493
+ let layout = 'split';
494
+
495
+ // ── Layout controls ──
496
+ function setLayout(mode) {
497
+ layout = mode;
498
+ mainPanel.className = 'main' + (mode !== 'split' ? ' ' + mode : '');
499
+ document.getElementById('btnTerminal').classList.toggle('active', mode === 'terminal-only');
500
+ document.getElementById('btnPreview').classList.toggle('active', mode === 'preview-only');
501
+ document.getElementById('btnSplit').classList.toggle('active', mode === 'split');
502
+ document.getElementById('btnEditor').classList.toggle('active', mode === 'editor');
503
+ // Editor panel visibility — drives display:flex via .active.
504
+ const editorPanel = document.getElementById('editorPanel');
505
+ const wantEditor = (mode === 'editor');
506
+ editorPanel.classList.toggle('active', wantEditor);
507
+ // Lazy-init Monaco + the file tree the FIRST time the user
508
+ // switches to editor mode. Cold session pays neither cost.
509
+ if (wantEditor) {
510
+ Editor.ensureLoaded();
511
+ FileTree.ensureLoaded();
512
+ }
513
+ setTimeout(() => {
514
+ // monaco-polish: re-apply persisted pane dims for this layout.
515
+ // PaneResizer uses inline flex which is mode-specific (split vs
516
+ // editor have different pane structures); onLayoutChange picks
517
+ // the right elements for the active mode.
518
+ try { if (typeof PaneResizer !== 'undefined') PaneResizer.onLayoutChange(); } catch {}
519
+ try { fitAddon.fit(); } catch {}
520
+ // Re-layout Monaco after the panel resizes. automaticLayout:true
521
+ // already handles this but explicit layout() on mode switch
522
+ // avoids a one-frame flash where the editor sees its old size.
523
+ if (window.__nimbusMonacoEditor) {
524
+ try { window.__nimbusMonacoEditor.layout(); } catch {}
525
+ }
526
+ }, 50);
527
+ }
528
+ document.getElementById('btnTerminal').onclick = () => setLayout('terminal-only');
529
+ document.getElementById('btnPreview').onclick = () => setLayout('preview-only');
530
+ document.getElementById('btnSplit').onclick = () => setLayout('split');
531
+ document.getElementById('btnEditor').onclick = () => setLayout('editor');
532
+ document.getElementById('btnRefresh').onclick = () => reloadPreview();
533
+ document.getElementById('btnReloadPreview').onclick = () => reloadPreview();
534
+
535
+ // ── monaco-wave-a (2026-05-13): Editor module ──
536
+ //
537
+ // Lazy-loads Monaco via cdnjs UMD on first call to ensureLoaded().
538
+ // Cold session (terminal-only flow) never pays the ~5MB download.
539
+ //
540
+ // Owns the open file, dirty state, Ctrl+P palette, Ctrl+S save.
541
+ // All fs operations route through the live WS via fs-read /
542
+ // fs-write / fs-list messages → init.ts handler → SqliteVFS.
543
+ //
544
+ // Perf-invariant: probes assert monaco-* scripts NOT loaded on a
545
+ // cold terminal-only session (see tests/behavioral/monaco-wave-a/
546
+ // new/monaco-lazy-not-on-cold.mjs).
547
+ const Editor = (function() {
548
+ // CDN choice: cdnjs hosts Monaco's UMD AMD-loader (`loader.min.js`)
549
+ // which fetches the remaining vs/* chunks on demand. Total cold
550
+ // download is ~2 MB for the editor + lazy languages. Pinned to
551
+ // 0.52.0 (latest as of 2025-Q1) — same major as VSCode 1.95+.
552
+ const MONACO_VERSION = '0.52.0';
553
+ const MONACO_BASE = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/' + MONACO_VERSION + '/min';
554
+ let loaded = false;
555
+ let loading = null;
556
+ // Cached fs-list result from a previous Ctrl+P (5s TTL).
557
+ let fileListCache = null;
558
+ let fileListCachedAt = 0;
559
+ // Pending fs-read / fs-write / fs-list resolves keyed by op id.
560
+ // The terminal WS multiplexes; we tag each message with a `reqId`
561
+ // so concurrent reads (e.g. preview tab + palette refresh) don't
562
+ // race on the same path.
563
+ const pending = new Map();
564
+ let nextReqId = 1;
565
+
566
+ // Current open file state.
567
+ let openPath = null; // POSIX absolute path or null
568
+ let originalContent = ''; // last-saved content (for dirty check)
569
+ let editor = null; // Monaco editor instance
570
+
571
+ function tabEl() { return document.getElementById('editorTab'); }
572
+ function setTabFile(path, dirty) {
573
+ const el = tabEl();
574
+ if (!path) {
575
+ el.textContent = 'no file';
576
+ el.classList.add('editor-tab-empty');
577
+ el.classList.remove('dirty');
578
+ return;
579
+ }
580
+ // Show just the basename in the tab for clarity; full path in
581
+ // the title attribute.
582
+ const base = path.split('/').pop() || path;
583
+ el.textContent = base;
584
+ el.title = path;
585
+ el.classList.remove('editor-tab-empty');
586
+ el.classList.toggle('dirty', !!dirty);
587
+ }
588
+
589
+ // Send a fs-* message; await the paired result. Resolves with the
590
+ // result frame; rejects on error or timeout (15s — generous for
591
+ // big trees, tight enough that a hung session surfaces fast).
592
+ // file-tree-fix (2026-05-14): outbound queue for fs-* frames
593
+ // sent before ws.onopen fires. User repro: "fs-list failed:
594
+ // WS not connected" when clicking Editor before terminal prompt.
595
+ //
596
+ // Race window: connect() runs at page-load and ws is in CONNECTING
597
+ // state (readyState === 0). If the user clicks Editor BEFORE
598
+ // ws.onopen fires, setLayout('editor') invokes Editor.ensureLoaded
599
+ // → eventually fsRequest, which pre-fix rejected synchronously.
600
+ //
601
+ // Fix: register the pending request synchronously (reqId assigned,
602
+ // resolve/reject stored), queue the frame in `outQueue`, return
603
+ // the promise. When ws.onopen fires (drainFsQueue below),
604
+ // we ws.send() every queued frame. The response handler
605
+ // (tryHandleFsResult) is unchanged; it resolves the same pending
606
+ // entry whenever the server replies.
607
+ const outQueue = [];
608
+ function fsRequest(frame, resultType) {
609
+ const reqId = nextReqId++;
610
+ frame.reqId = reqId;
611
+ return new Promise((resolve, reject) => {
612
+ const t = setTimeout(() => {
613
+ if (pending.has(reqId)) {
614
+ pending.delete(reqId);
615
+ reject(new Error('fs request timed out'));
616
+ }
617
+ }, 15000);
618
+ pending.set(reqId, { resolve, reject, t, resultType });
619
+ // Try to send now if the WS is OPEN; otherwise queue.
620
+ if (ws && ws.readyState === WebSocket.OPEN) {
621
+ try { ws.send(JSON.stringify(frame)); }
622
+ catch (e) {
623
+ // OPEN but send threw — surface the failure immediately.
624
+ clearTimeout(t); pending.delete(reqId); reject(e);
625
+ }
626
+ } else {
627
+ outQueue.push(frame);
628
+ }
629
+ });
630
+ }
631
+ // Drain the outbound queue. Called from ws.onopen. Idempotent
632
+ // (safe to call multiple times — empty queue is a no-op).
633
+ // Resilient to reconnects: queue accumulates while ws.readyState
634
+ // is CONNECTING/CLOSED and flushes on next onopen.
635
+ function drainFsQueue() {
636
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
637
+ const drained = outQueue.splice(0);
638
+ for (const frame of drained) {
639
+ try { ws.send(JSON.stringify(frame)); }
640
+ catch (e) {
641
+ // Send failed on a drained frame — reject the pending entry.
642
+ const slot = pending.get(frame.reqId);
643
+ if (slot) {
644
+ pending.delete(frame.reqId);
645
+ clearTimeout(slot.t);
646
+ slot.reject(e);
647
+ }
648
+ }
649
+ }
650
+ }
651
+ // Hook into the main WS onmessage to deliver fs-*-result frames.
652
+ // Called by connect() right after parsing each incoming message —
653
+ // returns true to indicate the message was handled here (and the
654
+ // shell-side dispatch should skip it).
655
+ function tryHandleFsResult(msg) {
656
+ if (!msg || typeof msg.type !== 'string') return false;
657
+ if (!msg.type.endsWith('-result')) return false;
658
+ const reqId = msg.reqId;
659
+ if (typeof reqId !== 'number') return false;
660
+ const slot = pending.get(reqId);
661
+ if (!slot) return false;
662
+ pending.delete(reqId);
663
+ clearTimeout(slot.t);
664
+ slot.resolve(msg);
665
+ return true;
666
+ }
667
+
668
+ // Detect language from extension. Monaco built-ins cover most.
669
+ function languageFor(path) {
670
+ const m = /\.([^.]+)$/.exec(path || '');
671
+ const ext = m ? m[1].toLowerCase() : '';
672
+ const map = {
673
+ js: 'javascript', mjs: 'javascript', cjs: 'javascript',
674
+ ts: 'typescript', tsx: 'typescript', jsx: 'javascript',
675
+ py: 'python', rb: 'ruby', go: 'go', rs: 'rust',
676
+ c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', hpp: 'cpp',
677
+ java: 'java', kt: 'kotlin', swift: 'swift',
678
+ json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'ini',
679
+ md: 'markdown', html: 'html', css: 'css', scss: 'scss',
680
+ sh: 'shell', bash: 'shell', zsh: 'shell',
681
+ xml: 'xml', sql: 'sql', dockerfile: 'dockerfile',
682
+ };
683
+ return map[ext] || 'plaintext';
684
+ }
685
+
686
+ function ensureLoaded() {
687
+ if (loaded) return Promise.resolve();
688
+ if (loading) return loading;
689
+ loading = new Promise((resolve, reject) => {
690
+ // Step 1: AMD loader. cdnjs ships the UMD `loader.min.js` that
691
+ // bootstraps Monaco's internal AMD module graph.
692
+ const script = document.createElement('script');
693
+ script.src = MONACO_BASE + '/vs/loader.min.js';
694
+ script.onload = () => {
695
+ // Step 2: configure paths + require the editor module.
696
+ const requireJs = window.require;
697
+ if (!requireJs || typeof requireJs.config !== 'function') {
698
+ reject(new Error('Monaco AMD loader failed to register'));
699
+ return;
700
+ }
701
+ requireJs.config({ paths: { 'vs': MONACO_BASE + '/vs' } });
702
+ requireJs(['vs/editor/editor.main'], () => {
703
+ try {
704
+ createEditor();
705
+ loaded = true;
706
+ resolve();
707
+ } catch (e) {
708
+ reject(e);
709
+ }
710
+ });
711
+ };
712
+ script.onerror = () => reject(new Error('Failed to load Monaco AMD loader from ' + script.src));
713
+ document.head.appendChild(script);
714
+ });
715
+ return loading;
716
+ }
717
+
718
+ function createEditor() {
719
+ const container = document.getElementById('monaco-container');
720
+ container.style.display = 'block';
721
+ // Hide the placeholder once Monaco mounts.
722
+ const ph = document.getElementById('editorPlaceholder');
723
+ if (ph) ph.style.display = 'none';
724
+
725
+ // monaco-wave-a hotfix (2026-05-13): full VSCode-like options
726
+ // (per user feedback "current is useless — make it proper, nice").
727
+ // This config mirrors VSCode 1.95+ defaults for a TS/JS file.
728
+ // Verified against actual VSCode source (monaco.IEditorOptions).
729
+ editor = window.monaco.editor.create(container, {
730
+ value: '',
731
+ language: 'plaintext',
732
+ theme: 'vs-dark', // VSCode dark theme
733
+ // Critical for split-pane resize — Monaco re-measures on
734
+ // ResizeObserver hits without explicit layout() calls.
735
+ automaticLayout: true,
736
+ // Visual chrome.
737
+ minimap: { enabled: true, side: 'right', renderCharacters: true, maxColumn: 120, scale: 1 },
738
+ lineNumbers: 'on',
739
+ lineNumbersMinChars: 3,
740
+ glyphMargin: false,
741
+ folding: true,
742
+ renderLineHighlight: 'all',
743
+ renderWhitespace: 'selection',
744
+ renderControlCharacters: false,
745
+ renderFinalNewline: 'on',
746
+ // Cursor + selection chrome.
747
+ cursorBlinking: 'smooth',
748
+ cursorSmoothCaretAnimation: 'on',
749
+ cursorStyle: 'line',
750
+ cursorWidth: 2,
751
+ // Typography. VSCode defaults to 14px Menlo/Monaco/Consolas.
752
+ fontFamily: "Menlo, Monaco, 'Courier New', 'JetBrains Mono', 'Fira Code', monospace",
753
+ fontSize: 14,
754
+ lineHeight: 22,
755
+ fontLigatures: true,
756
+ // Whitespace / indent.
757
+ tabSize: 2,
758
+ insertSpaces: true,
759
+ detectIndentation: true,
760
+ trimAutoWhitespace: true,
761
+ // Bracket / indent guides — the colorful nested brackets +
762
+ // dotted vertical indent lines users expect from VSCode.
763
+ bracketPairColorization: { enabled: true, independentColorPoolPerBracketType: true },
764
+ guides: {
765
+ bracketPairs: 'active',
766
+ bracketPairsHorizontal: 'active',
767
+ indentation: true,
768
+ highlightActiveIndentation: true,
769
+ highlightActiveBracketPair: true,
770
+ },
771
+ matchBrackets: 'always',
772
+ // Editing behavior.
773
+ autoIndent: 'full',
774
+ autoClosingBrackets: 'languageDefined',
775
+ autoClosingQuotes: 'languageDefined',
776
+ autoSurround: 'languageDefined',
777
+ formatOnPaste: true,
778
+ formatOnType: true,
779
+ // Multi-cursor and selection ergonomics.
780
+ multiCursorModifier: 'alt',
781
+ columnSelection: false,
782
+ // Word wrap (toggle-friendly default).
783
+ wordWrap: 'on',
784
+ wordWrapColumn: 120,
785
+ wrappingIndent: 'indent',
786
+ // Scrolling.
787
+ smoothScrolling: true,
788
+ mouseWheelScrollSensitivity: 1,
789
+ fastScrollSensitivity: 5,
790
+ scrollBeyondLastLine: false,
791
+ scrollbar: {
792
+ vertical: 'auto',
793
+ horizontal: 'auto',
794
+ useShadows: true,
795
+ verticalScrollbarSize: 14,
796
+ horizontalScrollbarSize: 14,
797
+ },
798
+ // Suggest / IntelliSense (works for built-in languages even
799
+ // without LSP — Monaco ships JS/TS/JSON/HTML/CSS workers
800
+ // lazily; we leave the defaults).
801
+ suggestOnTriggerCharacters: true,
802
+ acceptSuggestionOnEnter: 'on',
803
+ quickSuggestions: { other: true, comments: false, strings: false },
804
+ parameterHints: { enabled: true, cycle: true },
805
+ // Misc polish.
806
+ stickyScroll: { enabled: true },
807
+ links: true,
808
+ dragAndDrop: true,
809
+ mouseStyle: 'text',
810
+ contextmenu: true,
811
+ copyWithSyntaxHighlighting: false,
812
+ });
813
+ window.__nimbusMonacoEditor = editor;
814
+ // Dirty-state tracking. Also propagate to FileTree so the
815
+ // tree row shows the dirty dot (matches editor tab).
816
+ editor.onDidChangeModelContent(() => {
817
+ if (!openPath) return;
818
+ const cur = editor.getValue();
819
+ const dirty = cur !== originalContent;
820
+ setTabFile(openPath, dirty);
821
+ if (typeof FileTree !== 'undefined') {
822
+ try { FileTree.markDirty(openPath, dirty); } catch {}
823
+ }
824
+ });
825
+ // Cursor position → status bar.
826
+ const statusPos = document.getElementById('editorStatusPos');
827
+ const statusLang = document.getElementById('editorStatusLang');
828
+ editor.onDidChangeCursorPosition((e) => {
829
+ if (statusPos) statusPos.textContent = 'Ln ' + e.position.lineNumber + ', Col ' + e.position.column;
830
+ });
831
+ editor.onDidChangeModelLanguage((e) => {
832
+ if (statusLang) statusLang.textContent = e.newLanguage || 'plaintext';
833
+ });
834
+ // Ctrl+S save — Monaco intercepts the keystroke when focused.
835
+ editor.addCommand(window.monaco.KeyMod.CtrlCmd | window.monaco.KeyCode.KeyS, () => save());
836
+ // Cmd+P / Ctrl+P inside the editor → palette (palette also has
837
+ // a global keydown handler but registering at the editor level
838
+ // is what real VSCode does, so feel matches).
839
+ editor.addCommand(window.monaco.KeyMod.CtrlCmd | window.monaco.KeyCode.KeyP, () => openPalette());
840
+ // Layout once after creation so the editor sizes correctly.
841
+ setTimeout(() => editor.layout(), 0);
842
+ }
843
+
844
+ async function openFile(path) {
845
+ if (!path) return;
846
+ // monaco-wave-b: dirty-prompt before switching files. If the
847
+ // current buffer has unsaved changes, ask the user whether
848
+ // to (a) save first, (b) discard and switch, or (c) cancel.
849
+ // Skip when switching to the SAME file (no-op) or when there
850
+ // is no editor yet.
851
+ if (editor && openPath && openPath !== path) {
852
+ const cur = editor.getValue();
853
+ if (cur !== originalContent) {
854
+ const choice = confirm(
855
+ 'Unsaved changes in ' + openPath + '\n\n' +
856
+ 'OK = save and switch.\n' +
857
+ 'Cancel = discard changes and switch.'
858
+ );
859
+ if (choice) {
860
+ await save();
861
+ }
862
+ // Either way, proceed to load the new file (the user
863
+ // explicitly chose to leave the current buffer).
864
+ }
865
+ }
866
+ await ensureLoaded();
867
+ let result;
868
+ try {
869
+ result = await fsRequest({ type: 'fs-read', path }, 'fs-read-result');
870
+ } catch (e) {
871
+ setStatusFlash('open failed: ' + (e?.message || e));
872
+ return;
873
+ }
874
+ if (result.error) {
875
+ if (result.binary) {
876
+ setStatusFlash('binary file refused: ' + path);
877
+ } else {
878
+ setStatusFlash('open failed: ' + result.error);
879
+ }
880
+ return;
881
+ }
882
+ const content = result.content || '';
883
+ openPath = path;
884
+ originalContent = content;
885
+ const lang = languageFor(path);
886
+ // Recreate model so language detection takes effect (Monaco
887
+ // doesn't auto-relanguage on setValue).
888
+ const model = window.monaco.editor.createModel(content, lang);
889
+ const old = editor.getModel();
890
+ editor.setModel(model);
891
+ if (old) try { old.dispose(); } catch {}
892
+ setTabFile(path, false);
893
+ // Reflect language in the bottom statusbar.
894
+ const statusLang = document.getElementById('editorStatusLang');
895
+ if (statusLang) statusLang.textContent = lang;
896
+ const statusPos = document.getElementById('editorStatusPos');
897
+ if (statusPos) statusPos.textContent = 'Ln 1, Col 1';
898
+ // monaco-wave-b: mirror selection into the file tree (so
899
+ // palette/Ctrl+P opens highlight the row).
900
+ if (typeof FileTree !== 'undefined') {
901
+ try { FileTree.setSelected(path); FileTree.markDirty(path, false); } catch {}
902
+ }
903
+ editor.focus();
904
+ }
905
+
906
+ async function save() {
907
+ if (!openPath || !editor) return;
908
+ const content = editor.getValue();
909
+ let result;
910
+ try {
911
+ result = await fsRequest({ type: 'fs-write', path: openPath, content }, 'fs-write-result');
912
+ } catch (e) {
913
+ setStatusFlash('save failed: ' + (e?.message || e));
914
+ return;
915
+ }
916
+ if (!result.ok) {
917
+ setStatusFlash('save failed: ' + (result.error || 'unknown'));
918
+ return;
919
+ }
920
+ originalContent = content;
921
+ setTabFile(openPath, false);
922
+ if (typeof FileTree !== 'undefined') {
923
+ try { FileTree.markDirty(openPath, false); } catch {}
924
+ }
925
+ setStatusFlash('saved');
926
+ }
927
+
928
+ let flashTimer = null;
929
+ function setStatusFlash(text) {
930
+ const el = tabEl();
931
+ const prior = el.textContent;
932
+ el.textContent = text;
933
+ if (flashTimer) clearTimeout(flashTimer);
934
+ flashTimer = setTimeout(() => setTabFile(openPath, false), 1400);
935
+ }
936
+
937
+ // ── Ctrl+P palette ──
938
+ const overlay = document.getElementById('paletteOverlay');
939
+ const input = document.getElementById('paletteInput');
940
+ const list = document.getElementById('paletteList');
941
+ let paletteFiles = [];
942
+ let paletteFiltered = [];
943
+ let paletteActive = 0;
944
+
945
+ async function openPalette() {
946
+ await ensureLoaded();
947
+ overlay.classList.add('active');
948
+ input.value = '';
949
+ input.focus();
950
+ // Re-fetch list if cache stale.
951
+ const now = Date.now();
952
+ if (!fileListCache || (now - fileListCachedAt) > 5000) {
953
+ list.innerHTML = '<div class="palette-empty">Loading…</div>';
954
+ try {
955
+ const r = await fsRequest({ type: 'fs-list', dir: '/home/user', recursive: true }, 'fs-list-result');
956
+ fileListCache = (r.entries || []).filter(e => e.type !== 'directory').map(e => e.path);
957
+ fileListCachedAt = Date.now();
958
+ } catch (e) {
959
+ list.innerHTML = '<div class="palette-empty">fs-list failed: ' + (e?.message || e) + '</div>';
960
+ return;
961
+ }
962
+ }
963
+ paletteFiles = fileListCache.slice();
964
+ renderPalette('');
965
+ }
966
+ function closePalette() {
967
+ overlay.classList.remove('active');
968
+ if (editor) editor.focus();
969
+ }
970
+ function renderPalette(q) {
971
+ const needle = q.toLowerCase();
972
+ // Simple substring + suffix-rank filter. Plenty fast for a few
973
+ // thousand files (5 MB cap on fs-list anyway).
974
+ paletteFiltered = paletteFiles
975
+ .filter(p => p.toLowerCase().includes(needle))
976
+ .slice(0, 80);
977
+ paletteActive = 0;
978
+ if (paletteFiltered.length === 0) {
979
+ list.innerHTML = '<div class="palette-empty">No matches.</div>';
980
+ return;
981
+ }
982
+ list.innerHTML = paletteFiltered.map((p, i) =>
983
+ '<div class="palette-item' + (i === 0 ? ' active' : '') + '" data-idx="' + i + '">' + p + '</div>'
984
+ ).join('');
985
+ // Click to open.
986
+ list.querySelectorAll('.palette-item').forEach(el => {
987
+ el.addEventListener('click', () => {
988
+ const idx = parseInt(el.dataset.idx, 10);
989
+ const path = paletteFiltered[idx];
990
+ if (path) {
991
+ closePalette();
992
+ openFile(path);
993
+ }
994
+ });
995
+ });
996
+ }
997
+ function moveActive(delta) {
998
+ const next = Math.max(0, Math.min(paletteFiltered.length - 1, paletteActive + delta));
999
+ if (next === paletteActive) return;
1000
+ paletteActive = next;
1001
+ list.querySelectorAll('.palette-item').forEach((el, i) => {
1002
+ el.classList.toggle('active', i === paletteActive);
1003
+ if (i === paletteActive) el.scrollIntoView({ block: 'nearest' });
1004
+ });
1005
+ }
1006
+ input.addEventListener('input', () => renderPalette(input.value));
1007
+ input.addEventListener('keydown', (e) => {
1008
+ if (e.key === 'Escape') { e.preventDefault(); closePalette(); return; }
1009
+ if (e.key === 'ArrowDown') { e.preventDefault(); moveActive(1); return; }
1010
+ if (e.key === 'ArrowUp') { e.preventDefault(); moveActive(-1); return; }
1011
+ if (e.key === 'Enter') {
1012
+ e.preventDefault();
1013
+ const path = paletteFiltered[paletteActive];
1014
+ if (path) { closePalette(); openFile(path); }
1015
+ }
1016
+ });
1017
+ overlay.addEventListener('click', (e) => {
1018
+ if (e.target === overlay) closePalette();
1019
+ });
1020
+
1021
+ // Global Ctrl+P / Cmd+P binding. Fires anywhere on the page so
1022
+ // users can open the palette from the terminal pane too — matches
1023
+ // VSCode UX.
1024
+ document.addEventListener('keydown', (e) => {
1025
+ const isCtrlP = (e.ctrlKey || e.metaKey) && e.key === 'p' && !e.shiftKey && !e.altKey;
1026
+ if (isCtrlP) {
1027
+ e.preventDefault();
1028
+ openPalette();
1029
+ return;
1030
+ }
1031
+ // Ctrl+S anywhere also saves (Monaco's own binding covers
1032
+ // the editor-focus case but a click outside the editor would
1033
+ // otherwise lose the keystroke).
1034
+ const isCtrlS = (e.ctrlKey || e.metaKey) && e.key === 's' && !e.shiftKey && !e.altKey;
1035
+ if (isCtrlS && openPath) {
1036
+ e.preventDefault();
1037
+ save();
1038
+ }
1039
+ });
1040
+
1041
+ // file-tree-watch (2026-05-15): external invalidation of the
1042
+ // Cmd+P 5s-TTL palette cache. Called by the fs-watch-event
1043
+ // dispatcher in ws.onmessage so a terminal-side write makes
1044
+ // the next openPalette re-run fs-list instead of serving
1045
+ // stale results.
1046
+ function invalidateFileListCache() {
1047
+ fileListCache = null;
1048
+ fileListCachedAt = 0;
1049
+ }
1050
+
1051
+ return { ensureLoaded, openFile, save, openPalette, tryHandleFsResult, drainFsQueue, invalidateFileListCache };
1052
+ })();
1053
+
1054
+ // ── monaco-wave-b (2026-05-14): FileTree module ──
1055
+ //
1056
+ // VSCode-style left sidebar showing the user VFS (rooted at
1057
+ // /home/user). Lazy-loads on first editor-mode activation. Tree
1058
+ // is flat in DOM (one .tree-node per row) with padding-left
1059
+ // calculated per depth — gives a clean expand/collapse animation
1060
+ // without nested ULs.
1061
+ //
1062
+ // Behavior:
1063
+ // - First render: lazy fs-list /home/user (recursive=false)
1064
+ // - Folder click: toggle expanded; on first expand, lazy
1065
+ // fs-list that folder
1066
+ // - File click: Editor.openFile(path) — Editor handles dirty
1067
+ // prompt + Monaco model swap
1068
+ // - Search filter: client-side substring on path, hides nodes
1069
+ // - New file/folder: prompt + fs-write (empty content for
1070
+ // file; for folder we synthesize an empty .keep file inside
1071
+ // so fs-list shows it — VFS has no separate mkdir over WS)
1072
+ // - Refresh: re-fetch root, drop cached children
1073
+ // - Expanded state persisted via localStorage keyed on session id
1074
+ //
1075
+ // Memory: tree state is a flat Map<path, node>; max 10k entries
1076
+ // before we stop expanding to protect the browser.
1077
+ const FileTree = (function() {
1078
+ const panel = document.getElementById('treePanel');
1079
+ const body = document.getElementById('treeBody');
1080
+ const searchInput = document.getElementById('treeSearch');
1081
+ const btnNewFile = document.getElementById('btnTreeNewFile');
1082
+ const btnNewDir = document.getElementById('btnTreeNewDir');
1083
+ const btnRefresh = document.getElementById('btnTreeRefresh');
1084
+
1085
+ const ROOT = '/home/user';
1086
+ const MAX_NODES = 10_000;
1087
+ const LS_KEY = 'nimbus.tree.expanded.' + (SESSION_PREFIX || 'noid');
1088
+
1089
+ // node: { path, name, type:'file'|'directory', depth, parent, children?:Set<path>, loaded:bool, selected:bool, hidden:bool }
1090
+ const nodes = new Map();
1091
+ // Insertion-ordered list of paths for stable render.
1092
+ const renderOrder = [];
1093
+ let loaded = false;
1094
+ let pending = null;
1095
+ let nextReqId = 70_000;
1096
+ const pendingReqs = new Map();
1097
+ let selectedPath = null;
1098
+
1099
+ function loadExpandedSet() {
1100
+ try {
1101
+ const raw = localStorage.getItem(LS_KEY);
1102
+ if (!raw) return new Set([ROOT]);
1103
+ return new Set(JSON.parse(raw));
1104
+ } catch { return new Set([ROOT]); }
1105
+ }
1106
+ function saveExpandedSet(set) {
1107
+ try { localStorage.setItem(LS_KEY, JSON.stringify(Array.from(set))); } catch {}
1108
+ }
1109
+ const expanded = loadExpandedSet();
1110
+ expanded.add(ROOT);
1111
+
1112
+ // file-tree-fix (2026-05-14): outbound queue mirroring Editor's
1113
+ // pattern. See Editor.fsRequest for the race rationale.
1114
+ const outQueue = [];
1115
+ function fsRequest(frame, resultType) {
1116
+ const reqId = nextReqId++;
1117
+ frame.reqId = reqId;
1118
+ return new Promise((resolve, reject) => {
1119
+ const t = setTimeout(() => {
1120
+ if (pendingReqs.has(reqId)) {
1121
+ pendingReqs.delete(reqId);
1122
+ reject(new Error('fs request timed out'));
1123
+ }
1124
+ }, 15000);
1125
+ pendingReqs.set(reqId, { resolve, reject, t });
1126
+ if (ws && ws.readyState === WebSocket.OPEN) {
1127
+ try { ws.send(JSON.stringify(frame)); }
1128
+ catch (e) {
1129
+ clearTimeout(t); pendingReqs.delete(reqId); reject(e);
1130
+ }
1131
+ } else {
1132
+ outQueue.push(frame);
1133
+ }
1134
+ });
1135
+ }
1136
+ function drainFsQueue() {
1137
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
1138
+ const drained = outQueue.splice(0);
1139
+ for (const frame of drained) {
1140
+ try { ws.send(JSON.stringify(frame)); }
1141
+ catch (e) {
1142
+ const slot = pendingReqs.get(frame.reqId);
1143
+ if (slot) {
1144
+ pendingReqs.delete(frame.reqId);
1145
+ clearTimeout(slot.t);
1146
+ if (slot.reject) slot.reject(e);
1147
+ }
1148
+ }
1149
+ }
1150
+ }
1151
+ // Hook into main WS onmessage like the Editor module does.
1152
+ function tryHandleFsResult(msg) {
1153
+ if (!msg || typeof msg.type !== 'string') return false;
1154
+ if (!msg.type.endsWith('-result')) return false;
1155
+ const reqId = msg.reqId;
1156
+ if (typeof reqId !== 'number') return false;
1157
+ const slot = pendingReqs.get(reqId);
1158
+ if (!slot) return false;
1159
+ pendingReqs.delete(reqId);
1160
+ clearTimeout(slot.t);
1161
+ slot.resolve(msg);
1162
+ return true;
1163
+ }
1164
+
1165
+ function iconFor(node) {
1166
+ if (node.type === 'directory') return node.expanded ? '📂' : '📁';
1167
+ // Light extension-based icons.
1168
+ const ext = (node.name.match(/\.([^.]+)$/) || [, ''])[1].toLowerCase();
1169
+ const m = {
1170
+ js:'📜', mjs:'📜', cjs:'📜', ts:'📘', tsx:'📘', jsx:'📜',
1171
+ py:'🐍', rb:'💎', go:'🐹', rs:'🦀',
1172
+ c:'🔧', h:'🔧', cpp:'🔧', hpp:'🔧',
1173
+ json:'⚙️', yaml:'⚙️', yml:'⚙️', toml:'⚙️',
1174
+ md:'📝', html:'🌐', css:'🎨', scss:'🎨',
1175
+ sh:'🐚', bash:'🐚',
1176
+ png:'🖼️', jpg:'🖼️', jpeg:'🖼️', gif:'🖼️', svg:'🖼️',
1177
+ lock:'🔒',
1178
+ };
1179
+ return m[ext] || '📄';
1180
+ }
1181
+
1182
+ function relativeName(path) {
1183
+ const i = path.lastIndexOf('/');
1184
+ return i >= 0 ? path.substring(i + 1) : path;
1185
+ }
1186
+ function parentOf(path) {
1187
+ const i = path.lastIndexOf('/');
1188
+ if (i <= 0) return '';
1189
+ return path.substring(0, i);
1190
+ }
1191
+ function depthOf(path) {
1192
+ // /home/user/foo/bar.txt → 2 (relative to /home/user which is root, depth 0)
1193
+ if (!path.startsWith(ROOT)) return 0;
1194
+ const rest = path.substring(ROOT.length).replace(/^\/+/, '');
1195
+ if (!rest) return 0;
1196
+ return rest.split('/').length;
1197
+ }
1198
+
1199
+ function ensureNode(path, type) {
1200
+ let n = nodes.get(path);
1201
+ if (n) {
1202
+ if (type) n.type = type;
1203
+ return n;
1204
+ }
1205
+ n = {
1206
+ path, name: relativeName(path) || path,
1207
+ type: type || 'file',
1208
+ depth: depthOf(path),
1209
+ loaded: false, expanded: expanded.has(path),
1210
+ dirty: false,
1211
+ };
1212
+ nodes.set(path, n);
1213
+ return n;
1214
+ }
1215
+
1216
+ async function loadFolder(path) {
1217
+ // Show a loading hint if this is the FIRST load AND the WS
1218
+ // hasn't opened yet (queue fix — request is pending until
1219
+ // ws.onopen drains it). Without this hint, users staring at
1220
+ // an empty tree-body would see no feedback. Cleared on
1221
+ // success / error path below.
1222
+ if (path === ROOT && !loaded && (!ws || ws.readyState !== WebSocket.OPEN)) {
1223
+ body.innerHTML = '<div class="tree-empty">Waiting for connection…</div>';
1224
+ }
1225
+ let result;
1226
+ try {
1227
+ result = await fsRequest({ type: 'fs-list', dir: path, recursive: false }, 'fs-list-result');
1228
+ } catch (e) {
1229
+ body.innerHTML = '<div class="tree-empty">fs-list failed: ' + escapeHtml(e?.message || String(e)) + '</div>';
1230
+ return;
1231
+ }
1232
+ if (result.error) {
1233
+ body.innerHTML = '<div class="tree-empty">' + escapeHtml(result.error) + '</div>';
1234
+ return;
1235
+ }
1236
+ const entries = result.entries || [];
1237
+ const folder = ensureNode(path, 'directory');
1238
+ folder.loaded = true;
1239
+ // Insert children — sort: directories first, alphabetical.
1240
+ entries.sort((a, b) => {
1241
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
1242
+ return a.path.localeCompare(b.path);
1243
+ });
1244
+ for (const e of entries) {
1245
+ if (nodes.size >= MAX_NODES) break;
1246
+ ensureNode(e.path, e.type);
1247
+ }
1248
+ rerender();
1249
+ }
1250
+
1251
+ function escapeHtml(s) {
1252
+ return String(s).replace(/[&<>"']/g, (c) => ({
1253
+ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;',
1254
+ })[c]);
1255
+ }
1256
+
1257
+ // Compute the rendered order: walk tree depth-first, including
1258
+ // only children of expanded folders. Hidden filter applied at
1259
+ // search time.
1260
+ function computeOrder() {
1261
+ const out = [];
1262
+ function walk(parentPath, depth) {
1263
+ // Collect direct children of parentPath in the nodes map.
1264
+ const children = [];
1265
+ for (const n of nodes.values()) {
1266
+ if (parentOf(n.path) === parentPath) children.push(n);
1267
+ }
1268
+ children.sort((a, b) => {
1269
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
1270
+ return a.name.localeCompare(b.name);
1271
+ });
1272
+ for (const c of children) {
1273
+ out.push(c);
1274
+ if (c.type === 'directory' && expanded.has(c.path)) {
1275
+ walk(c.path, depth + 1);
1276
+ }
1277
+ }
1278
+ }
1279
+ walk(ROOT, 1);
1280
+ return out;
1281
+ }
1282
+
1283
+ function rerender() {
1284
+ const order = computeOrder();
1285
+ const filter = (searchInput.value || '').trim().toLowerCase();
1286
+ body.innerHTML = '';
1287
+ if (order.length === 0) {
1288
+ body.innerHTML = '<div class="tree-empty">Empty</div>';
1289
+ return;
1290
+ }
1291
+ const frag = document.createDocumentFragment();
1292
+ let visibleCount = 0;
1293
+ for (const n of order) {
1294
+ const matchesFilter = !filter || n.path.toLowerCase().includes(filter);
1295
+ if (!matchesFilter) continue;
1296
+ visibleCount++;
1297
+ const div = document.createElement('div');
1298
+ let cls = 'tree-node ' + (n.type === 'directory' ? 'dir' : 'file');
1299
+ if (n.type === 'directory' && expanded.has(n.path)) cls += ' expanded';
1300
+ if (selectedPath === n.path) cls += ' selected';
1301
+ if (n.dirty) cls += ' dirty';
1302
+ div.className = cls;
1303
+ div.dataset.path = n.path;
1304
+ div.style.paddingLeft = (8 + (n.depth - 1) * 14) + 'px';
1305
+ div.innerHTML =
1306
+ '<span class="tree-chevron">▶</span>' +
1307
+ '<span class="tree-icon">' + iconFor(n) + '</span>' +
1308
+ '<span class="tree-label"></span>';
1309
+ div.querySelector('.tree-label').textContent = n.name;
1310
+ div.addEventListener('click', () => handleNodeClick(n));
1311
+ frag.appendChild(div);
1312
+ }
1313
+ body.appendChild(frag);
1314
+ if (visibleCount === 0 && filter) {
1315
+ body.innerHTML = '<div class="tree-empty">No matches for "' + escapeHtml(filter) + '"</div>';
1316
+ }
1317
+ }
1318
+
1319
+ function setSelected(path) {
1320
+ selectedPath = path;
1321
+ rerender();
1322
+ }
1323
+
1324
+ async function handleNodeClick(node) {
1325
+ if (node.type === 'directory') {
1326
+ if (expanded.has(node.path)) {
1327
+ expanded.delete(node.path);
1328
+ } else {
1329
+ expanded.add(node.path);
1330
+ if (!node.loaded) {
1331
+ // Show a tentative state immediately, then async load.
1332
+ rerender();
1333
+ await loadFolder(node.path);
1334
+ saveExpandedSet(expanded);
1335
+ return;
1336
+ }
1337
+ }
1338
+ saveExpandedSet(expanded);
1339
+ rerender();
1340
+ } else {
1341
+ setSelected(node.path);
1342
+ Editor.openFile(node.path);
1343
+ }
1344
+ }
1345
+
1346
+ async function ensureLoaded() {
1347
+ if (loaded) return;
1348
+ if (pending) return pending;
1349
+ panel.classList.add('loading');
1350
+ pending = (async () => {
1351
+ // Ensure root node + load it.
1352
+ ensureNode(ROOT, 'directory');
1353
+ await loadFolder(ROOT);
1354
+ // Auto-expand any persisted-expanded folders that are
1355
+ // already in the tree from the root load.
1356
+ for (const path of expanded) {
1357
+ if (path === ROOT) continue;
1358
+ const n = nodes.get(path);
1359
+ if (n && n.type === 'directory' && !n.loaded) {
1360
+ await loadFolder(path);
1361
+ }
1362
+ }
1363
+ loaded = true;
1364
+ })();
1365
+ try { await pending; } finally { pending = null; panel.classList.remove('loading'); }
1366
+ }
1367
+
1368
+ // Toolbar actions.
1369
+ async function refreshTree() {
1370
+ // Drop all cached children of root + re-fetch.
1371
+ nodes.clear();
1372
+ loaded = false;
1373
+ await ensureLoaded();
1374
+ }
1375
+ async function newFile() {
1376
+ // Place in the currently-selected directory, or the root.
1377
+ let parent = ROOT;
1378
+ if (selectedPath) {
1379
+ const n = nodes.get(selectedPath);
1380
+ if (n) parent = n.type === 'directory' ? n.path : parentOf(n.path);
1381
+ }
1382
+ const name = prompt('New file name (relative to ' + parent + '):');
1383
+ if (!name) return;
1384
+ const path = parent + '/' + name.replace(/^\/+/, '');
1385
+ try {
1386
+ await fsRequest({ type: 'fs-write', path, content: '' }, 'fs-write-result');
1387
+ // Refresh the parent folder.
1388
+ const pn = nodes.get(parent);
1389
+ if (pn) {
1390
+ pn.loaded = false;
1391
+ await loadFolder(parent);
1392
+ }
1393
+ expanded.add(parent);
1394
+ saveExpandedSet(expanded);
1395
+ setSelected(path);
1396
+ Editor.openFile(path);
1397
+ } catch (e) {
1398
+ alert('Create file failed: ' + (e?.message || e));
1399
+ }
1400
+ }
1401
+ async function newDir() {
1402
+ let parent = ROOT;
1403
+ if (selectedPath) {
1404
+ const n = nodes.get(selectedPath);
1405
+ if (n) parent = n.type === 'directory' ? n.path : parentOf(n.path);
1406
+ }
1407
+ const name = prompt('New folder name (relative to ' + parent + '):');
1408
+ if (!name) return;
1409
+ const dirPath = parent + '/' + name.replace(/^\/+/, '');
1410
+ // Synthesize via .keep file — fs-write auto-mkdirs the parent
1411
+ // chain (init.ts handler does this).
1412
+ try {
1413
+ await fsRequest({ type: 'fs-write', path: dirPath + '/.keep', content: '' }, 'fs-write-result');
1414
+ const pn = nodes.get(parent);
1415
+ if (pn) { pn.loaded = false; await loadFolder(parent); }
1416
+ expanded.add(dirPath);
1417
+ saveExpandedSet(expanded);
1418
+ rerender();
1419
+ } catch (e) {
1420
+ alert('Create folder failed: ' + (e?.message || e));
1421
+ }
1422
+ }
1423
+
1424
+ btnNewFile.addEventListener('click', newFile);
1425
+ btnNewDir.addEventListener('click', newDir);
1426
+ btnRefresh.addEventListener('click', refreshTree);
1427
+ searchInput.addEventListener('input', rerender);
1428
+
1429
+ // monaco-polish (2026-05-14): tree-resize handler moved into
1430
+ // PaneResizer (below module def, top-level scope) so all 3
1431
+ // handles share a single persistence layer. Inline width is
1432
+ // restored from localStorage in PaneResizer.restoreDims().
1433
+
1434
+ // Mark a path dirty in the tree (called by Editor when buffer
1435
+ // diverges from disk).
1436
+ function markDirty(path, isDirty) {
1437
+ const n = nodes.get(path);
1438
+ if (!n) return;
1439
+ if (n.dirty === !!isDirty) return;
1440
+ n.dirty = !!isDirty;
1441
+ rerender();
1442
+ }
1443
+
1444
+ // ── file-tree-watch (2026-05-15): subscribe + event apply ─────
1445
+ //
1446
+ // Server-side fs-watch protocol (src/session/fs-watch.ts):
1447
+ //
1448
+ // IN { type:'fs-watch-subscribe', reqId, paths:['/home/user'] }
1449
+ // OUT { type:'fs-watch-subscribe-result', reqId, ok, subId? }
1450
+ // PUSH { type:'fs-watch-event', subId, events:VfsEvent[], dropped? }
1451
+ // IN { type:'fs-watch-unsubscribe', reqId, subId? }
1452
+ //
1453
+ // Local lifecycle:
1454
+ // - subscribeOnce() is called from ws.onopen (after drainFsQueue).
1455
+ // Re-invocations are no-ops while a subId is live; on WS close
1456
+ // the server-side listener is reaped (ws.ts cleanup) so the
1457
+ // next reconnect re-subscribes fresh.
1458
+ // - applyWatchEvent() coalesces all 'fs-watch-event' frames into
1459
+ // per-parent refresh actions on a single 100ms window — 5+
1460
+ // events that share the same parent dir produce ONE re-fetch
1461
+ // of that folder, not N.
1462
+ //
1463
+ // Coalescing layers (composed):
1464
+ // Server (50ms debounce, SUB_MAX_QUEUE=200, drop-oldest)
1465
+ // → batches of up to 200 VfsEvents per frame
1466
+ // Client (100ms per-parent debounce)
1467
+ // → one fs-list per unique parent per window
1468
+ //
1469
+ // A `dropped:N` field on the incoming frame means the server
1470
+ // dropped N events to honor its cap; we respond by force-
1471
+ // refreshing the entire root (cheapest correct recovery).
1472
+ let watchSubId = null;
1473
+ const pendingParentRefresh = new Set();
1474
+ let watchRefreshTimer = null;
1475
+ let lastWatchFrameAt = 0;
1476
+ let watchFrameCount = 0;
1477
+ let watchEventCount = 0;
1478
+ const WATCH_COALESCE_MS = 100;
1479
+
1480
+ function subscribeOnce() {
1481
+ if (watchSubId) return; // already subscribed
1482
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
1483
+ // Fire-and-forget — record the subId on the success result.
1484
+ // We use fsRequest's queued-send semantics for the reply
1485
+ // routing (pendingReqs keyed by reqId), but tolerate failures.
1486
+ fsRequest(
1487
+ { type: 'fs-watch-subscribe', paths: [ROOT] },
1488
+ 'fs-watch-subscribe-result',
1489
+ ).then((r) => {
1490
+ if (r && r.ok && typeof r.subId === 'string') {
1491
+ watchSubId = r.subId;
1492
+ }
1493
+ }).catch(() => { /* server may not support fs-watch yet */ });
1494
+ }
1495
+
1496
+ function scheduleParentRefreshFlush() {
1497
+ if (watchRefreshTimer) return;
1498
+ watchRefreshTimer = setTimeout(async () => {
1499
+ watchRefreshTimer = null;
1500
+ if (pendingParentRefresh.size === 0) return;
1501
+ const parents = Array.from(pendingParentRefresh);
1502
+ pendingParentRefresh.clear();
1503
+ for (const parent of parents) {
1504
+ const pn = nodes.get(parent);
1505
+ // Only re-fetch dirs we've already loaded (cheap + bounded).
1506
+ // Unloaded dirs are loaded lazily on expand.
1507
+ if (pn && pn.type === 'directory' && pn.loaded) {
1508
+ // Drop cached children then re-load the folder. The
1509
+ // ensureNode at the end of loadFolder keeps the node
1510
+ // entry alive (avoiding flicker on the user's selection).
1511
+ // We drop child nodes from the Map so loadFolder's
1512
+ // re-insert produces a clean view that reflects deletes.
1513
+ const prefix = parent + '/';
1514
+ for (const path of Array.from(nodes.keys())) {
1515
+ if (path !== parent && parentOf(path) === parent) {
1516
+ nodes.delete(path);
1517
+ }
1518
+ else if (path !== parent && path.startsWith(prefix)) {
1519
+ // children of children — drop too so a deleted subtree
1520
+ // disappears in one cycle
1521
+ nodes.delete(path);
1522
+ }
1523
+ }
1524
+ try { await loadFolder(parent); } catch { /* fail-soft */ }
1525
+ }
1526
+ }
1527
+ }, WATCH_COALESCE_MS);
1528
+ }
1529
+
1530
+ // file-tree-watch: dispatch an inbound fs-watch-event frame to
1531
+ // the tree's incremental refresh pipeline. Returns true if the
1532
+ // frame was consumed (caller short-circuits the rest of its
1533
+ // onmessage chain), false otherwise.
1534
+ function applyWatchEvent(msg) {
1535
+ if (!msg || msg.type !== 'fs-watch-event') return false;
1536
+ watchFrameCount++;
1537
+ lastWatchFrameAt = Date.now();
1538
+ // Server-side drop signal → cheapest correct recovery is a
1539
+ // full root re-fetch. Clear the queue + force refresh.
1540
+ if (typeof msg.dropped === 'number' && msg.dropped > 0) {
1541
+ pendingParentRefresh.clear();
1542
+ if (watchRefreshTimer) {
1543
+ clearTimeout(watchRefreshTimer);
1544
+ watchRefreshTimer = null;
1545
+ }
1546
+ // schedule async refresh of root (preserves expanded state)
1547
+ (async () => {
1548
+ const expandedSnapshot = new Set(expanded);
1549
+ const wasLoaded = new Set();
1550
+ for (const n of nodes.values()) {
1551
+ if (n.type === 'directory' && n.loaded) wasLoaded.add(n.path);
1552
+ }
1553
+ nodes.clear();
1554
+ loaded = false;
1555
+ try { await ensureLoaded(); } catch {}
1556
+ // Re-load previously-loaded subdirs that are still expanded.
1557
+ for (const path of wasLoaded) {
1558
+ if (path === ROOT) continue;
1559
+ if (!expandedSnapshot.has(path)) continue;
1560
+ try { await loadFolder(path); } catch {}
1561
+ }
1562
+ })();
1563
+ return true;
1564
+ }
1565
+ const events = Array.isArray(msg.events) ? msg.events : [];
1566
+ if (events.length === 0) return true;
1567
+ watchEventCount += events.length;
1568
+ // Per-parent coalescing — collect unique parent dirs that need
1569
+ // a refresh. SqliteVFS paths are bare ('home/user/x.txt'); we
1570
+ // prepend '/' to match the absolute-path convention used by
1571
+ // the FileTree (ROOT === '/home/user').
1572
+ for (const ev of events) {
1573
+ if (!ev || typeof ev.path !== 'string') continue;
1574
+ const abs = ev.path.startsWith('/') ? ev.path : '/' + ev.path;
1575
+ if (!abs.startsWith(ROOT)) continue; // outside our watched root
1576
+ // For unlinkDir / rename(old), the affected parent is the
1577
+ // *old* path's parent; the new-path's parent gets refresh
1578
+ // via the same path entry below. For other events, just the
1579
+ // event path's parent.
1580
+ pendingParentRefresh.add(parentOf(abs));
1581
+ if (ev.type === 'rename' && typeof ev.oldPath === 'string') {
1582
+ const oldAbs = ev.oldPath.startsWith('/') ? ev.oldPath : '/' + ev.oldPath;
1583
+ if (oldAbs.startsWith(ROOT)) {
1584
+ pendingParentRefresh.add(parentOf(oldAbs));
1585
+ }
1586
+ }
1587
+ }
1588
+ // Invalidate the Cmd+P palette cache so the next openPalette
1589
+ // re-fetches the file list. Editor exposes this; tolerate
1590
+ // either lazy-init order.
1591
+ try {
1592
+ if (typeof Editor !== 'undefined' && typeof Editor.invalidateFileListCache === 'function') {
1593
+ Editor.invalidateFileListCache();
1594
+ }
1595
+ } catch { /* best-effort */ }
1596
+ if (pendingParentRefresh.size > 0) scheduleParentRefreshFlush();
1597
+ return true;
1598
+ }
1599
+
1600
+ // Diagnostic accessor for probes (cleanup-on-disconnect verifies
1601
+ // counts return to 0 on the server side; this is the client-side
1602
+ // mirror — useful for the coalescing probe).
1603
+ function getWatchStats() {
1604
+ return {
1605
+ subId: watchSubId,
1606
+ frameCount: watchFrameCount,
1607
+ eventCount: watchEventCount,
1608
+ lastFrameAt: lastWatchFrameAt,
1609
+ pendingParents: pendingParentRefresh.size,
1610
+ };
1611
+ }
1612
+
1613
+ return {
1614
+ ensureLoaded, tryHandleFsResult, markDirty, setSelected, drainFsQueue,
1615
+ subscribeOnce, applyWatchEvent, getWatchStats,
1616
+ };
1617
+ })();
1618
+
1619
+ // Which preview backend the iframe currently points at. Driven by
1620
+ // /api/stats polling: wrangler.running > vite.running > placeholder.
1621
+ // Tracked separately from iframe.src so user reloads / hash changes
1622
+ // don't confuse the switcher. Starts as null so the first
1623
+ // applyPreviewTarget call always paints (even if nothing is running
1624
+ // yet — that still wants the VITE_URL placeholder loaded).
1625
+ let activePreview = null; // null | 'vite' | 'worker' | 'none'
1626
+
1627
+ function previewUrlFor(kind) {
1628
+ return kind === 'worker' ? WORKER_URL : VITE_URL;
1629
+ }
1630
+
1631
+ function applyPreviewTarget(kind) {
1632
+ // Idempotent: only update iframe + URL bar when the backend kind
1633
+ // actually changes. Avoids tearing down the inner app on every
1634
+ // 5-second stats poll.
1635
+ if (activePreview === kind) return;
1636
+ activePreview = kind;
1637
+ const url = previewUrlFor(kind);
1638
+ // Always use the VITE_URL path when nothing is running — that
1639
+ // endpoint serves the styled "no dev server" placeholder, which
1640
+ // self-refreshes via its own polling loop.
1641
+ previewFrame.src = url;
1642
+ const urlBar = document.getElementById('urlBar');
1643
+ if (urlBar) urlBar.value = url;
1644
+ // If the user had collapsed the preview because nothing was up,
1645
+ // surface it automatically when the first backend comes online.
1646
+ if (kind !== 'none' && layout === 'terminal-only') setLayout('split');
1647
+ }
1648
+
1649
+ function reloadPreview() {
1650
+ // Refreshing without a running backend should re-poll the
1651
+ // placeholder so users see dev-server status update promptly.
1652
+ if (previewFrame.src === 'about:blank' ||
1653
+ (!previewFrame.src.includes('/preview') && !previewFrame.src.includes('/worker'))) {
1654
+ previewFrame.src = previewUrlFor(
1655
+ (activePreview === 'none' || activePreview === null) ? 'vite' : activePreview
1656
+ );
1657
+ } else {
1658
+ try { previewFrame.contentWindow?.location.reload(); }
1659
+ catch { previewFrame.src = previewFrame.src; }
1660
+ }
1661
+ }
1662
+
1663
+ // ── monaco-polish (2026-05-14): pane resize manager ──
1664
+ //
1665
+ // Owns the 3 resize handles in editor mode + 1 in split mode:
1666
+ // H1 tree ↔ editor-stack (col, in editor mode) — TreeResizeHandle in FileTree IIFE
1667
+ // H2 editor-stack ↔ preview (col, in editor + split) — #resizeHandle (mode-aware)
1668
+ // V1 editor ↔ terminal (row, in editor mode) — #editorTerminalResizeHandle
1669
+ //
1670
+ // Each handle persists its dimension to localStorage so a reload
1671
+ // restores the user's layout. Keyed per-session.
1672
+ //
1673
+ // Triggers Monaco editor.layout() + xterm fitAddon.fit() after
1674
+ // every drag-end so the wrapped panes re-measure. Mid-drag does
1675
+ // a cheap fit too (xterm) for the "live shrink" feel; Monaco
1676
+ // automaticLayout:true handles itself.
1677
+ const PaneResizer = (function() {
1678
+ const LS_DIMS = 'nimbus.pane.dims.' + (SESSION_PREFIX || 'noid');
1679
+ // Default flex basis percentages — match the CSS defaults above.
1680
+ // Persisted to LS; restored before first setLayout call.
1681
+ let dims = {
1682
+ treeWidth: 250, // px
1683
+ middlePct: 60, // % of main width for left-stack/terminal (rest = preview)
1684
+ editorPct: 60, // % of left-stack height for editor (rest = terminal)
1685
+ };
1686
+
1687
+ function loadDims() {
1688
+ try {
1689
+ const raw = localStorage.getItem(LS_DIMS);
1690
+ if (raw) Object.assign(dims, JSON.parse(raw));
1691
+ } catch {}
1692
+ }
1693
+ function saveDims() {
1694
+ try { localStorage.setItem(LS_DIMS, JSON.stringify(dims)); } catch {}
1695
+ }
1696
+
1697
+ function applyMonacoLayout() {
1698
+ if (window.__nimbusMonacoEditor) {
1699
+ try { window.__nimbusMonacoEditor.layout(); } catch {}
1700
+ }
1701
+ }
1702
+ function applyTerminalFit() {
1703
+ try { fitAddon.fit(); } catch {}
1704
+ }
1705
+ function applyAll() {
1706
+ // Tree width — applied as inline width on .panel-tree.
1707
+ const tree = document.getElementById('treePanel');
1708
+ if (tree) tree.style.width = dims.treeWidth + 'px';
1709
+ // Middle ↔ preview flex.
1710
+ applyMiddlePreviewFlex();
1711
+ // Editor ↔ terminal flex (inside left-stack).
1712
+ applyEditorTerminalFlex();
1713
+ applyTerminalFit();
1714
+ applyMonacoLayout();
1715
+ }
1716
+
1717
+ // Map middlePct → flex basis on the two children that actually
1718
+ // compete for horizontal space in each mode:
1719
+ // - split mode: .panel-terminal vs .panel-preview
1720
+ // - editor mode: .panel-left-stack vs .panel-preview
1721
+ // In editor mode .panel-left-stack is flex:1 (CSS rule); we
1722
+ // override with explicit basis. Same for .panel-terminal in split.
1723
+ function applyMiddlePreviewFlex() {
1724
+ const stack = document.getElementById('leftStack');
1725
+ const term = mainPanel.querySelector('.panel-terminal');
1726
+ const prev = mainPanel.querySelector('.panel-preview');
1727
+ const middlePct = Math.max(20, Math.min(80, dims.middlePct));
1728
+ if (layout === 'editor') {
1729
+ // stack drives middle width; terminal is INSIDE stack.
1730
+ if (stack) stack.style.flex = middlePct + ' 0 0%';
1731
+ if (prev) prev.style.flex = (100 - middlePct) + ' 0 0%';
1732
+ if (term) term.style.flex = ''; // unset; vertical handler owns it
1733
+ } else if (layout === 'split') {
1734
+ if (term) term.style.flex = middlePct + ' 0 0%';
1735
+ if (prev) prev.style.flex = (100 - middlePct) + ' 0 0%';
1736
+ if (stack) stack.style.flex = '';
1737
+ } else {
1738
+ // Other modes (terminal-only / preview-only) don't use either
1739
+ // handle visibly; clear inline so CSS defaults take effect.
1740
+ if (term) term.style.flex = '';
1741
+ if (prev) prev.style.flex = '';
1742
+ if (stack) stack.style.flex = '';
1743
+ }
1744
+ }
1745
+
1746
+ function applyEditorTerminalFlex() {
1747
+ if (layout !== 'editor') return;
1748
+ const editor = document.getElementById('editorPanel');
1749
+ const term = mainPanel.querySelector('.panel-terminal');
1750
+ const editorPct = Math.max(10, Math.min(90, dims.editorPct));
1751
+ if (editor) editor.style.flex = editorPct + ' 0 0%';
1752
+ if (term) term.style.flex = (100 - editorPct) + ' 0 0%';
1753
+ }
1754
+
1755
+ // Drag wiring.
1756
+ let activeHandle = null;
1757
+ let dragStartXY = 0;
1758
+ let dragStartDim = 0;
1759
+ let dragStartTotal = 0;
1760
+
1761
+ function startDrag(kind, e) {
1762
+ activeHandle = kind;
1763
+ e.preventDefault();
1764
+ const rect = mainPanel.getBoundingClientRect();
1765
+ const stack = document.getElementById('leftStack');
1766
+ if (kind === 'tree') {
1767
+ dragStartXY = e.clientX;
1768
+ dragStartDim = dims.treeWidth;
1769
+ dragStartTotal = rect.width;
1770
+ document.body.style.cursor = 'col-resize';
1771
+ } else if (kind === 'middle') {
1772
+ dragStartXY = e.clientX;
1773
+ dragStartDim = dims.middlePct;
1774
+ dragStartTotal = rect.width;
1775
+ document.body.style.cursor = 'col-resize';
1776
+ } else if (kind === 'editor-terminal') {
1777
+ dragStartXY = e.clientY;
1778
+ dragStartDim = dims.editorPct;
1779
+ // The vertical handle moves within the left-stack column.
1780
+ dragStartTotal = stack ? stack.getBoundingClientRect().height : rect.height;
1781
+ document.body.style.cursor = 'row-resize';
1782
+ }
1783
+ }
1784
+
1785
+ function onMove(e) {
1786
+ if (!activeHandle) return;
1787
+ if (activeHandle === 'tree') {
1788
+ const dx = e.clientX - dragStartXY;
1789
+ dims.treeWidth = Math.max(160, Math.min(600, dragStartDim + dx));
1790
+ const tree = document.getElementById('treePanel');
1791
+ if (tree) tree.style.width = dims.treeWidth + 'px';
1792
+ applyTerminalFit();
1793
+ applyMonacoLayout();
1794
+ } else if (activeHandle === 'middle') {
1795
+ const dx = e.clientX - dragStartXY;
1796
+ const pctDelta = (dx / dragStartTotal) * 100;
1797
+ dims.middlePct = Math.max(20, Math.min(80, dragStartDim + pctDelta));
1798
+ applyMiddlePreviewFlex();
1799
+ applyTerminalFit();
1800
+ applyMonacoLayout();
1801
+ } else if (activeHandle === 'editor-terminal') {
1802
+ const dy = e.clientY - dragStartXY;
1803
+ const pctDelta = (dy / dragStartTotal) * 100;
1804
+ dims.editorPct = Math.max(10, Math.min(90, dragStartDim + pctDelta));
1805
+ applyEditorTerminalFlex();
1806
+ applyTerminalFit();
1807
+ applyMonacoLayout();
1808
+ }
1809
+ }
1810
+
1811
+ function endDrag() {
1812
+ if (!activeHandle) return;
1813
+ activeHandle = null;
1814
+ document.body.style.cursor = '';
1815
+ saveDims();
1816
+ applyTerminalFit();
1817
+ applyMonacoLayout();
1818
+ }
1819
+
1820
+ // Wire up the three handles.
1821
+ function bindHandles() {
1822
+ const h1 = document.getElementById('treeResizeHandle');
1823
+ const h2 = document.getElementById('resizeHandle');
1824
+ const v1 = document.getElementById('editorTerminalResizeHandle');
1825
+ if (h1) h1.addEventListener('mousedown', (e) => startDrag('tree', e));
1826
+ if (h2) h2.addEventListener('mousedown', (e) => startDrag('middle', e));
1827
+ if (v1) v1.addEventListener('mousedown', (e) => startDrag('editor-terminal', e));
1828
+ document.addEventListener('mousemove', onMove);
1829
+ document.addEventListener('mouseup', endDrag);
1830
+ }
1831
+
1832
+ function restoreDims() {
1833
+ loadDims();
1834
+ // Apply tree width inline so first paint shows the persisted size.
1835
+ const tree = document.getElementById('treePanel');
1836
+ if (tree) tree.style.width = dims.treeWidth + 'px';
1837
+ }
1838
+
1839
+ // Re-apply dims any time setLayout() switches modes — the inline
1840
+ // flex values are mode-specific (split vs editor have different
1841
+ // pane structures).
1842
+ function onLayoutChange() {
1843
+ applyMiddlePreviewFlex();
1844
+ applyEditorTerminalFlex();
1845
+ }
1846
+
1847
+ bindHandles();
1848
+ return { restoreDims, applyAll, onLayoutChange };
1849
+ })();
1850
+
1851
+ // ── Status ──
1852
+ function setStatus(s) {
1853
+ dot.className = 'status-dot ' + s;
1854
+ stText.textContent = s === 'connected' ? 'Connected' : s === 'connecting' ? 'Connecting...' : 'Disconnected';
1855
+ }
1856
+ function fmtB(b) {
1857
+ if (b >= 1e9) return (b/1e9).toFixed(1)+' GB';
1858
+ if (b >= 1e6) return (b/1e6).toFixed(1)+' MB';
1859
+ if (b >= 1e3) return (b/1e3).toFixed(1)+' KB';
1860
+ return b+' B';
1861
+ }
1862
+ function refreshStats() {
1863
+ fetch(STATS_URL).then(r => r.json()).then(s => {
1864
+ document.getElementById('stFiles').textContent = s.files + ' files, ' + s.directories + ' dirs';
1865
+ document.getElementById('stSize').textContent = fmtB(s.usedBytes);
1866
+ if (s.cache) {
1867
+ document.getElementById('stCache').textContent = s.cache.entries + '/' + s.cache.maxEntries;
1868
+ document.getElementById('stHitRate').textContent = s.cache.hitRate + '%';
1869
+ }
1870
+ // Pick the active preview backend: wrangler takes precedence
1871
+ // over vite when both are running, because users only start
1872
+ // wrangler deliberately (vs. vite which auto-starts on certain
1873
+ // workflows). If neither is running, we still point at the
1874
+ // vite /preview/ URL so its placeholder page renders.
1875
+ let kind = 'none';
1876
+ if (s.wrangler?.running) kind = 'worker';
1877
+ else if (s.vite?.running) kind = 'vite';
1878
+ applyPreviewTarget(kind);
1879
+
1880
+ if (s.wrangler?.running) {
1881
+ viteStatus.textContent = 'worker running (' + (s.wrangler.name || '?') + ')';
1882
+ viteStatus.className = 'vite-status running';
1883
+ } else if (s.vite?.running) {
1884
+ viteStatus.textContent = 'vite running (' + s.vite.root + ')';
1885
+ viteStatus.className = 'vite-status running';
1886
+ } else {
1887
+ viteStatus.textContent = '';
1888
+ viteStatus.className = 'vite-status';
1889
+ }
1890
+ }).catch(() => {});
1891
+ }
1892
+
1893
+ // ── WebSocket ──
1894
+ function connect() {
1895
+ setStatus('connecting');
1896
+ ws = new WebSocket(WS_URL);
1897
+ ws.onopen = () => {
1898
+ setStatus('connected'); rd = 1000;
1899
+ ws.send(JSON.stringify({type:'resize',cols:term.cols,rows:term.rows}));
1900
+ // file-tree-fix (2026-05-14): drain any fs-* frames that
1901
+ // Editor / FileTree queued before WS opened. User repro
1902
+ // was a cold session where clicking Editor before the first
1903
+ // ws.onopen produced "fs-list failed: WS not connected".
1904
+ // Drain order doesn't matter — server processes per-reqId.
1905
+ try { if (typeof Editor !== 'undefined') Editor.drainFsQueue(); } catch {}
1906
+ try { if (typeof FileTree !== 'undefined') FileTree.drainFsQueue(); } catch {}
1907
+ // file-tree-watch (2026-05-15): subscribe the FileTree to
1908
+ // server-pushed fs-watch-event frames so terminal-side writes
1909
+ // (e.g. `touch foo.c`) appear in the sidebar within ~150 ms
1910
+ // without needing a manual refresh click.
1911
+ try { if (typeof FileTree !== 'undefined') FileTree.subscribeOnce(); } catch {}
1912
+ refreshStats();
1913
+ if (statsTimer) clearInterval(statsTimer);
1914
+ statsTimer = setInterval(() => {
1915
+ refreshStats();
1916
+ // Keep the process count + tab state reconciled even if a
1917
+ // spawn or exit event was dropped during a reconnect or
1918
+ // brief network blip. /api/processes is cheap (in-memory).
1919
+ if (typeof LogTabs !== 'undefined') LogTabs.hydrate();
1920
+ }, 5000);
1921
+ // Initial hydrate — DO is warm, ProcessTable populated.
1922
+ if (typeof LogTabs !== 'undefined') LogTabs.hydrate();
1923
+ };
1924
+ ws.onmessage = (e) => {
1925
+ try {
1926
+ const msg = JSON.parse(e.data);
1927
+ // monaco-wave-a (2026-05-13): fs-*-result frames route to
1928
+ // Editor's pending-request resolver. Return early so the
1929
+ // shell-side dispatch doesn't see them as unknown types.
1930
+ if (Editor.tryHandleFsResult(msg)) return;
1931
+ // monaco-wave-b: FileTree consumes fs-list results too.
1932
+ if (FileTree.tryHandleFsResult(msg)) return;
1933
+ // file-tree-watch (2026-05-15): server-pushed fs-watch-event
1934
+ // frames (no reqId) route to FileTree.applyWatchEvent which
1935
+ // coalesces per-parent refreshes on a 100ms window.
1936
+ if (FileTree.applyWatchEvent(msg)) return;
1937
+ if (msg.type === 'output') {
1938
+ term.write(msg.data);
1939
+ // Snappy preview switch: both vite (/preview/) and wrangler
1940
+ // (/worker/, "Worker built") emit recognisable output lines
1941
+ // when they come online. Triggering an immediate stats
1942
+ // refresh here shaves up to 5s off the otherwise-periodic
1943
+ // polling interval so the iframe flips as soon as the user
1944
+ // sees the dev-server banner in the terminal.
1945
+ if (msg.data.includes('/preview/') ||
1946
+ msg.data.includes('/worker/') ||
1947
+ msg.data.includes('Worker built') ||
1948
+ msg.data.includes('wrangler ready')) {
1949
+ refreshStats();
1950
+ }
1951
+ } else if (msg.type === 'ready') {
1952
+ refreshStats();
1953
+ } else if (msg.type === 'hmr') {
1954
+ // Forward HMR to preview iframe
1955
+ if (previewFrame.contentWindow) {
1956
+ previewFrame.contentWindow.postMessage(msg.data, '*');
1957
+ }
1958
+ } else if (msg.type === 'spawn') {
1959
+ // Auto-open a log tab for EVERY process the supervisor spawns.
1960
+ // Short-lived processes (node foo.js) immediately receive an
1961
+ // exit event and the tab flips to "exited" — kept around for
1962
+ // 10 min so the user can read the final output. Long-running
1963
+ // processes stream live. Gating on longRunning made the tabs
1964
+ // panel invisible for the most common user actions (node
1965
+ // script.js, npm run <non-dev-script>), which defeats the
1966
+ // whole point of the panel.
1967
+ LogTabs.openTab(msg.pid, msg.command);
1968
+ } else if (msg.type === 'exit') {
1969
+ // An exit can arrive for a pid we never saw a spawn for
1970
+ // (e.g. `node -e`, which the supervisor intentionally
1971
+ // suppresses the spawn banner for). Surface those too so
1972
+ // the user sees SOMETHING — we open a tab on-exit, fetch
1973
+ // the command from /api/processes if we haven't already,
1974
+ // and mark it exited. The ring buffer keeps the output
1975
+ // for 10 min.
1976
+ if (!LogTabs.hasTab(msg.pid)) {
1977
+ LogTabs.openTab(msg.pid, msg.command || ('pid ' + msg.pid));
1978
+ }
1979
+ LogTabs.markExit(msg.pid, msg.code, msg.reason);
1980
+ }
1981
+ } catch {}
1982
+ };
1983
+ ws.onclose = () => {
1984
+ setStatus('disconnected');
1985
+ if (statsTimer) { clearInterval(statsTimer); statsTimer = null; }
1986
+ setTimeout(() => { rd = Math.min(rd*1.5, 10000); connect(); }, rd);
1987
+ };
1988
+ ws.onerror = () => ws.close();
1989
+ }
1990
+
1991
+ term.onData(d => { if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({type:'input',data:d})); });
1992
+ window.addEventListener('resize', () => {
1993
+ fitAddon.fit();
1994
+ if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({type:'resize',cols:term.cols,rows:term.rows}));
1995
+ });
1996
+
1997
+ // ── Process log tabs panel ─────────────────────────────────────────
1998
+ // One tab per PID streaming `/api/logs/<pid>` over a WebSocket.
1999
+ // Auto-opens on EVERY `{type:'spawn'}` from the main WS; manually
2000
+ // closable via the ✕ on each tab, or collapsible via the toolbar
2001
+ // "Processes" button. The ring buffer retains logs for 10 min
2002
+ // post-exit, so a tab left open after a crash keeps showing the
2003
+ // final output until the buffer expires (or the user closes it).
2004
+ const LogTabs = (function() {
2005
+ const panel = document.getElementById('logsPanel');
2006
+ const tabBar = document.getElementById('logsTabBar');
2007
+ const body = document.getElementById('logsPanelBody');
2008
+ const btnProcs = document.getElementById('btnProcs');
2009
+ const procsCount = document.getElementById('procsCount');
2010
+ /** pid -> { tabEl, viewEl, ws, exited, failed, lastPainted, label } */
2011
+ const tabs = new Map();
2012
+ let activePid = null;
2013
+ // User-controlled visibility. `true` = show panel when ≥1 tab open.
2014
+ // `false` = keep panel collapsed even with tabs (toolbar button
2015
+ // dropped down once). Defaults to true; toggled by #btnProcs.
2016
+ let userWantsVisible = true;
2017
+
2018
+ // Minimal ANSI CSI stripper — the log stream passes ANSI through;
2019
+ // we keep stderr coloring via a CSS class rather than trying to
2020
+ // faithfully render xterm sequences inside a <pre>.
2021
+ const ANSI_RE = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[=>()][^\s]?/g;
2022
+ function stripAnsi(s) { return s.replace(ANSI_RE, ''); }
2023
+
2024
+ function escapeHtml(s) {
2025
+ return s
2026
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;')
2027
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;')
2028
+ .replace(/'/g, '&#39;');
2029
+ }
2030
+
2031
+ // Derive a short label from the command: strip argv[0] path prefix,
2032
+ // drop anything after the first flag-looking token, cap at ~28 chars.
2033
+ function labelFor(command) {
2034
+ if (!command) return '';
2035
+ const trimmed = command.trim();
2036
+ const parts = trimmed.split(/\s+/);
2037
+ // Strip env-var prefixes like FOO=bar
2038
+ let i = 0;
2039
+ while (i < parts.length && /^[A-Z_][A-Z0-9_]*=/.test(parts[i])) i++;
2040
+ let display = parts.slice(i, i + 3).join(' ');
2041
+ if (display.length > 32) display = display.slice(0, 29) + '…';
2042
+ return display || trimmed.slice(0, 32);
2043
+ }
2044
+
2045
+ // Refit the xterm + push new cols/rows to the server. Called after
2046
+ // ANY change to the panel's visibility or tab count — because the
2047
+ // terminal's viewport just grew or shrunk and the shell needs the
2048
+ // new size so its line-wrapping stays correct.
2049
+ function refitTerminal() {
2050
+ try { fitAddon.fit(); } catch {}
2051
+ if (ws?.readyState === WebSocket.OPEN) {
2052
+ ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
2053
+ }
2054
+ }
2055
+
2056
+ // Single source of truth for "should the panel be visible right
2057
+ // now?". The panel shows iff there's ≥1 tab AND the user hasn't
2058
+ // collapsed it. The toolbar button is visible iff we've ever seen
2059
+ // a process (so brand-new sessions don't see a useless "0 Processes"
2060
+ // chip), and its "active" highlight tracks panel visibility.
2061
+ function applyVisibility() {
2062
+ const haveTabs = tabs.size > 0;
2063
+ const shouldShow = haveTabs && userWantsVisible;
2064
+ const wasShown = panel.classList.contains('active');
2065
+ if (shouldShow) panel.classList.add('active');
2066
+ else panel.classList.remove('active');
2067
+ if (wasShown !== shouldShow) refitTerminal();
2068
+
2069
+ // Running vs total — the dot pulses when ≥1 tab is running.
2070
+ let running = 0;
2071
+ for (const t of tabs.values()) if (!t.exited) running++;
2072
+ procsCount.textContent = running > 0
2073
+ ? (running === tabs.size ? String(running) : running + '/' + tabs.size)
2074
+ : String(tabs.size);
2075
+ btnProcs.classList.toggle('visible', haveTabs);
2076
+ btnProcs.classList.toggle('has-running', running > 0);
2077
+ btnProcs.classList.toggle('active', shouldShow);
2078
+ }
2079
+
2080
+ // Hard cap on child nodes per view — a long-running `vite` session
2081
+ // can produce thousands of chunks per hour; without this, the tab's
2082
+ // DOM grows unboundedly and the browser tab eventually becomes
2083
+ // sluggish. When over the cap we drop the oldest 20% in one go to
2084
+ // amortize layout thrash (vs. one-at-a-time on every render).
2085
+ const MAX_NODES_PER_VIEW = 2000;
2086
+ const TRIM_BATCH = 400;
2087
+
2088
+ function appendToView(tab, el) {
2089
+ // Pre-compute bottom-ness against the actively-rendered view (not
2090
+ // the panel body, which might be scrolled by a different tab).
2091
+ const view = tab.viewEl;
2092
+ const isActive = activePid === tab.pid;
2093
+ const atBottom = isActive &&
2094
+ (body.scrollHeight - body.scrollTop - body.clientHeight) < 24;
2095
+ view.appendChild(el);
2096
+ if (view.childElementCount > MAX_NODES_PER_VIEW) {
2097
+ // Trim oldest. DocumentFragment swap would be faster for huge
2098
+ // drops, but TRIM_BATCH is small and this runs at most once per
2099
+ // append so the measured cost is negligible.
2100
+ for (let i = 0; i < TRIM_BATCH && view.firstChild; i++) {
2101
+ view.removeChild(view.firstChild);
2102
+ }
2103
+ }
2104
+ if (atBottom) body.scrollTop = body.scrollHeight;
2105
+ }
2106
+
2107
+ function renderChunk(tab, chunk) {
2108
+ const span = document.createElement('span');
2109
+ span.textContent = stripAnsi(chunk.data);
2110
+ if (chunk.binary) span.className = 'binary';
2111
+ else if (chunk.stream === 'stderr') span.className = 'stderr';
2112
+ appendToView(tab, span);
2113
+ }
2114
+
2115
+ function renderBanner(tab, text, cls) {
2116
+ const div = document.createElement('span');
2117
+ div.className = 'banner' + (cls ? ' ' + cls : '');
2118
+ div.textContent = text;
2119
+ appendToView(tab, div);
2120
+ }
2121
+
2122
+ function setActive(pid) {
2123
+ activePid = pid;
2124
+ for (const [p, t] of tabs) {
2125
+ t.tabEl.classList.toggle('active', p === pid);
2126
+ t.viewEl.classList.toggle('active', p === pid);
2127
+ }
2128
+ if (pid !== null) body.scrollTop = body.scrollHeight;
2129
+ }
2130
+
2131
+ function closeTab(pid) {
2132
+ const t = tabs.get(pid);
2133
+ if (!t) return;
2134
+ try { t.ws?.close(); } catch {}
2135
+ t.tabEl.remove();
2136
+ t.viewEl.remove();
2137
+ tabs.delete(pid);
2138
+ if (activePid === pid) {
2139
+ // Pick any other open tab, or collapse.
2140
+ const next = tabs.keys().next().value;
2141
+ if (next !== undefined) setActive(next);
2142
+ else activePid = null;
2143
+ }
2144
+ applyVisibility();
2145
+ }
2146
+
2147
+ // Open the log-stream WebSocket for a tab, if it hasn't been
2148
+ // opened already. Audit R3 / PERF-AUDIT.md: since auto-open-
2149
+ // every-spawn landed, every short-lived `node foo.js` opened a
2150
+ // persistent log WS that stayed alive 10 min. Every facet
2151
+ // stdout chunk then fanned out to every idle subscriber on the
2152
+ // DO side, competing with the shell WS for input-gate time and
2153
+ // making typing feel laggy.
2154
+ //
2155
+ // We now defer the WS connection until the user actually clicks
2156
+ // the tab. The tab strip still appears immediately on spawn
2157
+ // (good UX — "I see you, here's your process") but the server
2158
+ // never ships bytes to a reader nobody's reading. The 95% of
2159
+ // short-lived processes that the user never clicks pay zero
2160
+ // network/DO cost beyond the initial {spawn}/{exit} frames on
2161
+ // the main WS.
2162
+ function ensureLogWs(tab) {
2163
+ if (tab.wsOpened) return;
2164
+ tab.wsOpened = true;
2165
+ // Drop the "click this tab to attach" placeholder (if any).
2166
+ // Backlog + live chunks will repopulate the view.
2167
+ tab.viewEl.replaceChildren();
2168
+ const pid = tab.pid;
2169
+ const wsUrl = (location.protocol === 'https:' ? 'wss:' : 'ws:') +
2170
+ '//' + location.host + SESSION_PREFIX + '/api/logs/' + pid;
2171
+ let logWs;
2172
+ try { logWs = new WebSocket(wsUrl); } catch {
2173
+ renderBanner(tab, '[log stream could not be opened]', 'failed');
2174
+ return;
2175
+ }
2176
+ tab.ws = logWs;
2177
+ logWs.onmessage = (ev) => {
2178
+ let msg;
2179
+ try { msg = JSON.parse(ev.data); } catch { return; }
2180
+ if (msg.type === 'backlog') {
2181
+ for (const c of (msg.chunks || [])) renderChunk(tab, c);
2182
+ } else if (msg.type === 'chunk') {
2183
+ renderChunk(tab, msg);
2184
+ } else if (msg.type === 'exit') {
2185
+ markExit(pid, msg.code, msg.reason);
2186
+ } else if (msg.type === 'notfound') {
2187
+ // Mark exited so the server-side close doesn't emit a second
2188
+ // "[log stream closed]" banner on top of this one.
2189
+ tab.exited = true;
2190
+ tab.failed = true;
2191
+ tab.tabEl.classList.add('exited', 'failed');
2192
+ renderBanner(tab, '[no log buffer for pid ' + pid + ' — process may have been reaped]', 'failed');
2193
+ }
2194
+ };
2195
+ logWs.onclose = () => {
2196
+ // If we haven't seen an exit yet, this is a server-side tear-down
2197
+ // or a network blip. The UI keeps the tab open so the user can
2198
+ // still read the backlog; reconnect would require a manual close
2199
+ // + reopen via the shell.
2200
+ if (!tab.exited) {
2201
+ renderBanner(tab, '[log stream closed]', '');
2202
+ }
2203
+ };
2204
+ logWs.onerror = () => {
2205
+ renderBanner(tab, '[log stream error]', 'failed');
2206
+ };
2207
+ }
2208
+
2209
+ function openTab(pid, command) {
2210
+ if (tabs.has(pid)) { setActive(pid); userWantsVisible = true; applyVisibility(); return; }
2211
+
2212
+ const label = labelFor(command);
2213
+ const tabEl = document.createElement('div');
2214
+ tabEl.className = 'logs-tab';
2215
+ tabEl.title = 'pid ' + pid + ': ' + (command || '(unknown)');
2216
+ tabEl.innerHTML =
2217
+ '<span class="logs-tab-dot"></span>' +
2218
+ '<span class="logs-tab-label"></span>' +
2219
+ '<span class="logs-tab-pid" style="color:#484f58">#' + pid + '</span>' +
2220
+ // Primitives wave (P11): Kill (^C/SIGTERM-equivalent) and
2221
+ // Restart per-tab. Hidden via CSS when the process has
2222
+ // exited. The close X is unchanged — it just removes the
2223
+ // local tab; Kill is what tears down the process.
2224
+ '<span class="logs-tab-action logs-tab-restart" title="Restart process (kill + spawn fresh; vite only)">↻</span>' +
2225
+ '<span class="logs-tab-action logs-tab-kill" title="Kill process">⏹</span>' +
2226
+ '<span class="logs-tab-close" title="Close tab (process keeps running)">✕</span>';
2227
+ tabEl.querySelector('.logs-tab-label').textContent = label;
2228
+ tabEl.addEventListener('click', (e) => {
2229
+ const target = e.target;
2230
+ if (target.classList && (
2231
+ target.classList.contains('logs-tab-close') ||
2232
+ target.classList.contains('logs-tab-action')
2233
+ )) return;
2234
+ // User click: this is the signal that they want to read the
2235
+ // log stream. Open the WS if it isn't already, then activate.
2236
+ const t = tabs.get(pid);
2237
+ if (t) ensureLogWs(t);
2238
+ setActive(pid);
2239
+ });
2240
+ tabEl.querySelector('.logs-tab-close').addEventListener('click', (e) => {
2241
+ e.stopPropagation();
2242
+ closeTab(pid);
2243
+ });
2244
+ const sendAction = async (action) => {
2245
+ // POST /api/<action>/<pid>; surface failures as a banner inside
2246
+ // the tab so the user sees WHY without hunting in network tools.
2247
+ let resp;
2248
+ try {
2249
+ resp = await fetch(SESSION_PREFIX + '/api/' + action + '/' + pid, {
2250
+ method: 'POST', cache: 'no-store',
2251
+ });
2252
+ } catch (err) {
2253
+ const t = tabs.get(pid);
2254
+ if (t) renderBanner(t, '[' + action + ' failed: network] ' + (err && err.message || err), 'failed');
2255
+ return;
2256
+ }
2257
+ const t = tabs.get(pid);
2258
+ if (!t) return;
2259
+ if (!resp.ok) {
2260
+ let msg = '[' + action + ' failed: HTTP ' + resp.status + ']';
2261
+ try {
2262
+ const j = await resp.json();
2263
+ if (j && j.error) msg += ' ' + j.error;
2264
+ } catch {}
2265
+ renderBanner(t, msg, 'failed');
2266
+ return;
2267
+ }
2268
+ renderBanner(t, '[' + action + ' requested]', 'ok');
2269
+ };
2270
+ tabEl.querySelector('.logs-tab-kill').addEventListener('click', (e) => {
2271
+ e.stopPropagation();
2272
+ sendAction('kill');
2273
+ });
2274
+ tabEl.querySelector('.logs-tab-restart').addEventListener('click', (e) => {
2275
+ e.stopPropagation();
2276
+ sendAction('restart');
2277
+ });
2278
+
2279
+ const viewEl = document.createElement('div');
2280
+ viewEl.className = 'logs-view';
2281
+ viewEl.dataset.pid = String(pid);
2282
+
2283
+ tabBar.appendChild(tabEl);
2284
+ body.appendChild(viewEl);
2285
+
2286
+ const tab = { pid, tabEl, viewEl, ws: null, wsOpened: false, exited: false, failed: false, label };
2287
+ tabs.set(pid, tab);
2288
+ // Placeholder until the user clicks — so the active tab doesn't
2289
+ // look broken on auto-activate. renderBanner uses the existing
2290
+ // `.banner` styling. Replaced by real content when the WS
2291
+ // delivers a backlog frame (ensureLogWs → onmessage).
2292
+ renderBanner(tab, '[click this tab to attach to the log stream]', '');
2293
+ // Any new tab re-opens the panel if the user had collapsed it —
2294
+ // seeing a new process spawn is a stronger signal than a stale
2295
+ // "I don't want this right now" click. setActive is purely a
2296
+ // UI paint here (no WS opened) so the panel and "Processes N"
2297
+ // chip still react immediately to the spawn event.
2298
+ userWantsVisible = true;
2299
+ setActive(pid);
2300
+ applyVisibility();
2301
+ }
2302
+
2303
+ function markExit(pid, code, reason) {
2304
+ const tab = tabs.get(pid);
2305
+ if (!tab) return;
2306
+ if (tab.exited) return; // idempotent — server + log-WS may both fire
2307
+ tab.exited = true;
2308
+ tab.failed = code !== 0;
2309
+ tab.tabEl.classList.add('exited');
2310
+ if (tab.failed) tab.tabEl.classList.add('failed');
2311
+ // Update the dot color via the class; append the exit banner.
2312
+ const reasonSuffix = reason ? ' (' + reason + ')' : '';
2313
+ renderBanner(
2314
+ tab,
2315
+ '[process exited with code ' + code + reasonSuffix + ' — tab kept; close with ✕]',
2316
+ code === 0 ? 'ok' : 'failed',
2317
+ );
2318
+ // Running-count changed; refresh toolbar badge.
2319
+ applyVisibility();
2320
+ }
2321
+
2322
+ // Restore any processes already in the DO's ring buffer / process
2323
+ // table when the page loads. Covers two cases: (a) full page
2324
+ // refresh mid-session — the DO's process table persists, so we
2325
+ // should re-attach to any running process; (b) main WS reconnect
2326
+ // after a transient network blip — same table, same goal.
2327
+ //
2328
+ // We open tabs for ALL state='running' processes so users see
2329
+ // everything happening. Exited ones stay accessible via `logs
2330
+ // <pid>` in the shell for 10 min, but we don't auto-open them
2331
+ // here because that would clutter the UI after a handful of
2332
+ // short-lived commands.
2333
+ function hydrate() {
2334
+ fetch(SESSION_PREFIX + '/api/processes', { cache: 'no-store' })
2335
+ .then(r => r.ok ? r.json() : null)
2336
+ .then(data => {
2337
+ if (!data?.processes) return;
2338
+ for (const p of data.processes) {
2339
+ if (p.state === 'running' && !tabs.has(p.pid)) {
2340
+ openTab(p.pid, p.command);
2341
+ } else if (p.state !== 'running' && tabs.has(p.pid)) {
2342
+ // Server says exited; reconcile in case our main-WS
2343
+ // exit event got dropped during reconnect.
2344
+ markExit(p.pid, p.exitCode ?? 0, undefined);
2345
+ }
2346
+ }
2347
+ applyVisibility();
2348
+ })
2349
+ .catch(() => {});
2350
+ }
2351
+
2352
+ // Toolbar button: toggle user-facing visibility. If currently
2353
+ // shown, a click hides; if hidden, a click re-shows. Clicking
2354
+ // with no tabs open is a no-op (the button is hidden via CSS in
2355
+ // that state anyway).
2356
+ btnProcs.addEventListener('click', () => {
2357
+ userWantsVisible = !panel.classList.contains('active');
2358
+ applyVisibility();
2359
+ });
2360
+
2361
+ return {
2362
+ openTab, closeTab, markExit, hydrate, applyVisibility,
2363
+ hasTab: (pid) => tabs.has(pid),
2364
+ };
2365
+ })();
2366
+ // LogTabs.hydrate is invoked from ws.onopen above — the DO is warm
2367
+ // and ProcessTable is populated by then, no arbitrary delay needed.
2368
+
2369
+ term.focus();
2370
+ // monaco-polish (2026-05-14): default to editor mode on page load.
2371
+ // Restore any persisted pane dims (tree width, middle/preview ratio,
2372
+ // editor/terminal ratio) BEFORE activating layout so the first paint
2373
+ // shows the user's prior arrangement.
2374
+ PaneResizer.restoreDims();
2375
+ setLayout('editor');
2376
+ connect();
2377
+ </script>
2378
+ </body>
2379
+ </html>