@gavin-hu/my-pi-agent 0.18.0 → 0.20.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 (324) hide show
  1. package/README.md +66 -52
  2. package/README.zh-CN.md +56 -46
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +507 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
  6. package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
  7. package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
  8. package/docs/extensions/file-browse.md +561 -0
  9. package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
  10. package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
  11. package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
  12. package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
  14. package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +298 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
  19. package/docs/extensions/team.md +254 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
  25. package/extensions/board/commands.ts +105 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +170 -0
  28. package/extensions/board/index.ts +40 -0
  29. package/extensions/board/input.ts +54 -0
  30. package/extensions/board/join.ts +42 -0
  31. package/extensions/board/page.ts +388 -0
  32. package/extensions/board/result.ts +108 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +90 -0
  35. package/extensions/board/seat.ts +28 -0
  36. package/extensions/board/snapshot.ts +42 -0
  37. package/extensions/board/tools.ts +444 -0
  38. package/extensions/board/transport/router.ts +91 -0
  39. package/extensions/board/transport/server.ts +138 -0
  40. package/extensions/board/types.ts +121 -0
  41. package/extensions/board/view/client.ts +51 -0
  42. package/extensions/board/view/dialog.ts +43 -0
  43. package/extensions/board/view/keys.ts +125 -0
  44. package/extensions/board/view/live.ts +231 -0
  45. package/extensions/board/view/names.ts +91 -0
  46. package/extensions/board/view/page.ts +118 -0
  47. package/extensions/board/view/render.ts +519 -0
  48. package/extensions/board/view/styles.ts +228 -0
  49. package/extensions/board/view/team.ts +327 -0
  50. package/extensions/bytefly/footer/budget.ts +6 -5
  51. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  52. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  53. package/extensions/bytefly/footer/config.ts +19 -8
  54. package/extensions/bytefly/footer/price/price.ts +30 -8
  55. package/extensions/bytefly/footer/register.ts +5 -5
  56. package/extensions/bytefly/footer/render/format.ts +18 -0
  57. package/extensions/bytefly/footer/render/lines.ts +23 -7
  58. package/extensions/bytefly/footer/render/segments.ts +117 -31
  59. package/extensions/bytefly/footer/types.ts +29 -5
  60. package/extensions/bytefly/footer/unpriced.ts +42 -17
  61. package/extensions/bytefly/panel/commands.ts +67 -7
  62. package/extensions/bytefly/panel/filter.ts +68 -0
  63. package/extensions/bytefly/panel/format.ts +16 -15
  64. package/extensions/bytefly/panel/join.ts +56 -13
  65. package/extensions/bytefly/panel/prices.ts +43 -0
  66. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  67. package/extensions/bytefly/panel/settings.ts +75 -43
  68. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  69. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  70. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  71. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  72. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  73. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  74. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  75. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  76. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  77. package/extensions/bytefly/panel/tui.ts +306 -259
  78. package/extensions/bytefly/picker/discovery.ts +0 -2
  79. package/extensions/bytefly/picker/types.ts +0 -2
  80. package/extensions/bytefly/update/check.ts +39 -0
  81. package/extensions/bytefly/update/format.ts +15 -0
  82. package/extensions/bytefly/update/register.ts +73 -34
  83. package/extensions/bytefly/update/state.ts +6 -3
  84. package/extensions/check/commands.ts +2 -1
  85. package/extensions/context/tui.ts +1 -1
  86. package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
  87. package/extensions/file-browse/files/classify.ts +136 -0
  88. package/extensions/file-browse/files/files.ts +12 -0
  89. package/extensions/file-browse/files/index.ts +90 -0
  90. package/extensions/file-browse/files/list.ts +261 -0
  91. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  92. package/extensions/file-browse/files/tree.ts +101 -0
  93. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  94. package/extensions/file-browse/index.ts +4 -4
  95. package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
  96. package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
  97. package/extensions/file-browse/transport/server.ts +110 -0
  98. package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
  99. package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
  100. package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
  101. package/extensions/file-browse/view/emit.ts +51 -0
  102. package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
  103. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  104. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
  105. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
  106. package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
  107. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  108. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  109. package/extensions/file-read/schema.ts +2 -1
  110. package/extensions/job/runtime.ts +4 -4
  111. package/extensions/memory/store.ts +12 -5
  112. package/extensions/plan/blocks.ts +23 -2
  113. package/extensions/plan/commands.ts +21 -16
  114. package/extensions/plan/feedback.ts +185 -52
  115. package/extensions/plan/format.ts +10 -3
  116. package/extensions/plan/index.ts +23 -5
  117. package/extensions/plan/plan-files.ts +88 -0
  118. package/extensions/plan/plan-index.ts +43 -9
  119. package/extensions/plan/plan-text.ts +33 -23
  120. package/extensions/plan/plans.ts +27 -34
  121. package/extensions/plan/policy.ts +30 -17
  122. package/extensions/plan/review.ts +3 -27
  123. package/extensions/plan/runtime.ts +52 -11
  124. package/extensions/plan/sections.ts +112 -0
  125. package/extensions/plan/steps.ts +3 -22
  126. package/extensions/plan/todos.ts +51 -3
  127. package/extensions/plan/tools/exit.ts +20 -95
  128. package/extensions/plan/tools/todo-seed.ts +4 -2
  129. package/extensions/plan/tools/write.ts +12 -1
  130. package/extensions/plan/tui.ts +5 -34
  131. package/extensions/plan/types.ts +15 -8
  132. package/extensions/plan/web-blocks.ts +89 -0
  133. package/extensions/plan/web-chrome.ts +3 -5
  134. package/extensions/plan/web-client.ts +179 -330
  135. package/extensions/plan/web-collab.ts +3 -3
  136. package/extensions/plan/web-comments.ts +18 -8
  137. package/extensions/plan/web-composer.ts +80 -0
  138. package/extensions/plan/web-draft.ts +40 -0
  139. package/extensions/plan/web-keys.ts +134 -0
  140. package/extensions/plan/web-outline.ts +192 -0
  141. package/extensions/plan/web-page.ts +4 -8
  142. package/extensions/plan/web-plan.ts +150 -0
  143. package/extensions/plan/web-router.ts +3 -58
  144. package/extensions/plan/web-settle.ts +194 -0
  145. package/extensions/plan/web-share-session.ts +24 -30
  146. package/extensions/plan/web-streams.ts +14 -14
  147. package/extensions/plan/web-tokens.ts +2 -2
  148. package/extensions/plan/web.ts +66 -120
  149. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  150. package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
  151. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  152. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  153. package/extensions/rewind/index.ts +3 -3
  154. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  155. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  156. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  157. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  158. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  159. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  160. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  161. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  162. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  163. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  164. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  165. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  166. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  167. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  168. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  169. package/extensions/session-bus/commands.ts +17 -69
  170. package/extensions/session-bus/format.ts +22 -45
  171. package/extensions/session-bus/index.ts +13 -7
  172. package/extensions/session-bus/join-request.ts +40 -0
  173. package/extensions/session-bus/mail-watch.ts +134 -0
  174. package/extensions/session-bus/notice.ts +32 -4
  175. package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
  176. package/extensions/session-bus/protocol.ts +19 -49
  177. package/extensions/session-bus/resolve.ts +67 -0
  178. package/extensions/session-bus/result.ts +16 -51
  179. package/extensions/session-bus/rows.ts +11 -5
  180. package/extensions/session-bus/runtime.ts +220 -267
  181. package/extensions/session-bus/store.ts +55 -7
  182. package/extensions/session-bus/tools.ts +31 -66
  183. package/extensions/session-bus/tui.ts +5 -5
  184. package/extensions/session-bus/types.ts +23 -14
  185. package/extensions/subagent/agents.ts +9 -8
  186. package/extensions/subagent/commands.ts +2 -1
  187. package/extensions/subagent/invocation.ts +4 -2
  188. package/extensions/team/commands.ts +145 -0
  189. package/extensions/team/dispatch.ts +133 -0
  190. package/extensions/team/format.ts +119 -0
  191. package/extensions/team/index.ts +34 -0
  192. package/extensions/team/schema.ts +29 -0
  193. package/extensions/team/tools.ts +267 -0
  194. package/extensions/team/types.ts +65 -0
  195. package/extensions/todo/index.ts +4 -1
  196. package/extensions/todo/nudge.ts +8 -2
  197. package/extensions/web-access/fetch/page.ts +4 -1
  198. package/extensions/web-access/fetch/structured.ts +447 -0
  199. package/extensions/web-access/fetch/tool.ts +2 -1
  200. package/extensions/web-access/search/tool.ts +2 -1
  201. package/extensions/wechat/bridge.ts +106 -251
  202. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  203. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  204. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  205. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  206. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  207. package/extensions/wechat/dialog/channels.ts +136 -0
  208. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
  209. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
  210. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  211. package/extensions/wechat/index.ts +10 -10
  212. package/extensions/wechat/intake.ts +245 -0
  213. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  214. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  215. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
  216. package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
  217. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
  218. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  219. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  220. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  221. package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
  222. package/extensions/wechat/tools.ts +29 -12
  223. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  224. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
  225. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  226. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  227. package/extensions/wechat/transport/outbound.ts +142 -0
  228. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  229. package/extensions/wechat/types.ts +10 -1
  230. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  231. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  232. package/extensions/worktree/commands.ts +24 -17
  233. package/extensions/worktree/exit.ts +12 -6
  234. package/extensions/worktree/git.ts +22 -14
  235. package/extensions/worktree/guard.ts +18 -290
  236. package/extensions/worktree/handoff.ts +10 -2
  237. package/extensions/worktree/index.ts +2 -1
  238. package/extensions/worktree/lifecycle.ts +2 -0
  239. package/extensions/worktree/registry.ts +20 -11
  240. package/extensions/worktree/root-tools.ts +13 -3
  241. package/extensions/worktree/runtime.ts +8 -2
  242. package/extensions/worktree/screen.ts +15 -8
  243. package/extensions/worktree/shell.ts +303 -0
  244. package/extensions/worktree/state.ts +1 -1
  245. package/extensions/worktree/status.ts +42 -9
  246. package/extensions/worktree/tools.ts +19 -13
  247. package/lib/README.md +11 -8
  248. package/lib/board/card.ts +479 -0
  249. package/lib/board/links.ts +141 -0
  250. package/lib/board/page-pointer.ts +310 -0
  251. package/lib/board/round-text.ts +81 -0
  252. package/lib/board/round.ts +94 -0
  253. package/lib/board/rules.ts +437 -0
  254. package/lib/board/session.ts +105 -0
  255. package/lib/board/store.ts +497 -0
  256. package/lib/board/view.ts +234 -0
  257. package/lib/errors.ts +12 -4
  258. package/lib/format.ts +35 -0
  259. package/lib/http/guard.ts +112 -0
  260. package/lib/http/index.ts +16 -5
  261. package/lib/http/serve.ts +237 -0
  262. package/lib/http/server.ts +12 -10
  263. package/lib/http/sink.ts +135 -0
  264. package/lib/http/token.ts +38 -0
  265. package/lib/plan-entry.ts +11 -0
  266. package/lib/policy.ts +25 -2
  267. package/lib/requirements.ts +6 -7
  268. package/lib/session-label.ts +64 -0
  269. package/lib/session-roster.ts +201 -0
  270. package/lib/session-tool.ts +184 -0
  271. package/lib/spawn.ts +37 -3
  272. package/lib/stray-fields.ts +45 -0
  273. package/lib/tool-names.ts +38 -2
  274. package/lib/tui/glyphs.ts +3 -2
  275. package/lib/web/README.md +57 -4
  276. package/lib/web/client-runtime.ts +123 -0
  277. package/lib/web/components.ts +5 -0
  278. package/lib/web/index.ts +4 -2
  279. package/lib/web/markup.ts +88 -1
  280. package/lib/web/names.ts +42 -0
  281. package/lib/web/runtime.ts +7 -1
  282. package/lib/web/tokens.ts +25 -2
  283. package/package.json +26 -14
  284. package/skills/next/SKILL.md +4 -3
  285. package/skills/project-docs/SKILL.md +133 -0
  286. package/skills/project-docs/references/audit.md +131 -0
  287. package/skills/project-docs/references/changelog.md +105 -0
  288. package/skills/project-docs/references/discovery.md +127 -0
  289. package/skills/project-docs/references/readme.md +80 -0
  290. package/skills/project-docs/references/release.md +84 -0
  291. package/skills/project-docs/references/roadmap.md +93 -0
  292. package/skills/project-docs/scripts/state.ts +316 -0
  293. package/skills/refactor/SKILL.md +100 -125
  294. package/skills/refactor/references/handoff.md +35 -0
  295. package/skills/refactor/references/signals.md +54 -0
  296. package/skills/refactor/scripts/inventory.ts +587 -0
  297. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  298. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  299. package/extensions/file-browse/README.md +0 -506
  300. package/extensions/file-browse/files.ts +0 -564
  301. package/extensions/file-browse/server.ts +0 -141
  302. package/extensions/job/process.ts +0 -35
  303. package/extensions/plan/split.ts +0 -87
  304. package/extensions/session-bus/README.md +0 -247
  305. package/extensions/session-bus/team.ts +0 -102
  306. package/extensions/wechat/outbound.ts +0 -100
  307. package/lib/plans/sections.ts +0 -172
  308. package/lib/plans/setting.ts +0 -52
  309. package/lib/plans/split.ts +0 -215
  310. package/lib/plans/store.ts +0 -19
  311. package/lib/plans/team.ts +0 -151
  312. package/skills/changelog/SKILL.md +0 -148
  313. package/skills/parallel-dev/SKILL.md +0 -150
  314. /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
  315. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  316. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  317. /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
  318. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  319. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  320. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  321. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  322. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  323. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  324. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * The `session` tool, as the extensions that are not `session-bus` call it.
3
+ *
4
+ * `board` and `team` are the user's surface for the team; the bus is the transport
5
+ * underneath, and neither may import it (`test/structure.test.ts`). So the one place
6
+ * that turns a call into a typed answer lives here, next to the tool's name in
7
+ * `lib/tool-names.ts` — two copies would be two copies of the details-shape
8
+ * knowledge, and a bus that renamed a field would be broken in one of them.
9
+ *
10
+ * There are two transports, and the difference is the context, not the caller's
11
+ * choice: `join` travels as an in-process request on `pi.events` because a *command*
12
+ * context has no `ctx.executeTool` — the same constraint `lib/plan-entry.ts` exists
13
+ * for — while `peers` and `send` are nested tool calls, which only ever run inside a
14
+ * tool. One function either way, so a caller never has to know which it is holding.
15
+ *
16
+ * Every function returns a result and none throws: "the bus is not loaded", "a plan
17
+ * is open", and "that label is taken" are all answers a caller reports, not reasons
18
+ * to abandon a board write or a card operation that has nothing to do with the bus.
19
+ *
20
+ * This module is the *caller* side only. Reading the bus's records from disk is
21
+ * `session-roster.ts`, which is what works when the bus is not loaded at all.
22
+ */
23
+
24
+ import type { ExtensionAPI, ExtensionContext, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
25
+ import { messageOf } from "./errors.ts";
26
+ import { watchingOf } from "./session-roster.ts";
27
+ import { SESSION_TOOL } from "./tool-names.ts";
28
+
29
+ /**
30
+ * The `session send` text limit, which is also the longest handoff or offer.
31
+ *
32
+ * It is copied from the bus's protocol rather than imported — the dependency runs one
33
+ * way — and it lives here so `team` and `board` clip to one number instead of two.
34
+ */
35
+ export const MAX_MESSAGE_TEXT = 4_000;
36
+
37
+ /** Whether a session is listening for mail, and whether it raises a turn. */
38
+ export interface Watching {
39
+ armed: boolean;
40
+ wake: boolean;
41
+ }
42
+
43
+ /** One session the bus lists. */
44
+ export interface Peer {
45
+ id: string;
46
+ label: string;
47
+ /** Absent when the record does not say, which is not the same as "not listening". */
48
+ listening?: boolean;
49
+ }
50
+
51
+ /** What a join did: the label it landed on, and whether mail will reach the session. */
52
+ export type JoinResult = { ok: true; label: string; watching: Watching } | { ok: false; error: string };
53
+
54
+ /** EventBus channel on which a caller asks `session-bus` to join the team for it. */
55
+ export const TEAM_JOIN_REQUEST = "my-pi-agent/team-join/request";
56
+
57
+ /** What a caller sends on {@link TEAM_JOIN_REQUEST}. */
58
+ export interface TeamJoinRequest {
59
+ /** The caller's context; the bus starts and names through it. */
60
+ ctx: ExtensionContext;
61
+ /** What to join as and how to listen. */
62
+ input: { label?: string; wake?: boolean; listen?: boolean };
63
+ /** Called once, when the bus has answered. */
64
+ respond(result: JoinResult): void;
65
+ }
66
+
67
+ /** What a send did. */
68
+ export type SendResult = { ok: true; delivered: string[] } | { ok: false; error: string };
69
+
70
+ /** One nested call's answer: the details on success, the sentence on failure. */
71
+ type Answer = { details: Record<string, unknown> } | { error: string };
72
+
73
+ /**
74
+ * Call the `session` tool once.
75
+ *
76
+ * A nested call reports failure in its result rather than by rejecting; a rejection
77
+ * is still caught, because a bus that is not registered at all throws before the
78
+ * tool runs and the caller deserves the sentence rather than an exception.
79
+ */
80
+ async function call(ctx: ExtensionToolContext, args: Record<string, unknown>): Promise<Answer> {
81
+ const outcome = await ctx.executeTool(SESSION_TOOL, args).catch((error: unknown) => ({
82
+ isError: true as const,
83
+ result: { content: [{ type: "text", text: messageOf(error) }], details: undefined },
84
+ }));
85
+ if (outcome.isError) return { error: nestedMessage(outcome.result) };
86
+ const details = outcome.result.details;
87
+ return { details: typeof details === "object" && details !== null ? (details as Record<string, unknown>) : {} };
88
+ }
89
+
90
+ /** The message a failed nested call carries, or a sentence that says only that it failed. */
91
+ function nestedMessage(result: { content?: Array<{ type: string; text?: string }> } | undefined): string {
92
+ const text = result?.content?.find((block) => block.type === "text")?.text;
93
+ return text !== undefined && text !== "" ? text.replace(/^Error:\s*/, "") : "the session bus did not answer.";
94
+ }
95
+
96
+ /**
97
+ * How an extension joins the team, whatever kind of context it is holding.
98
+ *
99
+ * Two extensions ask — `board` when the page opens or a card is worked, `team` for
100
+ * `/team join` — and both hold a command or a tool context, so the request is the one
101
+ * transport that works from either.
102
+ */
103
+ export interface TeamJoin {
104
+ /** Join the team, naming this session when `label` is given. Never throws. */
105
+ join(ctx: ExtensionContext, label?: string): Promise<JoinResult>;
106
+ }
107
+
108
+ /** The production join, bound to one extension's `pi`. */
109
+ export function createTeamJoin(pi: Pick<ExtensionAPI, "events" | "getAllTools">): TeamJoin {
110
+ return {
111
+ join: (ctx, label) => join(pi, ctx, label === undefined ? {} : { label }),
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Join the team: on the bus, named when a label is given, and listening for mail.
117
+ *
118
+ * One call for all three, because that is one act — see `join` in
119
+ * `extensions/session-bus` — and one transport for both kinds of caller: the request
120
+ * travels on `pi.events` and the bus answers it.
121
+ *
122
+ * With no bus loaded there is no listener, so the answer is the sentence the caller
123
+ * reports rather than a promise that never settles: the `session` tool's presence is
124
+ * the same check `lib/plan-entry.ts` makes, and the listener always answers.
125
+ */
126
+ export async function join(
127
+ pi: Pick<ExtensionAPI, "events" | "getAllTools">,
128
+ ctx: ExtensionContext,
129
+ input: { label?: string; wake?: boolean; listen?: boolean } = {},
130
+ ): Promise<JoinResult> {
131
+ if (!pi.getAllTools().some((tool) => tool.name === SESSION_TOOL)) {
132
+ return { ok: false, error: "the session bus is not loaded" };
133
+ }
134
+ return new Promise<JoinResult>((resolve) => {
135
+ let settled = false;
136
+ const respond = (result: JoinResult): void => {
137
+ if (settled) return;
138
+ settled = true;
139
+ resolve(result);
140
+ };
141
+ pi.events.emit(TEAM_JOIN_REQUEST, { ctx, input, respond } satisfies TeamJoinRequest);
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Every session the bus lists, or `undefined` when it cannot answer.
147
+ *
148
+ * Named `listPeers` rather than `peers` so it cannot be mistaken for a caller's own
149
+ * formatting helper — `wechat`'s doctor has one — and a caller reads the verb.
150
+ *
151
+ * A caller renders "the record does not say" differently from "nobody is here", so a
152
+ * failed read is not an empty list.
153
+ */
154
+ export async function listPeers(ctx: ExtensionToolContext): Promise<Peer[] | undefined> {
155
+ const answer = await call(ctx, { action: "status" });
156
+ if ("error" in answer) return undefined;
157
+ const status = answer.details.status;
158
+ if (typeof status !== "object" || status === null) return undefined;
159
+ const sessions = (status as { sessions?: unknown }).sessions;
160
+ if (!Array.isArray(sessions)) return undefined;
161
+ const peers: Peer[] = [];
162
+ for (const entry of sessions) {
163
+ if (typeof entry !== "object" || entry === null) continue;
164
+ const { id, label, watching } = entry as Record<string, unknown>;
165
+ if (typeof id !== "string" || typeof label !== "string") continue;
166
+ const state = watchingOf(watching);
167
+ peers.push({ id, label, ...(state === undefined ? {} : { listening: state.armed }) });
168
+ }
169
+ return peers;
170
+ }
171
+
172
+ /** Send one message, and report the labels a message file was written for. */
173
+ export async function send(
174
+ ctx: ExtensionToolContext,
175
+ input: { kind: "handoff" | "event"; to: string; text: string },
176
+ ): Promise<SendResult> {
177
+ const answer = await call(ctx, { action: "send", kind: input.kind, to: input.to, text: input.text });
178
+ if ("error" in answer) return { ok: false, error: answer.error };
179
+ const delivered = answer.details.delivered;
180
+ return {
181
+ ok: true,
182
+ delivered: Array.isArray(delivered) ? delivered.filter((label): label is string => typeof label === "string") : [],
183
+ };
184
+ }
package/lib/spawn.ts CHANGED
@@ -8,11 +8,14 @@
8
8
  *
9
9
  * IO is split from the decision: {@link spawnOptions} and {@link killPlan} are
10
10
  * pure and testable on every platform, while {@link defaultSpawn} and
11
- * {@link defaultKillTree} execute them. Value-only, like every `lib/` module:
12
- * no module state, and each caller keeps its own children.
11
+ * {@link defaultKillTree} execute them. The liveness pair — {@link isPidAlive}
12
+ * and {@link startTokenOf} — is here too, because asking whether a process is
13
+ * still the one a record named is the same question as killing it: `job` guards
14
+ * a reattached pid with it, and so does the session roster. Value-only, like
15
+ * every `lib/` module: no module state, and each caller keeps its own children.
13
16
  */
14
17
 
15
- import { spawn as nodeSpawn, spawnSync } from "node:child_process";
18
+ import { execFileSync, spawn as nodeSpawn, spawnSync } from "node:child_process";
16
19
  import type { Readable } from "node:stream";
17
20
 
18
21
  /**
@@ -124,6 +127,37 @@ export function isPidAlive(pid: number, probe: KillProbe = process.kill): boolea
124
127
  }
125
128
  }
126
129
 
130
+ /** Whether a pid is still alive. */
131
+ export type LivenessFn = (pid: number) => boolean;
132
+
133
+ /**
134
+ * Best-effort OS start-time token for a pid, used to detect pid reuse.
135
+ *
136
+ * Returns undefined when the platform cannot provide one (for example Windows, or a
137
+ * process that is already gone), in which case callers fall back to liveness alone.
138
+ */
139
+ export type StartTokenFn = (pid: number) => string | undefined;
140
+
141
+ /**
142
+ * Default start-time token via `ps`. Undefined where `ps` is unavailable.
143
+ *
144
+ * It spawns a process, so a caller asks for it only where liveness alone cannot
145
+ * decide: a pid that is alive may be a different program than the one a record
146
+ * named, and nothing cheaper than the start time can tell them apart.
147
+ */
148
+ export const startTokenOf: StartTokenFn = (pid) => {
149
+ try {
150
+ const out = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
151
+ stdio: ["ignore", "pipe", "ignore"],
152
+ timeout: 2000,
153
+ });
154
+ const text = out.toString().trim();
155
+ return text || undefined;
156
+ } catch {
157
+ return undefined;
158
+ }
159
+ };
160
+
127
161
  /** The host seams {@link applyKillPlan} runs through. */
128
162
  export interface KillSeams {
129
163
  /** Signal one pid; throws when the pid is gone or not ours. */
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A field a tool call carries but its action does not act on.
3
+ *
4
+ * An action-shaped tool publishes one flat parameter object, so every field is
5
+ * offered to every action at once; that is harmless only while each action reads
6
+ * its own. A field that reaches an action which never reads it used to be dropped
7
+ * in silence — `board add` with `dependsOn` wrote the card without the link and
8
+ * answered success, and `team offer` with `to` broadcast to every live session
9
+ * while the caller believed it had named one. The caller could not tell what was
10
+ * recorded from what was ignored, and nothing in the result said which.
11
+ *
12
+ * So the table of what each action reads lives with the tool (`ACTION_FIELDS`),
13
+ * and this is the sentence a call is refused with: it names every stray field and
14
+ * the action that does take it, which makes the retry one call away. The table is
15
+ * keyed in the order the actions are documented, because that order is how the
16
+ * message lists the verbs that take a field.
17
+ *
18
+ * Value-only, as `lib/` must be: the tables are the callers'.
19
+ */
20
+
21
+ /** The fields each action reads, keyed in the order the actions are documented. */
22
+ export type ActionFields = Readonly<Record<string, readonly string[]>>;
23
+
24
+ /**
25
+ * Why a call carries a field its action does not act on, or nothing when it is clean.
26
+ *
27
+ * `undefined` is not a stray: an optional field the caller left out was never passed,
28
+ * and a schema makes every unset one `undefined`.
29
+ */
30
+ export function strayField(action: string, args: Record<string, unknown>, fields: ActionFields): string | undefined {
31
+ const mine = fields[action] ?? [];
32
+ const stray = Object.entries(args)
33
+ .filter(([field, value]) => value !== undefined && field !== "action" && !mine.includes(field))
34
+ .map(([field]) => field);
35
+ if (stray.length === 0) return undefined;
36
+ // One stray reads as "takes it"; several need their names, or the sentence stops
37
+ // saying which field goes where.
38
+ const named = (field: string): string => {
39
+ const taking = Object.keys(fields).filter((candidate) => fields[candidate]?.includes(field));
40
+ if (taking.length === 0) return `${field} belongs to no action`;
41
+ const verbs = taking.map((one) => `\`${one}\``).join(" and ");
42
+ return `${verbs} ${taking.length === 1 ? "takes" : "take"} ${stray.length === 1 ? "it" : field}`;
43
+ };
44
+ return `${action} does not act on ${stray.join(", ")}: ${stray.map(named).join("; ")}.`;
45
+ }
package/lib/tool-names.ts CHANGED
@@ -4,8 +4,9 @@
4
4
  * Tool names are otherwise owned by the extension that registers the tool, and
5
5
  * no extension imports another's modules (see this directory's README). When an
6
6
  * orchestrating tool calls another extension's tool through `ctx.executeTool`,
7
- * or reads another extension's registered parameter schema, the shared name
8
- * lives here so a rename breaks the build on both sides instead of silently
7
+ * reads another extension's registered parameter schema, lists the name in a
8
+ * read-only policy, or passes it on a child's `--tools` allowlist, the shared
9
+ * name lives here so a rename breaks the build on both sides instead of silently
9
10
  * disabling the integration.
10
11
  */
11
12
 
@@ -31,9 +32,44 @@ export const SUBAGENT_TOOL = "subagent";
31
32
  */
32
33
  export const ASK_USER_QUESTION_TOOL = "ask_user_question";
33
34
 
35
+ /**
36
+ * The `session` tool. `session-bus` registers it, and `team` calls it through
37
+ * `ctx.executeTool` to send a handoff, so a rename breaks both sides.
38
+ */
39
+ export const SESSION_TOOL = "session";
40
+
34
41
  /**
35
42
  * Parameter on the `subagent` tool that forces every spawned agent to a
36
43
  * read-only tool set. Plan mode sets it on every call while planning, and
37
44
  * refuses a `subagent` whose schema does not declare it.
38
45
  */
39
46
  export const SUBAGENT_READ_ONLY_PARAM = "readOnly";
47
+
48
+ /**
49
+ * The `board` tool: the shared feature list. Plan mode keeps it active because its
50
+ * `list` action is a read of the repository's cards, so the name is shared while the
51
+ * write actions stay the board's to refuse.
52
+ */
53
+ export const BOARD_TOOL = "board";
54
+
55
+ /**
56
+ * The one `board` action that reads. Plan mode admits it and refuses the rest, which
57
+ * is the distinction the tool's `readOnlyHint` cannot carry: the same tool writes.
58
+ */
59
+ export const BOARD_READ_ACTION = "list";
60
+
61
+ /**
62
+ * The `read_doc` tool. `file-read` registers it, and the plan, rewind, and
63
+ * subagent reader lists carry the name, so the document reader is one reader
64
+ * everywhere instead of three spellings.
65
+ */
66
+ export const READ_DOC_TOOL = "read_doc";
67
+
68
+ /**
69
+ * The `web_search` tool. `web-access` registers it; plan's and subagent's reader
70
+ * lists carry the name, so a rename cannot silently drop the web reader.
71
+ */
72
+ export const WEB_SEARCH_TOOL = "web_search";
73
+
74
+ /** The `web_fetch` tool, shared the same way as {@link WEB_SEARCH_TOOL}. */
75
+ export const WEB_FETCH_TOOL = "web_fetch";
package/lib/tui/glyphs.ts CHANGED
@@ -30,6 +30,7 @@ export const GLYPHS = {
30
30
  jobsFailure: "✗",
31
31
  stopped: "⊘",
32
32
  browse: "◉",
33
+ board: "⊞",
33
34
  delegation: "↳",
34
35
  wechat: "✆",
35
36
  gaugeFull: "▰",
@@ -56,10 +57,9 @@ export const GLYPHS = {
56
57
  /** Columns between a rail body glyph and its text. */
57
58
  export const GLYPH_GAP = 1;
58
59
 
59
- /** Padding used to join (`item`) or group (`group`) UI segments. */
60
+ /** Padding used to join UI segments. */
60
61
  export const SEPARATORS = {
61
62
  item: " · ",
62
- group: ` ${GLYPHS.divider} `,
63
63
  } as const;
64
64
 
65
65
  /** `ctx.ui.setStatus` keys; one per extension that publishes a chip. */
@@ -71,6 +71,7 @@ export const STATUS_KEYS = {
71
71
  watch: "watch",
72
72
  watchMatches: "watch-matches",
73
73
  browse: "browse",
74
+ board: "board",
74
75
  rewind: "rewind",
75
76
  agents: "agents",
76
77
  wechat: "wechat",
package/lib/web/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The tokens, base rules, app shell, `bf-*` components, HTML atoms, and client
4
4
  runtime the two loopback pages draw with: the plan review
5
5
  ([`../../extensions/plan/web-page.ts`](../../extensions/plan/web-page.ts)) and
6
- file browse ([`../../extensions/file-browse/styles.ts`](../../extensions/file-browse/styles.ts)).
6
+ file browse ([`../../extensions/file-browse/view/styles.ts`](../../extensions/file-browse/view/styles.ts)).
7
7
  The module-by-module inventory is the `web/` row of [`../README.md`](../README.md);
8
8
  this file is the conventions those modules exist to make easy.
9
9
 
@@ -21,6 +21,47 @@ counts its own stylesheet (`pages.test.ts`, `web.test.ts`). A page rule is
21
21
  namespaced (`review-*`, `browse-*`) and overrides a kit class rather than
22
22
  re-declaring it.
23
23
 
24
+ ## One browser runtime, spliced in
25
+
26
+ Three surfaces render pages from a bundle held as a template literal, and each had written
27
+ the same three helpers for itself: an element builder, a "did this key land in a field?"
28
+ test, and a status line's paint. No type checker and no AST scan compares text inside a
29
+ string, so the plan's `el(tag, className)` and the board's `el(tag, className, text)` were
30
+ two builders, and the plan compared `INPUT` where the board lowercased the tag.
31
+
32
+ `client-runtime.ts` holds one body of each, as ordinary TypeScript, and exports
33
+ `CLIENT_RUNTIME_JS` — the four functions as source, in the order a client reads them. A
34
+ page carries it the way it carries `plural` and its class table: the source crosses the
35
+ boundary, and the page runs the function instead of a copy of it.
36
+
37
+ The bodies are JavaScript on purpose — no annotation survives into the string, and no
38
+ backtick or `${` may appear in one. The DOM is reached through `globalThis`, because this
39
+ project compiles without a DOM lib; the cast names the guarantee the page makes.
40
+
41
+ What stays page-side: what a status *says*. `paintStatus` owns the text, its tooltip, and
42
+ the `bf-status--<tone>` class; the board composes `live · 3m` and appends its own class.
43
+ A shared runtime that also decided the wording would be a second copy of the page's voice.
44
+
45
+ ## Name a class once, in a table
46
+
47
+ A page's document is TypeScript and its client is a string inside a template literal, so
48
+ the two halves cannot import from each other: a class name used to be a literal in each,
49
+ and one assembled at runtime (`"board-card__reason" + kind`) could not be found by
50
+ reading either half. That is how a class reaches a stylesheet with no rule behind it —
51
+ an unstyled element, and nothing fails.
52
+
53
+ So a page declares its vocabulary once, in a `ClassNames` table beside its own modules,
54
+ and both halves read it: the document interpolates `${N.toolbar}`, the bundle carries
55
+ `namesScript(table)`. A name is then a symbol, and a test can compare the set a page
56
+ wears with the set its stylesheet draws, in both directions.
57
+
58
+ `namesScript` is the boundary, and it throws rather than trusting a caller: a value must
59
+ be `KEBAB` — lowercase, hyphenated — because the table is spliced into a template
60
+ literal, and a backtick would end the string while `${` would interpolate at build time.
61
+ The global it binds must be a JavaScript `IDENTIFIER`. The table's *keys* are the page's
62
+ own English; its *values* are what CSS matches, and nothing else in the page may spell
63
+ one.
64
+
24
65
  ## Color is a role
25
66
 
26
67
  `WEB_THEME_TOKENS` is derived from `themes/bytefly-dark.json` and
@@ -34,7 +75,10 @@ are computed rather than mapped:
34
75
  - `--scrim` and `--shadow` — per appearance, because a dimming layer and a lifted
35
76
  panel need their own alpha rather than a palette entry.
36
77
 
37
- Components read tokens only: no literal hex or `rgba()` in `components.ts`, and a
78
+ `--raised` is the one derived neutral: `--surface` mixed 6% (dark) or 3% (light)
79
+ toward `--text`, for a block that has to read as a plane above the page rather than
80
+ as an outline — the light step is smaller because a darker accent on a lighter step
81
+ falls below AA. Components read tokens only: no literal hex or `rgba()` in `components.ts`, and a
38
82
  test fails one. Contrast is asserted, not assumed — text, muted, and accent at
39
83
  4.5:1 on both surfaces, the primary label at 4.5:1 on the accent fill, `--dim` at
40
84
  3:1 because it carries the line-number gutter, and the status colours at 3:1.
@@ -96,8 +140,17 @@ choice. Neither page reloads for a theme.
96
140
 
97
141
  ## Markup atoms and escaping
98
142
 
99
- `docHead`, `brandLockup`, `themeToggle`, `chip`, `button`, and `keysSheet` return
100
- strings. Every dynamic **value** goes through `escapeHtml` from
143
+ `docHead`, `brandLockup`, `markIcon`, `tabTitle`, `themeToggle`, `chip`, `button`, and
144
+ `keysSheet` return strings. `docHead` links the mark as the tab icon — two SVG
145
+ data URIs from the same blocks the lockup draws, the dark one plain and the light
146
+ one behind `prefers-color-scheme` — because an icon is its own document and
147
+ cannot read the page's tokens.
148
+
149
+ `tabTitle` is where the three served pages' tab titles agree: `<page> · <subject> ·
150
+ <count>`, the page's name lowercase and the same word its `brandLockup` carries, a part
151
+ the page does not have left out rather than drawn empty. The board's count is live, so
152
+ its page repaints the title on every frame and the body crosses into the bundle as
153
+ `TAB_TITLE_JS`, the way `plural` does — one shape for the string, not two. Every dynamic **value** goes through `escapeHtml` from
101
154
  [`../escape.ts`](../escape.ts); the fields documented as trusted (`css`, an
102
155
  `icon` glyph, an attribute key, a client id) are code-authored constants, never
103
156
  model or file text. A page that renders its own document follows the same rule.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The browser runtime both page clients splice into their bundles: the element builder,
3
+ * the "is the user typing?" test, the status line, and the JSON POST.
4
+ *
5
+ * Two surfaces had written each of these for themselves, and because a bundle is a
6
+ * template literal no type checker and no AST scan ever compared them: the plan's
7
+ * `el(tag, className)` and the board's `el(tag, className, text)` were two builders, and
8
+ * the plan's `typing` compared `INPUT` where the board's lowercased the tag. Both are
9
+ * now one body, written here as real TypeScript, and each client carries the *source*
10
+ * (`CLIENT_RUNTIME_JS`) the same way it carries `plural` and the class table.
11
+ *
12
+ * The functions are plain JavaScript on purpose: their text is spliced into a bundle, so
13
+ * no annotation may survive into the string and no backtick or `${` may appear in a body.
14
+ * The DOM is reached through `globalThis`, because this project compiles without a DOM
15
+ * lib — the cast names the guarantee the page makes: it is a browser, and it has one.
16
+ */
17
+
18
+ /**
19
+ * The pieces of a DOM node the runtime touches, and the ones a page client needs beyond
20
+ * it. The page supplies the real thing; this is the shape its bundle is typed against.
21
+ */
22
+ export interface ClientNode {
23
+ className: string;
24
+ textContent: string | null;
25
+ title: string;
26
+ hidden: boolean;
27
+ href: string;
28
+ id: string;
29
+ style: { width: string };
30
+ type: string;
31
+ scrollTop: number;
32
+ contains(other: ClientNode | null): boolean;
33
+ focus(): void;
34
+ classList: { add(name: string): void; remove(name: string): void; toggle(name: string, on?: boolean): void };
35
+ setAttribute(name: string, value: string): void;
36
+ getAttribute(name: string): string | null;
37
+ removeAttribute(name: string): void;
38
+ appendChild(child: ClientNode): void;
39
+ remove(): void;
40
+ replaceChildren(): void;
41
+ querySelector(selector: string): ClientNode | null;
42
+ querySelectorAll(selector: string): ArrayLike<ClientNode>;
43
+ scrollIntoView?(options?: { block?: string }): void;
44
+ addEventListener(type: string, listener: (event: { target?: unknown }) => void): void;
45
+ }
46
+
47
+ /** What `document.createElement` has to answer for these helpers. */
48
+ interface RuntimeDocument {
49
+ createElement(tag: string): ClientNode;
50
+ }
51
+
52
+ /**
53
+ * One element with an optional class and text.
54
+ *
55
+ * `text` is set as `textContent`, never parsed: a card title and a plan's prose are both
56
+ * model- or user-written, and the one property that cannot execute is the one to use.
57
+ */
58
+ export function el(tag: string, className?: string, text?: string | null): ClientNode {
59
+ // Reached through `globalThis` and cast here, because this project compiles without a
60
+ // DOM lib; the cast names what the page guarantees — it is a browser, and it has one.
61
+ const node = (globalThis as unknown as { document: RuntimeDocument }).document.createElement(tag);
62
+ if (className) node.className = className;
63
+ if (text !== undefined && text !== null) node.textContent = String(text);
64
+ return node;
65
+ }
66
+
67
+ /**
68
+ * Whether a key event landed in a field, where every key is text rather than a shortcut.
69
+ *
70
+ * The tag is lowercased once, so a document parsed as XHTML (`tagName` upper case) and a
71
+ * page built by `createElement` (`tagName` lower case) both answer the same.
72
+ */
73
+ export function typing(target: { tagName?: string; isContentEditable?: boolean }): boolean {
74
+ if (!target || !target.tagName) return target?.isContentEditable === true;
75
+ const tag = target.tagName.toLowerCase();
76
+ return tag === "input" || tag === "textarea" || tag === "select" || target.isContentEditable === true;
77
+ }
78
+
79
+ /**
80
+ * Paint a status line: its text, the same text as its tooltip, and its tone's class.
81
+ *
82
+ * The status slot is the page's own id, so the caller passes the node; what is shared is
83
+ * the rule that a tone is a `bf-status--*` class and that the text is also the title (a
84
+ * clamped line has no other way to be read).
85
+ */
86
+ export function paintStatus(node: ClientNode, text: string, kind?: string): void {
87
+ node.textContent = text || "";
88
+ node.title = text || "";
89
+ node.className = kind ? "bf-status bf-status--" + kind : "bf-status";
90
+ }
91
+
92
+ /** One answer from the server: whether it accepted, its status, and its parsed body. */
93
+ interface PostResult {
94
+ ok: boolean;
95
+ status: number;
96
+ body: unknown;
97
+ }
98
+
99
+ /**
100
+ * POST JSON and read the reply's body, whatever the status.
101
+ *
102
+ * A page that only honoured the happy path would report `server returned 409` where the
103
+ * server had said *why*, so the body is parsed on both branches and the caller decides.
104
+ */
105
+ export function postJson(url: string, payload: unknown): Promise<PostResult> {
106
+ return fetch(url, {
107
+ method: "POST",
108
+ headers: { "Content-Type": "application/json" },
109
+ body: JSON.stringify(payload),
110
+ }).then((response: { ok: boolean; status: number; json(): Promise<unknown> }) =>
111
+ response.json().then(
112
+ (body: unknown): PostResult => ({ ok: response.ok, status: response.status, body }),
113
+ (): PostResult => ({ ok: response.ok, status: response.status, body: null }),
114
+ ),
115
+ );
116
+ }
117
+
118
+ /**
119
+ * The runtime, as a bundle receives it: four bodies, in the order a client reads them.
120
+ *
121
+ * `test` holds each to its module body and proves the string is safe to splice.
122
+ */
123
+ export const CLIENT_RUNTIME_JS = [el, typing, paintStatus, postJson].map(String).join("\n\n");
@@ -18,6 +18,8 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
18
18
  .bf-btn:hover{border-color:var(--accent);color:var(--accent);text-decoration:none}
19
19
  .bf-btn[aria-pressed="true"]{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}
20
20
  .bf-btn--primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
21
+ .bf-btn--danger{border-color:var(--danger);color:var(--danger)}
22
+ .bf-btn--danger:hover{border-color:var(--danger);background:var(--danger);color:var(--on-accent)}
21
23
  .bf-btn--primary:hover{filter:brightness(1.06);color:var(--on-accent)}
22
24
  .bf-btn--ghost{border-color:transparent;background:none;padding:0;font:inherit;color:inherit}
23
25
  .bf-btn--ghost:hover{color:var(--accent)}
@@ -36,6 +38,9 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
36
38
  .bf-segmented__item[aria-current="page"],.bf-segmented__item[aria-pressed="true"]{background:var(--accent-soft);color:var(--accent)}
37
39
  .bf-input{border:0;background:transparent;color:var(--text);font:var(--fs-sm) var(--font-mono);width:100%;outline:none}
38
40
  .bf-field{display:flex;align-items:center;gap:var(--space-1);background:var(--canvas);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-1) var(--space-2);color:var(--muted);font-size:var(--fs-sm)}
41
+ /* The field is the control: ringing the input inside it drew a border within a border. */
42
+ .bf-field:focus-within{outline:var(--focus-ring) solid var(--accent);outline-offset:2px}
43
+ .bf-field .bf-input:focus-visible{outline:none}
39
44
  .bf-textarea{width:100%;min-height:72px;padding:var(--space-2);font:inherit;color:var(--text);background:var(--canvas);border:1px solid var(--border);border-radius:var(--radius-md);resize:vertical}
40
45
  .bf-panel{min-width:0;overflow:auto;overscroll-behavior:contain}
41
46
  .bf-panel__summary{display:none}
package/lib/web/index.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * The Bytefly web brand system: tokens, base rules, components, layout,
3
- * runtime, and markup atoms. Import from `lib/web/index.ts` rather than a
4
- * leaf module so the surface reads as one kit.
3
+ * runtime, markup atoms, and the class-name table a page's bundle carries. Import
4
+ * from `lib/web/index.ts` rather than a leaf module so the surface reads as one kit.
5
5
  */
6
6
 
7
7
  export * from "./base.ts";
8
+ export * from "./client-runtime.ts";
8
9
  export * from "./components.ts";
9
10
  export * from "./layout.ts";
10
11
  export * from "./markup.ts";
12
+ export * from "./names.ts";
11
13
  export * from "./runtime.ts";
12
14
  export * from "./tokens.ts";