@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
@@ -1,35 +1,30 @@
1
1
  /**
2
- * The Settings section: its own state (the editor and the JSON pane), its key
3
- * handling, and the screen it draws.
2
+ * The Settings section's own state: the text editor, the JSON pane, and the
3
+ * writes. Everything it draws comes from `tui.ts` and `side-pane.ts`, because all
4
+ * three sections draw one frame.
4
5
  *
5
- * The component owns the section switch, the write scope, and the cursor. This
6
- * class owns what only Settings has, so `tui.ts` stays the switch and the shared
7
- * list keys, and the section reads and writes through the same surface.
6
+ * The component owns the section switch, the write scope, the cursor, the filter
7
+ * and the keys the three sections share; this class owns what only Settings has.
8
8
  */
9
9
 
10
- import type { Theme } from "@earendil-works/pi-coding-agent";
11
- import { Key, matchesKey } from "@earendil-works/pi-tui";
12
- import type { ViewportRowsSource } from "../../../../lib/tui/chrome.ts";
13
10
  import type { ListCursor } from "../../../../lib/tui/cursor.ts";
14
11
  import type { WriteScope } from "../../picker/types.ts";
12
+ import { settingMatches, type PanelFilter } from "../filter.ts";
15
13
  import type { SettingsSurface } from "../settings-runtime.ts";
16
- import { SETTING_ROWS } from "../settings.ts";
14
+ import { SETTING_ROWS, type SettingView } from "../settings.ts";
17
15
  import { applyEditKey, type EditingState } from "./editing.ts";
18
16
  import { settingEntries, type PanelEntry } from "./entries.ts";
19
- import { detailRows, groupLine, panelFrame, sectionBar } from "./frame.ts";
20
17
  import { clampSelection } from "./nav.ts";
21
- import { projectNote, sectionJsonPane, settingDetail, settingRow, settingsSummary } from "./settings-view.ts";
22
18
 
23
19
  /** What the Settings section needs from the component that hosts it. */
24
20
  export interface SettingsSectionDeps {
25
21
  /** The rows, values, and writes the section reads and calls. */
26
22
  surface: SettingsSurface;
27
- theme: Theme;
23
+ /** The write scope the component owns. */
28
24
  scope(): WriteScope;
25
+ /** The filter the component owns. */
26
+ filter(): PanelFilter;
29
27
  cursor: ListCursor;
30
- viewportRows: ViewportRowsSource | undefined;
31
- cwd: string;
32
- agentDir: string;
33
28
  /** Keep the focused row across a rebuild and return it. */
34
29
  retain(entries: PanelEntry[]): PanelEntry | undefined;
35
30
  /** Panel navigation, for the keys this section does not own. */
@@ -38,13 +33,9 @@ export interface SettingsSectionDeps {
38
33
  switchScope(): void;
39
34
  notify(message: string): void;
40
35
  requestRender(): void;
41
- /** Keep the frame's project-note marker in step for the mouse mapping. */
42
- setNoteShown(shown: boolean): void;
43
- /** The hint row, already assembled (or the notice that replaces it). */
44
- hint(width: number): string;
45
36
  }
46
37
 
47
- /** The Settings section's state and screen. */
38
+ /** The Settings section's state. */
48
39
  export class SettingsSection {
49
40
  private chain: Promise<void> = Promise.resolve();
50
41
  private editing: EditingState | undefined;
@@ -52,16 +43,21 @@ export class SettingsSection {
52
43
 
53
44
  constructor(private readonly deps: SettingsSectionDeps) {}
54
45
 
55
- /** Whether a text row is open in the editor, so the hint row and mouse defer. */
46
+ /** Whether a text row is open in the editor, so the hint row and keys defer. */
56
47
  get isEditing(): boolean {
57
48
  return this.editing !== undefined;
58
49
  }
59
50
 
60
- /** Whether the detail pane shows the focused row's section as JSON. */
51
+ /** Whether the focused row's section is shown as JSON. */
61
52
  get jsonOpen(): boolean {
62
53
  return this.json;
63
54
  }
64
55
 
56
+ /** The editor's buffer and caret, for the row that is being typed into. */
57
+ get editingState(): EditingState | undefined {
58
+ return this.editing;
59
+ }
60
+
65
61
  /** Run `task` after the previous write, then repaint once. */
66
62
  private write(task: () => Promise<unknown>, onSettled?: (result: unknown) => void): void {
67
63
  this.chain = this.chain
@@ -71,16 +67,30 @@ export class SettingsSection {
71
67
  .then(() => this.deps.requestRender());
72
68
  }
73
69
 
74
- /** The rows for the active scope, as display entries. */
75
- private entries(): PanelEntry[] {
76
- return settingEntries(this.deps.surface.views(this.deps.scope()));
70
+ /** The rows the active scope shows, after the filter. */
71
+ views(): SettingView[] {
72
+ const filter = this.deps.filter();
73
+ return this.deps.surface.views(this.deps.scope()).filter((view) => settingMatches(view, filter));
74
+ }
75
+
76
+ /** Those rows as display entries, grouped under their headings. */
77
+ entries(): PanelEntry[] {
78
+ return settingEntries(this.views());
77
79
  }
78
80
 
79
- /** The focused entry, after clamping the cursor onto a row. */
80
- private focused(): PanelEntry | undefined {
81
+ /** The focused setting, after clamping the cursor onto a row. */
82
+ focused(): SettingView | undefined {
81
83
  const entries = this.entries();
82
84
  clampSelection(entries, this.deps.cursor);
83
- return entries[this.deps.cursor.selected];
85
+ const entry = entries[this.deps.cursor.selected];
86
+ return entry?.kind === "setting" ? entry.setting : undefined;
87
+ }
88
+
89
+ /** The stored document behind the focused row, when the JSON pane is open. */
90
+ jsonFor(view: SettingView | undefined): { section: string; document: unknown } | undefined {
91
+ if (!this.json || view?.config === undefined) return undefined;
92
+ const { section } = view.config;
93
+ return { section, document: this.deps.surface.sectionDocument(this.deps.scope(), section) };
84
94
  }
85
95
 
86
96
  /** Drop the editor and the JSON pane when another section takes over. */
@@ -96,10 +106,9 @@ export class SettingsSection {
96
106
  }
97
107
 
98
108
  /** Change the focused setting: cycle it, or open the editor for a text row. */
99
- private changeSetting(): void {
100
- const entry = this.focused();
101
- if (entry?.kind !== "setting") return;
102
- const view = entry.setting;
109
+ changeSetting(): void {
110
+ const view = this.focused();
111
+ if (view === undefined) return;
103
112
  if (view.kind === "text") {
104
113
  this.editing = { id: view.id, buffer: view.value, caret: view.value.length };
105
114
  this.deps.requestRender();
@@ -110,22 +119,22 @@ export class SettingsSection {
110
119
  }
111
120
 
112
121
  /** Delete the focused setting's leaf, so its default applies again. */
113
- private resetSetting(): void {
114
- const entry = this.focused();
115
- if (entry?.kind !== "setting") return;
122
+ resetSetting(): void {
123
+ const view = this.focused();
124
+ if (view === undefined) return;
116
125
  const scope = this.deps.scope();
117
- this.write(() => this.deps.surface.reset(scope, entry.setting.id));
126
+ this.write(() => this.deps.surface.reset(scope, view.id));
118
127
  }
119
128
 
120
- /** Toggle the detail pane between the row and its whole section as stored. */
121
- private toggleJson(): void {
122
- const entry = this.focused();
123
- if (entry?.kind !== "setting") return;
124
- if (!entry.setting.config) {
129
+ /** Toggle the focused row between its value and the whole section as stored. */
130
+ toggleJson(): void {
131
+ const view = this.focused();
132
+ if (view === undefined) return;
133
+ if (!view.config) {
125
134
  // There is no section to show, so leave the mode off rather than keeping a
126
135
  // hidden toggle that would reopen the pane on another row.
127
136
  this.json = false;
128
- this.deps.notify(`${entry.setting.label} is a Pi setting; bytefly.json has no section for it.`);
137
+ this.deps.notify(`${view.label} is a Pi setting; bytefly.json has no section for it.`);
129
138
  return;
130
139
  }
131
140
  this.json = !this.json;
@@ -133,7 +142,7 @@ export class SettingsSection {
133
142
  }
134
143
 
135
144
  /** Store the editor's buffer; a refusal keeps it open with the reason shown. */
136
- private commitEdit(): void {
145
+ commitEdit(): void {
137
146
  const editing = this.editing;
138
147
  if (!editing) return;
139
148
  const scope = this.deps.scope();
@@ -149,13 +158,13 @@ export class SettingsSection {
149
158
  * Editor keys. `Esc`/`Ctrl+C` cancel the edit rather than closing the screen,
150
159
  * and everything else the section binds is inert until the editor closes.
151
160
  */
152
- private handleEditingInput(data: string): void {
161
+ handleEditingInput(data: string): void {
153
162
  const editing = this.editing;
154
163
  if (!editing) return;
155
164
  const max = SETTING_ROWS.find((row) => row.id === editing.id)?.text?.maxLength;
156
165
  const action = applyEditKey(data, editing, max);
157
166
  if (action.kind === "commit") {
158
- void this.commitEdit();
167
+ this.commitEdit();
159
168
  return;
160
169
  }
161
170
  if (action.kind === "cancel") {
@@ -167,84 +176,4 @@ export class SettingsSection {
167
176
  this.editing = action.state;
168
177
  this.deps.requestRender();
169
178
  }
170
-
171
- /** One key: editor keys first, then change, scope, reset, JSON, navigation. */
172
- handleInput(data: string): void {
173
- if (this.editing) {
174
- this.handleEditingInput(data);
175
- return;
176
- }
177
- this.focused();
178
- if (matchesKey(data, Key.space) || matchesKey(data, Key.enter)) {
179
- this.changeSetting();
180
- return;
181
- }
182
- if (matchesKey(data, Key.tab)) {
183
- this.deps.switchScope();
184
- return;
185
- }
186
- if (data === "u") {
187
- this.resetSetting();
188
- return;
189
- }
190
- if (data === "J") {
191
- this.toggleJson();
192
- return;
193
- }
194
- this.deps.navigate(data);
195
- }
196
-
197
- /** The Settings body: scope counts, the project note, rows, and detail. */
198
- render(width: number, header: string): string[] {
199
- const { theme, surface, viewportRows, cwd, agentDir } = this.deps;
200
- const scope = this.deps.scope();
201
- const views = surface.views(scope);
202
- const entries = settingEntries(views);
203
- const focusedEntry = this.deps.retain(entries);
204
- const focused = focusedEntry?.kind === "setting" ? focusedEntry.setting : undefined;
205
- // A row with no section leaves the mode off, so moving away and back cannot
206
- // reopen a pane the user closed.
207
- if (focused && !focused.config) this.json = false;
208
- const jsonSection = this.json ? focused?.config : undefined;
209
- const paths = { scope, cwd, agentDir };
210
- const detail = jsonSection
211
- ? sectionJsonPane({
212
- theme,
213
- width,
214
- section: jsonSection.section,
215
- document: surface.sectionDocument(scope, jsonSection.section),
216
- budget: detailRows(true, viewportRows),
217
- dirty: surface.isDirty(),
218
- paths,
219
- })
220
- : settingDetail({
221
- theme,
222
- width,
223
- view: focused,
224
- budget: detailRows(false, viewportRows),
225
- dirty: surface.isDirty(),
226
- paths,
227
- });
228
-
229
- const { lines, noteShown } = panelFrame({
230
- theme,
231
- viewportRows,
232
- cursor: this.deps.cursor,
233
- width,
234
- header,
235
- summary: sectionBar(theme, width, "settings", settingsSummary(theme, width, scope, views.length)),
236
- entries,
237
- note: scope === "project" ? projectNote(theme, width) : undefined,
238
- detail,
239
- hint: this.deps.hint(width),
240
- draw: (entry, index) => {
241
- if (entry.kind === "group") return groupLine(theme, width, entry);
242
- if (entry.kind !== "setting") return undefined;
243
- const editing = this.editing?.id === entry.setting.id ? this.editing : undefined;
244
- return settingRow(theme, width, entry.setting, index === this.deps.cursor.selected, editing);
245
- },
246
- });
247
- this.deps.setNoteShown(noteShown);
248
- return lines;
249
- }
250
179
  }
@@ -1,33 +1,28 @@
1
1
  /**
2
- * The Settings section: its summary line, one row per preference, and the
3
- * detail pane that names the value's source.
2
+ * The Settings section's rows and the two words only it says.
4
3
  *
5
- * Pure functions over the catalogue's views; the component owns the cursor, the
6
- * scope, and the write.
4
+ * A row is a checkbox for a boolean, the label, and the value in a column of its
5
+ * own — the column is what makes twelve values scannable, and it is measured from
6
+ * the labels actually on screen rather than from the longest label in the
7
+ * catalogue, so a narrow terminal keeps the value instead of the padding.
8
+ *
9
+ * Pure: pure functions over the catalogue's views, theme passed in.
7
10
  */
8
11
 
9
- import { join } from "node:path";
10
12
  import type { Theme } from "@earendil-works/pi-coding-agent";
11
- import { sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
13
+ import { sliceByColumn, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
12
14
  import { sanitizeLine } from "../../../../lib/format.ts";
13
- import { clip } from "../../../../lib/tui/columns.ts";
14
- import { BODY_INDENT } from "../../../../lib/tui/indent.ts";
15
+ import { clip, padTo } from "../../../../lib/tui/columns.ts";
15
16
  import { checkbox, listRow } from "../../../../lib/tui/rows.ts";
16
17
  import { paint } from "../../../../lib/tui/tokens.ts";
17
- import { settingsSummarySegments, type PanelScope } from "../format.ts";
18
18
  import type { SettingView } from "../settings.ts";
19
- import { configPath } from "../settings-runtime.ts";
20
- import type { WriteScope } from "../../picker/types.ts";
21
- import { countsLine, INDENT, savedLine } from "./frame.ts";
22
-
23
- /** Pi's personal settings file, named in the detail pane. */
24
- const PI_SETTINGS_FILE = "settings.json";
19
+ import { INDENT } from "./frame.ts";
25
20
 
26
21
  /** Columns the marker and the leading cell take before the label. */
27
22
  const ROW_PREFIX_WIDTH = 4;
28
23
  /** Columns a value keeps when the row has to choose between the label and the value. */
29
24
  const MIN_VALUE_WIDTH = 8;
30
- /** Columns between the label and its value. */
25
+ /** Columns between the label column and its value. */
31
26
  const VALUE_GAP = 2;
32
27
 
33
28
  /**
@@ -53,141 +48,45 @@ function editingCell(buffer: string, caret: number, budget: number): string {
53
48
  return `${before}▏${after}`;
54
49
  }
55
50
 
56
- /** The rows this section shows, and the scope they were read in. */
57
- export function settingsSummary(theme: Theme, width: number, scope: PanelScope, count: number): string {
58
- return countsLine(theme, width, settingsSummarySegments(scope, count));
51
+ /**
52
+ * The label column: the widest label on screen, capped so a value always keeps
53
+ * `MIN_VALUE_WIDTH` columns and a long label is clipped rather than the value
54
+ * pushed off the row.
55
+ */
56
+ export function settingLabelColumn(views: readonly SettingView[], width: number): number {
57
+ const widest = views.reduce((columns, view) => Math.max(columns, visibleWidth(sanitizeLine(view.label))), 0);
58
+ const room = Math.max(0, width - ROW_PREFIX_WIDTH - VALUE_GAP - MIN_VALUE_WIDTH);
59
+ return Math.max(0, Math.min(widest, room));
59
60
  }
60
61
 
61
- /** One setting row: a checkbox for booleans, and the value beside the label. */
62
+ /** One setting row: a checkbox for a boolean, the label, and the value in its column. */
62
63
  export function settingRow(
63
64
  theme: Theme,
64
65
  width: number,
65
66
  view: SettingView,
66
67
  selected: boolean,
67
- editing?: { buffer: string; caret: number },
68
+ editing: { buffer: string; caret: number } | undefined,
69
+ labelColumn: number,
68
70
  ): string {
69
71
  const label = sanitizeLine(view.label);
70
- // Keep the whole label when it fits, and never let a long value or an editing
71
- // buffer erase it: the value gets what is left, with a floor of eight columns.
72
- const available = Math.max(1, width - ROW_PREFIX_WIDTH - VALUE_GAP);
73
- const labelReserve = Math.min(visibleWidth(label), Math.max(0, available - MIN_VALUE_WIDTH));
74
- const valueBudget = Math.max(1, available - labelReserve);
75
- const rawValue = editing ? editingCell(editing.buffer, editing.caret, valueBudget) : view.valueLabel;
72
+ const available = Math.max(1, width - ROW_PREFIX_WIDTH - VALUE_GAP - labelColumn);
73
+ const rawValue = editing ? editingCell(editing.buffer, editing.caret, available) : view.valueLabel;
76
74
  return listRow(theme, width, {
77
75
  selected,
78
76
  leading: view.kind === "boolean" ? checkbox(theme, view.value === "true") : " ",
79
- label,
77
+ label: padTo(label, labelColumn),
80
78
  labelRole: selected ? "accent" : "label",
81
- trailing: clip(sanitizeLine(rawValue), valueBudget),
79
+ trailing: clip(sanitizeLine(rawValue), available),
82
80
  trailingRole: editing ? "accent" : "meta",
83
- // The value sits beside the label, so a wide terminal never strands it at
84
- // the far edge where the eye has to travel to read it.
81
+ // The value sits in its own column rather than at the far edge, so a wide
82
+ // terminal does not put the label and its value at opposite ends of the row.
85
83
  align: "inline",
86
84
  });
87
85
  }
88
86
 
89
87
  /** The project note: app rows are absent because Pi has no project setter. */
90
88
  export function projectNote(theme: Theme, width: number): string {
91
- return clip(`${INDENT}${paint(theme, "faint", "App settings are Global only.")}`, width);
92
- }
93
-
94
- /** Where the focused row's value is written, and which scope supplied it. */
95
- function originLine(view: SettingView, paths: { scope: WriteScope; cwd: string; agentDir: string }): string {
96
- const scope = view.origin === "inherited" ? "global" : paths.scope;
97
- const path =
98
- view.source === "app" ? join(paths.agentDir, PI_SETTINGS_FILE) : configPath(scope, paths.cwd, paths.agentDir);
99
- const file = sanitizeLine(path);
100
- switch (view.origin) {
101
- case "inherited":
102
- return `Inherited from Global · ${file}`;
103
- case "project":
104
- return `Project · ${file}`;
105
- case "global":
106
- return `Global · ${file}`;
107
- default:
108
- return `Default · ${file}`;
109
- }
110
- }
111
-
112
- /** The focused setting's source, description, origin, and pending changes. */
113
- export function settingDetail(options: {
114
- theme: Theme;
115
- width: number;
116
- view: SettingView | undefined;
117
- budget: number;
118
- dirty: boolean;
119
- paths: { scope: WriteScope; cwd: string; agentDir: string };
120
- }): string[] {
121
- const { theme, width, view, budget, dirty, paths } = options;
122
- if (!view) return savedLine(theme, width, dirty);
123
- const source = view.source === "app" ? "Pi setting" : "bytefly.json";
124
- const head = `${paint(theme, "meta", sanitizeLine(view.label))} ${paint(theme, "faint", "—")} ${paint(theme, "faint", source)}`;
125
- const headLine = clip(`${INDENT}${head}`, width);
126
- // The pending-changes marker survives the collapsed body: on a short terminal
127
- // the value still changed, and the screen has to say so.
128
- const saved = savedLine(theme, width, dirty);
129
- if (budget === 0) return saved;
130
-
131
- const lines = [headLine];
132
- const descriptionWidth = Math.max(1, width - BODY_INDENT * 2);
133
- const description = wrapTextWithAnsi(sanitizeLine(view.description), descriptionWidth).slice(
134
- 0,
135
- Math.max(0, Math.min(2, budget - 2 - saved.length)),
136
- );
137
- for (const line of description) lines.push(clip(`${INDENT}${paint(theme, "meta", line)}`, width));
138
- lines.push(clip(`${INDENT}${paint(theme, "faint", originLine(view, paths))}`, width));
139
- lines.push(...saved);
140
- return lines;
141
- }
142
-
143
- /** The section's key hints, in the order they drop as the width runs out. */
144
- export function settingsHints(json = false): string[] {
145
- return ["Esc close", "←/→", "Space change", "Tab scope", "u reset", json ? "J hide" : "J JSON", "↑/↓ select"];
146
- }
147
-
148
- /**
149
- * The focused row's section as the active scope stores it.
150
- *
151
- * This is the only view of the keys the catalogue leaves to JSON, so it names the
152
- * section and its file rather than pretending to be complete: the pane is clipped
153
- * to the detail budget and says how many lines it left out. `section` is the row's
154
- * `bytefly.json` section, which the caller has already established.
155
- */
156
- export function sectionJsonPane(options: {
157
- theme: Theme;
158
- width: number;
159
- section: string;
160
- document: unknown;
161
- budget: number;
162
- dirty: boolean;
163
- paths: { scope: WriteScope; cwd: string; agentDir: string };
164
- }): string[] {
165
- const { theme, width, section, document, budget, dirty, paths } = options;
166
- const saved = savedLine(theme, width, dirty);
167
- if (budget === 0) return saved;
168
- const scope = paths.scope === "global" ? "Global" : "Project";
169
- const file = sanitizeLine(configPath(paths.scope, paths.cwd, paths.agentDir));
170
- const source = paint(theme, "faint", `${scope} · ${file}`);
171
- const head = `${paint(theme, "meta", sanitizeLine(section))} ${paint(theme, "faint", "—")} ${source}`;
172
- const lines = [clip(`${INDENT}${head}`, width)];
173
- const room = Math.max(0, budget - lines.length - saved.length);
174
- const body = document === undefined ? [] : JSON.stringify(document, null, 2).split("\n");
175
-
176
- if (body.length === 0) {
177
- if (room > 0) lines.push(clip(`${INDENT}${paint(theme, "faint", "Not set in this scope.")}`, width));
178
- lines.push(...saved);
179
- return lines;
180
- }
181
-
182
- const shown = body.slice(0, room);
183
- if (body.length > room && shown.length > 0) {
184
- // The marker replaces one already-shown line, so the hidden count is one
185
- // larger than the number of lines the slice left out.
186
- shown[shown.length - 1] = `… ${body.length - room + 1} more lines`;
187
- }
188
- for (const line of shown) lines.push(clip(`${INDENT}${paint(theme, "faint", sanitizeLine(line))}`, width));
189
- lines.push(...saved);
190
- return lines;
89
+ return clip(`${INDENT}${paint(theme, "faint", "App settings are Global only.")}`, Math.max(1, width));
191
90
  }
192
91
 
193
92
  /** The editor's key hints: `Esc` cancels the edit, it does not close the screen. */