@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
@@ -26,8 +26,8 @@ import { formatExtension, formatReport, hasProblems } from "../doctor/format.ts"
26
26
  import type { DoctorRuntime } from "../doctor/runtime.ts";
27
27
  import { updateOutcomeText } from "../update/format.ts";
28
28
  import type { UpdateHandle } from "../update/register.ts";
29
- import { createSettingsStores, discoverResources, type DiscoveryDeps } from "../picker/discovery.ts";
30
- import { createPickerRuntime, type PickerRuntime } from "../picker/runtime.ts";
29
+ import type { DiscoveryDeps } from "../picker/discovery.ts";
30
+ import type { PickerRuntime } from "../picker/runtime.ts";
31
31
  import type { Discovery } from "../picker/types.ts";
32
32
  import { usageRecords } from "../footer/collect/entries.ts";
33
33
  import { loadByteflyConfig } from "../footer/price/config.ts";
@@ -35,10 +35,9 @@ import { createPricer } from "../footer/price/price.ts";
35
35
  import { formatListing, formatSettingsListing, SKILL_NOUN } from "./format.ts";
36
36
  import { pricesReport } from "./prices.ts";
37
37
  import { buildPanelModel, buildSkillModel, extensionName } from "./join.ts";
38
- import { createSettingsRuntime, type SettingsRuntime, type SettingsRuntimeDeps } from "./settings-runtime.ts";
39
- import { loadSkillIndex, resolvedSkillPaths, type SkillIndex } from "./skills.ts";
38
+ import type { SettingsRuntime, SettingsRuntimeDeps } from "./settings-runtime.ts";
39
+ import type { SkillIndex } from "./skills.ts";
40
40
  import { shippedThemeNames } from "./themes.ts";
41
- import { PanelComponent } from "./tui.ts";
42
41
  import type { PanelSection } from "./types.ts";
43
42
 
44
43
  /** Host seams for the panel command, injected by tests and the composition root. */
@@ -174,12 +173,186 @@ function settingsThemeNames(ctx: ExtensionCommandContext): string[] {
174
173
  }
175
174
  }
176
175
 
176
+ /** The state one `/bytefly` invocation carries into the branch that answers it. */
177
+ interface PanelRun {
178
+ pi: ExtensionAPI;
179
+ deps: PanelCommandDeps;
180
+ ctx: ExtensionCommandContext;
181
+ cwd: string;
182
+ agentDir: string;
183
+ trusted: boolean;
184
+ themes: string[];
185
+ section: PanelSection;
186
+ name: string | undefined;
187
+ refresh: boolean;
188
+ mode: PanelMode;
189
+ }
190
+
191
+ /** The mounted panel, so a notice before the screen exists still reaches the transcript. */
192
+ interface PanelHolder {
193
+ current: InstanceType<typeof import("./tui.ts").PanelComponent> | undefined;
194
+ }
195
+
196
+ /** `/bytefly prices`: one question about the transcript, no discovery. */
197
+ function pricesAnswer(ctx: ExtensionCommandContext, cwd: string): void {
198
+ const config = loadByteflyConfig(cwd);
199
+ const active = ctx.model ? { provider: ctx.model.provider, model: ctx.model.id } : undefined;
200
+ const scanned = usageRecords(ctx.sessionManager.getEntries(), active);
201
+ ctx.ui.notify(pricesReport(createPricer(config)(scanned.records), config.currency).join("\n"), "info");
202
+ }
203
+
204
+ /** `/bytefly update` and `/bytefly upgrade`: the check, and the install it offers. */
205
+ async function updateAnswer(
206
+ deps: PanelCommandDeps,
207
+ mode: "update" | "upgrade",
208
+ ctx: ExtensionCommandContext,
209
+ ): Promise<void> {
210
+ if (mode === "update") {
211
+ const outcome = await deps.update.recheck();
212
+ const { text, severity } = updateOutcomeText(outcome, pkg.version);
213
+ ctx.ui.notify(text, severity);
214
+ return;
215
+ }
216
+ const confirmed = await ctx.ui.confirm(
217
+ "Update packages?",
218
+ `Run Pi's package manager for every configured package, then reload to use it.`,
219
+ );
220
+ if (!confirmed) {
221
+ ctx.ui.notify("bytefly: update cancelled.", "info");
222
+ return;
223
+ }
224
+ const outcome = await deps.update.apply();
225
+ const { text, severity } = updateOutcomeText(outcome, pkg.version);
226
+ ctx.ui.notify(text, severity);
227
+ }
228
+
229
+ /** Settings outside `tui`: the grouped listing without resolving packages. */
230
+ function settingsText(
231
+ run: PanelRun,
232
+ makeSettings: NonNullable<PanelCommandDeps["settings"]>,
233
+ createSettingsStores: (cwd: string, agentDir: string, trusted: boolean) => Discovery["stores"],
234
+ surfaceNotice: (message: string, kind?: "warning" | "error") => void,
235
+ ): void {
236
+ const stores = createSettingsStores(run.cwd, run.agentDir, run.trusted);
237
+ const settings = makeSettings({
238
+ stores,
239
+ cwd: run.cwd,
240
+ agentDir: run.agentDir,
241
+ trusted: run.trusted,
242
+ themes: run.themes,
243
+ onError: (message) => surfaceNotice(message, "warning"),
244
+ });
245
+ run.ctx.ui.notify(formatSettingsListing(settings.views("global")), "info");
246
+ }
247
+
248
+ /** Skills outside `tui`: the grouped listing from Pi's metadata, with no probe. */
249
+ function skillsText(
250
+ run: PanelRun,
251
+ runtime: PickerRuntime,
252
+ discovery: Discovery,
253
+ loadSkills: (paths: readonly string[], cwd: string, agentDir: string) => SkillIndex,
254
+ resolveSkillPaths: (discovery: Discovery) => string[],
255
+ ): void {
256
+ const index = loadSkills(resolveSkillPaths(discovery), run.cwd, run.agentDir);
257
+ const model = buildSkillModel({ items: runtime.items("skills"), index, agentDir: run.agentDir });
258
+ run.ctx.ui.notify(
259
+ formatListing(model, runtime.scope(), { noun: SKILL_NOUN, emptyLabel: "No skills found." }),
260
+ "info",
261
+ );
262
+ }
263
+
264
+ /** Text modes: probe now, then print the report, a named extension, or the listing. */
265
+ async function reportText(run: PanelRun, runtime: PickerRuntime): Promise<void> {
266
+ let reports: ExtensionReport[];
267
+ try {
268
+ reports = await run.deps.doctor.probeAll({ refresh: run.refresh });
269
+ } catch (error) {
270
+ run.ctx.ui.notify(`Could not probe extensions: ${error instanceof Error ? error.message : String(error)}`, "error");
271
+ return;
272
+ }
273
+ if (run.name !== undefined) {
274
+ notifyExtension(run.ctx, run.name, reports, runtime);
275
+ return;
276
+ }
277
+ if (run.mode === "doctor") {
278
+ run.ctx.ui.notify(formatReport(reports), hasProblems(reports) ? "warning" : "info");
279
+ return;
280
+ }
281
+ const model = buildPanelModel({ items: runtime.items("extensions"), reports, agentDir: run.agentDir });
282
+ run.ctx.ui.notify(formatListing(model, runtime.scope()), "info");
283
+ }
284
+
285
+ /** The TUI branch: mount the screen, then apply any pending write. */
286
+ async function mountPanel(
287
+ run: PanelRun,
288
+ state: {
289
+ runtime: PickerRuntime;
290
+ settings: SettingsRuntime;
291
+ discovery: Discovery;
292
+ loadSkills: (paths: readonly string[], cwd: string, agentDir: string) => SkillIndex;
293
+ resolveSkillPaths: (discovery: Discovery) => string[];
294
+ cached: ExtensionReport[] | undefined;
295
+ holder: PanelHolder;
296
+ },
297
+ ): Promise<void> {
298
+ const { runtime, settings, discovery, loadSkills, resolveSkillPaths, cached, holder } = state;
299
+ const { ctx, section, name, refresh, agentDir, cwd } = run;
300
+ let reports: ExtensionReport[] = cached ?? [];
301
+
302
+ const { PanelComponent } = await import("./tui.ts");
303
+ // The dock screen owns the editor slot; hide the rails for its lifetime.
304
+ await withRailsSuppressed(run.pi, () =>
305
+ ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
306
+ const unsubscribe = runtime.onChange(() => tui.requestRender());
307
+ // The settings runtime re-reads its snapshot after a write; without a
308
+ // subscriber that repaint depends on every write chain's own callback.
309
+ const unsubscribeSettings = settings.onChange(() => tui.requestRender());
310
+ const component = new PanelComponent({
311
+ runtime,
312
+ reports: () => reports,
313
+ settings,
314
+ filter: section === "extensions" ? name : undefined,
315
+ section,
316
+ theme,
317
+ agentDir,
318
+ cwd,
319
+ // A cached session probes only when the command asked for it.
320
+ needsProbe: cached === undefined || refresh,
321
+ initialRefresh: refresh,
322
+ viewportRows: () => tui.terminal?.rows,
323
+ requestRender: () => tui.requestRender(),
324
+ skills: () => loadSkills(resolveSkillPaths(discovery), cwd, agentDir),
325
+ refresh: async (force) => {
326
+ reports = await run.deps.doctor.probeAll({ refresh: force === true });
327
+ },
328
+ reload: async () => {
329
+ // The reload takes the session, so the screen asks first and says
330
+ // what will change; a refusal leaves the dock exactly as it was.
331
+ const reload = await ctx.ui.confirm("Reload to apply changes?", "Changes take effect after a reload.");
332
+ if (!reload) return;
333
+ await ctx.reload();
334
+ },
335
+ onClose: () => {
336
+ // A late error falls back to the transcript, never a disposed screen.
337
+ holder.current = undefined;
338
+ unsubscribe();
339
+ unsubscribeSettings();
340
+ done();
341
+ },
342
+ });
343
+ holder.current = component;
344
+ component.prime();
345
+ return component;
346
+ }),
347
+ );
348
+
349
+ if (!runtime.isDirty() && !settings.isDirty()) return;
350
+ const reload = await ctx.ui.confirm("Reload to apply changes?", "Changes take effect after a reload.");
351
+ if (reload) await ctx.reload();
352
+ }
353
+
177
354
  /** Register the single `/bytefly` command. */
178
355
  export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
179
- const discover = deps.discover ?? discoverResources;
180
- const makeSettings = deps.settings ?? createSettingsRuntime;
181
- const loadSkills = deps.skills ?? loadSkillIndex;
182
-
183
356
  const handler = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
184
357
  const { mode, section, name, refresh } = parseArgs(args);
185
358
  // Only the Extensions section takes an extension argument; a stray word is a
@@ -193,13 +366,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
193
366
  // prices what the transcript already recorded and names what that left out. It runs
194
367
  // in every mode, because the footer that would carry the same fact exists only in
195
368
  // `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
- }
369
+ if (mode === "prices") return pricesAnswer(ctx, cwd);
203
370
 
204
371
  const agentDir = getAgentDir();
205
372
  const trusted = ctx.isProjectTrusted();
@@ -208,47 +375,28 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
208
375
  // the frame, not appended to the transcript behind it. Before the screen
209
376
  // exists (the non-TUI settings path, a pre-mount failure) the transcript is
210
377
  // the only surface.
211
- let panel: PanelComponent | undefined;
378
+ const holder: PanelHolder = { current: undefined };
212
379
  const surfaceNotice = (message: string, kind: "warning" | "error" = "warning"): void => {
213
- if (panel) panel.notify(message);
380
+ if (holder.current) holder.current.notify(message);
214
381
  else ctx.ui.notify(message, kind);
215
382
  };
216
383
  const settingsNotice = (message: string): void => surfaceNotice(message, "warning");
384
+ const run: PanelRun = { pi, deps, ctx, cwd, agentDir, trusted, themes, section, name, refresh, mode };
217
385
 
218
- // `/bytefly update` answers one question and resolves nothing: no package
219
- // discovery, no doctor probe, no screen. It is the one path a version check
220
- // takes on demand, so the request happens here and nowhere else.
221
- if (mode === "update") {
222
- const outcome = await deps.update.recheck();
223
- const { text, severity } = updateOutcomeText(outcome, pkg.version);
224
- ctx.ui.notify(text, severity);
225
- return;
226
- }
386
+ // `/bytefly update` answers one question and resolves nothing; `upgrade` is the
387
+ // same subject one verb further, behind a confirmation because it writes.
388
+ if (mode === "update" || mode === "upgrade") return updateAnswer(deps, mode, ctx);
227
389
 
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
- }
390
+ // Settings, discovery, and the picker are the command's, not registration's: the
391
+ // price, update, and upgrade answers above resolve none of them.
392
+ const makeSettings = deps.settings ?? (await import("./settings-runtime.ts")).createSettingsRuntime;
393
+ const { createSettingsStores, discoverResources } = await import("../picker/discovery.ts");
394
+ const discover = deps.discover ?? discoverResources;
245
395
 
246
396
  // Settings outside `tui` prints its grouped listing without resolving
247
397
  // packages or probing the doctor.
248
398
  if (section === "settings" && ctx.mode !== "tui") {
249
- const stores = createSettingsStores(cwd, agentDir, trusted);
250
- const settings = makeSettings({ stores, cwd, agentDir, trusted, themes, onError: settingsNotice });
251
- ctx.ui.notify(formatSettingsListing(settings.views("global")), "info");
399
+ settingsText(run, makeSettings, createSettingsStores, surfaceNotice);
252
400
  return;
253
401
  }
254
402
 
@@ -269,6 +417,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
269
417
  onError: settingsNotice,
270
418
  });
271
419
 
420
+ const { createPickerRuntime } = await import("../picker/runtime.ts");
272
421
  const runtime = createPickerRuntime({
273
422
  discovery,
274
423
  cwd,
@@ -283,15 +432,13 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
283
432
  return;
284
433
  }
285
434
 
435
+ const { loadSkillIndex, resolvedSkillPaths } = await import("./skills.ts");
436
+ const loadSkills = deps.skills ?? loadSkillIndex;
437
+
286
438
  // Skills outside `tui` print the same grouped listing, from the metadata Pi's
287
439
  // loader reads; there is no report to probe for and no package to resolve.
288
440
  if (section === "skills" && ctx.mode !== "tui") {
289
- const index = loadSkills(resolvedSkillPaths(discovery), cwd, agentDir);
290
- const model = buildSkillModel({ items: runtime.items("skills"), index, agentDir });
291
- ctx.ui.notify(
292
- formatListing(model, runtime.scope(), { noun: SKILL_NOUN, emptyLabel: "No skills found." }),
293
- "info",
294
- );
441
+ skillsText(run, runtime, discovery, loadSkills, resolvedSkillPaths);
295
442
  return;
296
443
  }
297
444
 
@@ -299,77 +446,20 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
299
446
  // the session cache and probes on its first Extensions view instead, which
300
447
  // keeps `/bytefly` and `/bytefly settings` instant.
301
448
  const cached = deps.doctor.peek();
302
- let reports: ExtensionReport[] = cached ?? [];
303
-
304
449
  if (ctx.mode !== "tui") {
305
- try {
306
- reports = await deps.doctor.probeAll({ refresh });
307
- } catch (error) {
308
- ctx.ui.notify(`Could not probe extensions: ${error instanceof Error ? error.message : String(error)}`, "error");
309
- return;
310
- }
311
- if (name !== undefined) {
312
- notifyExtension(ctx, name, reports, runtime);
313
- return;
314
- }
315
- if (mode === "doctor") {
316
- ctx.ui.notify(formatReport(reports), hasProblems(reports) ? "warning" : "info");
317
- return;
318
- }
319
- const model = buildPanelModel({ items: runtime.items("extensions"), reports, agentDir });
320
- ctx.ui.notify(formatListing(model, runtime.scope()), "info");
450
+ await reportText(run, runtime);
321
451
  return;
322
452
  }
323
453
 
324
- // The dock screen owns the editor slot; hide the rails for its lifetime.
325
- await withRailsSuppressed(pi, () =>
326
- ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
327
- const unsubscribe = runtime.onChange(() => tui.requestRender());
328
- // The settings runtime re-reads its snapshot after a write; without a
329
- // subscriber that repaint depends on every write chain's own callback.
330
- const unsubscribeSettings = settings.onChange(() => tui.requestRender());
331
- const component = new PanelComponent({
332
- runtime,
333
- reports: () => reports,
334
- settings,
335
- filter: section === "extensions" ? name : undefined,
336
- section,
337
- theme,
338
- agentDir,
339
- cwd,
340
- // A cached session probes only when the command asked for it.
341
- needsProbe: cached === undefined || refresh,
342
- initialRefresh: refresh,
343
- viewportRows: () => tui.terminal?.rows,
344
- requestRender: () => tui.requestRender(),
345
- skills: () => loadSkills(resolvedSkillPaths(discovery), cwd, agentDir),
346
- refresh: async (force) => {
347
- reports = await deps.doctor.probeAll({ refresh: force === true });
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
- },
356
- onClose: () => {
357
- // A late error falls back to the transcript, never a disposed screen.
358
- panel = undefined;
359
- unsubscribe();
360
- unsubscribeSettings();
361
- done();
362
- },
363
- });
364
- panel = component;
365
- component.prime();
366
- return component;
367
- }),
368
- );
369
-
370
- if (!runtime.isDirty() && !settings.isDirty()) return;
371
- const reload = await ctx.ui.confirm("Reload to apply changes?", "Changes take effect after a reload.");
372
- if (reload) await ctx.reload();
454
+ await mountPanel(run, {
455
+ runtime,
456
+ settings,
457
+ discovery,
458
+ loadSkills,
459
+ resolveSkillPaths: resolvedSkillPaths,
460
+ cached,
461
+ holder,
462
+ });
373
463
  };
374
464
 
375
465
  pi.registerCommand("bytefly", {
@@ -9,9 +9,9 @@
9
9
  * from a probe or a settings file is sanitized before it is printed.
10
10
  *
11
11
  * A row has two enablement axes: the picker's settings (`◼`/`◻`) and this
12
- * session's `PI_DISABLED_EXTENSIONS`. `rowBadge` is the one place that decides
13
- * how they read, so the screen, the text listing, and the counts cannot
14
- * contradict each other.
12
+ * session's environment — the disable list, and the opt-in list a default-off
13
+ * extension needs. `rowBadge` is the one place that decides how they read, so
14
+ * the screen, the text listing, and the counts cannot contradict each other.
15
15
  */
16
16
 
17
17
  import { sanitizeLine } from "../../../lib/format.ts";
@@ -182,7 +182,7 @@ interface RowBadge {
182
182
  /**
183
183
  * The row's badge. Settings is the enablement axis the checkbox already shows,
184
184
  * so an off row gets one verdict (`off`) instead of a health word that
185
- * contradicts it. `PI_DISABLED_EXTENSIONS` is a different axis, so it is named
185
+ * contradicts it. The environment switch is a different axis, so it is named
186
186
  * rather than sharing the word: `env disabled`. A skill has no probed report, so
187
187
  * its own file supplies the verdict: `not loaded` when Pi refused it, `hidden`
188
188
  * when it is reachable only through `/skill:<name>`. The glyph marks what needs
@@ -9,8 +9,9 @@
9
9
  * The catalogue is the shortlist a session changes, not the whole document: every
10
10
  * row is one a workflow differs on, and each states its own rule so a value the
11
11
  * screen accepts is a value the loader keeps. The catalogue order is the screen's
12
- * order, and it is capped so the section fits a 24-row terminal (`settings.test.ts`
13
- * holds the ceiling); a new row has to displace one. Everything else stays
12
+ * order, and it is capped — fifteen rows and their five headings want a 28-row
13
+ * terminal, and a shorter one scrolls the list rather than the frame
14
+ * (`settings.test.ts` holds the ceiling); a new row has to displace one. Everything else stays
14
15
  * `bytefly.json` — a list (`doctor.skip`), a table (`price.rates`), a schedule
15
16
  * (`price.peak`), and the tuning numbers and labels this screen deliberately does
16
17
  * not carry — documented in `docs/configuration.md` and reachable from the JSON
@@ -25,6 +26,9 @@
25
26
  */
26
27
 
27
28
  import { configSection, configValueAt } from "../../../lib/config.ts";
29
+ import { DEFAULT_BOARD_CONFIG, normalizeBoardConfig } from "../../../lib/board/config.ts";
30
+ import { MAX_LABEL } from "../../../lib/session-label.ts";
31
+ import { MAX_WIP } from "../../../lib/board/limits.ts";
28
32
  import { DEFAULT_CONFIG as DEFAULT_DOCTOR_CONFIG, normalizeConfig as normalizeDoctorConfig } from "../doctor/config.ts";
29
33
  import {
30
34
  CURRENCY_DEFAULTS,
@@ -53,7 +57,7 @@ type SettingOrigin = "default" | "global" | "project" | "inherited";
53
57
  * rows read as four short lists: what a session starts with, what it tells you
54
58
  * about, what it keeps, and what it costs.
55
59
  */
56
- export type SettingGroup = "Startup" | "Notifications" | "Session" | "Price";
60
+ export type SettingGroup = "Startup" | "Notifications" | "Session" | "Board" | "Price";
57
61
 
58
62
  /** One curated value a row can take. */
59
63
  interface SettingOption {
@@ -70,7 +74,7 @@ interface TextRule {
70
74
  }
71
75
 
72
76
  /** The `bytefly.json` sections a row still reaches. */
73
- export type ConfigSectionName = "notify" | "doctor" | "price" | "rewind" | "job";
77
+ export type ConfigSectionName = "notify" | "doctor" | "price" | "rewind" | "job" | "board";
74
78
 
75
79
  /** Where a `bytefly.json` row lives: a section, and the path to its leaf. */
76
80
  interface SettingConfig {
@@ -153,12 +157,29 @@ function amountRule(min: number, max: number): TextRule {
153
157
  };
154
158
  }
155
159
 
160
+ /**
161
+ * A text rule for a whole number inside an inclusive range: the board's two WIP limits, at the
162
+ * `1..MAX_WIP` the loader clamps to, so the screen cannot save a number the board would shorten.
163
+ */
164
+ function countRule(min: number, max: number): TextRule {
165
+ return {
166
+ maxLength: 2,
167
+ validate(raw) {
168
+ const text = raw.trim();
169
+ if (!/^\d+$/.test(text)) return `Use a whole number, ${min} to ${max}.`;
170
+ const value = Number(text);
171
+ if (value < min || value > max) return `Out of range: ${min} to ${max}.`;
172
+ return undefined;
173
+ },
174
+ };
175
+ }
176
+
156
177
  /**
157
178
  * The catalogue, in screen order.
158
179
  *
159
- * Global scope lists all twelve; Project lists the ten `bytefly.json` rows,
180
+ * Global scope lists all fifteen; Project lists the thirteen `bytefly.json` rows,
160
181
  * because Pi's setters write the personal `settings.json` only. The list is
161
- * capped so the screen keeps fitting a 24-row terminal.
182
+ * capped, and the cap with its reason is stated where the screen's height is.
162
183
  */
163
184
  export const SETTING_ROWS: readonly SettingDescriptor[] = [
164
185
  {
@@ -281,6 +302,50 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
281
302
  options: ON_OFF,
282
303
  config: { section: "doctor", path: ["proactive"] },
283
304
  },
305
+ {
306
+ id: "board.wip.plan",
307
+ group: "Board",
308
+ label: "Plan limit",
309
+ description: "How many cards the board lets a repository plan at once, 1 to 20; `bytefly.json` `board.wip.plan`.",
310
+ source: "bytefly",
311
+ kind: "text",
312
+ fallback: String(DEFAULT_BOARD_CONFIG.wip.plan),
313
+ json: "number",
314
+ options: [],
315
+ text: countRule(1, MAX_WIP),
316
+ config: { section: "board", path: ["wip", "plan"] },
317
+ },
318
+ {
319
+ id: "board.wip.execute",
320
+ group: "Board",
321
+ label: "Execute limit",
322
+ description:
323
+ "How many cards the board lets a repository execute at once, 1 to 20; `bytefly.json` `board.wip.execute`.",
324
+ source: "bytefly",
325
+ kind: "text",
326
+ fallback: String(DEFAULT_BOARD_CONFIG.wip.execute),
327
+ json: "number",
328
+ options: [],
329
+ text: countRule(1, MAX_WIP),
330
+ config: { section: "board", path: ["wip", "execute"] },
331
+ },
332
+ {
333
+ id: "board.lead",
334
+ group: "Board",
335
+ label: "Lead",
336
+ description:
337
+ "The team name whose signature approves a contract and spot-checks the work; a card's holder never signs.",
338
+ source: "bytefly",
339
+ kind: "text",
340
+ fallback: DEFAULT_BOARD_CONFIG.lead,
341
+ options: [],
342
+ text: {
343
+ maxLength: MAX_LABEL,
344
+ validate: (raw) =>
345
+ raw.trim() === "" ? "A lead needs a name; `u` clears the key and the default comes back." : undefined,
346
+ },
347
+ config: { section: "board", path: ["lead"] },
348
+ },
284
349
  {
285
350
  id: "price.currency.code",
286
351
  group: "Price",
@@ -432,6 +497,8 @@ const SECTION_READERS: Partial<Record<ConfigSectionName, SectionReader>> = {
432
497
  configValueAt(normalizeDoctorConfig(projectRaw, normalizeDoctorConfig(globalRaw, DEFAULT_DOCTOR_CONFIG)), path),
433
498
  price: (globalRaw, projectRaw, path) =>
434
499
  configValueAt(normalizeByteflyConfig(projectRaw, normalizeByteflyConfig(globalRaw, DEFAULT_BYTEFLY_CONFIG)), path),
500
+ board: (globalRaw, projectRaw, path) =>
501
+ configValueAt(normalizeBoardConfig(projectRaw, normalizeBoardConfig(globalRaw, DEFAULT_BOARD_CONFIG)), path),
435
502
  };
436
503
 
437
504
  /**
@@ -16,7 +16,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
16
16
  import { Key, matchesKey } from "@earendil-works/pi-tui";
17
17
  import type { ScreenHint } from "../../../../lib/tui/chrome.ts";
18
18
  import { clip, padTo } from "../../../../lib/tui/columns.ts";
19
- import { isCloseKey } from "../../../../lib/tui/cursor.ts";
19
+ import { isCloseKey, isShiftTab } from "../../../../lib/tui/cursor.ts";
20
20
  import { BODY_INDENT } from "../../../../lib/tui/indent.ts";
21
21
  import { paint } from "../../../../lib/tui/tokens.ts";
22
22
  import type { PanelSection } from "../types.ts";
@@ -58,9 +58,6 @@ export interface PanelKey {
58
58
  const ALL: readonly PanelSection[] = ["extensions", "skills", "settings"];
59
59
  const LISTS: readonly PanelSection[] = ["extensions", "skills"];
60
60
 
61
- /** Shift+Tab arrives as this sequence on terminals without the Kitty protocol. */
62
- const SHIFT_TAB = "\u001b[Z";
63
-
64
61
  /** The screen's keys, in help order. */
65
62
  export const PANEL_KEYS: readonly PanelKey[] = [
66
63
  {
@@ -75,8 +72,7 @@ export const PANEL_KEYS: readonly PanelKey[] = [
75
72
  label: "←",
76
73
  sections: ALL,
77
74
  help: "Previous section",
78
- match: (data) =>
79
- matchesKey(data, Key.left) || data === "h" || matchesKey(data, Key.shift("tab")) || data === SHIFT_TAB,
75
+ match: (data) => matchesKey(data, Key.left) || data === "h" || isShiftTab(data),
80
76
  },
81
77
  {
82
78
  id: "nextSection",
@@ -21,7 +21,7 @@ import { writeConfigFile } from "../../../lib/config.ts";
21
21
  import type { PackageUpdate } from "./types.ts";
22
22
  import { parseVersion } from "./version.ts";
23
23
 
24
- /** The file name under `<agentDir>/bytefly/`. */
24
+ /** The file name under `<agent-dir>/bytefly/`. */
25
25
  export const UPDATE_CACHE_FILE = "update.json";
26
26
 
27
27
  /** How long a cached snapshot is reused before the next session re-reads. */
@@ -30,7 +30,7 @@ import {
30
30
  import { applyUpdates, fetchAvailableUpdates, fetchLatestVersion } from "./check.ts";
31
31
  import { isOwnPackage, updateNotice } from "./format.ts";
32
32
  import { paintLatest, type UpdateOutcome, type UpdateSource } from "./state.ts";
33
- import { suppressPiPackageUpdates, type UpdateSuppression } from "./suppress.ts";
33
+ import type { UpdateSuppression } from "./suppress.ts";
34
34
  import type { PackageUpdate, PackageUpdateCheck } from "./types.ts";
35
35
  import { isNewerVersion } from "./version.ts";
36
36
 
@@ -112,7 +112,7 @@ export function registerUpdate(pi: ExtensionAPI, updates: UpdateSource, deps: Up
112
112
  const offline = deps.isOffline ?? isOffline;
113
113
  const cache = deps.cache ?? diskCache();
114
114
  const now = deps.now ?? Date.now;
115
- const suppress = deps.suppress ?? suppressPiPackageUpdates;
115
+ const suppress = deps.suppress ?? (async () => (await import("./suppress.ts")).suppressPiPackageUpdates());
116
116
  // A session is announced at most once per load, even if `session_start` fires
117
117
  // again after a resume or a session replacement.
118
118
  let announced = false;
@@ -203,12 +203,12 @@ export function registerUpdate(pi: ExtensionAPI, updates: UpdateSource, deps: Up
203
203
  }
204
204
  };
205
205
 
206
- pi.on("session_start", (_event, ctx) => {
206
+ pi.on("session_start", async (_event, ctx) => {
207
207
  // `/bytefly update` can answer in any mode; only the TUI installs the host
208
208
  // shim and posts a notice.
209
209
  context = ctx;
210
210
  if (ctx.mode !== "tui") return;
211
- suppression ??= suppress();
211
+ suppression ??= await suppress();
212
212
  const cached = cache.read();
213
213
  const key = keyFor(ctx);
214
214
  if (offline()) {
@@ -1,19 +1,17 @@
1
1
  /**
2
2
  * Tuning for bytefly's live working line.
3
3
  *
4
- * While Pi's working row is up, the message is written into the row beside Pi's
5
- * spinner. Edit this file to change the words or how often the count advances.
4
+ * While the composer's top border carries Pi's working indicator, the message is
5
+ * written into the border beside Pi's spinner. Edit this file to change the word
6
+ * or how often the count advances.
6
7
  */
7
8
 
8
9
  export const WORKING_CONFIG = {
9
- /** Word before the count, before the first tool starts. */
10
- working: "Working",
11
- /** Between the word and the count. */
12
- ellipsis: "…",
10
+ /** The word the line leads with, after Pi's spinner. */
11
+ working: "working",
13
12
  /** How often the count advances, in milliseconds. */
14
13
  intervalMs: 1000,
15
14
  } as const satisfies {
16
15
  working: string;
17
- ellipsis: string;
18
16
  intervalMs: number;
19
17
  };