@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 (298) hide show
  1. package/README.md +46 -30
  2. package/README.zh-CN.md +40 -25
  3. package/assets/screens/board.png +0 -0
  4. package/assets/screens/chrome.png +0 -0
  5. package/assets/screens/file-browse.png +0 -0
  6. package/assets/screens/plan-review.png +0 -0
  7. package/assets/screens/plan.png +0 -0
  8. package/docs/extensions/README.md +22 -23
  9. package/docs/extensions/board.md +397 -168
  10. package/docs/extensions/bytefly.md +234 -197
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +57 -63
  14. package/docs/extensions/file-open.md +9 -11
  15. package/docs/extensions/file-read.md +12 -14
  16. package/docs/extensions/goal.md +15 -17
  17. package/docs/extensions/job.md +54 -42
  18. package/docs/extensions/memory.md +17 -19
  19. package/docs/extensions/plan.md +197 -140
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +145 -0
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +83 -44
  26. package/docs/extensions/todo.md +18 -18
  27. package/docs/extensions/watch.md +31 -31
  28. package/docs/extensions/web-access.md +2 -4
  29. package/docs/extensions/wechat.md +34 -35
  30. package/docs/extensions/worktree.md +56 -50
  31. package/extensions/board/commands.ts +79 -25
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +17 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +5 -20
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +65 -93
  38. package/extensions/board/result.ts +30 -39
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +49 -11
  41. package/extensions/board/snapshot.ts +8 -6
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +154 -55
  44. package/extensions/board/transport/router.ts +3 -5
  45. package/extensions/board/transport/server.ts +59 -28
  46. package/extensions/board/types.ts +36 -42
  47. package/extensions/board/view/card.ts +229 -0
  48. package/extensions/board/view/client.ts +35 -16
  49. package/extensions/board/view/dialog.ts +169 -6
  50. package/extensions/board/view/empty.ts +47 -0
  51. package/extensions/board/view/keys.ts +13 -2
  52. package/extensions/board/view/live.ts +90 -28
  53. package/extensions/board/view/names.ts +13 -4
  54. package/extensions/board/view/page.ts +38 -13
  55. package/extensions/board/view/render.ts +219 -277
  56. package/extensions/board/view/styles.ts +130 -32
  57. package/extensions/board/view/team.ts +297 -76
  58. package/extensions/bytefly/block/fallback.ts +1 -1
  59. package/extensions/bytefly/doctor/runtime.ts +5 -2
  60. package/extensions/bytefly/footer/collect/label.ts +60 -0
  61. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  62. package/extensions/bytefly/footer/config.ts +7 -8
  63. package/extensions/bytefly/footer/register.ts +33 -4
  64. package/extensions/bytefly/footer/render/footer.ts +18 -4
  65. package/extensions/bytefly/footer/render/key.ts +46 -0
  66. package/extensions/bytefly/footer/render/layout.ts +13 -3
  67. package/extensions/bytefly/footer/render/lines.ts +10 -7
  68. package/extensions/bytefly/footer/render/segments.ts +41 -38
  69. package/extensions/bytefly/footer/render/status.ts +0 -11
  70. package/extensions/bytefly/footer/types.ts +7 -0
  71. package/extensions/bytefly/header/component.ts +4 -20
  72. package/extensions/bytefly/header/config.ts +1 -1
  73. package/extensions/bytefly/index.ts +32 -6
  74. package/extensions/bytefly/mode/editor.ts +115 -0
  75. package/extensions/bytefly/panel/commands.ts +210 -120
  76. package/extensions/bytefly/panel/format.ts +4 -4
  77. package/extensions/bytefly/panel/settings.ts +73 -6
  78. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  79. package/extensions/bytefly/update/cache.ts +1 -1
  80. package/extensions/bytefly/update/register.ts +4 -4
  81. package/extensions/bytefly/working/config.ts +5 -7
  82. package/extensions/bytefly/working/format.ts +10 -13
  83. package/extensions/bytefly/working/register.ts +22 -27
  84. package/extensions/check/commands.ts +3 -2
  85. package/extensions/check/render.ts +10 -1
  86. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  87. package/extensions/context/commands.ts +2 -2
  88. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  89. package/extensions/file-browse/config/config.ts +4 -19
  90. package/extensions/file-browse/index.ts +89 -79
  91. package/extensions/file-browse/live/live.ts +1 -1
  92. package/extensions/file-browse/transport/router.ts +12 -4
  93. package/extensions/file-browse/transport/server.ts +61 -28
  94. package/extensions/file-browse/view/chrome.ts +4 -4
  95. package/extensions/file-browse/view/client.ts +3 -1
  96. package/extensions/file-browse/view/html.ts +73 -40
  97. package/extensions/file-browse/view/live-client.ts +2 -2
  98. package/extensions/file-browse/view/markdown.ts +13 -14
  99. package/extensions/file-browse/view/palette.ts +2 -2
  100. package/extensions/file-browse/view/raw-html.ts +9 -9
  101. package/extensions/file-browse/view/tree.ts +1 -1
  102. package/extensions/file-browse/view/urls.ts +20 -6
  103. package/extensions/file-read/render.ts +4 -4
  104. package/extensions/goal/plan-route.ts +2 -2
  105. package/extensions/goal/tui.ts +12 -2
  106. package/extensions/job/child.ts +527 -0
  107. package/extensions/job/clock.ts +142 -0
  108. package/extensions/job/commands.ts +4 -2
  109. package/extensions/job/config.ts +8 -1
  110. package/extensions/job/format.ts +22 -1
  111. package/extensions/job/log-writer.ts +13 -2
  112. package/extensions/job/registry.ts +63 -7
  113. package/extensions/job/render.ts +34 -0
  114. package/extensions/job/runtime.ts +296 -655
  115. package/extensions/job/session.ts +132 -19
  116. package/extensions/job/store.ts +7 -7
  117. package/extensions/job/tools.ts +21 -9
  118. package/extensions/job/types.ts +47 -0
  119. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
  120. package/extensions/job/waiters.ts +44 -1
  121. package/extensions/memory/commands.ts +4 -2
  122. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  123. package/extensions/plan/commands.ts +12 -9
  124. package/extensions/plan/feedback.ts +62 -16
  125. package/extensions/plan/format.ts +8 -2
  126. package/extensions/plan/git-bash.ts +115 -16
  127. package/extensions/plan/index.ts +58 -51
  128. package/extensions/plan/plan-index.ts +4 -2
  129. package/extensions/plan/plan-text.ts +114 -0
  130. package/extensions/plan/plans.ts +5 -54
  131. package/extensions/plan/policy.ts +8 -10
  132. package/extensions/plan/review-lazy.ts +85 -0
  133. package/extensions/plan/review.ts +2 -2
  134. package/extensions/plan/runtime.ts +61 -41
  135. package/extensions/plan/sections.ts +1 -1
  136. package/extensions/plan/steps.ts +3 -3
  137. package/extensions/plan/todos.ts +1 -1
  138. package/extensions/plan/tools/cancel.ts +27 -62
  139. package/extensions/plan/tools/enter.ts +55 -81
  140. package/extensions/plan/tools/exit.ts +221 -238
  141. package/extensions/plan/tools/refusal.ts +18 -0
  142. package/extensions/plan/tools/steps.ts +75 -92
  143. package/extensions/plan/tools/write.ts +77 -120
  144. package/extensions/plan/tools.ts +188 -30
  145. package/extensions/plan/tui.ts +5 -5
  146. package/extensions/plan/types.ts +48 -5
  147. package/extensions/plan/web-blocks.ts +76 -5
  148. package/extensions/plan/web-chrome.ts +4 -0
  149. package/extensions/plan/web-client.ts +102 -2
  150. package/extensions/plan/web-collab.ts +4 -4
  151. package/extensions/plan/web-comments.ts +39 -8
  152. package/extensions/plan/web-composer.ts +14 -3
  153. package/extensions/plan/web-keys.ts +149 -25
  154. package/extensions/plan/web-lazy.ts +53 -0
  155. package/extensions/plan/web-markdown.ts +10 -68
  156. package/extensions/plan/web-outline.ts +22 -2
  157. package/extensions/plan/web-page.ts +56 -86
  158. package/extensions/plan/web-payload.ts +188 -0
  159. package/extensions/plan/web-plan.ts +23 -13
  160. package/extensions/plan/web-router.ts +18 -9
  161. package/extensions/plan/web-settle.ts +14 -7
  162. package/extensions/plan/web-share-session.ts +24 -55
  163. package/extensions/plan/web-share.ts +19 -37
  164. package/extensions/plan/web-write.ts +112 -0
  165. package/extensions/plan/web.ts +67 -131
  166. package/extensions/question/tools.ts +6 -2
  167. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  168. package/extensions/rewind/commands/commands.ts +6 -3
  169. package/extensions/rewind/state/runtime.ts +1 -1
  170. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  171. package/extensions/rewind/view/review-tui.ts +4 -4
  172. package/extensions/serve/config.ts +39 -0
  173. package/extensions/serve/index.ts +85 -0
  174. package/extensions/serve/registry.ts +260 -0
  175. package/extensions/session-bus/commands.ts +11 -6
  176. package/extensions/session-bus/join-request.ts +17 -9
  177. package/extensions/session-bus/mail-watch.ts +26 -17
  178. package/extensions/session-bus/protocol.ts +15 -3
  179. package/extensions/session-bus/resolve.ts +5 -7
  180. package/extensions/session-bus/result.ts +1 -27
  181. package/extensions/session-bus/runtime.ts +40 -12
  182. package/extensions/session-bus/store.ts +101 -19
  183. package/extensions/session-bus/tools.ts +5 -4
  184. package/extensions/session-bus/types.ts +19 -0
  185. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  186. package/extensions/subagent/agents.ts +1 -1
  187. package/extensions/subagent/commands.ts +4 -2
  188. package/extensions/subagent/format.ts +15 -15
  189. package/extensions/subagent/index.ts +1 -1
  190. package/extensions/subagent/render.ts +3 -3
  191. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  192. package/extensions/team/commands.ts +24 -5
  193. package/extensions/team/dispatch.ts +18 -4
  194. package/extensions/team/format.ts +17 -2
  195. package/extensions/team/index.ts +9 -8
  196. package/extensions/team/schema.ts +6 -5
  197. package/extensions/team/tools.ts +107 -45
  198. package/extensions/team/types.ts +3 -2
  199. package/extensions/todo/commands.ts +4 -2
  200. package/extensions/todo/format.ts +11 -0
  201. package/extensions/todo/render.ts +57 -0
  202. package/extensions/todo/runtime.ts +1 -1
  203. package/extensions/todo/tools.ts +1 -1
  204. package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
  205. package/extensions/todo/widget.ts +47 -0
  206. package/extensions/watch/commands.ts +4 -2
  207. package/extensions/watch/format.ts +36 -7
  208. package/extensions/watch/render.ts +57 -0
  209. package/extensions/watch/runtime.ts +27 -22
  210. package/extensions/watch/tools.ts +4 -4
  211. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  212. package/extensions/web-access/fetch/tool.ts +12 -4
  213. package/extensions/wechat/commands/commands.ts +18 -14
  214. package/extensions/wechat/index.ts +72 -37
  215. package/extensions/wechat/state/credentials.ts +1 -1
  216. package/extensions/wechat/state/state.ts +1 -1
  217. package/extensions/wechat/text/status.ts +112 -0
  218. package/extensions/wechat/types.ts +2 -2
  219. package/extensions/wechat/view/status-screen.ts +4 -103
  220. package/extensions/worktree/commands.ts +11 -9
  221. package/extensions/worktree/enter.ts +2 -2
  222. package/extensions/worktree/exit.ts +2 -2
  223. package/extensions/worktree/index.ts +5 -5
  224. package/extensions/worktree/screen.ts +3 -3
  225. package/extensions/worktree/status.ts +2 -2
  226. package/extensions/worktree/tools.ts +113 -116
  227. package/lib/README.md +28 -13
  228. package/lib/board/actions.ts +111 -0
  229. package/lib/board/artifact.ts +149 -0
  230. package/lib/board/card.ts +163 -52
  231. package/lib/board/config.ts +58 -0
  232. package/lib/board/contract.ts +113 -0
  233. package/lib/board/join.ts +38 -0
  234. package/lib/board/lanes.ts +182 -0
  235. package/lib/board/lead.ts +130 -0
  236. package/lib/board/limits.ts +78 -0
  237. package/lib/board/links.ts +31 -10
  238. package/lib/board/page-pointer.ts +30 -5
  239. package/lib/board/read.ts +83 -0
  240. package/lib/board/reason.ts +102 -0
  241. package/lib/board/round-text.ts +68 -6
  242. package/lib/board/round.ts +31 -15
  243. package/lib/board/rules.ts +151 -307
  244. package/lib/board/seat.ts +35 -0
  245. package/lib/board/session.ts +199 -51
  246. package/lib/board/skipped.ts +22 -0
  247. package/lib/board/steps.ts +487 -0
  248. package/lib/board/store.ts +119 -36
  249. package/lib/board/tone.ts +92 -0
  250. package/lib/board/view.ts +81 -63
  251. package/lib/bus-request.ts +83 -0
  252. package/{extensions/watch → lib}/child.ts +40 -21
  253. package/lib/env.ts +65 -7
  254. package/lib/format.ts +13 -0
  255. package/lib/http/index.ts +17 -11
  256. package/lib/http/mount.ts +142 -0
  257. package/lib/http/port.ts +30 -0
  258. package/lib/http/serve.ts +170 -46
  259. package/lib/http/server.ts +3 -3
  260. package/lib/lazy.ts +45 -0
  261. package/lib/plan-entry.ts +46 -21
  262. package/lib/plans/paths.ts +82 -0
  263. package/lib/policy.ts +9 -6
  264. package/lib/report.ts +3 -2
  265. package/lib/requirements.ts +10 -10
  266. package/lib/session-label.ts +129 -7
  267. package/lib/session-roster.ts +43 -1
  268. package/lib/session-tool.ts +39 -22
  269. package/lib/stray-fields.ts +7 -2
  270. package/lib/tool-names.ts +5 -10
  271. package/lib/tui/README.md +29 -11
  272. package/lib/tui/chrome.ts +16 -15
  273. package/lib/tui/cursor.ts +18 -3
  274. package/lib/tui/glyphs.ts +11 -11
  275. package/lib/tui/render-cache.ts +29 -0
  276. package/lib/tui/tokens.ts +33 -2
  277. package/lib/tui/tool.ts +33 -4
  278. package/lib/web/README.md +21 -0
  279. package/lib/web/client-runtime.ts +27 -1
  280. package/lib/web/components.ts +5 -1
  281. package/lib/web/index.ts +1 -0
  282. package/lib/web/markdown.ts +83 -0
  283. package/lib/web/markup.ts +20 -2
  284. package/lib/web/runtime.ts +9 -10
  285. package/package.json +8 -2
  286. package/skills/init/SKILL.md +5 -5
  287. package/skills/next/SKILL.md +3 -5
  288. package/extensions/board/config.ts +0 -36
  289. package/extensions/board/seat.ts +0 -28
  290. package/lib/tui/index.ts +0 -34
  291. package/skills/project-docs/SKILL.md +0 -133
  292. package/skills/project-docs/references/audit.md +0 -131
  293. package/skills/project-docs/references/changelog.md +0 -105
  294. package/skills/project-docs/references/discovery.md +0 -127
  295. package/skills/project-docs/references/readme.md +0 -80
  296. package/skills/project-docs/references/release.md +0 -84
  297. package/skills/project-docs/references/roadmap.md +0 -93
  298. package/skills/project-docs/scripts/state.ts +0 -316
package/lib/tui/glyphs.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { keyText, type Theme } from "@earendil-works/pi-coding-agent";
11
+ import { paint } from "./tokens.ts";
11
12
 
12
13
  /**
13
14
  * Leading glyphs for status chips, footer segments, and transcript rails.
@@ -29,8 +30,7 @@ export const GLYPHS = {
29
30
  jobsRunning: "▸",
30
31
  jobsFailure: "✗",
31
32
  stopped: "⊘",
32
- browse: "◉",
33
- board: "⊞",
33
+ http: "◉",
34
34
  delegation: "↳",
35
35
  wechat: "✆",
36
36
  gaugeFull: "▰",
@@ -51,6 +51,7 @@ export const GLYPHS = {
51
51
  prev: "‹",
52
52
  next: "›",
53
53
  update: "↑",
54
+ autoMode: "▶",
54
55
  prompt: "❯",
55
56
  } as const;
56
57
 
@@ -70,8 +71,7 @@ export const STATUS_KEYS = {
70
71
  jobsFailure: "jobs-failure",
71
72
  watch: "watch",
72
73
  watchMatches: "watch-matches",
73
- browse: "browse",
74
- board: "board",
74
+ http: "http",
75
75
  rewind: "rewind",
76
76
  agents: "agents",
77
77
  wechat: "wechat",
@@ -84,25 +84,25 @@ export const STATUS_KEYS = {
84
84
  */
85
85
  export const EXPAND_KEYBINDING = "app.tools.expand";
86
86
 
87
- /** The bound expand key, styled dim, or `""` when the binding is unknown. */
87
+ /** The bound expand key, in the faint role, or `""` when the binding is unknown. */
88
88
  export function expandKey(theme: Theme): string {
89
89
  const key = keyText(EXPAND_KEYBINDING);
90
- return key ? theme.fg("dim", key) : "";
90
+ return key ? paint(theme, "faint", key) : "";
91
91
  }
92
92
 
93
93
  /**
94
- * Pi's key-hint format: the key dim, its description muted. An unbound key
94
+ * Pi's key-hint format: the key faint, its description meta. An unbound key
95
95
  * (`""`) yields `""`, so a hint never prints a stray description.
96
96
  */
97
97
  export function keyHintText(theme: Theme, key: string, description: string): string {
98
- return key ? theme.fg("dim", key) + theme.fg("muted", ` ${description}`) : "";
98
+ return key ? paint(theme, "faint", key) + paint(theme, "meta", ` ${description}`) : "";
99
99
  }
100
100
 
101
101
  /**
102
- * The built-in-style expand affordance: a muted `(ctrl+o to expand)` with the
103
- * key in dim. Degrades to `(to expand)` when the binding is unknown.
102
+ * The built-in-style expand affordance: a meta `(ctrl+o to expand)` with the
103
+ * key in faint. Degrades to `(to expand)` when the binding is unknown.
104
104
  */
105
105
  export function expandHint(theme: Theme): string {
106
106
  const hint = keyHintText(theme, keyText(EXPAND_KEYBINDING), "to expand)");
107
- return theme.fg("muted", "(") + (hint || theme.fg("muted", "to expand)"));
107
+ return paint(theme, "meta", "(") + (hint || paint(theme, "meta", "to expand)"));
108
108
  }
@@ -31,6 +31,8 @@
31
31
  export class RenderCache {
32
32
  private width: number | null = null;
33
33
  private source: readonly string[] | undefined;
34
+ /** The last key's parts, for the value-keyed form; `undefined` until one is set. */
35
+ private parts: readonly unknown[] | undefined;
34
36
  private cached: string[] = [];
35
37
 
36
38
  /**
@@ -47,9 +49,36 @@ export class RenderCache {
47
49
  return this.cached;
48
50
  }
49
51
 
52
+ /**
53
+ * The lines from `build()`, for a component that derives them from a key.
54
+ *
55
+ * The identity form above chains on the inner component's array. A component with no
56
+ * inner component has nothing to chain on — a header's view and a footer's snapshot are
57
+ * rebuilt every frame — so it describes its inputs instead, and this compares them:
58
+ * primitives by value, and anything else by identity (a theme object is the case that
59
+ * matters, since its strings are what get cached).
60
+ *
61
+ * The key is only as complete as the caller makes it: a part left out is a frame that
62
+ * shows stale lines. Positions are compared, so an undefined part is a real one.
63
+ */
64
+ keyed(parts: readonly unknown[], build: () => string[]): string[] {
65
+ const previous = this.parts;
66
+ if (
67
+ previous !== undefined &&
68
+ previous.length === parts.length &&
69
+ previous.every((part, at) => part === parts[at])
70
+ ) {
71
+ return this.cached;
72
+ }
73
+ this.cached = build();
74
+ this.parts = [...parts];
75
+ return this.cached;
76
+ }
77
+
50
78
  /** Drop the memo; call from the component's `invalidate()`. */
51
79
  invalidate(): void {
52
80
  this.width = null;
81
+ this.parts = undefined;
53
82
  this.cached = [];
54
83
  }
55
84
  }
package/lib/tui/tokens.ts CHANGED
@@ -5,9 +5,16 @@
5
5
  * A component names a role (`title`, `meta`, `bad`) and the active theme
6
6
  * supplies the color, so switching themes re-skins every surface without a
7
7
  * component changing. The mapping is deliberately indirect: Pi's token set is
8
- * closed, so the kit only ever references it, never a literal color.
8
+ * closed, so the kit only ever references it, never a literal color — and this
9
+ * table is the one place it does, tool vocabulary included, so a remap here
10
+ * reaches every surface the kit and the renderers draw.
9
11
  *
10
12
  * Value-only: one frozen lookup table and pure helpers.
13
+ *
14
+ * Two vocabularies live here and they do not mix: `ok`/`warn`/`bad` are verdicts —
15
+ * they say how something *is* — and `hue1`…`hue5` are categories, which say only
16
+ * "this is not those". A surface that shows hues keeps its verdicts to the places
17
+ * that earn one, because a reader cannot tell a borrowed green from a judgement.
11
18
  */
12
19
 
13
20
  import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
@@ -35,7 +42,24 @@ export type TuiRole =
35
42
  /** Error. */
36
43
  | "bad"
37
44
  /** Neutral notice. */
38
- | "info";
45
+ | "info"
46
+ /**
47
+ * A category hue: identity, never a verdict.
48
+ *
49
+ * Blue, purple, pink, green, and amber are the shipped palette's five colourful
50
+ * tokens, so a categorical surface reads as a chart. Green and amber are the same
51
+ * tokens `ok` and `warn` resolve to — that is the cost of Pi's closed token set —
52
+ * which is why a hue never appears in a slot that also carries a judgement.
53
+ */
54
+ | "hue1"
55
+ | "hue2"
56
+ | "hue3"
57
+ | "hue4"
58
+ | "hue5"
59
+ /** A tool call's own name: the transcript block's only accent. */
60
+ | "toolTitle"
61
+ /** A tool result's body text. */
62
+ | "toolOutput";
39
63
 
40
64
  /** Role -> theme color. The single place the kit names a Pi token. */
41
65
  export const TUI_TOKENS: Readonly<Record<TuiRole, ThemeColor>> = {
@@ -50,6 +74,13 @@ export const TUI_TOKENS: Readonly<Record<TuiRole, ThemeColor>> = {
50
74
  warn: "warning",
51
75
  bad: "error",
52
76
  info: "muted",
77
+ hue1: "accent",
78
+ hue2: "mdListBullet",
79
+ hue3: "mdCode",
80
+ hue4: "success",
81
+ hue5: "warning",
82
+ toolTitle: "toolTitle",
83
+ toolOutput: "toolOutput",
53
84
  };
54
85
 
55
86
  /** Paint `text` in a role's color. */
package/lib/tui/tool.ts CHANGED
@@ -22,6 +22,7 @@ import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
22
22
  import { formatDuration, sanitizeLine, stripControlChars } from "../format.ts";
23
23
  import { clip, clipMiddle } from "./columns.ts";
24
24
  import { expandHint, SEPARATORS } from "./glyphs.ts";
25
+ import { RenderCache } from "./render-cache.ts";
25
26
  import { paint } from "./tokens.ts";
26
27
 
27
28
  /** The parts of a tool result a renderer reads. */
@@ -30,9 +31,9 @@ export interface ToolMessageResult {
30
31
  isError?: boolean;
31
32
  }
32
33
 
33
- /** The bold tool name in Pi's tool-title token, the block's only accent. */
34
+ /** The bold tool name in the toolTitle role, the block's only accent. */
34
35
  export function toolCallTitle(theme: Theme, name: string): string {
35
- return theme.fg("toolTitle", theme.bold(name));
36
+ return paint(theme, "toolTitle", theme.bold(name));
36
37
  }
37
38
 
38
39
  /**
@@ -223,10 +224,18 @@ export type TailResultInput<Row> =
223
224
  * section, or the themed tail with an "earlier" note. The caller owns the row
224
225
  * renderer, so the extension's own row grammar — a peer-session tag, a status
225
226
  * glyph — stays with its vocabulary.
227
+ *
228
+ * `render` runs on every frame Pi draws, for every block in the transcript, and
229
+ * this block is the shared one behind the board's card, a session's peer list and
230
+ * a shaped tail — each of which re-derived its rows and clipped every line per
231
+ * frame before this memo. The lines depend on the input, the theme and the width,
232
+ * and none of them changes without `setInput`, `invalidate`, or a new width, so
233
+ * the memo is keyed on the width and dropped by those two calls.
226
234
  */
227
235
  export class TailResult<Row> implements Component {
228
236
  private input: TailResultInput<Row>;
229
237
  private theme: Theme;
238
+ private readonly cache = new RenderCache();
230
239
 
231
240
  constructor(input: TailResultInput<Row>, theme: Theme) {
232
241
  this.input = input;
@@ -237,12 +246,19 @@ export class TailResult<Row> implements Component {
237
246
  setInput(input: TailResultInput<Row>, theme: Theme): void {
238
247
  this.input = input;
239
248
  this.theme = theme;
249
+ this.cache.invalidate();
240
250
  }
241
251
 
242
- invalidate(): void {}
252
+ invalidate(): void {
253
+ this.cache.invalidate();
254
+ }
243
255
 
244
256
  render(width: number): string[] {
245
257
  const w = Math.max(1, width);
258
+ return this.cache.lines(w, undefined, () => this.build(w));
259
+ }
260
+
261
+ private build(w: number): string[] {
246
262
  const lines: string[] = [""];
247
263
  const input = this.input;
248
264
  if (input.kind === "list") {
@@ -259,11 +275,24 @@ export class TailResult<Row> implements Component {
259
275
  return lines;
260
276
  }
261
277
  if (input.earlier > 0) lines.push(clip(earlierHint(this.theme, input.earlier, input.earlierLabel), w));
262
- for (const line of input.lines) lines.push(this.theme.fg("toolOutput", clip(line, w)));
278
+ for (const line of input.lines) lines.push(paint(this.theme, "toolOutput", clip(line, w)));
263
279
  return lines;
264
280
  }
265
281
  }
266
282
 
283
+ /**
284
+ * Whether the component the transcript handed back is this kit's tail block.
285
+ *
286
+ * `TailResult` carries its row type and `instanceof` erases it. The caller is the
287
+ * one that knows which rows it drew, so the claim lives here as a guard rather
288
+ * than as a cast in each extension that reuses its block through
289
+ * `context.lastComponent` — and a component from another tool can only reach that
290
+ * check by being this class, which is why the erased type is safe to assert.
291
+ */
292
+ export function isTailResult<Row>(value: unknown): value is TailResult<Row> {
293
+ return value instanceof TailResult;
294
+ }
295
+
267
296
  /** The render context fields the elapsed timer needs. */
268
297
  export interface ToolRenderClock {
269
298
  /** Pi's per-call render state, opaque here: this module only ever casts it. */
package/lib/web/README.md CHANGED
@@ -42,6 +42,27 @@ What stays page-side: what a status *says*. `paintStatus` owns the text, its too
42
42
  the `bf-status--<tone>` class; the board composes `live · 3m` and appends its own class.
43
43
  A shared runtime that also decided the wording would be a second copy of the page's voice.
44
44
 
45
+ ## One inline Markdown renderer, spliced in
46
+
47
+ Two served pages read Markdown written by a model or a person: the plan review's document
48
+ and the board's card notes. Both need the same two answers — what an inline token is, and
49
+ which target a link may point at — so `markdown.ts` holds one body of each and exports
50
+ `INLINE_MARKDOWN_JS` for a bundle to splice. The review page's block half
51
+ ([`../../extensions/plan/web-markdown.ts`](../../extensions/plan/web-markdown.ts)) calls
52
+ `renderInline` for the text inside a block; the board calls it for each note. A page that
53
+ writes its own is how one of them starts accepting a `javascript:` target the other refuses.
54
+
55
+ The renderer appends a text node or one of four atoms — `code`, `strong`, `em`, `a` — and
56
+ never parses markup, so the board's rule that a stored field is never `innerHTML` holds with
57
+ Markdown in play. A link is made only when `safeHref` allows the target: `http`, `https`, or
58
+ `mailto`, no credentials, and no protocol-relative or disguised relative target, resolved
59
+ against the page's own URL at call time.
60
+
61
+ The body is JavaScript on purpose, like the runtime's: no annotation survives into the
62
+ string, and no backtick or `${` may appear in one. It is the inline half only — a card note
63
+ is one sanitized line, so inline is the whole of it, while blocks, tables, and fenced code
64
+ stay the review page's own half.
65
+
45
66
  ## Name a class once, in a table
46
67
 
47
68
  A page's document is TypeScript and its client is a string inside a template literal, so
@@ -116,7 +116,33 @@ export function postJson(url: string, payload: unknown): Promise<PostResult> {
116
116
  }
117
117
 
118
118
  /**
119
- * The runtime, as a bundle receives it: four bodies, in the order a client reads them.
119
+ * The prefix this page was served under, as the server wrote it on `<html data-base>`.
120
+ *
121
+ * A session serves several pages on one origin, so a bundle cannot assume it owns the
122
+ * root: `""` at the origin's root, `/files` under a mount. The attribute is read every
123
+ * time rather than cached at load, because a fragment runs in whatever order the bundle
124
+ * lists it and none of them may depend on another having run first.
125
+ */
126
+ export function pageBase(): string {
127
+ // Cast for the same reason as `el`: this project compiles without a DOM lib, and the
128
+ // page guarantees a document with a root element.
129
+ const root = (
130
+ globalThis as unknown as { document: { documentElement: { getAttribute(name: string): string | null } } }
131
+ ).document.documentElement;
132
+ return root.getAttribute("data-base") ?? "";
133
+ }
134
+
135
+ /**
136
+ * {@link pageBase} on its own, for a bundle to splice at its **outermost** scope.
137
+ *
138
+ * A page's bundle is a stack of fragments, and a fragment's helper is only visible to the
139
+ * ones nested inside it — so this one goes where every fragment can reach it, which is not
140
+ * where the rest of the runtime goes.
141
+ */
142
+ export const CLIENT_BASE_JS = String(pageBase);
143
+
144
+ /**
145
+ * The runtime, as a bundle receives it: the four bodies, in the order a client reads them.
120
146
  *
121
147
  * `test` holds each to its module body and proves the string is safe to splice.
122
148
  */
@@ -28,6 +28,8 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
28
28
  .bf-btn:disabled{opacity:.5;cursor:default}
29
29
  .bf-chip{display:inline-flex;align-items:center;gap:5px;font-size:var(--fs-xs);color:var(--muted);white-space:nowrap}
30
30
  .bf-chip--accent{color:var(--accent);border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px}
31
+ /* A field the card carries rather than a state it is in: the same pill, worn quietly. */
32
+ .bf-chip--outline{color:var(--muted);border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px}
31
33
  .bf-chip--success{color:var(--success)}
32
34
  .bf-chip--warn{color:var(--warn)}
33
35
  .bf-chip--danger{color:var(--danger)}
@@ -35,7 +37,7 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
35
37
  .bf-segmented__item{appearance:none;border:0;border-radius:0;background:transparent;color:var(--text);font:var(--fs-xs) var(--font-ui);padding:5px var(--space-3);cursor:pointer}
36
38
  .bf-segmented__item+.bf-segmented__item{border-left:1px solid var(--border)}
37
39
  .bf-segmented__item:hover{color:var(--accent);text-decoration:none}
38
- .bf-segmented__item[aria-current="page"],.bf-segmented__item[aria-pressed="true"]{background:var(--accent-soft);color:var(--accent)}
40
+ .bf-segmented__item[aria-current="page"],.bf-segmented__item[aria-selected="true"],.bf-segmented__item[aria-pressed="true"]{background:var(--accent-soft);color:var(--accent)}
39
41
  .bf-input{border:0;background:transparent;color:var(--text);font:var(--fs-sm) var(--font-mono);width:100%;outline:none}
40
42
  .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
43
  /* The field is the control: ringing the input inside it drew a border within a border. */
@@ -73,6 +75,8 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
73
75
  .bf-keys{display:grid;grid-template-columns:auto 1fr;gap:8px var(--space-4);margin:0;padding:var(--space-4)}
74
76
  .bf-keys dt{white-space:nowrap}
75
77
  .bf-keys dd{margin:0;color:var(--muted);font-size:var(--fs-sm)}
78
+ /* One line under the table, for a rule that is not a key: what an open dialog keeps. */
79
+ .bf-keys__note{margin:0;padding:0 var(--space-4) var(--space-4);color:var(--muted);font-size:var(--fs-xs)}
76
80
  .bf-kbd{display:inline-block;min-width:20px;padding:1px 6px;border:1px solid var(--border);border-bottom-width:2px;border-radius:var(--radius-sm);background:var(--surface);color:var(--text);font:var(--fs-xs) var(--font-mono);text-align:center}
77
81
  .bf-tree{font-family:var(--font-mono);font-size:var(--fs-sm);overflow-x:auto}
78
82
  .bf-tree ul{list-style:none;margin:0;padding-left:14px}
package/lib/web/index.ts CHANGED
@@ -8,6 +8,7 @@ export * from "./base.ts";
8
8
  export * from "./client-runtime.ts";
9
9
  export * from "./components.ts";
10
10
  export * from "./layout.ts";
11
+ export * from "./markdown.ts";
11
12
  export * from "./markup.ts";
12
13
  export * from "./names.ts";
13
14
  export * from "./runtime.ts";
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Inline Markdown, for every served page: one renderer and one link policy.
3
+ *
4
+ * Two pages read Markdown written by a model or a person — the plan review's document and
5
+ * the board's card notes — and each needs the same two answers: what an inline token is,
6
+ * and which target a link may point at. A second copy of either is how one page starts
7
+ * accepting a `javascript:` target the other refuses, so the body lives here and each page
8
+ * splices it into its own bundle.
9
+ *
10
+ * The renderer appends **text nodes and four elements only** — `code`, `strong`, `em`, and
11
+ * `a` — so plan, comment, and note text never becomes markup and no page needs `innerHTML`.
12
+ * A link is made only for a target `safeHref` allows: `http`, `https`, or `mailto`, no
13
+ * credentials, and no protocol-relative or disguised relative target.
14
+ *
15
+ * It is the inline half only. A card note is one sanitized line, so inline is the whole of
16
+ * it; the review page's blocks, tables, fenced code, and lists are its own half, and they
17
+ * call `renderInline` for the text inside each.
18
+ *
19
+ * The body is JavaScript: no annotation survives into a string, and no backtick or `${`
20
+ * may appear in one. `base` is the page's own URL at call time, which is what makes a
21
+ * target that only looks relative land as text.
22
+ */
23
+ export const INLINE_MARKDOWN_JS = String.raw` // A link is rendered only for an http(s) or mailto target. Resolving against
24
+ // the review origin is what catches a target that only looks relative: the
25
+ // URL parser strips a tab or newline, so "/\t/evil.com" is cross-origin once
26
+ // it is parsed, and a disguised target must stay text.
27
+ function safeHref(url, base) {
28
+ var trimmed = String(url).trim();
29
+ if (!trimmed || /^\/\//.test(trimmed)) return null;
30
+ var resolved;
31
+ try { resolved = new URL(trimmed, base); } catch (error) { return null; }
32
+ if (resolved.protocol === "mailto:") return trimmed;
33
+ if (resolved.protocol !== "http:" && resolved.protocol !== "https:") return null;
34
+ if (resolved.username || resolved.password) return null;
35
+ if (resolved.origin === new URL(base).origin) return resolved.href;
36
+ return /^https?:\/\//i.test(trimmed) ? resolved.href : null;
37
+ }
38
+
39
+ // Inline token order matters: code, emphasis, and links are matched before a
40
+ // bare URL, so a URL inside a link target or a code span is never re-split.
41
+ var INLINE = /(\x60[^\x60]+\x60|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\)|(?:https?:\/\/|www\.)[^\s<]+)/g;
42
+
43
+ function renderInline(container, text) {
44
+ String(text).split(INLINE).forEach(function (part) {
45
+ if (!part) return;
46
+ var match;
47
+ if ((match = /^\x60([^\x60]+)\x60$/.exec(part))) {
48
+ var code = el("code"); code.textContent = match[1]; container.appendChild(code); return;
49
+ }
50
+ if ((match = /^\*\*([^*]+)\*\*$/.exec(part))) {
51
+ var strong = el("strong"); strong.textContent = match[1]; container.appendChild(strong); return;
52
+ }
53
+ if ((match = /^\*([^*]+)\*$/.exec(part))) {
54
+ var em = el("em"); em.textContent = match[1]; container.appendChild(em); return;
55
+ }
56
+ if ((match = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(part))) {
57
+ var href = safeHref(match[2], location.href);
58
+ if (!href) { container.appendChild(document.createTextNode(part)); return; }
59
+ var link = el("a"); link.textContent = match[1]; link.href = href; link.target = "_blank"; link.rel = "noreferrer noopener";
60
+ container.appendChild(link); return;
61
+ }
62
+ // GFM extended autolink: a bare http(s) or www. URL in running text.
63
+ if ((match = /^(?:https?:\/\/|www\.)[^\s<]+$/.exec(part))) {
64
+ var url = match[0].replace(/[.,;:!?*_~]+$/, "");
65
+ while (url.charAt(url.length - 1) === ")" && (url.match(/\(/g) || []).length < (url.match(/\)/g) || []).length) {
66
+ url = url.slice(0, -1).replace(/[.,;:!?*_~]+$/, "");
67
+ }
68
+ var dot = url.indexOf(".");
69
+ var target = /^www\./i.test(url) ? "http://" + url : url;
70
+ var safe = dot > 0 && /\.[^./]/.test(url.slice(dot)) ? safeHref(target, location.href) : null;
71
+ if (safe) {
72
+ var auto = el("a");
73
+ auto.textContent = url;
74
+ auto.href = safe;
75
+ auto.target = "_blank";
76
+ auto.rel = "noreferrer noopener";
77
+ container.appendChild(auto);
78
+ return;
79
+ }
80
+ }
81
+ container.appendChild(document.createTextNode(part));
82
+ });
83
+ }`;
package/lib/web/markup.ts CHANGED
@@ -16,6 +16,12 @@ export interface DocHeadOptions {
16
16
  title: string;
17
17
  /** Trusted CSS assembled by the page. */
18
18
  css: string;
19
+ /**
20
+ * The mount prefix this page answers under, e.g. `/files`; `""` for the origin's
21
+ * root. A page's scripts and links are emitted under it, because a session serves
22
+ * several pages on one origin.
23
+ */
24
+ base?: string;
19
25
  }
20
26
 
21
27
  /**
@@ -41,6 +47,17 @@ export function tabTitle(product: string, subject?: string, count?: string): str
41
47
  */
42
48
  export const TAB_TITLE_JS = String(tabTitle);
43
49
 
50
+ /**
51
+ * The `data-base` attribute a page's `<html>` carries, which its own bundle reads
52
+ * (`base()` in `client-runtime.ts`) to build every URL it emits.
53
+ *
54
+ * `""` — the origin's root — writes nothing at all, so a page served alone is markup
55
+ * without a mount to name.
56
+ */
57
+ export function baseAttribute(base: string): string {
58
+ return base === "" ? "" : ` data-base="${escapeHtml(base)}"`;
59
+ }
60
+
44
61
  /**
45
62
  * The shared document head: charset, viewport, colour scheme, title, the page
46
63
  * CSS, and the pre-paint theme bootstrap. The body and its scripts stay with
@@ -53,7 +70,7 @@ export function docHead(options: DocHeadOptions): string {
53
70
  <title>${escapeHtml(options.title)}</title>
54
71
  ${markIcon()}
55
72
  <style>${options.css}</style>
56
- <script src="${THEME_JS_PATH}"></script>`;
73
+ <script src="${options.base ?? ""}${THEME_JS_PATH}"></script>`;
57
74
  }
58
75
 
59
76
  /**
@@ -202,7 +219,7 @@ export interface KeyHint {
202
219
  * for the sheet, `#keys-close` for its own close button — and the trigger button
203
220
  * is the page's own concern, since the two pages name it differently.
204
221
  */
205
- export function keysSheet(rows: readonly KeyHint[]): string {
222
+ export function keysSheet(rows: readonly KeyHint[], note?: string): string {
206
223
  const kbd = (key: string): string => `<kbd class="bf-kbd">${escapeHtml(key)}</kbd>`;
207
224
  const body = rows
208
225
  .map((row) => {
@@ -214,6 +231,7 @@ export function keysSheet(rows: readonly KeyHint[]): string {
214
231
  <div class="bf-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="keys-title">
215
232
  <div class="bf-dialog__head"><h2 id="keys-title">Keyboard shortcuts</h2><span class="bf-spacer"></span><button type="button" class="bf-btn" id="keys-close">Close</button></div>
216
233
  <dl class="bf-keys">${body}</dl>
234
+ ${note === undefined ? "" : `<p class="bf-keys__note">${escapeHtml(note)}</p>`}
217
235
  </div>
218
236
  </div>`;
219
237
  }
@@ -156,20 +156,19 @@ export function dialogScript(options: DialogOptions): string {
156
156
  });
157
157
 
158
158
  // Tab cycles inside the dialog: leaving it would put focus behind the scrim,
159
- // where the page looks inert but is not.
159
+ // where the page looks inert but is not. Every Tab is redirected rather than only
160
+ // the two that wrap, because the panel is the first node and the *container* of the
161
+ // others: a browser's own backwards step from the last node has no sibling to land
162
+ // on and walks out of the dialog instead.
160
163
  dialog.addEventListener("keydown", function (event) {
161
164
  if (event.key !== "Tab") return;
162
165
  var nodes = focusables();
163
166
  if (nodes.length === 0) { event.preventDefault(); return; }
164
- var first = nodes[0];
165
- var last = nodes[nodes.length - 1];
166
- if (event.shiftKey && document.activeElement === first) {
167
- event.preventDefault();
168
- last.focus();
169
- } else if (!event.shiftKey && document.activeElement === last) {
170
- event.preventDefault();
171
- first.focus();
172
- }
167
+ var at = nodes.indexOf(document.activeElement);
168
+ var step = event.shiftKey ? -1 : 1;
169
+ var next = at === -1 ? (event.shiftKey ? nodes.length - 1 : 0) : (at + step + nodes.length) % nodes.length;
170
+ event.preventDefault();
171
+ nodes[next].focus();
173
172
  });
174
173
 
175
174
  // Capture, so Esc closes the dialog before a page handler peels another layer.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gavin-hu/my-pi-agent",
3
- "version": "0.20.0",
4
- "description": "Personal Pi customizations, packaged as a Pi package: worktree isolation and rewind snapshots, question, todo, goal, plan, subagent, job, watch, web-access, file-browse, file-open, file-read, check, memory, context, session-bus, board, team, wechat, and bytefly.",
3
+ "version": "0.22.0",
4
+ "description": "Personal Pi customizations, packaged as a Pi package: worktree isolation and rewind snapshots, question, todo, goal, plan, serve, subagent, job, watch, web-access, file-browse, file-open, file-read, check, memory, context, session-bus, board, team, wechat, and bytefly.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi",
@@ -84,6 +84,11 @@
84
84
  "lib/**",
85
85
  "skills/**",
86
86
  "themes/**",
87
+ "assets/screens/board.png",
88
+ "assets/screens/chrome.png",
89
+ "assets/screens/file-browse.png",
90
+ "assets/screens/plan-review.png",
91
+ "assets/screens/plan.png",
87
92
  "!**/*.test.ts"
88
93
  ],
89
94
  "publishConfig": {
@@ -92,6 +97,7 @@
92
97
  },
93
98
  "pi": {
94
99
  "extensions": [
100
+ "./extensions/serve/index.ts",
95
101
  "./extensions/worktree/index.ts",
96
102
  "./extensions/rewind/index.ts",
97
103
  "./extensions/question/index.ts",
@@ -13,7 +13,7 @@ cannot show; the repository supplies every command, path, and convention that ca
13
13
  be verified. Plan mode is the review gate: no write happens before approval.
14
14
 
15
15
  `/skill:init` loads this skill. If plan mode is not active, call
16
- `enter_plan_mode` and wait for confirmation. Keep the interview and the plan
16
+ `plan` with action `enter` and wait for confirmation. Keep the interview and the plan
17
17
  review in that order; do not write the file early to save a step. A declined plan
18
18
  mode ends the skill: stop and explain, rather than writing directly.
19
19
 
@@ -29,7 +29,7 @@ mode ends the skill: stop and explain, rather than writing directly.
29
29
  └─> draft: new file, exact edits, or an honest no-op
30
30
  └─> quality gate
31
31
  └─> plan outline with ## Steps and ## Verification
32
- └─> write_plan ──> exit_plan_mode
32
+ └─> plan · write ──> plan · exit
33
33
  ├─ not approved ──> refine the same plan
34
34
  └─ approved ──────> write/edit the target
35
35
  └─ read back + quality gate
@@ -147,7 +147,7 @@ evidence, not as a replacement for reading the repository.
147
147
 
148
148
  ## 5. Plan and review
149
149
 
150
- - Save the proposal with `write_plan` in the documented outline. The plan must
150
+ - Save the proposal with `plan` action `write` in the documented outline. The plan must
151
151
  include:
152
152
  - `## Goal` — the outcome for the instruction file;
153
153
  - `## Context` — the sourced evidence sheet, the effective-file finding, and the
@@ -162,9 +162,9 @@ evidence, not as a replacement for reading the repository.
162
162
  - `## Verification` — the project's own check when it declares one (`run_check`,
163
163
  or the narrower docs or structure test that guards the instruction file), then
164
164
  reading the written file back against the checklist.
165
- - Call `exit_plan_mode` with the returned path.
165
+ - Call `plan` action `exit` with the returned path.
166
166
  - If the user refines or rejects the plan, stay in plan mode, rewrite the same
167
- plan file with `write_plan` (`plan_path`), and present it again.
167
+ plan file with `plan` action `write` (`plan_path`), and present it again.
168
168
  - No write to the target may happen before approval.
169
169
 
170
170
  ## 6. Apply and verify
@@ -15,11 +15,9 @@ project wins.
15
15
  The conversation decides where the candidates come from (section 3); the request
16
16
  decides only what to ask and where to look.
17
17
 
18
- Two sibling skills, where they are installed, own a narrower question: an audit
19
- of *where to refactor* is `refactor`, and *whether a document still tells the
20
- truth* — the changelog, the roadmap, the front door — is `project-docs`. When the
21
- request is already scoped to one of those, name the skill that answers it and
22
- hand over, instead of returning a generic list.
18
+ A sibling skill, where it is installed, owns a narrower question: an audit of
19
+ *where to refactor* is `refactor`. When the request is already scoped to it, name
20
+ that skill and hand over, instead of returning a generic list.
23
21
 
24
22
  A next-steps block that the host draws itself is not this skill's output: that is
25
23
  the reply's suggested follow-up, a turn-level hint. This skill inspects state, so
@@ -1,36 +0,0 @@
1
- /**
2
- * The board's one setting: the port its page is pinned to.
3
- *
4
- * The `board` section of the shared `bytefly.json` (global then project, project
5
- * wins). `0` — the default — takes one free port for the session and remembers it,
6
- * which is what a session that never bookmarks its URL wants; a positive value pins
7
- * the page, so a bookmarked URL survives a restart. A pinned port that is taken
8
- * fails the start rather than moving the page out from under the bookmark.
9
- *
10
- * Pin it in the project document when more than one repository runs a board: a
11
- * global value is one port for every board on the machine.
12
- */
13
-
14
- import { clampInteger, loadConfigSection } from "../../lib/config.ts";
15
-
16
- /** The `board` section. */
17
- export interface BoardConfig {
18
- /** TCP port on `127.0.0.1`; `0` picks one for the session, a positive value pins it. */
19
- port: number;
20
- }
21
-
22
- export const DEFAULT_BOARD_CONFIG: BoardConfig = { port: 0 };
23
-
24
- /** Validate and clamp a raw config object over the defaults. */
25
- export function normalizeBoardConfig(
26
- raw: Record<string, unknown> | undefined,
27
- base: BoardConfig = DEFAULT_BOARD_CONFIG,
28
- ): BoardConfig {
29
- if (!raw) return base;
30
- return { port: clampInteger(raw.port, base.port, 0, 65_535) };
31
- }
32
-
33
- /** Load the effective config for a working directory. */
34
- export function loadBoardConfig(cwd: string): BoardConfig {
35
- return loadConfigSection(cwd, "board", DEFAULT_BOARD_CONFIG, normalizeBoardConfig);
36
- }