@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,12 +1,14 @@
1
1
  /**
2
- * The `/bytefly` surface: one command, `/bytefly [doctor] [settings] [skills] [update] [extension] [refresh]`.
2
+ * The `/bytefly` surface: one command, `/bytefly [doctor] [settings] [skills] [update] [upgrade] [extension] [refresh]`.
3
3
  *
4
4
  * Without a section word it opens the extension screen: the dock in `tui`, the
5
5
  * grouped listing elsewhere. `settings` opens the Settings section, or prints
6
6
  * its grouped listing outside `tui` without resolving packages. `skills` opens
7
7
  * the Skills section and lists the resolved skills in text modes. `update`
8
8
  * re-runs the published-version check and reports what it found, which is the
9
- * one place a request is made on demand. With `doctor` it produces the doctor
9
+ * one place a request is made on demand; `upgrade` asks Pi's own package manager
10
+ * to install what that check found, behind a confirmation, so the two verbs stay
11
+ * one job each — ask, and act. With `doctor` it produces the doctor
10
12
  * report — the same text `run_doctor` returns — for the whole set or one named
11
13
  * extension; in `tui` the report is the dock screen. Discovery, the doctor
12
14
  * probe, and the dirty-to-reload confirm all live here. The doctor runtime and
@@ -27,7 +29,11 @@ import type { UpdateHandle } from "../update/register.ts";
27
29
  import { createSettingsStores, discoverResources, type DiscoveryDeps } from "../picker/discovery.ts";
28
30
  import { createPickerRuntime, type PickerRuntime } from "../picker/runtime.ts";
29
31
  import type { Discovery } from "../picker/types.ts";
32
+ import { usageRecords } from "../footer/collect/entries.ts";
33
+ import { loadByteflyConfig } from "../footer/price/config.ts";
34
+ import { createPricer } from "../footer/price/price.ts";
30
35
  import { formatListing, formatSettingsListing, SKILL_NOUN } from "./format.ts";
36
+ import { pricesReport } from "./prices.ts";
31
37
  import { buildPanelModel, buildSkillModel, extensionName } from "./join.ts";
32
38
  import { createSettingsRuntime, type SettingsRuntime, type SettingsRuntimeDeps } from "./settings-runtime.ts";
33
39
  import { loadSkillIndex, resolvedSkillPaths, type SkillIndex } from "./skills.ts";
@@ -63,14 +69,23 @@ function selectableNames(discovery: Discovery): Set<string> {
63
69
  }
64
70
 
65
71
  /** The values a bare `/bytefly <tab>` offers, in screen order. */
66
- const FIRST_TOKENS = ["doctor", "settings", "skills", "update", ...Object.keys(REQUIREMENTS), "refresh"] as const;
72
+ const FIRST_TOKENS = [
73
+ "doctor",
74
+ "prices",
75
+ "settings",
76
+ "skills",
77
+ "update",
78
+ "upgrade",
79
+ ...Object.keys(REQUIREMENTS),
80
+ "refresh",
81
+ ] as const;
67
82
 
68
83
  /**
69
84
  * Argument completions. The host replaces the whole argument text with the
70
85
  * returned `value`, so a second token returns the full `doctor <name>` or
71
86
  * `<name> refresh` argument rather than just the word. `/bytefly settings`,
72
- * `/bytefly skills`, and `/bytefly update` take no name, and `refresh` is never
73
- * followed by another token.
87
+ * `/bytefly skills`, `/bytefly update`, and `/bytefly upgrade` take no name, and
88
+ * `refresh` is never followed by another token.
74
89
  */
75
90
  function completions(prefix: string): Array<{ value: string; label: string }> | null {
76
91
  const [first = "", second, ...rest] = prefix.trimStart().split(/\s+/);
@@ -79,7 +94,8 @@ function completions(prefix: string): Array<{ value: string; label: string }> |
79
94
  const options = FIRST_TOKENS.filter((option) => option.startsWith(first));
80
95
  return options.length > 0 ? options.map((option) => ({ value: `${option} `, label: option })) : null;
81
96
  }
82
- if (first === "settings" || first === "skills" || first === "refresh" || first === "update") return null;
97
+ if (first === "settings" || first === "skills" || first === "refresh" || first === "update" || first === "upgrade")
98
+ return null;
83
99
  const options = (first === "doctor" ? [...Object.keys(REQUIREMENTS), "refresh"] : ["refresh"]).filter((option) =>
84
100
  option.startsWith(second),
85
101
  );
@@ -87,7 +103,7 @@ function completions(prefix: string): Array<{ value: string; label: string }> |
87
103
  }
88
104
 
89
105
  /** Which bytefly surface the arguments select. */
90
- type PanelMode = "screen" | "doctor" | "update";
106
+ type PanelMode = "screen" | "doctor" | "update" | "upgrade" | "prices";
91
107
 
92
108
  /** `/bytefly [doctor] [settings] [extension] [refresh]`, parsed. */
93
109
  interface PanelArgs {
@@ -109,6 +125,13 @@ function parseArgs(args: string): PanelArgs {
109
125
  // `update` is already a refresh of the published version, so a stray `refresh`
110
126
  // beside it is accepted and ignored rather than refused.
111
127
  if (rest[0] === "update") return { mode: "update", section: "extensions", refresh };
128
+ // `upgrade` is the same subject as `update`, one verb further: it installs what a
129
+ // check found. Refusing to fold them into one word is what keeps a command that
130
+ // writes behind a confirmation instead of behind an argument.
131
+ if (rest[0] === "upgrade") return { mode: "upgrade", section: "extensions", refresh };
132
+ // `prices` answers a question about this session's totals, so it takes no name and
133
+ // no refresh: it prices what the session holds, once, when asked.
134
+ if (rest[0] === "prices") return { mode: "prices", section: "extensions", refresh };
112
135
  if (rest[0] === "settings") return { mode: "screen", section: "settings", name: rest[1], refresh };
113
136
  if (rest[0] === "skills") return { mode: "screen", section: "skills", name: rest[1], refresh };
114
137
  return { mode: "screen", section: "extensions", name: rest[0], refresh };
@@ -166,6 +189,18 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
166
189
  return;
167
190
  }
168
191
  const cwd = ctx.cwd ?? process.cwd();
192
+ // `/bytefly prices` answers one question about the session and resolves nothing: it
193
+ // prices what the transcript already recorded and names what that left out. It runs
194
+ // in every mode, because the footer that would carry the same fact exists only in
195
+ // `tui`.
196
+ if (mode === "prices") {
197
+ const config = loadByteflyConfig(cwd);
198
+ const active = ctx.model ? { provider: ctx.model.provider, model: ctx.model.id } : undefined;
199
+ const scanned = usageRecords(ctx.sessionManager.getEntries(), active);
200
+ ctx.ui.notify(pricesReport(createPricer(config)(scanned.records), config.currency).join("\n"), "info");
201
+ return;
202
+ }
203
+
169
204
  const agentDir = getAgentDir();
170
205
  const trusted = ctx.isProjectTrusted();
171
206
  const themes = settingsThemeNames(ctx);
@@ -190,6 +225,24 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
190
225
  return;
191
226
  }
192
227
 
228
+ // `/bytefly upgrade` is the only path here that writes, and it writes outside
229
+ // the session: Pi's package manager replaces the installed files, so the
230
+ // confirmation and the reload line are part of the answer, not decoration.
231
+ if (mode === "upgrade") {
232
+ const confirmed = await ctx.ui.confirm(
233
+ "Update packages?",
234
+ `Run Pi's package manager for every configured package, then reload to use it.`,
235
+ );
236
+ if (!confirmed) {
237
+ ctx.ui.notify("bytefly: update cancelled.", "info");
238
+ return;
239
+ }
240
+ const outcome = await deps.update.apply();
241
+ const { text, severity } = updateOutcomeText(outcome, pkg.version);
242
+ ctx.ui.notify(text, severity);
243
+ return;
244
+ }
245
+
193
246
  // Settings outside `tui` prints its grouped listing without resolving
194
247
  // packages or probing the doctor.
195
248
  if (section === "settings" && ctx.mode !== "tui") {
@@ -293,6 +346,13 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
293
346
  refresh: async (force) => {
294
347
  reports = await deps.doctor.probeAll({ refresh: force === true });
295
348
  },
349
+ reload: async () => {
350
+ // The reload takes the session, so the screen asks first and says
351
+ // what will change; a refusal leaves the dock exactly as it was.
352
+ const reload = await ctx.ui.confirm("Reload to apply changes?", "Changes take effect after a reload.");
353
+ if (!reload) return;
354
+ await ctx.reload();
355
+ },
296
356
  onClose: () => {
297
357
  // A late error falls back to the transcript, never a disposed screen.
298
358
  panel = undefined;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The two filters the screen offers: a text query, and a switch that keeps only
3
+ * the rows worth acting on.
4
+ *
5
+ * A filter is part of the model build, not a post-pass over a rendered list, so a
6
+ * group's count and the range row stay truthful about what is on screen. The chips
7
+ * the tab row prints come from the same object, so the screen cannot say it is
8
+ * filtered without saying by what.
9
+ *
10
+ * Value-only: pure predicates over rows and views.
11
+ */
12
+
13
+ import type { SummarySegment } from "./format.ts";
14
+ import { rowBadge } from "./format.ts";
15
+ import type { PanelRow } from "./types.ts";
16
+ import type { SettingView } from "./settings.ts";
17
+
18
+ /** What the reader narrowed the list to. */
19
+ export interface PanelFilter {
20
+ /** Case-insensitive text matched against a row's name, path and source. */
21
+ query: string;
22
+ /** Keep only rows that need attention (settings: values changed from default). */
23
+ attentionOnly: boolean;
24
+ }
25
+
26
+ /** The unfiltered state. */
27
+ export const NO_FILTER: PanelFilter = { query: "", attentionOnly: false };
28
+
29
+ /** Whether anything is narrowed, so the chrome can say so. */
30
+ export function isFiltering(filter: PanelFilter): boolean {
31
+ return filter.query.trim() !== "" || filter.attentionOnly;
32
+ }
33
+
34
+ /** Whether the row's own text mentions `query`. */
35
+ function mentions(text: readonly (string | undefined)[], query: string): boolean {
36
+ const needle = query.toLowerCase();
37
+ return text.some((value) => value !== undefined && value.toLowerCase().includes(needle));
38
+ }
39
+
40
+ /**
41
+ * Whether a resource row survives the filter. The query reaches the picker's path
42
+ * and source as well as the name, because "which file is this" is the question a
43
+ * reader asks of a row they cannot place.
44
+ */
45
+ export function rowMatches(row: PanelRow, filter: PanelFilter): boolean {
46
+ if (filter.attentionOnly && rowBadge(row).glyph === undefined) return false;
47
+ if (!mentions([row.label, row.key, row.item?.path, row.report?.extension, row.skill?.description], filter.query)) {
48
+ return false;
49
+ }
50
+ return true;
51
+ }
52
+
53
+ /** Whether a settings row survives the filter; "changed" means not at its default. */
54
+ export function settingMatches(view: SettingView, filter: PanelFilter): boolean {
55
+ if (filter.attentionOnly && view.origin === "default") return false;
56
+ return mentions([view.label, view.id, view.description, view.valueLabel], filter.query);
57
+ }
58
+
59
+ /** The chips the tab row shows while a filter is on. */
60
+ export function filterChips(filter: PanelFilter, kind: "rows" | "settings"): SummarySegment[] {
61
+ const chips: SummarySegment[] = [];
62
+ const query = filter.query.trim();
63
+ if (query !== "") chips.push({ text: `/${query}`, color: "meta" });
64
+ if (filter.attentionOnly) {
65
+ chips.push({ text: kind === "settings" ? "changed only" : "needs attention", color: "warn" });
66
+ }
67
+ return chips;
68
+ }
@@ -127,23 +127,24 @@ const SECTION_LABELS: Record<PanelSection, string> = {
127
127
  settings: "Settings",
128
128
  };
129
129
 
130
+ /** One section's name, as the tab row prints it. */
131
+ export function sectionLabel(section: PanelSection): string {
132
+ return SECTION_LABELS[section];
133
+ }
134
+
130
135
  /**
131
- * The section bar's segments, in screen order. The active section takes the
132
- * accent and the others recede, so the bar reads as a left-to-right switch.
136
+ * The header's attention line: the worst thing the screen knows, then how many
137
+ * changes a reload would apply. `all clear` is said out loud rather than left
138
+ * blank, so the header never reads as "nothing loaded".
133
139
  */
134
- export function sectionSegments(active: PanelSection): SummarySegment[] {
135
- return SECTION_ORDER.map((section) => ({
136
- text: SECTION_LABELS[section],
137
- color: active === section ? "accent" : "faint",
138
- }));
139
- }
140
-
141
- /** The Settings section's counts: the scope and how many rows it shows. */
142
- export function settingsSummarySegments(scope: PanelScope, count: number): SummarySegment[] {
143
- return [
144
- { text: scope === "global" ? "Global" : "Project", color: "accent" },
145
- { text: `${count} settings`, color: "meta" },
146
- ];
140
+ export function attentionSegments(counts: PanelCounts, dirty: boolean): SummarySegment[] {
141
+ const segments: SummarySegment[] = [];
142
+ if (counts.missing > 0) segments.push({ text: `${counts.missing} missing`, color: "bad" });
143
+ else if (counts.degraded > 0) segments.push({ text: `${counts.degraded} degraded`, color: "warn" });
144
+ else if (counts.off > 0) segments.push({ text: `${counts.off} off`, color: "faint" });
145
+ else segments.push({ text: "all clear", color: "ok" });
146
+ if (dirty) segments.push({ text: "unsaved changes", color: "warn" });
147
+ return segments;
147
148
  }
148
149
 
149
150
  /**
@@ -31,6 +31,17 @@ export function extensionName(path: string): string {
31
31
  return basename(dirname(path));
32
32
  }
33
33
 
34
+ /**
35
+ * The name a resource is called by on screen: the directory for an entry file
36
+ * (`…/worktree/index.ts`, `…/pdf/SKILL.md`), and the file's own stem otherwise, so
37
+ * a flat `extensions/auto.ts` is `auto` rather than the directory every flat file
38
+ * shares.
39
+ */
40
+ export function resourceName(path: string): string {
41
+ const stem = basename(path).replace(/\.(ts|js|mjs|cjs|md)$/u, "");
42
+ return stem === "index" || stem === "SKILL" ? basename(dirname(path)) : stem;
43
+ }
44
+
34
45
  /** Row key for a report that has no resolved extension item. */
35
46
  function reportKey(name: string): string {
36
47
  return `report:${name}`;
@@ -46,13 +57,24 @@ export interface PanelJoinOptions {
46
57
  agentDir: string;
47
58
  /** Narrow to one extension directory or report name, or `undefined` for the full list. */
48
59
  filter?: string;
60
+ /** Keep only the rows this accepts; a group's count follows what survives. */
61
+ where?: (row: PanelRow) => boolean;
49
62
  /** Which resource kind the list is, for labels and grouping. */
50
63
  kind?: ResourceKind;
51
64
  }
52
65
 
66
+ /**
67
+ * The rows one group list produces, group headings included. The screen measures
68
+ * a section's height from this rather than from a built model, so a section it is
69
+ * not showing costs no `SKILL.md` read.
70
+ */
71
+ export function groupedEntryCount(items: readonly ResourceItem[], agentDir: string, kind: ResourceKind): number {
72
+ return buildEntries([...items], agentDir, kind).length;
73
+ }
74
+
53
75
  /** Build the merged model for the current items and reports. */
54
76
  export function buildPanelModel(options: PanelJoinOptions): PanelModel {
55
- const { items, reports, agentDir, filter, kind = "extensions" } = options;
77
+ const { items, reports, agentDir, filter, where, kind = "extensions" } = options;
56
78
  const byName = new Map(reports.map((report) => [report.extension, report]));
57
79
 
58
80
  // A directory name may claim a report only when exactly one unique resource
@@ -60,7 +82,7 @@ export function buildPanelModel(options: PanelJoinOptions): PanelModel {
60
82
  // the report and it stays available to the trailing `Not resolved` group.
61
83
  const pathsByName = new Map<string, Set<string>>();
62
84
  for (const item of items) {
63
- const name = extensionName(item.path);
85
+ const name = resourceName(item.path);
64
86
  const paths = pathsByName.get(name) ?? new Set<string>();
65
87
  paths.add(resourceKey(item));
66
88
  pathsByName.set(name, paths);
@@ -83,12 +105,23 @@ export function buildPanelModel(options: PanelJoinOptions): PanelModel {
83
105
  const report = unambiguous(name) ? byName.get(name) : undefined;
84
106
  if (report) claimed.add(name);
85
107
  if (filter !== undefined && name !== filter) continue;
86
- rows.push({ key: resourceKey(item), item, report, label: entry.label, group: current?.key ?? "" });
108
+ // The row is named for the resource, not for its file: a name is only unique
109
+ // enough to be a name when one resource uses it, which is the same rule that
110
+ // lets a directory claim a report. Otherwise the picker's `folder/file.ts`
111
+ // stays, because two rows would otherwise read alike.
112
+ const label = unambiguous(name) ? resourceName(item.path) : entry.label;
113
+ const row: PanelRow = { key: resourceKey(item), item, report, label, group: current?.key ?? "" };
114
+ if (where !== undefined && !where(row)) continue;
115
+ rows.push(row);
87
116
  if (current) current.count += 1;
88
117
  }
89
118
 
90
119
  const extras = reports.filter(
91
- (report) => !claimed.has(report.extension) && (filter === undefined || report.extension === filter),
120
+ (report) =>
121
+ !claimed.has(report.extension) &&
122
+ (filter === undefined || report.extension === filter) &&
123
+ (where === undefined ||
124
+ where({ key: reportKey(report.extension), report, label: report.extension, group: REPORT_ONLY_GROUP })),
92
125
  );
93
126
  if (extras.length > 0) {
94
127
  const group: PanelGroup = { key: REPORT_ONLY_GROUP, label: REPORT_ONLY_LABEL, count: extras.length };
@@ -111,14 +144,24 @@ export function buildSkillModel(options: {
111
144
  items: readonly ResourceItem[];
112
145
  index: ReadonlyMap<string, SkillInfo>;
113
146
  agentDir: string;
147
+ where?: (row: PanelRow) => boolean;
114
148
  }): PanelModel {
115
- const model = buildPanelModel({ items: options.items, reports: [], agentDir: options.agentDir, kind: "skills" });
116
- return {
117
- groups: model.groups,
118
- rows: model.rows.map((row) => {
119
- const skill = row.item ? options.index.get(row.item.path) : undefined;
120
- if (skill === undefined) return row;
121
- return { ...row, label: skill.name ?? row.label, skill };
122
- }),
123
- };
149
+ // The filter runs after the join: a skill's name and its `SKILL.md` facts are
150
+ // what a reader searches and what decides whether it needs attention.
151
+ const model = buildPanelModel({
152
+ items: options.items,
153
+ reports: [],
154
+ agentDir: options.agentDir,
155
+ kind: "skills",
156
+ });
157
+ const joined = model.rows.map((row) => {
158
+ const skill = row.item ? options.index.get(row.item.path) : undefined;
159
+ if (skill === undefined) return row;
160
+ return { ...row, label: skill.name ?? row.label, skill };
161
+ });
162
+ const rows = options.where === undefined ? joined : joined.filter(options.where);
163
+ const groups = model.groups
164
+ .map((group) => ({ ...group, count: rows.filter((row) => row.group === group.key).length }))
165
+ .filter((group) => group.count > 0);
166
+ return { groups, rows };
124
167
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The `/bytefly prices` report: which models this session could not price.
3
+ *
4
+ * A record nothing priced contributes no cost, so the footer's total is smaller than
5
+ * the work done and the reason is a *model*, not a record count. This is the answer to
6
+ * the question the footer can only hint at: the keys, in the `price.rates` vocabulary,
7
+ * and the one line saying what to do with them.
8
+ *
9
+ * Nothing here quotes a price: a rate is the user's to state, and inventing one is the
10
+ * rate table this command exists instead of.
11
+ */
12
+
13
+ import { sanitizeLine } from "../../../lib/format.ts";
14
+ import type { PricedUsage } from "../footer/types.ts";
15
+ import { formatCost } from "../footer/render/format.ts";
16
+ import { recordsText } from "../footer/unpriced.ts";
17
+ import type { Currency } from "../footer/price/types.ts";
18
+
19
+ /** How many keys the report prints before it counts the rest. */
20
+ export const REPORTED_MODELS = 8;
21
+
22
+ /**
23
+ * The report as the lines a reader gets.
24
+ *
25
+ * The headline says what the total is and how much of the session it leaves out; one
26
+ * line per key says what to add; the last line says where to add it. Pure: the caller
27
+ * has already priced the session.
28
+ */
29
+ export function pricesReport(priced: PricedUsage, currency: Currency): string[] {
30
+ const models = priced.unpricedModels;
31
+ if (models.length === 0) return ["bytefly: every record in this session has a rate — nothing to add."];
32
+ const count = models.reduce((total, model) => total + model.records, 0);
33
+ const headline =
34
+ `bytefly: ${formatCost(priced.cost, currency)} priced · ` +
35
+ `${recordsText(count)} in ${models.length === 1 ? "1 model" : `${models.length} models`} ${count === 1 ? "has" : "have"} no rate`;
36
+ const keys = models
37
+ .slice(0, REPORTED_MODELS)
38
+ .map((model) => ` ${sanitizeLine(model.key)} · ${recordsText(model.records)}`);
39
+ const rest = models.length - keys.length;
40
+ if (rest > 0) keys.push(` +${rest} more`);
41
+ keys.push(' add these under "price.rates" in bytefly.json — a key may be `*`, or a prefix like `gpt-*`.');
42
+ return [headline, ...keys];
43
+ }
@@ -88,12 +88,6 @@ function applyAppSetting(store: SettingsStore, row: SettingDescriptor, raw: stri
88
88
  case "byteflyUi":
89
89
  store.setQuietStartup(value as QuietStartup);
90
90
  return;
91
- case "outputPad":
92
- store.setOutputPad(value as 0 | 1);
93
- return;
94
- case "showHardwareCursor":
95
- store.setShowHardwareCursor(value as boolean);
96
- return;
97
91
  case "theme":
98
92
  store.setTheme(value as string);
99
93
  return;