@astrale-os/shell-react 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 (378) hide show
  1. package/LICENSE +202 -0
  2. package/dist/application/apps/app-host.d.ts +31 -0
  3. package/dist/application/apps/app-host.d.ts.map +1 -0
  4. package/dist/application/apps/app-host.js +59 -0
  5. package/dist/application/apps/app-logo.d.ts +19 -0
  6. package/dist/application/apps/app-logo.d.ts.map +1 -0
  7. package/dist/application/apps/app-logo.js +31 -0
  8. package/dist/application/apps/apps.hook.d.ts +12 -0
  9. package/dist/application/apps/apps.hook.d.ts.map +1 -0
  10. package/dist/application/apps/apps.hook.js +51 -0
  11. package/dist/application/apps/open-app.hook.d.ts +10 -0
  12. package/dist/application/apps/open-app.hook.d.ts.map +1 -0
  13. package/dist/application/apps/open-app.hook.js +7 -0
  14. package/dist/application/index.d.ts +18 -0
  15. package/dist/application/index.d.ts.map +1 -0
  16. package/dist/application/index.js +12 -0
  17. package/dist/application/roles.hook.d.ts +7 -0
  18. package/dist/application/roles.hook.d.ts.map +1 -0
  19. package/dist/application/roles.hook.js +23 -0
  20. package/dist/application/spaces/current-space.hook.d.ts +7 -0
  21. package/dist/application/spaces/current-space.hook.d.ts.map +1 -0
  22. package/dist/application/spaces/current-space.hook.js +27 -0
  23. package/dist/application/spaces/current-space.store.d.ts +32 -0
  24. package/dist/application/spaces/current-space.store.d.ts.map +1 -0
  25. package/dist/application/spaces/current-space.store.js +79 -0
  26. package/dist/application/spaces/space.hook.d.ts +5 -0
  27. package/dist/application/spaces/space.hook.d.ts.map +1 -0
  28. package/dist/application/spaces/space.hook.js +25 -0
  29. package/dist/application/spaces/spaces.hook.d.ts +14 -0
  30. package/dist/application/spaces/spaces.hook.d.ts.map +1 -0
  31. package/dist/application/spaces/spaces.hook.js +82 -0
  32. package/dist/application/user/user.hook.d.ts +10 -0
  33. package/dist/application/user/user.hook.d.ts.map +1 -0
  34. package/dist/application/user/user.hook.js +19 -0
  35. package/dist/application/user/user.store.d.ts +35 -0
  36. package/dist/application/user/user.store.d.ts.map +1 -0
  37. package/dist/application/user/user.store.js +51 -0
  38. package/dist/auth/act-as.context.d.ts +40 -0
  39. package/dist/auth/act-as.context.d.ts.map +1 -0
  40. package/dist/auth/act-as.context.js +84 -0
  41. package/dist/auth/auth.hook.d.ts +3 -0
  42. package/dist/auth/auth.hook.d.ts.map +1 -0
  43. package/dist/auth/auth.hook.js +25 -0
  44. package/dist/auth/can.hook.d.ts +40 -0
  45. package/dist/auth/can.hook.d.ts.map +1 -0
  46. package/dist/auth/can.hook.js +119 -0
  47. package/dist/auth/index.d.ts +6 -0
  48. package/dist/auth/index.d.ts.map +1 -0
  49. package/dist/auth/index.js +5 -0
  50. package/dist/auth/self.hook.d.ts +3 -0
  51. package/dist/auth/self.hook.d.ts.map +1 -0
  52. package/dist/auth/self.hook.js +11 -0
  53. package/dist/auth/session-kernel.d.ts +4 -0
  54. package/dist/auth/session-kernel.d.ts.map +1 -0
  55. package/dist/auth/session-kernel.js +21 -0
  56. package/dist/graph/action.d.ts +22 -0
  57. package/dist/graph/action.d.ts.map +1 -0
  58. package/dist/graph/action.js +85 -0
  59. package/dist/graph/async.d.ts +11 -0
  60. package/dist/graph/async.d.ts.map +1 -0
  61. package/dist/graph/async.js +60 -0
  62. package/dist/graph/call/call.hook.d.ts +21 -0
  63. package/dist/graph/call/call.hook.d.ts.map +1 -0
  64. package/dist/graph/call/call.hook.js +23 -0
  65. package/dist/graph/call/stream.hook.d.ts +28 -0
  66. package/dist/graph/call/stream.hook.d.ts.map +1 -0
  67. package/dist/graph/call/stream.hook.js +137 -0
  68. package/dist/graph/graph.context.d.ts +4 -0
  69. package/dist/graph/graph.context.d.ts.map +1 -0
  70. package/dist/graph/graph.context.js +10 -0
  71. package/dist/graph/guards.error.d.ts +9 -0
  72. package/dist/graph/guards.error.d.ts.map +1 -0
  73. package/dist/graph/guards.error.js +7 -0
  74. package/dist/graph/index.d.ts +20 -0
  75. package/dist/graph/index.d.ts.map +1 -0
  76. package/dist/graph/index.js +9 -0
  77. package/dist/graph/memory/base.d.ts +21 -0
  78. package/dist/graph/memory/base.d.ts.map +1 -0
  79. package/dist/graph/memory/base.js +33 -0
  80. package/dist/graph/memory/read/envelope.d.ts +71 -0
  81. package/dist/graph/memory/read/envelope.d.ts.map +1 -0
  82. package/dist/graph/memory/read/envelope.js +29 -0
  83. package/dist/graph/memory/read/idle.d.ts +18 -0
  84. package/dist/graph/memory/read/idle.d.ts.map +1 -0
  85. package/dist/graph/memory/read/idle.js +16 -0
  86. package/dist/graph/memory/read/read.d.ts +9 -0
  87. package/dist/graph/memory/read/read.d.ts.map +1 -0
  88. package/dist/graph/memory/read/read.js +87 -0
  89. package/dist/graph/memory/scope.d.ts +7 -0
  90. package/dist/graph/memory/scope.d.ts.map +1 -0
  91. package/dist/graph/memory/scope.js +25 -0
  92. package/dist/graph/mutate/move.hook.d.ts +7 -0
  93. package/dist/graph/mutate/move.hook.d.ts.map +1 -0
  94. package/dist/graph/mutate/move.hook.js +49 -0
  95. package/dist/graph/mutate/mutate.hook.d.ts +17 -0
  96. package/dist/graph/mutate/mutate.hook.d.ts.map +1 -0
  97. package/dist/graph/mutate/mutate.hook.js +45 -0
  98. package/dist/graph/query/children.hook.d.ts +29 -0
  99. package/dist/graph/query/children.hook.d.ts.map +1 -0
  100. package/dist/graph/query/children.hook.js +64 -0
  101. package/dist/graph/query/graph.hook.d.ts +9 -0
  102. package/dist/graph/query/graph.hook.d.ts.map +1 -0
  103. package/dist/graph/query/graph.hook.js +5 -0
  104. package/dist/graph/query/neighbors.hook.d.ts +7 -0
  105. package/dist/graph/query/neighbors.hook.d.ts.map +1 -0
  106. package/dist/graph/query/neighbors.hook.js +79 -0
  107. package/dist/graph/query/node.hook.d.ts +14 -0
  108. package/dist/graph/query/node.hook.d.ts.map +1 -0
  109. package/dist/graph/query/node.hook.js +43 -0
  110. package/dist/graph/query/query.hook.d.ts +14 -0
  111. package/dist/graph/query/query.hook.d.ts.map +1 -0
  112. package/dist/graph/query/query.hook.js +23 -0
  113. package/dist/index.d.ts +43 -0
  114. package/dist/index.d.ts.map +1 -0
  115. package/dist/index.js +34 -0
  116. package/dist/intents/envelope.d.ts +33 -0
  117. package/dist/intents/envelope.d.ts.map +1 -0
  118. package/dist/intents/envelope.js +19 -0
  119. package/dist/intents/index.d.ts +6 -0
  120. package/dist/intents/index.d.ts.map +1 -0
  121. package/dist/intents/index.js +3 -0
  122. package/dist/intents/listen-intent.hook.d.ts +16 -0
  123. package/dist/intents/listen-intent.hook.d.ts.map +1 -0
  124. package/dist/intents/listen-intent.hook.js +75 -0
  125. package/dist/intents/reply.d.ts +9 -0
  126. package/dist/intents/reply.d.ts.map +1 -0
  127. package/dist/intents/reply.js +14 -0
  128. package/dist/intents/send-intent.hook.d.ts +12 -0
  129. package/dist/intents/send-intent.hook.d.ts.map +1 -0
  130. package/dist/intents/send-intent.hook.js +86 -0
  131. package/dist/navigation/adapters/adapter.d.ts +15 -0
  132. package/dist/navigation/adapters/adapter.d.ts.map +1 -0
  133. package/dist/navigation/adapters/adapter.js +1 -0
  134. package/dist/navigation/adapters/memory.adapter.d.ts +9 -0
  135. package/dist/navigation/adapters/memory.adapter.d.ts.map +1 -0
  136. package/dist/navigation/adapters/memory.adapter.js +15 -0
  137. package/dist/navigation/adapters/url.adapter.d.ts +6 -0
  138. package/dist/navigation/adapters/url.adapter.d.ts.map +1 -0
  139. package/dist/navigation/adapters/url.adapter.js +59 -0
  140. package/dist/navigation/history.d.ts +63 -0
  141. package/dist/navigation/history.d.ts.map +1 -0
  142. package/dist/navigation/history.js +144 -0
  143. package/dist/navigation/hooks/ancestors.hook.d.ts +13 -0
  144. package/dist/navigation/hooks/ancestors.hook.d.ts.map +1 -0
  145. package/dist/navigation/hooks/ancestors.hook.js +56 -0
  146. package/dist/navigation/hooks/location.hook.d.ts +4 -0
  147. package/dist/navigation/hooks/location.hook.d.ts.map +1 -0
  148. package/dist/navigation/hooks/location.hook.js +17 -0
  149. package/dist/navigation/hooks/navigate.hook.d.ts +12 -0
  150. package/dist/navigation/hooks/navigate.hook.d.ts.map +1 -0
  151. package/dist/navigation/hooks/navigate.hook.js +33 -0
  152. package/dist/navigation/hooks/scope.hook.d.ts +12 -0
  153. package/dist/navigation/hooks/scope.hook.d.ts.map +1 -0
  154. package/dist/navigation/hooks/scope.hook.js +21 -0
  155. package/dist/navigation/index.d.ts +16 -0
  156. package/dist/navigation/index.d.ts.map +1 -0
  157. package/dist/navigation/index.js +10 -0
  158. package/dist/navigation/link.d.ts +11 -0
  159. package/dist/navigation/link.d.ts.map +1 -0
  160. package/dist/navigation/link.js +62 -0
  161. package/dist/navigation/location.d.ts +48 -0
  162. package/dist/navigation/location.d.ts.map +1 -0
  163. package/dist/navigation/location.js +88 -0
  164. package/dist/navigation/scope.context.d.ts +64 -0
  165. package/dist/navigation/scope.context.d.ts.map +1 -0
  166. package/dist/navigation/scope.context.js +178 -0
  167. package/dist/navigation/snapshot.d.ts +22 -0
  168. package/dist/navigation/snapshot.d.ts.map +1 -0
  169. package/dist/navigation/snapshot.js +65 -0
  170. package/dist/schema/bind-read.d.ts +37 -0
  171. package/dist/schema/bind-read.d.ts.map +1 -0
  172. package/dist/schema/bind-read.js +205 -0
  173. package/dist/schema/binding.d.ts +21 -0
  174. package/dist/schema/binding.d.ts.map +1 -0
  175. package/dist/schema/binding.js +55 -0
  176. package/dist/schema/domain.hook.d.ts +10 -0
  177. package/dist/schema/domain.hook.d.ts.map +1 -0
  178. package/dist/schema/domain.hook.js +7 -0
  179. package/dist/schema/function.hook.d.ts +9 -0
  180. package/dist/schema/function.hook.d.ts.map +1 -0
  181. package/dist/schema/function.hook.js +40 -0
  182. package/dist/schema/index.d.ts +10 -0
  183. package/dist/schema/index.d.ts.map +1 -0
  184. package/dist/schema/index.js +8 -0
  185. package/dist/schema/kernel.hook.d.ts +10 -0
  186. package/dist/schema/kernel.hook.d.ts.map +1 -0
  187. package/dist/schema/kernel.hook.js +8 -0
  188. package/dist/schema/method.hook.d.ts +14 -0
  189. package/dist/schema/method.hook.d.ts.map +1 -0
  190. package/dist/schema/method.hook.js +49 -0
  191. package/dist/schema/tag.d.ts +11 -0
  192. package/dist/schema/tag.d.ts.map +1 -0
  193. package/dist/schema/tag.js +11 -0
  194. package/dist/schema/types.d.ts +73 -0
  195. package/dist/schema/types.d.ts.map +1 -0
  196. package/dist/schema/types.js +1 -0
  197. package/dist/schema/write.d.ts +15 -0
  198. package/dist/schema/write.d.ts.map +1 -0
  199. package/dist/schema/write.js +45 -0
  200. package/dist/selection/index.d.ts +4 -0
  201. package/dist/selection/index.d.ts.map +1 -0
  202. package/dist/selection/index.js +4 -0
  203. package/dist/selection/live.d.ts +21 -0
  204. package/dist/selection/live.d.ts.map +1 -0
  205. package/dist/selection/live.js +116 -0
  206. package/dist/selection/scope.context.d.ts +26 -0
  207. package/dist/selection/scope.context.d.ts.map +1 -0
  208. package/dist/selection/scope.context.js +100 -0
  209. package/dist/selection/selection.hook.d.ts +4 -0
  210. package/dist/selection/selection.hook.d.ts.map +1 -0
  211. package/dist/selection/selection.hook.js +14 -0
  212. package/dist/session/boot/boot.d.ts +19 -0
  213. package/dist/session/boot/boot.d.ts.map +1 -0
  214. package/dist/session/boot/boot.js +45 -0
  215. package/dist/session/boot/ownership.d.ts +18 -0
  216. package/dist/session/boot/ownership.d.ts.map +1 -0
  217. package/dist/session/boot/ownership.js +39 -0
  218. package/dist/session/boot/status.d.ts +24 -0
  219. package/dist/session/boot/status.d.ts.map +1 -0
  220. package/dist/session/boot/status.js +11 -0
  221. package/dist/session/connection/connection.hook.d.ts +3 -0
  222. package/dist/session/connection/connection.hook.d.ts.map +1 -0
  223. package/dist/session/connection/connection.hook.js +12 -0
  224. package/dist/session/connection/connection.store.d.ts +24 -0
  225. package/dist/session/connection/connection.store.d.ts.map +1 -0
  226. package/dist/session/connection/connection.store.js +31 -0
  227. package/dist/session/index.d.ts +8 -0
  228. package/dist/session/index.d.ts.map +1 -0
  229. package/dist/session/index.js +6 -0
  230. package/dist/session/kernel-client.hook.d.ts +9 -0
  231. package/dist/session/kernel-client.hook.d.ts.map +1 -0
  232. package/dist/session/kernel-client.hook.js +5 -0
  233. package/dist/session/kernel-instance.hook.d.ts +9 -0
  234. package/dist/session/kernel-instance.hook.d.ts.map +1 -0
  235. package/dist/session/kernel-instance.hook.js +10 -0
  236. package/dist/session/provider.d.ts +31 -0
  237. package/dist/session/provider.d.ts.map +1 -0
  238. package/dist/session/provider.js +165 -0
  239. package/dist/session/session.context.d.ts +18 -0
  240. package/dist/session/session.context.d.ts.map +1 -0
  241. package/dist/session/session.context.js +8 -0
  242. package/dist/session/shell.hook.d.ts +4 -0
  243. package/dist/session/shell.hook.d.ts.map +1 -0
  244. package/dist/session/shell.hook.js +15 -0
  245. package/dist/window/adopt.d.ts +27 -0
  246. package/dist/window/adopt.d.ts.map +1 -0
  247. package/dist/window/adopt.js +27 -0
  248. package/dist/window/hooks/close-guard.hook.d.ts +6 -0
  249. package/dist/window/hooks/close-guard.hook.d.ts.map +1 -0
  250. package/dist/window/hooks/close-guard.hook.js +90 -0
  251. package/dist/window/hooks/target-node.hook.d.ts +7 -0
  252. package/dist/window/hooks/target-node.hook.d.ts.map +1 -0
  253. package/dist/window/hooks/target-node.hook.js +77 -0
  254. package/dist/window/hooks/views-for.hook.d.ts +8 -0
  255. package/dist/window/hooks/views-for.hook.d.ts.map +1 -0
  256. package/dist/window/hooks/views-for.hook.js +51 -0
  257. package/dist/window/hooks/window.hook.d.ts +4 -0
  258. package/dist/window/hooks/window.hook.d.ts.map +1 -0
  259. package/dist/window/hooks/window.hook.js +14 -0
  260. package/dist/window/hooks/windows.hook.d.ts +15 -0
  261. package/dist/window/hooks/windows.hook.d.ts.map +1 -0
  262. package/dist/window/hooks/windows.hook.js +25 -0
  263. package/dist/window/host.d.ts +31 -0
  264. package/dist/window/host.d.ts.map +1 -0
  265. package/dist/window/host.js +270 -0
  266. package/dist/window/hub.d.ts +41 -0
  267. package/dist/window/hub.d.ts.map +1 -0
  268. package/dist/window/hub.js +100 -0
  269. package/dist/window/index.d.ts +15 -0
  270. package/dist/window/index.d.ts.map +1 -0
  271. package/dist/window/index.js +10 -0
  272. package/dist/window/lifecycle/mount.d.ts +47 -0
  273. package/dist/window/lifecycle/mount.d.ts.map +1 -0
  274. package/dist/window/lifecycle/mount.js +92 -0
  275. package/dist/window/lifecycle/states.d.ts +33 -0
  276. package/dist/window/lifecycle/states.d.ts.map +1 -0
  277. package/dist/window/lifecycle/states.js +7 -0
  278. package/dist/window/placement.d.ts +33 -0
  279. package/dist/window/placement.d.ts.map +1 -0
  280. package/dist/window/placement.js +57 -0
  281. package/dist/window/stage.d.ts +3 -0
  282. package/dist/window/stage.d.ts.map +1 -0
  283. package/dist/window/stage.js +41 -0
  284. package/package.json +59 -0
  285. package/src/application/apps/app-host.tsx +113 -0
  286. package/src/application/apps/app-logo.tsx +56 -0
  287. package/src/application/apps/apps.hook.ts +81 -0
  288. package/src/application/apps/open-app.hook.ts +18 -0
  289. package/src/application/index.ts +22 -0
  290. package/src/application/roles.hook.ts +41 -0
  291. package/src/application/spaces/current-space.hook.ts +43 -0
  292. package/src/application/spaces/current-space.store.ts +110 -0
  293. package/src/application/spaces/space.hook.ts +41 -0
  294. package/src/application/spaces/spaces.hook.ts +113 -0
  295. package/src/application/user/user.hook.ts +27 -0
  296. package/src/application/user/user.store.ts +85 -0
  297. package/src/auth/act-as.context.tsx +124 -0
  298. package/src/auth/auth.hook.ts +36 -0
  299. package/src/auth/can.hook.ts +149 -0
  300. package/src/auth/index.ts +6 -0
  301. package/src/auth/self.hook.ts +14 -0
  302. package/src/auth/session-kernel.ts +24 -0
  303. package/src/graph/action.ts +127 -0
  304. package/src/graph/async.ts +87 -0
  305. package/src/graph/call/call.hook.ts +42 -0
  306. package/src/graph/call/stream.hook.ts +200 -0
  307. package/src/graph/graph.context.ts +14 -0
  308. package/src/graph/guards.error.ts +58 -0
  309. package/src/graph/index.ts +20 -0
  310. package/src/graph/memory/base.ts +50 -0
  311. package/src/graph/memory/read/envelope.ts +98 -0
  312. package/src/graph/memory/read/idle.ts +24 -0
  313. package/src/graph/memory/read/read.ts +103 -0
  314. package/src/graph/memory/scope.ts +29 -0
  315. package/src/graph/mutate/move.hook.ts +70 -0
  316. package/src/graph/mutate/mutate.hook.ts +69 -0
  317. package/src/graph/query/children.hook.ts +136 -0
  318. package/src/graph/query/graph.hook.ts +13 -0
  319. package/src/graph/query/neighbors.hook.ts +124 -0
  320. package/src/graph/query/node.hook.ts +83 -0
  321. package/src/graph/query/query.hook.ts +42 -0
  322. package/src/index.ts +202 -0
  323. package/src/intents/envelope.ts +52 -0
  324. package/src/intents/index.ts +6 -0
  325. package/src/intents/listen-intent.hook.ts +92 -0
  326. package/src/intents/reply.ts +24 -0
  327. package/src/intents/send-intent.hook.ts +111 -0
  328. package/src/navigation/adapters/adapter.ts +13 -0
  329. package/src/navigation/adapters/memory.adapter.ts +22 -0
  330. package/src/navigation/adapters/url.adapter.ts +61 -0
  331. package/src/navigation/history.ts +170 -0
  332. package/src/navigation/hooks/ancestors.hook.ts +71 -0
  333. package/src/navigation/hooks/location.hook.ts +21 -0
  334. package/src/navigation/hooks/navigate.hook.ts +43 -0
  335. package/src/navigation/hooks/scope.hook.ts +34 -0
  336. package/src/navigation/index.ts +17 -0
  337. package/src/navigation/link.tsx +110 -0
  338. package/src/navigation/location.ts +122 -0
  339. package/src/navigation/scope.context.tsx +267 -0
  340. package/src/navigation/snapshot.ts +79 -0
  341. package/src/schema/bind-read.ts +319 -0
  342. package/src/schema/binding.ts +91 -0
  343. package/src/schema/domain.hook.ts +16 -0
  344. package/src/schema/function.hook.ts +68 -0
  345. package/src/schema/index.ts +25 -0
  346. package/src/schema/kernel.hook.ts +17 -0
  347. package/src/schema/method.hook.ts +103 -0
  348. package/src/schema/tag.ts +19 -0
  349. package/src/schema/types.ts +150 -0
  350. package/src/schema/write.ts +80 -0
  351. package/src/selection/index.ts +5 -0
  352. package/src/selection/live.ts +142 -0
  353. package/src/selection/scope.context.tsx +151 -0
  354. package/src/selection/selection.hook.ts +18 -0
  355. package/src/session/boot/boot.ts +57 -0
  356. package/src/session/boot/ownership.ts +54 -0
  357. package/src/session/boot/status.ts +32 -0
  358. package/src/session/connection/connection.hook.ts +16 -0
  359. package/src/session/connection/connection.store.ts +58 -0
  360. package/src/session/index.ts +8 -0
  361. package/src/session/kernel-client.hook.ts +13 -0
  362. package/src/session/kernel-instance.hook.ts +22 -0
  363. package/src/session/provider.tsx +205 -0
  364. package/src/session/session.context.ts +27 -0
  365. package/src/session/shell.hook.ts +19 -0
  366. package/src/window/adopt.ts +57 -0
  367. package/src/window/hooks/close-guard.hook.ts +100 -0
  368. package/src/window/hooks/target-node.hook.ts +94 -0
  369. package/src/window/hooks/views-for.hook.ts +82 -0
  370. package/src/window/hooks/window.hook.ts +18 -0
  371. package/src/window/hooks/windows.hook.ts +40 -0
  372. package/src/window/host.tsx +347 -0
  373. package/src/window/hub.ts +156 -0
  374. package/src/window/index.ts +17 -0
  375. package/src/window/lifecycle/mount.ts +134 -0
  376. package/src/window/lifecycle/states.ts +23 -0
  377. package/src/window/placement.ts +84 -0
  378. package/src/window/stage.tsx +46 -0
@@ -0,0 +1,32 @@
1
+ /**
2
+ * BootStatus union + transitions (API §2). The provider's boot state machine:
3
+ * `connecting → ready` (with the resolved runtime) or `connecting → error`. The
4
+ * reducer is the single transition table children render behind.
5
+ */
6
+ import type { ShellRuntime } from '../session.context'
7
+
8
+ export type BootStatus = 'connecting' | 'ready' | 'error'
9
+
10
+ export interface BootState {
11
+ readonly status: BootStatus
12
+ readonly error: unknown
13
+ readonly runtime: ShellRuntime | null
14
+ }
15
+
16
+ export type BootAction =
17
+ | { readonly type: 'connecting' }
18
+ | { readonly type: 'ready'; readonly runtime: ShellRuntime }
19
+ | { readonly type: 'error'; readonly error: unknown }
20
+
21
+ export const INITIAL_BOOT: BootState = { status: 'connecting', error: undefined, runtime: null }
22
+
23
+ export function bootReducer(state: BootState, action: BootAction): BootState {
24
+ switch (action.type) {
25
+ case 'connecting':
26
+ return state.status === 'connecting' && state.runtime === null ? state : INITIAL_BOOT
27
+ case 'ready':
28
+ return { status: 'ready', error: undefined, runtime: action.runtime }
29
+ case 'error':
30
+ return { status: 'error', error: action.error, runtime: null }
31
+ }
32
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * useConnection (API §3) — transport status + memory journal position + retry.
3
+ * Reads the provider-owned connection store for the subtree's Shell via
4
+ * `useSyncExternalStore` (rule 3). Throws outside a `<ShellProvider>`.
5
+ */
6
+ import { useSyncExternalStore } from 'react'
7
+
8
+ import type { ConnectionValue } from './connection.store'
9
+
10
+ import { useShell } from '../shell.hook'
11
+ import { connectionStore } from './connection.store'
12
+
13
+ export function useConnection(): ConnectionValue {
14
+ const store = connectionStore(useShell())
15
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)
16
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The connection store (API §3, rule 3). ONE per Shell — the provider owns it and
3
+ * registers its `retry`; `useConnection` reads it via `useSyncExternalStore` so
4
+ * connection churn re-renders only its subscribers. `status`/`seq` are a pinned
5
+ * stub ('ready'/0) until the transport-status + result-`seq` feed lands (G6).
6
+ */
7
+ import type { Shell } from '@astrale-os/shell'
8
+
9
+ export type ConnectionStatus = 'connecting' | 'ready' | 'reconnecting' | 'error'
10
+
11
+ /** Transport + memory-journal status for the subtree (API §3). */
12
+ export interface ConnectionValue {
13
+ readonly status: ConnectionStatus
14
+ readonly error: unknown
15
+ readonly seq: number
16
+ retry(): void
17
+ }
18
+
19
+ export interface ConnectionStore {
20
+ subscribe(cb: () => void): () => void
21
+ getSnapshot(): ConnectionValue
22
+ /** The provider registers its boot-retry here; a change re-stamps the snapshot. */
23
+ setRetry(retry: () => void): void
24
+ }
25
+
26
+ const NOOP_RETRY = (): void => {}
27
+
28
+ function createConnectionStore(): ConnectionStore {
29
+ const listeners = new Set<() => void>()
30
+ let retry: () => void = NOOP_RETRY
31
+ let snapshot: ConnectionValue = { status: 'ready', error: undefined, seq: 0, retry }
32
+
33
+ return {
34
+ subscribe(cb) {
35
+ listeners.add(cb)
36
+ return () => listeners.delete(cb)
37
+ },
38
+ getSnapshot: () => snapshot,
39
+ setRetry(next) {
40
+ if (next === retry) return
41
+ retry = next
42
+ snapshot = { status: snapshot.status, error: snapshot.error, seq: snapshot.seq, retry }
43
+ for (const l of listeners) l()
44
+ },
45
+ }
46
+ }
47
+
48
+ const stores = new WeakMap<Shell, ConnectionStore>()
49
+
50
+ /** The one connection store for a Shell (created on first touch). */
51
+ export function connectionStore(shell: Shell): ConnectionStore {
52
+ let s = stores.get(shell)
53
+ if (s === undefined) {
54
+ s = createConnectionStore()
55
+ stores.set(shell, s)
56
+ }
57
+ return s
58
+ }
@@ -0,0 +1,8 @@
1
+ // session — API §2 (provider) + §3 (session hooks). See SHELL_REACT_API.md.
2
+ export { ShellProvider } from './provider'
3
+ export type { ShellError, ShellProviderProps } from './provider'
4
+ export { useShell } from './shell.hook'
5
+ export { useKernelClient } from './kernel-client.hook'
6
+ export { useKernelInstance } from './kernel-instance.hook'
7
+ export type { KernelInstance } from './kernel-instance.hook'
8
+ export { useConnection } from './connection/connection.hook'
@@ -0,0 +1,13 @@
1
+ /**
2
+ * useKernelClient (API §3) — `shell.kernel`, the raw bound kernel view, untouched
3
+ * (the untyped floor). The TYPED handles `useKernel()`/`useDomain(schema)` live in
4
+ * the schema module (§6).
5
+ */
6
+ import type { KernelClient } from '@astrale-os/shell'
7
+
8
+ import { useShell } from './shell.hook'
9
+
10
+ /** `shell.kernel` — the raw bound kernel view, untouched (untyped floor). */
11
+ export function useKernelClient(): KernelClient {
12
+ return useShell().kernel
13
+ }
@@ -0,0 +1,22 @@
1
+ import { useMemo } from 'react'
2
+
3
+ import { useKernelClient } from './kernel-client.hook'
4
+
5
+ export interface KernelInstance {
6
+ /** Kernel envelope endpoint this shell is bound to. */
7
+ readonly url: string
8
+ /** Current kernel credential. Never cache; read per request. */
9
+ token(): string
10
+ }
11
+
12
+ /** Low-level access for direct kernel-envelope calls and token exchange code. */
13
+ export function useKernelInstance(): KernelInstance {
14
+ const kernel = useKernelClient()
15
+ return useMemo<KernelInstance>(
16
+ () => ({
17
+ url: kernel.url,
18
+ token: () => kernel.authToken(),
19
+ }),
20
+ [kernel],
21
+ )
22
+ }
@@ -0,0 +1,205 @@
1
+ import type { MemoryEvent } from '@astrale-os/kernel-client/store'
2
+ import type { Shell, ShellConfig } from '@astrale-os/shell'
3
+ import type { ReactElement, ReactNode } from 'react'
4
+
5
+ import { createShell } from '@astrale-os/shell'
6
+ /**
7
+ * ShellProvider — the composition root (API §2, SPEC §5): owns the shell boot
8
+ * state machine (a config OR a prebuilt Shell; `createShell → init → whoami →
9
+ * ready`), seats `ShellContext` and `GraphContext`, and registers its boot-retry
10
+ * into the per-shell connection store. One live shell per mounted provider,
11
+ * StrictMode/HMR-safe (deferred teardown), keyed on config identity. Children
12
+ * render only at `ready`, so useShell/useSelf are synchronous below.
13
+ */
14
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
15
+
16
+ import type { GraphMemory } from '../graph/graph.context'
17
+ import type { BootRef } from './boot/ownership'
18
+ import type { SelfNode } from './session.context'
19
+
20
+ import { primeUser } from '../application/user/user.store'
21
+ import { GraphContext } from '../graph/graph.context'
22
+ import { baseMemoryFor, setMemoryErrorSink } from '../graph/memory/base'
23
+ import { configIdentity, isShellConfig, resolveSelf } from './boot/boot'
24
+ import { cancelTeardown, disposeShell, scheduleTeardown } from './boot/ownership'
25
+ import { bootReducer, INITIAL_BOOT } from './boot/status'
26
+ import { connectionStore } from './connection/connection.store'
27
+ import { ShellContext } from './session.context'
28
+
29
+ /** Global error channel payload (API §2). */
30
+ export type ShellError =
31
+ | { kind: 'refresh'; shape: unknown; error: unknown }
32
+ | { kind: 'mutation'; mutationId: string; error: unknown }
33
+ | { kind: 'connection'; error: unknown }
34
+
35
+ export interface ShellProviderProps {
36
+ /**
37
+ * Shell config — or a prebuilt Shell (tests, storybook). Keyed by identity: a
38
+ * config change disposes and reboots.
39
+ */
40
+ config: ShellConfig | Shell
41
+ /** Rendered until status === 'ready'. Default: null. */
42
+ fallback?: ReactNode
43
+ /** Rendered when boot fails; `retry` re-runs init. Default: rethrow to the nearest boundary. */
44
+ errorFallback?: (error: unknown, retry: () => void) => ReactNode
45
+ /** Global error channel: warm-read refresh failures, rolled-back mutations, watch drops. */
46
+ onError?: (error: ShellError) => void
47
+ children: ReactNode
48
+ }
49
+
50
+ /**
51
+ * Map a memory event to the documented `ShellError` union (API §2). `refresh`
52
+ * (warm-read revalidation failure) and `mutation` (rolled-back write) are the
53
+ * two the channel promises; `divergence`/`invariant` are Tier-0/dev tripwires
54
+ * outside the union and are not bridged here.
55
+ */
56
+ function toShellError(event: MemoryEvent): ShellError | null {
57
+ switch (event.kind) {
58
+ case 'refresh':
59
+ return { kind: 'refresh', shape: event.shape, error: event.error }
60
+ case 'mutation':
61
+ return { kind: 'mutation', mutationId: event.mutationId, error: event.error }
62
+ default:
63
+ return null
64
+ }
65
+ }
66
+
67
+ export function ShellProvider(props: ShellProviderProps): ReactElement | null {
68
+ const { config, fallback, errorFallback, onError, children } = props
69
+
70
+ const [state, dispatch] = useReducer(bootReducer, INITIAL_BOOT)
71
+ const [attempt, setAttempt] = useState(0)
72
+
73
+ const bootRef = useRef<BootRef>({ key: null, shell: null, generation: 0 })
74
+ const configRef = useRef(config)
75
+ configRef.current = config
76
+ const onErrorRef = useRef(onError)
77
+ onErrorRef.current = onError
78
+
79
+ const configKey = configIdentity(config)
80
+
81
+ const startBoot = useCallback((ref: BootRef) => {
82
+ const generation = (ref.generation += 1)
83
+ dispatch({ type: 'connecting' })
84
+
85
+ let shell: Shell
86
+ const current = configRef.current
87
+ try {
88
+ shell = isShellConfig(current) ? createShell(current) : current
89
+ } catch (err) {
90
+ dispatch({ type: 'error', error: err })
91
+ onErrorRef.current?.({ kind: 'connection', error: err })
92
+ return
93
+ }
94
+ ref.shell = shell
95
+
96
+ void (async () => {
97
+ try {
98
+ await shell.init()
99
+ if (generation !== ref.generation) return
100
+ const self: SelfNode | null = await resolveSelf(shell)
101
+ if (generation !== ref.generation) return
102
+ // Pre-resolve the OS-plane user binding (API §2/§12.2) — NON-FATAL: a
103
+ // missing OS plane marks it unavailable without failing boot (whoami above
104
+ // stays the fatal gate). Seeds the user store so `useUser` is synchronous.
105
+ await primeUser(shell)
106
+ if (generation !== ref.generation) return
107
+ dispatch({ type: 'ready', runtime: { shell, self } })
108
+ } catch (err) {
109
+ if (generation !== ref.generation) return
110
+ dispatch({ type: 'error', error: err })
111
+ onErrorRef.current?.({ kind: 'connection', error: err })
112
+ }
113
+ })()
114
+ }, [])
115
+
116
+ useEffect(() => {
117
+ const ref = bootRef.current
118
+ // The shell this provider will run with once the effect settles: a prebuilt
119
+ // Shell is the config object itself; a config-built one is whatever this
120
+ // instance already booted (adopted on a StrictMode/HMR re-setup). Cancel any
121
+ // pending deferred teardown for it before deciding — this also cancels a
122
+ // teardown another provider scheduled for the SAME prebuilt Shell.
123
+ const current = configRef.current
124
+ const adopting = isShellConfig(current) ? ref.shell : current
125
+ if (adopting !== null) cancelTeardown(adopting)
126
+
127
+ if (ref.key !== configKey) {
128
+ // First boot, config change, or a retry that cleared the key: reboot fresh.
129
+ if (ref.shell !== null) {
130
+ cancelTeardown(ref.shell)
131
+ disposeShell(ref.shell)
132
+ }
133
+ ref.key = configKey
134
+ ref.shell = null
135
+ startBoot(ref)
136
+ }
137
+ // Same key already booting/booted → adopt (StrictMode's second setup).
138
+
139
+ return () => {
140
+ // Defer teardown one macrotask; a synchronous re-setup (StrictMode/HMR or a
141
+ // different provider adopting the same prebuilt Shell) cancels it first and
142
+ // keeps the same live shell.
143
+ if (ref.shell !== null) scheduleTeardown(ref.shell, ref)
144
+ else ref.key = null
145
+ }
146
+ }, [configKey, attempt, startBoot])
147
+
148
+ const retry = useCallback(() => {
149
+ const ref = bootRef.current
150
+ ref.generation += 1
151
+ if (ref.shell !== null) {
152
+ disposeShell(ref.shell)
153
+ ref.shell = null
154
+ }
155
+ ref.key = null
156
+ dispatch({ type: 'connecting' })
157
+ setAttempt((a) => a + 1)
158
+ }, [])
159
+
160
+ // The base (default-scope) memory for the booted Shell — the SAME instance
161
+ // the graph/schema/nav hooks read through `graph/memory/scope.ts`. Seated in
162
+ // `GraphContext` so selection's liveness has a source; null until ready.
163
+ const shell = state.runtime?.shell ?? null
164
+ const memory = useMemo<GraphMemory | null>(
165
+ () => (shell === null ? null : baseMemoryFor(shell)),
166
+ [shell],
167
+ )
168
+
169
+ // Bridge the memory's event channel onto the provider `onError` (API §2/§8):
170
+ // warm-refresh failures and rolled-back mutations surface here rather than being
171
+ // swallowed. Registered while the booted shell is live; cleared on teardown.
172
+ useEffect(() => {
173
+ if (shell === null) return
174
+ setMemoryErrorSink(shell, (event) => {
175
+ const mapped = toShellError(event)
176
+ if (mapped !== null) onErrorRef.current?.(mapped)
177
+ })
178
+ return () => setMemoryErrorSink(shell, null)
179
+ }, [shell])
180
+
181
+ // Register this provider's boot-retry into the shell's connection store (rule 3),
182
+ // so `useConnection().retry` re-runs boot. Status/seq stay a stub ('ready'/0)
183
+ // until the transport-status + result-`seq` feed lands, with no surface change.
184
+ useEffect(() => {
185
+ if (shell === null) return
186
+ connectionStore(shell).setRetry(retry)
187
+ }, [shell, retry])
188
+
189
+ if (state.status === 'error') {
190
+ // Wrapped in a fragment so the component's declared return type stays
191
+ // `ReactElement | null` — valid JSX under @types/react 18 AND 19 (the react
192
+ // peer range is >=18), even though `errorFallback`/`fallback` yield ReactNode.
193
+ if (errorFallback !== undefined) return <>{errorFallback(state.error, retry)}</>
194
+ throw state.error
195
+ }
196
+ if (state.status !== 'ready' || state.runtime === null) {
197
+ return fallback !== undefined ? <>{fallback}</> : null
198
+ }
199
+
200
+ return (
201
+ <ShellContext.Provider value={state.runtime}>
202
+ <GraphContext.Provider value={memory}>{children}</GraphContext.Provider>
203
+ </ShellContext.Provider>
204
+ )
205
+ }
@@ -0,0 +1,27 @@
1
+ import type { KernelClient, Shell } from '@astrale-os/shell'
2
+
3
+ /**
4
+ * ShellContext — the shell session seat. Holds ONLY the stable `Shell` for a
5
+ * provider subtree (plus the whoami resolved once at boot). Connection state is
6
+ * NOT here: it is a provider-owned store read via `useConnection` (rule 3).
7
+ */
8
+ import { createContext } from 'react'
9
+
10
+ /**
11
+ * whoami's node type, derived from the auth surface so shell-react needs no
12
+ * direct `@astrale-os/kernel-core` dependency (`Node` is not yet re-exported by
13
+ * kernel-client — API §14 G8). Swap for the blessed re-export when it lands.
14
+ */
15
+ export type SelfNode = Awaited<ReturnType<KernelClient['auth']['whoami']>>
16
+
17
+ /** The live shell for a provider subtree (and, later, an `<ActAs>` subtree). */
18
+ export interface ShellRuntime {
19
+ readonly shell: Shell
20
+ /**
21
+ * whoami resolved at boot; the auth module (`useSelf`) reads it. Null when the
22
+ * bound kernel exposes no auth surface (a bare prebuilt Shell in tests).
23
+ */
24
+ readonly self: SelfNode | null
25
+ }
26
+ export const ShellContext = createContext<ShellRuntime | null>(null)
27
+ ShellContext.displayName = 'AstraleShellContext'
@@ -0,0 +1,19 @@
1
+ import type { Shell } from '@astrale-os/shell'
2
+
3
+ /**
4
+ * useShell (API §3) — the live shell for this subtree, read from ShellContext.
5
+ * Throws outside a `<ShellProvider>`. The imperative floor every other session
6
+ * hook builds on.
7
+ */
8
+ import { useContext } from 'react'
9
+
10
+ import { ShellContext } from './session.context'
11
+
12
+ /** The live shell for this subtree. Throws outside a `<ShellProvider>`. */
13
+ export function useShell(): Shell {
14
+ const runtime = useContext(ShellContext)
15
+ if (runtime === null) {
16
+ throw new Error('useShell must be used within a <ShellProvider>')
17
+ }
18
+ return runtime.shell
19
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Stash/adopt across re-key / re-parent (API §10). A live window handed off across
3
+ * an unmount so a remount can ADOPT it instead of closing + reloading it. Mirrors
4
+ * ShellProvider's deferred-teardown pattern: the unmounting host stashes the
5
+ * record, and either the remount takes it (one macrotask) or it disposes.
6
+ */
7
+ import type { Window } from '@astrale-os/shell'
8
+
9
+ import type { WindowState } from './lifecycle/states'
10
+ import type { StageSlot } from './placement'
11
+
12
+ export interface AdoptRecord {
13
+ readonly windowId: string
14
+ readonly window: Window
15
+ readonly slot: StageSlot
16
+ readonly state: WindowState
17
+ readonly targetId: string | undefined
18
+ /** Close the window, release its slot, drop it from the registry (fired if unadopted). */
19
+ dispose(): void
20
+ }
21
+
22
+ export interface AdoptStash {
23
+ /** Stash an unmounting host's live window; disposes after one macrotask if unadopted. */
24
+ stash(key: string, record: AdoptRecord): void
25
+ /** Reclaim a stashed window for a remount of the same source, cancelling its dispose. */
26
+ take(key: string): AdoptRecord | null
27
+ }
28
+
29
+ /** The pending-adoption registry — one per stage hub. */
30
+ export function createAdoptStash(): AdoptStash {
31
+ const pendingAdopt = new Map<
32
+ string,
33
+ { record: AdoptRecord; timer: ReturnType<typeof setTimeout> }
34
+ >()
35
+ return {
36
+ stash(key, record) {
37
+ // Rapid same-key churn: an unclaimed prior stash disposes now.
38
+ const prior = pendingAdopt.get(key)
39
+ if (prior !== undefined) {
40
+ clearTimeout(prior.timer)
41
+ prior.record.dispose()
42
+ }
43
+ const timer = setTimeout(() => {
44
+ pendingAdopt.delete(key)
45
+ record.dispose()
46
+ }, 0)
47
+ pendingAdopt.set(key, { record, timer })
48
+ },
49
+ take(key) {
50
+ const entry = pendingAdopt.get(key)
51
+ if (entry === undefined) return null
52
+ clearTimeout(entry.timer)
53
+ pendingAdopt.delete(key)
54
+ return entry.record
55
+ },
56
+ }
57
+ }
@@ -0,0 +1,100 @@
1
+ import type { Shell } from '@astrale-os/shell'
2
+
3
+ /**
4
+ * useCloseGuard (API §10) — child-side willClose negotiation. The parent sends a
5
+ * `willClose` intent before closing; the guard answers `closeAck` (allow) or
6
+ * `closeRefuse` (veto, with reason). Guards stack per shell and the last-mounted
7
+ * one wins; a single shared subscription owns the reply so a `willClose` is
8
+ * answered exactly once regardless of how many guards are mounted.
9
+ */
10
+ import { useEffect, useRef } from 'react'
11
+
12
+ import { useShell } from '../../session/shell.hook'
13
+
14
+ export type CloseVerdict = 'ack' | 'refuse' | { readonly refuse: string }
15
+ export type CloseGuard = () => CloseVerdict | Promise<CloseVerdict>
16
+
17
+ interface CloseGuardHub {
18
+ push(guard: CloseGuard): () => void
19
+ }
20
+
21
+ function sendAck(parent: NonNullable<Shell['parent']>): void {
22
+ parent.send({
23
+ type: 'intent',
24
+ version: 1,
25
+ envelope: { name: 'closeAck', payload: {}, sender: { windowId: 'self' } },
26
+ })
27
+ }
28
+
29
+ function sendRefuse(parent: NonNullable<Shell['parent']>, reason: string): void {
30
+ parent.send({
31
+ type: 'intent',
32
+ version: 1,
33
+ envelope: { name: 'closeRefuse', payload: { reason }, sender: { windowId: 'self' } },
34
+ })
35
+ }
36
+
37
+ const HUBS = new WeakMap<Shell, CloseGuardHub>()
38
+
39
+ function getCloseGuardHub(shell: Shell): CloseGuardHub {
40
+ const existing = HUBS.get(shell)
41
+ if (existing !== undefined) return existing
42
+
43
+ const stack: CloseGuard[] = []
44
+ let off: (() => void) | null = null
45
+
46
+ function ensureSubscribed(): void {
47
+ if (off !== null) return
48
+ const parent = shell.parent
49
+ if (parent === null) return
50
+ off = parent.on('intent', (message) => {
51
+ if (message.envelope.name !== 'willClose') return
52
+ const guard = stack[stack.length - 1]
53
+ if (guard === undefined) return
54
+ void answer(parent, guard)
55
+ })
56
+ }
57
+
58
+ async function answer(parent: NonNullable<Shell['parent']>, guard: CloseGuard): Promise<void> {
59
+ let verdict: CloseVerdict
60
+ try {
61
+ verdict = await guard()
62
+ } catch {
63
+ // A throwing guard must not trap the user in the window — allow the close.
64
+ verdict = 'ack'
65
+ }
66
+ if (verdict === 'ack') sendAck(parent)
67
+ else if (verdict === 'refuse') sendRefuse(parent, '')
68
+ else sendRefuse(parent, verdict.refuse)
69
+ }
70
+
71
+ const hub: CloseGuardHub = {
72
+ push(guard) {
73
+ stack.push(guard)
74
+ ensureSubscribed()
75
+ return () => {
76
+ const i = stack.lastIndexOf(guard)
77
+ if (i >= 0) stack.splice(i, 1)
78
+ if (stack.length === 0 && off !== null) {
79
+ off()
80
+ off = null
81
+ }
82
+ }
83
+ },
84
+ }
85
+ HUBS.set(shell, hub)
86
+ return hub
87
+ }
88
+
89
+ export function useCloseGuard(guard: CloseGuard): void {
90
+ const shell = useShell()
91
+ const guardRef = useRef(guard)
92
+ guardRef.current = guard
93
+
94
+ useEffect(() => {
95
+ const hub = getCloseGuardHub(shell)
96
+ // Push a stable wrapper so a changed guard closure never re-registers
97
+ // (latest-handler semantics; unmount pops this exact entry).
98
+ return hub.push(() => guardRef.current())
99
+ }, [shell])
100
+ }
@@ -0,0 +1,94 @@
1
+ import type { KernelClient } from '@astrale-os/shell'
2
+
3
+ /**
4
+ * useTargetNode (API §10) — inside a sandboxed view, the node it was opened on.
5
+ * The id starts from `shell.targetNodeId` (handshake) and a host `setTarget`
6
+ * intent retargets it; the id resolves to a live `Node` through the shell kernel.
7
+ *
8
+ * Honest subset (flagged): the read is a one-shot `kernel.get` re-run on id
9
+ * change, not a memory watch, so `live` is false. Once composed with `useNode(id)`
10
+ * it becomes real stale-while-revalidate liveness with no consumer change.
11
+ */
12
+ import { useCallback, useEffect, useRef, useState } from 'react'
13
+
14
+ import type { ReadState } from '../../graph/memory/read/envelope'
15
+ import type { Node } from '../lifecycle/mount'
16
+
17
+ import { idleReadState } from '../../graph/memory/read/idle'
18
+ import { useShell } from '../../session/shell.hook'
19
+
20
+ export interface NodeResult extends ReadState {
21
+ readonly node: Node | null
22
+ }
23
+
24
+ /** Address a bare node id by graph id (`@<id>`); pass paths/anchors through. */
25
+ function toRef(id: string): string {
26
+ return id.startsWith('/') || id.startsWith('@') ? id : `@${id}`
27
+ }
28
+
29
+ interface ReadSnapshot {
30
+ readonly node: Node | null
31
+ readonly pending: boolean
32
+ readonly error: unknown
33
+ }
34
+
35
+ const IDLE_READ: ReadSnapshot = { node: null, pending: false, error: undefined }
36
+
37
+ export function useTargetNode(): NodeResult {
38
+ const shell = useShell()
39
+ const [id, setId] = useState<string | null>(() => shell.targetNodeId ?? null)
40
+
41
+ useEffect(() => {
42
+ const parent = shell.parent
43
+ if (parent === null) return
44
+ return parent.on('intent', (message) => {
45
+ if (message.envelope.name !== 'setTarget') return
46
+ const nodeId = (message.envelope.payload as { nodeId?: string }).nodeId
47
+ if (typeof nodeId === 'string') setId(nodeId)
48
+ })
49
+ }, [shell])
50
+
51
+ return useNodeRead(shell.kernel, id)
52
+ }
53
+
54
+ /** One-shot get + refetch, in the read envelope. The graph-module seam. */
55
+ function useNodeRead(kernel: KernelClient, id: string | null): NodeResult {
56
+ const [snap, setSnap] = useState<ReadSnapshot>(IDLE_READ)
57
+ const aliveRef = useRef(true)
58
+ useEffect(() => {
59
+ aliveRef.current = true
60
+ return () => {
61
+ aliveRef.current = false
62
+ }
63
+ }, [])
64
+
65
+ const load = useCallback(async (): Promise<void> => {
66
+ if (id === null) {
67
+ if (aliveRef.current) setSnap(IDLE_READ)
68
+ return
69
+ }
70
+ if (aliveRef.current) setSnap((s) => ({ node: s.node, pending: true, error: undefined }))
71
+ try {
72
+ const node = await kernel.get(toRef(id))
73
+ if (aliveRef.current) setSnap({ node: node ?? null, pending: false, error: undefined })
74
+ } catch (error) {
75
+ if (aliveRef.current) setSnap((s) => ({ node: s.node, pending: false, error }))
76
+ }
77
+ }, [kernel, id])
78
+
79
+ useEffect(() => {
80
+ void load()
81
+ }, [load])
82
+
83
+ if (id === null) {
84
+ return { ...idleReadState(), node: null }
85
+ }
86
+ return {
87
+ node: snap.node,
88
+ pending: snap.pending,
89
+ error: snap.error,
90
+ live: false,
91
+ seq: 0,
92
+ refetch: load,
93
+ }
94
+ }