@gavin-hu/my-pi-agent 0.8.1 → 0.10.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 (174) hide show
  1. package/README.md +35 -8
  2. package/README.zh-CN.md +33 -8
  3. package/assets/bytefly.schema.json +17 -1
  4. package/assets/chrome.png +0 -0
  5. package/assets/file-browse.png +0 -0
  6. package/assets/job.png +0 -0
  7. package/assets/plan-review.png +0 -0
  8. package/assets/question.png +0 -0
  9. package/assets/rewind.png +0 -0
  10. package/assets/todo.png +0 -0
  11. package/assets/tool-renderings.png +0 -0
  12. package/assets/tools/ask_user_question.png +0 -0
  13. package/assets/tools/bash.png +0 -0
  14. package/assets/tools/edit.png +0 -0
  15. package/assets/tools/enter_plan_mode.png +0 -0
  16. package/assets/tools/enter_worktree.png +0 -0
  17. package/assets/tools/exit_plan_mode.png +0 -0
  18. package/assets/tools/exit_worktree.png +0 -0
  19. package/assets/tools/find.png +0 -0
  20. package/assets/tools/goal.png +0 -0
  21. package/assets/tools/grep.png +0 -0
  22. package/assets/tools/job.png +0 -0
  23. package/assets/tools/list_worktrees.png +0 -0
  24. package/assets/tools/ls.png +0 -0
  25. package/assets/tools/memory.png +0 -0
  26. package/assets/tools/notice.png +0 -0
  27. package/assets/tools/open_path.png +0 -0
  28. package/assets/tools/prune_worktrees.png +0 -0
  29. package/assets/tools/read.png +0 -0
  30. package/assets/tools/read_doc.png +0 -0
  31. package/assets/tools/run_doctor.png +0 -0
  32. package/assets/tools/send_wechat.png +0 -0
  33. package/assets/tools/set_plan_steps.png +0 -0
  34. package/assets/tools/subagent.png +0 -0
  35. package/assets/tools/todo.png +0 -0
  36. package/assets/tools/turn-rule.png +0 -0
  37. package/assets/tools/user-message.png +0 -0
  38. package/assets/tools/watch.png +0 -0
  39. package/assets/tools/web_fetch.png +0 -0
  40. package/assets/tools/web_search.png +0 -0
  41. package/assets/tools/write.png +0 -0
  42. package/assets/tools/write_plan.png +0 -0
  43. package/extensions/bytefly/README.md +306 -68
  44. package/extensions/bytefly/block/component.ts +282 -0
  45. package/extensions/bytefly/block/config.ts +24 -0
  46. package/extensions/bytefly/block/fallback.ts +120 -0
  47. package/extensions/bytefly/block/register.ts +164 -0
  48. package/extensions/bytefly/chrome.ts +97 -0
  49. package/extensions/bytefly/doctor/format.ts +36 -10
  50. package/extensions/bytefly/doctor/tools.ts +10 -6
  51. package/extensions/bytefly/footer/config.ts +1 -1
  52. package/extensions/bytefly/footer/render/format.ts +5 -18
  53. package/extensions/bytefly/footer/render/layout.ts +2 -1
  54. package/extensions/bytefly/footer/render/segments.ts +51 -78
  55. package/extensions/bytefly/header/component.ts +49 -21
  56. package/extensions/bytefly/header/config.ts +28 -21
  57. package/extensions/bytefly/header/register.ts +6 -1
  58. package/extensions/bytefly/index.ts +32 -12
  59. package/extensions/bytefly/message/config.ts +15 -0
  60. package/extensions/bytefly/message/format.ts +42 -0
  61. package/extensions/bytefly/message/register.ts +15 -0
  62. package/extensions/bytefly/notice/component.ts +107 -0
  63. package/extensions/bytefly/notice/config.ts +41 -0
  64. package/extensions/bytefly/notice/register.ts +27 -0
  65. package/extensions/bytefly/pad.ts +42 -0
  66. package/extensions/bytefly/panel/commands.ts +69 -35
  67. package/extensions/bytefly/panel/format.ts +8 -8
  68. package/extensions/bytefly/panel/tui.ts +45 -70
  69. package/extensions/bytefly/separator/component.ts +2 -1
  70. package/extensions/bytefly/separator/config.ts +16 -8
  71. package/extensions/bytefly/separator/format.ts +18 -38
  72. package/extensions/bytefly/separator/register.ts +3 -2
  73. package/extensions/bytefly/separator/types.ts +0 -10
  74. package/extensions/bytefly/working/config.ts +20 -0
  75. package/extensions/bytefly/working/format.ts +25 -0
  76. package/extensions/bytefly/working/register.ts +88 -0
  77. package/extensions/document/README.md +12 -5
  78. package/extensions/document/paging.ts +10 -10
  79. package/extensions/document/render.ts +17 -16
  80. package/extensions/document/tool.ts +1 -1
  81. package/extensions/file-browse/README.md +54 -10
  82. package/extensions/file-browse/client.ts +110 -2
  83. package/extensions/file-browse/config.ts +12 -0
  84. package/extensions/file-browse/diff.ts +187 -0
  85. package/extensions/file-browse/git.ts +180 -8
  86. package/extensions/file-browse/html.ts +401 -25
  87. package/extensions/file-browse/index.ts +15 -4
  88. package/extensions/file-browse/markdown.ts +33 -0
  89. package/extensions/file-browse/router.ts +94 -2
  90. package/extensions/file-browse/server.ts +4 -1
  91. package/extensions/file-open/README.md +6 -0
  92. package/extensions/goal/README.md +16 -10
  93. package/extensions/goal/format.ts +13 -25
  94. package/extensions/goal/tools.ts +11 -22
  95. package/extensions/goal/tui.ts +12 -40
  96. package/extensions/job/README.md +22 -13
  97. package/extensions/job/format.ts +36 -13
  98. package/extensions/job/index.ts +26 -21
  99. package/extensions/job/tools.ts +15 -12
  100. package/extensions/job/tui.ts +148 -222
  101. package/extensions/job/ui.ts +24 -49
  102. package/extensions/memory/README.md +7 -1
  103. package/extensions/memory/format.ts +5 -3
  104. package/extensions/memory/tools.ts +4 -3
  105. package/extensions/memory/tui.ts +32 -36
  106. package/extensions/plan/README.md +86 -36
  107. package/extensions/plan/blocks.ts +44 -2
  108. package/extensions/plan/feedback.ts +20 -5
  109. package/extensions/plan/index.ts +1 -1
  110. package/extensions/plan/runtime.ts +3 -2
  111. package/extensions/plan/tools.ts +43 -27
  112. package/extensions/plan/tui.ts +21 -27
  113. package/extensions/plan/web-client.ts +386 -40
  114. package/extensions/plan/web-page.ts +138 -33
  115. package/extensions/plan/web-router.ts +46 -13
  116. package/extensions/plan/web.ts +63 -12
  117. package/extensions/question/README.md +14 -2
  118. package/extensions/question/answers.ts +25 -18
  119. package/extensions/question/tools.ts +22 -16
  120. package/extensions/question/tui.ts +132 -76
  121. package/extensions/rewind/README.md +3 -1
  122. package/extensions/rewind/format.ts +35 -2
  123. package/extensions/rewind/index.ts +3 -1
  124. package/extensions/rewind/rewind-tui.ts +66 -62
  125. package/extensions/rewind/snapshot-run.ts +1 -1
  126. package/extensions/rewind/status.ts +3 -2
  127. package/extensions/subagent/README.md +6 -0
  128. package/extensions/subagent/index.ts +1 -1
  129. package/extensions/subagent/render.ts +108 -66
  130. package/extensions/todo/README.md +20 -13
  131. package/extensions/todo/format.ts +47 -34
  132. package/extensions/todo/tools.ts +6 -4
  133. package/extensions/todo/tui.ts +46 -26
  134. package/extensions/watch/README.md +16 -8
  135. package/extensions/watch/format.ts +33 -13
  136. package/extensions/watch/index.ts +29 -24
  137. package/extensions/watch/tools.ts +16 -9
  138. package/extensions/watch/tui.ts +142 -196
  139. package/extensions/watch/ui.ts +24 -46
  140. package/extensions/web-access/README.md +16 -7
  141. package/extensions/web-access/fetch/tool.ts +12 -11
  142. package/extensions/web-access/search/tool.ts +16 -13
  143. package/extensions/wechat/README.md +8 -2
  144. package/extensions/wechat/index.ts +4 -3
  145. package/extensions/wechat/login-screen.ts +8 -5
  146. package/extensions/wechat/status-screen.ts +41 -22
  147. package/extensions/wechat/tools.ts +6 -5
  148. package/extensions/worktree/README.md +6 -0
  149. package/extensions/worktree/enter.ts +1 -1
  150. package/extensions/worktree/index.ts +1 -1
  151. package/extensions/worktree/runtime.ts +1 -1
  152. package/lib/README.md +11 -7
  153. package/lib/format.ts +17 -0
  154. package/lib/notice.ts +87 -0
  155. package/lib/requirements.ts +2 -2
  156. package/lib/{tui.ts → tui/chrome.ts} +17 -34
  157. package/lib/tui/columns.ts +96 -0
  158. package/lib/{list-cursor.ts → tui/cursor.ts} +58 -4
  159. package/lib/tui/gauge.ts +78 -0
  160. package/lib/{ui.ts → tui/glyphs.ts} +10 -11
  161. package/lib/tui/indent.ts +166 -0
  162. package/lib/tui/index.ts +27 -0
  163. package/lib/tui/line-pane.ts +173 -0
  164. package/lib/tui/rows.ts +143 -0
  165. package/lib/tui/rules.ts +94 -0
  166. package/lib/tui/status-controller.ts +107 -0
  167. package/lib/tui/tokens.ts +72 -0
  168. package/lib/web/base.ts +2 -1
  169. package/lib/web/components.ts +3 -1
  170. package/lib/web/layout.ts +5 -1
  171. package/package.json +7 -3
  172. package/themes/bytefly-dark.json +7 -8
  173. package/themes/bytefly-light.json +10 -11
  174. package/assets/bytefly-chrome.png +0 -0
@@ -11,8 +11,9 @@
11
11
 
12
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
13
  import { formatTokens } from "../../../lib/format.ts";
14
- import { reuseText } from "../../../lib/tui.ts";
15
- import { SEPARATORS } from "../../../lib/ui.ts";
14
+ import { reuseText } from "../../../lib/tui/chrome.ts";
15
+ import { paint } from "../../../lib/tui/tokens.ts";
16
+ import { SEPARATORS } from "../../../lib/tui/glyphs.ts";
16
17
  import { charLength } from "../text.ts";
17
18
  import { oneLine } from "../transcript.ts";
18
19
  import { createPageCache, type PageCache } from "./cache.ts";
@@ -204,9 +205,9 @@ export function registerFetchTool(pi: ExtensionAPI, deps: FetchToolDeps = {}): v
204
205
  renderCall(args, theme, context) {
205
206
  const { url, find, startIndex } = args as WebFetchArgs;
206
207
  const text = reuseText(context?.lastComponent);
207
- let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + theme.fg("accent", oneLine(url ?? ""));
208
- if (find?.length) line += theme.fg("dim", ` find ${find.map((f) => `"${oneLine(f)}"`).join(", ")}`);
209
- else if (startIndex) line += theme.fg("dim", ` (from ${startIndex})`);
208
+ let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "label", oneLine(url ?? ""));
209
+ if (find?.length) line += paint(theme, "faint", ` find ${find.map((f) => `"${oneLine(f)}"`).join(", ")}`);
210
+ else if (startIndex) line += paint(theme, "faint", ` (from ${startIndex})`);
210
211
  text.setText(line);
211
212
  return text;
212
213
  },
@@ -216,21 +217,21 @@ export function registerFetchTool(pi: ExtensionAPI, deps: FetchToolDeps = {}): v
216
217
  const page = result.details as FetchResponse | undefined;
217
218
  if (page?.error) {
218
219
  const message = oneLine(page.error);
219
- text.setText(theme.fg("error", message.startsWith("Error:") ? message : `Error: ${message}`));
220
+ text.setText(paint(theme, "bad", message.startsWith("Error:") ? message : `Error: ${message}`));
220
221
  return text;
221
222
  }
222
223
  if (!page || result.isError) {
223
224
  const first = result.content[0];
224
225
  const message = oneLine(first?.type === "text" ? first.text : "Fetch failed");
225
- text.setText(theme.fg("error", message.startsWith("Error:") ? message : `Error: ${message}`));
226
+ text.setText(paint(theme, "bad", message.startsWith("Error:") ? message : `Error: ${message}`));
226
227
  return text;
227
228
  }
228
229
 
229
230
  const label = oneLine(page.title || page.finalUrl);
230
- let line = theme.fg("muted", label);
231
- if (page.matches.length > 0) line += theme.fg("dim", `${SEPARATORS.item}${page.matches.length} match(es)`);
232
- else line += theme.fg("dim", `${SEPARATORS.item}${formatTokens(page.totalChars)} chars`);
233
- if (page.cached) line += theme.fg("dim", " (cached)");
231
+ let line = paint(theme, "meta", label);
232
+ if (page.matches.length > 0) line += paint(theme, "faint", `${SEPARATORS.item}${page.matches.length} match(es)`);
233
+ else line += paint(theme, "faint", `${SEPARATORS.item}${formatTokens(page.totalChars)} chars`);
234
+ if (page.cached) line += paint(theme, "faint", " (cached)");
234
235
  text.setText(line);
235
236
  return text;
236
237
  },
@@ -10,8 +10,9 @@
10
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
11
  import { loadSearchConfig } from "../config.ts";
12
12
  import { oneLine } from "../transcript.ts";
13
- import { reuseText } from "../../../lib/tui.ts";
14
- import { expandHint, SEPARATORS } from "../../../lib/ui.ts";
13
+ import { reuseText } from "../../../lib/tui/chrome.ts";
14
+ import { paint } from "../../../lib/tui/tokens.ts";
15
+ import { expandHint, SEPARATORS } from "../../../lib/tui/glyphs.ts";
15
16
  import { formatResults } from "./format.ts";
16
17
  import { createThrottle, runSearch, type SearchDeps } from "./search.ts";
17
18
  import { resolveRequest, WebSearchOutput, WebSearchParams, type SearchResponse, type WebSearchArgs } from "./schema.ts";
@@ -68,8 +69,8 @@ export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): voi
68
69
  renderCall(args, theme, context) {
69
70
  const { query, maxResults } = args as WebSearchArgs;
70
71
  const text = reuseText(context?.lastComponent);
71
- let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + theme.fg("accent", oneLine(query ?? ""));
72
- if (maxResults) line += theme.fg("dim", ` (${maxResults} results)`);
72
+ let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "label", oneLine(query ?? ""));
73
+ if (maxResults) line += paint(theme, "faint", ` (max ${maxResults})`);
73
74
  text.setText(line);
74
75
  return text;
75
76
  },
@@ -80,11 +81,11 @@ export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): voi
80
81
  if (!details || result.isError) {
81
82
  const first = result.content[0];
82
83
  const message = first?.type === "text" ? first.text : "Search failed";
83
- text.setText(theme.fg("error", `Error: ${oneLine(message)}`));
84
+ text.setText(paint(theme, "bad", `Error: ${oneLine(message)}`));
84
85
  return text;
85
86
  }
86
87
  if (details.provider === "none") {
87
- text.setText(theme.fg("dim", `No results for "${oneLine(details.query)}".`));
88
+ text.setText(paint(theme, "faint", `No results for "${oneLine(details.query)}".`));
88
89
  return text;
89
90
  }
90
91
 
@@ -92,19 +93,21 @@ export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): voi
92
93
  // A blank line separates the call header from the result body.
93
94
  const lines: string[] = [
94
95
  "",
95
- theme.fg("dim", `via ${details.provider}${SEPARATORS.item}${count} result${count === 1 ? "" : "s"}`),
96
+ paint(theme, "faint", `via ${details.provider}${SEPARATORS.item}${count} result${count === 1 ? "" : "s"}`),
96
97
  ];
97
- if (details.answer) lines.push(`${theme.fg("dim", "answer")} ${theme.fg("muted", oneLine(details.answer, 160))}`);
98
+ if (details.answer)
99
+ lines.push(`${paint(theme, "faint", "Answer:")} ${paint(theme, "meta", oneLine(details.answer, 160))}`);
98
100
 
99
101
  const shown = options.expanded ? details.results : details.results.slice(0, 5);
100
- const indent = theme.fg("dim", " ");
102
+ const indent = paint(theme, "faint", " ");
101
103
  for (const [index, item] of shown.entries()) {
102
- lines.push(`${theme.fg("accent", `${index + 1}.`)} ${theme.fg("muted", oneLine(item.title, 100))}`);
103
- lines.push(`${indent}${theme.fg("dim", oneLine(item.url, 100))}`);
104
- if (options.expanded && item.snippet) lines.push(`${indent}${theme.fg("muted", oneLine(item.snippet, 160))}`);
104
+ lines.push(`${paint(theme, "accent", `${index + 1}.`)} ${paint(theme, "meta", oneLine(item.title, 100))}`);
105
+ lines.push(`${indent}${paint(theme, "faint", oneLine(item.url, 100))}`);
106
+ if (options.expanded && item.snippet)
107
+ lines.push(`${indent}${paint(theme, "meta", oneLine(item.snippet, 160))}`);
105
108
  }
106
109
  const extra = count - shown.length;
107
- if (extra > 0) lines.push(`${theme.fg("dim", `+${extra} more`)} ${expandHint(theme)}`);
110
+ if (extra > 0) lines.push(`${paint(theme, "faint", `+${extra} more`)} ${expandHint(theme)}`);
108
111
  text.setText(lines.join("\n"));
109
112
  return text;
110
113
  },
@@ -11,6 +11,12 @@ pi -e . # load the whole @gavin-hu/my-pi-agent pack
11
11
  pi install ./ # install the package
12
12
  ```
13
13
 
14
+ ## Tool rendering
15
+
16
+ How the `send_wechat` call and its result look in the transcript — the call line as a headline and the body indented under it:
17
+
18
+ <img src="../../assets/tools/send_wechat.png" alt="The send_wechat tool's call and result: the call line and an indented body with the message text and the sent confirmation" width="100%">
19
+
14
20
  ## What it does
15
21
 
16
22
  - **Inbound** — a long-poll loop receives WeChat text, images, and files and
@@ -252,8 +258,8 @@ status, which is what the old start/stop guidance used to do.
252
258
  | `outbound.ts` | Upload and send a local file: hash, encrypt, `getUploadUrl`, upload, `sendFileMessage`. |
253
259
  | `login.ts` | QR login state machine. |
254
260
  | `qrcode.ts` | QR matrix → Unicode half-block terminal lines. |
255
- | `login-screen.ts` | The `/wechat login` screen (`ctx.ui.custom`). |
256
- | `status-screen.ts` | The `/wechat` control screen (`ctx.ui.custom`) and the pure one-line/body renderers. |
261
+ | `login-screen.ts` | The `/wechat login` screen (`ctx.ui.custom`), drawn from the `lib/tui/` kit (`titleRule`, `clip`, `isCloseKey`). |
262
+ | `status-screen.ts` | The `/wechat` control screen (`ctx.ui.custom`) and the pure one-line/body renderers, drawn from the `lib/tui/` kit (`titleRule`, `keyValueRow`, `paint`). |
257
263
  | `config.ts` | The `wechat` section of `bytefly.json` loading and normalization. |
258
264
  | `credentials.ts` | `credentials.json` (`0600`, multi-account). |
259
265
  | `state.ts` | `state.json` (cursor, owner, per-peer tokens). |
@@ -14,7 +14,8 @@
14
14
  import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
15
15
  import { join } from "node:path";
16
16
  import { isExtensionEnabled } from "../../lib/env.ts";
17
- import { GLYPHS, STATUS_KEYS } from "../../lib/ui.ts";
17
+ import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
18
+ import { paint } from "../../lib/tui/tokens.ts";
18
19
  import { createBridge, type Bridge, type BridgeState } from "./bridge.ts";
19
20
  import { createFetchRunner, WechatClient } from "./client.ts";
20
21
  import { registerCommands, type CommandDeps } from "./commands.ts";
@@ -34,9 +35,9 @@ import { registerTools } from "./tools.ts";
34
35
  export function wechatChip(theme: Theme, state: BridgeState): string | undefined {
35
36
  switch (state) {
36
37
  case "running":
37
- return theme.fg("success", `${GLYPHS.wechat} wechat`);
38
+ return paint(theme, "ok", `${GLYPHS.wechat} wechat`);
38
39
  case "expired":
39
- return theme.fg("error", `${GLYPHS.wechat} wechat expired`);
40
+ return paint(theme, "bad", `${GLYPHS.wechat} wechat expired`);
40
41
  case "stopped":
41
42
  return undefined;
42
43
  }
@@ -7,8 +7,11 @@
7
7
  */
8
8
 
9
9
  import type { Theme } from "@earendil-works/pi-coding-agent";
10
- import { Key, matchesKey, type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
- import { screenHeader, screenHint } from "../../lib/tui.ts";
10
+ import { type Component, visibleWidth } from "@earendil-works/pi-tui";
11
+ import { screenHint } from "../../lib/tui/chrome.ts";
12
+ import { clip } from "../../lib/tui/columns.ts";
13
+ import { isCloseKey } from "../../lib/tui/cursor.ts";
14
+ import { titleRule } from "../../lib/tui/rules.ts";
12
15
 
13
16
  export interface LoginViewState {
14
17
  /** Raw `get_qrcode_status` value, or `wait` before the first poll. */
@@ -51,7 +54,7 @@ export function loginBody(state: LoginViewState, width: number): string[] {
51
54
  lines.push(state.url.slice(index, index + target));
52
55
  }
53
56
  }
54
- return lines.map((line) => truncateToWidth(line, target));
57
+ return lines.map((line) => clip(line, target));
55
58
  }
56
59
 
57
60
  export interface LoginScreenOptions {
@@ -69,7 +72,7 @@ export class LoginScreenComponent implements Component {
69
72
  constructor(private readonly options: LoginScreenOptions) {}
70
73
 
71
74
  handleInput(data: string): void {
72
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
75
+ if (isCloseKey(data)) {
73
76
  this.options.onCancel();
74
77
  }
75
78
  }
@@ -81,7 +84,7 @@ export class LoginScreenComponent implements Component {
81
84
  const target = Math.max(1, width);
82
85
  const theme = this.options.theme;
83
86
  return [
84
- screenHeader(theme, target, "wechat · login"),
87
+ titleRule(theme, target, "wechat · login"),
85
88
  "",
86
89
  ...loginBody(this.options.state(), target),
87
90
  "",
@@ -10,8 +10,13 @@
10
10
  */
11
11
 
12
12
  import type { Theme } from "@earendil-works/pi-coding-agent";
13
- import { Key, matchesKey, truncateToWidth, type Component } from "@earendil-works/pi-tui";
14
- import { screenHeader, screenHint, type ScreenHint } from "../../lib/tui.ts";
13
+ import type { Component } from "@earendil-works/pi-tui";
14
+ import { screenHint, type ScreenHint } from "../../lib/tui/chrome.ts";
15
+ import { clip } from "../../lib/tui/columns.ts";
16
+ import { isCloseKey } from "../../lib/tui/cursor.ts";
17
+ import { keyValueRow } from "../../lib/tui/rows.ts";
18
+ import { titleRule } from "../../lib/tui/rules.ts";
19
+ import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
15
20
  import type { BridgeStatus } from "./bridge.ts";
16
21
  import { peerLabel } from "./format.ts";
17
22
  import type { BotCredentials } from "./types.ts";
@@ -72,18 +77,32 @@ export function statusLine(view: StatusView): string {
72
77
  /** Label column width in the screen body. */
73
78
  const LABEL_WIDTH = 11;
74
79
 
75
- function row(theme: Theme, label: string, value: string): string {
76
- return ` ${theme.fg("muted", label.padEnd(LABEL_WIDTH))}${value}`;
80
+ /** One body row: a `meta` label column and a value in `valueRole`. */
81
+ function row(theme: Theme, width: number, label: string, value: string, valueRole: TuiRole = "label"): string {
82
+ return keyValueRow(theme, width, { label, value, labelWidth: LABEL_WIDTH, labelRole: "meta", valueRole });
77
83
  }
78
84
 
79
- function connectionText(theme: Theme, connection: StatusConnection): string {
85
+ /** The connection word, uncolored; {@link row} paints it. */
86
+ function connectionText(connection: StatusConnection): string {
80
87
  switch (connection) {
81
88
  case "open":
82
- return theme.fg("success", "open");
89
+ return "open";
83
90
  case "expired":
84
- return theme.fg("error", "expired");
91
+ return "expired";
85
92
  case "closed":
86
- return theme.fg("muted", "closed");
93
+ return "closed";
94
+ }
95
+ }
96
+
97
+ /** The design role the connection word is drawn in. */
98
+ function connectionRole(connection: StatusConnection): TuiRole {
99
+ switch (connection) {
100
+ case "open":
101
+ return "ok";
102
+ case "expired":
103
+ return "bad";
104
+ case "closed":
105
+ return "meta";
87
106
  }
88
107
  }
89
108
 
@@ -91,28 +110,28 @@ function connectionText(theme: Theme, connection: StatusConnection): string {
91
110
  export function statusBody(view: StatusView, theme: Theme, width: number): string[] {
92
111
  const target = Math.max(1, width);
93
112
  const lines = [
94
- row(theme, "Account", view.account ?? theme.fg("muted", "not logged in")),
95
- row(theme, "Connection", connectionText(theme, view.connection)),
113
+ row(theme, target, "Account", view.account ?? "not logged in", view.account ? "label" : "meta"),
114
+ row(theme, target, "Connection", connectionText(view.connection), connectionRole(view.connection)),
96
115
  ];
97
116
  if (view.bridge) {
98
- if (view.owner && view.owner !== view.account) lines.push(row(theme, "Owner", view.owner));
99
- lines.push(row(theme, "Peers", String(view.peers)));
100
- lines.push(row(theme, "Queued", String(view.queued)));
101
- if (view.refused > 0) lines.push(row(theme, "Refused", String(view.refused)));
102
- if (view.mediaDropped > 0) lines.push(row(theme, "Media", `${view.mediaDropped} dropped`));
103
- if (view.promptPending) lines.push(row(theme, "Prompt", theme.fg("accent", "waiting for a reply")));
117
+ if (view.owner && view.owner !== view.account) lines.push(row(theme, target, "Owner", view.owner));
118
+ lines.push(row(theme, target, "Peers", String(view.peers), "meta"));
119
+ lines.push(row(theme, target, "Queued", String(view.queued), "meta"));
120
+ if (view.refused > 0) lines.push(row(theme, target, "Refused", String(view.refused), "meta"));
121
+ if (view.mediaDropped > 0) lines.push(row(theme, target, "Media", `${view.mediaDropped} dropped`, "meta"));
122
+ if (view.promptPending) lines.push(row(theme, target, "Prompt", "waiting for a reply", "accent"));
104
123
  }
105
124
  if (view.connection === "expired") {
106
125
  lines.push("");
107
- lines.push(` ${theme.fg("error", "Session expired — press l to log in again.")}`);
126
+ lines.push(clip(` ${paint(theme, "bad", "Session expired — press l to log in again.")}`, target));
108
127
  } else if (view.connection === "closed" && view.account) {
109
128
  lines.push("");
110
- lines.push(` ${theme.fg("muted", "Not connected — press l to log in again.")}`);
129
+ lines.push(clip(` ${paint(theme, "meta", "Not connected — press l to log in again.")}`, target));
111
130
  } else if (view.connection === "closed") {
112
131
  lines.push("");
113
- lines.push(` ${theme.fg("muted", "Run /wechat login to connect an account.")}`);
132
+ lines.push(clip(` ${paint(theme, "meta", "Run /wechat login to connect an account.")}`, target));
114
133
  }
115
- return lines.map((line) => truncateToWidth(line, target));
134
+ return lines;
116
135
  }
117
136
 
118
137
  export interface StatusScreenOptions {
@@ -140,7 +159,7 @@ export class StatusScreenComponent implements Component {
140
159
  }
141
160
 
142
161
  handleInput(data: string): void {
143
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
162
+ if (isCloseKey(data)) {
144
163
  this.options.onClose("close");
145
164
  return;
146
165
  }
@@ -166,7 +185,7 @@ export class StatusScreenComponent implements Component {
166
185
  const target = Math.max(1, width);
167
186
  const theme = this.options.theme;
168
187
  return [
169
- screenHeader(theme, target, "wechat · control"),
188
+ titleRule(theme, target, "wechat · control"),
170
189
  "",
171
190
  ...statusBody(this.options.view(), theme, target),
172
191
  "",
@@ -11,7 +11,8 @@ import type { JsonValue } from "@earendil-works/pi-ai";
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import { Type, type Static } from "typebox";
13
13
  import { sanitizeLine } from "../../lib/format.ts";
14
- import { reuseText, toolDetails } from "../../lib/tui.ts";
14
+ import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
15
+ import { paint } from "../../lib/tui/tokens.ts";
15
16
  import type { Bridge } from "./bridge.ts";
16
17
 
17
18
  export const TOOL_NAME = "send_wechat";
@@ -85,7 +86,7 @@ export function registerTools(pi: ExtensionAPI, deps: ToolDeps): void {
85
86
  const text = reuseText(context.lastComponent);
86
87
  const body = (args as { text?: unknown }).text;
87
88
  const label = context.argsComplete && typeof body === "string" ? sanitizeLine(body) : "…";
88
- text.setText(theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + theme.fg("muted", label));
89
+ text.setText(theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "meta", label));
89
90
  return text;
90
91
  },
91
92
 
@@ -93,16 +94,16 @@ export function registerTools(pi: ExtensionAPI, deps: ToolDeps): void {
93
94
  const text = reuseText(context.lastComponent);
94
95
  const details = toolDetails<{ sent?: boolean; error?: string }>(result.details);
95
96
  if (details?.sent) {
96
- text.setText(theme.fg("success", "sent to WeChat owner"));
97
+ text.setText(paint(theme, "ok", "sent to WeChat owner"));
97
98
  return text;
98
99
  }
99
100
  if (details?.error) {
100
- text.setText(theme.fg("error", `Error: ${sanitizeLine(details.error)}`));
101
+ text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
101
102
  return text;
102
103
  }
103
104
  // Pi's rejection result carries `{}`; show its message instead.
104
105
  const first = result.content[0];
105
- text.setText(first?.type === "text" ? first.text : theme.fg("error", "Error: send failed"));
106
+ text.setText(first?.type === "text" ? first.text : paint(theme, "bad", "Error: send failed"));
106
107
  return text;
107
108
  },
108
109
  });
@@ -14,6 +14,12 @@ pi install ./ # install the package
14
14
  pi -e . --worktree feature-auth # load the package and enter a worktree at startup
15
15
  ```
16
16
 
17
+ ## Tool rendering
18
+
19
+ How the `list_worktrees` call and its result look in the transcript — the call line as a headline and the body indented under it:
20
+
21
+ <img src="../../assets/tools/list_worktrees.png" alt="The list_worktrees tool's call and result, drawn by the generic fallback" width="100%">
22
+
17
23
  ## What it does
18
24
 
19
25
  - `enter_worktree` creates `<repo>/.pi/worktrees/<name>` on branch
@@ -7,7 +7,7 @@ import { appendFileSync, existsSync, readFileSync } from "node:fs";
7
7
  import { basename, join, resolve } from "node:path";
8
8
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
9
  import { isInside } from "../../lib/path.ts";
10
- import { GLYPHS } from "../../lib/ui.ts";
10
+ import { GLYPHS } from "../../lib/tui/glyphs.ts";
11
11
  import { loadConfig, type WorktreeConfig } from "./config.ts";
12
12
  import { WorktreeCancelledError } from "./errors.ts";
13
13
  import {
@@ -24,7 +24,7 @@ import { existsSync } from "node:fs";
24
24
  import { fileURLToPath } from "node:url";
25
25
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
26
26
  import { isExtensionEnabled } from "../../lib/env.ts";
27
- import { GLYPHS } from "../../lib/ui.ts";
27
+ import { GLYPHS } from "../../lib/tui/glyphs.ts";
28
28
  import { canonicalize, checkCheckout, repoRoot, unlockWorktree } from "./git.ts";
29
29
  import { analyzeBashCommand, guardFileTool } from "./guard.ts";
30
30
  import {
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
- import { STATUS_KEYS } from "../../lib/ui.ts";
11
+ import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
12
12
  import { ENV_ROOT } from "../../lib/env.ts";
13
13
  import { loadConfig, type WorktreeConfig } from "./config.ts";
14
14
  import type { WorktreeState } from "./state.ts";
package/lib/README.md CHANGED
@@ -20,11 +20,12 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
20
20
  | [`env.ts`](./env.ts) | Env-var contracts shared across extensions: the worktree environment (`ENV_ROOT`, `worktreeRoot`, `resolveEffectiveCwd`) used by worktree, file-browse, job, and rewind, plus `isExtensionEnabled` / `ENV_DISABLED_EXTENSIONS` for the package-wide disable list. |
21
21
  | [`escape.ts`](./escape.ts) | `escapeHtml`: the single HTML text/attribute escape used by the browse page renderers and the Markdown and syntax highlighters. |
22
22
  | [`events.ts`](./events.ts) | Inter-extension `pi.events` channels and payloads: `JOB_FINISHED_CHANNEL` and `JobFinishedEvent`, published by `job` and consumed by bytefly's notify capability. |
23
+ | [`notice.ts`](./notice.ts) | The shared transcript notice: `NOTICE_TYPE`, the `NoticeDetails` / `NoticeItem` shape, `noticeMessage` (the `display: true` message both send paths accept), `isNotice`, and `pruneStaleNotices` — posted by `job` and `watch`, rendered by bytefly. |
23
24
  | [`format.ts`](./format.ts) | Pure text/number formatting shared by renderers: `formatTokens`, `stripControlChars`, `sanitize`, `sanitizeLine` — the boundary for untrusted, single-line text — and `shortenPath`. |
24
25
  | [`interaction.ts`](./interaction.ts) | Turn-affinity prompt contract: the channel a remote adapter installs on `ctx.ui` under a string key, the `InteractionChannel` interface (`request` / `post` / `postFile`), and `remoteChannel` / `isRemoteTurn` / `supportsCustomUI` / `askHuman` (with an optional file/text preface) plus the shared “Other” label. |
25
26
  | [`git/`](./git/) | Shared git plumbing. [`git/runner.ts`](./git/runner.ts) is the `RunGit` seam (`createExecRunner` with timeout/env handling, `runGitOrThrow`, `GitError`); [`git/read.ts`](./git/read.ts) holds the read helpers (`repoRoot`, `repoRootFor`, `gitDir`, `revParse`, `hasCommits`, `currentBranch`); [`git/index.ts`](./git/index.ts) is the barrel. |
26
27
  | [`highlight.ts`](./highlight.ts) | First-party, escape-first syntax tokenizer (`detectLanguage`, `fenceLanguage`, `highlightLines`, `HighlightLanguage`, `TokenClass`) for js/ts, json, css, html, shell, python, java, yaml, and sql; shared by `file-browse` and plan's code blocks. |
27
- | [`list-cursor.ts`](./list-cursor.ts) | Scrollable-list behavior for the `/rewind`, `/bytefly`, `/todos`, and `/jobs` screens: `fitRows`, `keepVisible`, `clampScroll`, `navIntent`, `navigateList`, `navigateScroll`, `wheelDelta`, `scrollList`, `isCloseKey`, `selectionMarker`, `formatRange`, and the `ListCursor` state. |
28
+ | [`tui/`](./tui/) | The Bytefly terminal design system shared by every interactive surface: [`tui/tokens.ts`](./tui/tokens.ts) (semantic role -> theme color, `paint`), [`tui/glyphs.ts`](./tui/glyphs.ts) (`GLYPHS`, `SEPARATORS`, `STATUS_KEYS`, `EXPAND_KEYBINDING` with `expandKey` / `expandHint`, `GLYPH_GAP` — the vocabulary both sides of a status must agree on), [`tui/chrome.ts`](./tui/chrome.ts) (`screenHint`, `dockListFrame` (the docked-list frame), `viewportRows`, `FULL_SCREEN_OVERLAY`, `reuseText`, `editorTheme`), [`tui/rules.ts`](./tui/rules.ts) (the rule glyph `RULE_GLYPH`, and `plainRule` / `titleRule` / `centeredRule`), [`tui/indent.ts`](./tui/indent.ts) (the transcript block indent: `BODY_INDENT`, an optional rail prefix that a wrapped call line is kept out of, and `bodyLines` / `indentBlockLines` / `indentedLines`), [`tui/rows.ts`](./tui/rows.ts) (`listRow`, `checkbox`, `chip`, `keyValueRow`, `emptyState`), [`tui/columns.ts`](./tui/columns.ts) (`splitRow`, `fitCells`, `flowCells`, `padTo`, `clip`), [`tui/gauge.ts`](./tui/gauge.ts) (`computeGauge`, `gaugeRole`, `gaugeBar`), [`tui/line-pane.ts`](./tui/line-pane.ts) (`LinePane`, the scrollable log/match pane), [`tui/status-controller.ts`](./tui/status-controller.ts) (`createStatusController`, the footer chips `job` and `watch` share), and [`tui/cursor.ts`](./tui/cursor.ts) (scroll and navigation: `fitRows`, `keepVisible`, `clampScroll`, `navIntent`, `navigateList`, `navigateScroll`, `wheelDelta`, `scrollList`, `isCloseKey`, `selectionMarker`, `formatRange`, and the `ListCursor` (by index) and `IdCursor` (by stable id) state). |
28
29
  | [`path.ts`](./path.ts) | `isInside` (string containment) plus `realPathOfNearest` / `isInsideReal` (symlink-aware): the containment predicates the file-browse, worktree, and doc guards share. |
29
30
  | [`pi-invocation.ts`](./pi-invocation.ts) | `getPiInvocation`: how to launch a nested `pi` process, shared by `subagent` and bytefly's doctor surface. |
30
31
  | [`policy.ts`](./policy.ts) | `createReadOnlyPolicy`: default-deny tool classification from `readOnlyHint` + explicit allow/deny. |
@@ -32,8 +33,6 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
32
33
  | [`requirements.ts`](./requirements.ts) | Per-extension runtime prerequisite table plus a pure `probeExtension(seams)` over injected seams (exec, config reads, optional imports): bytefly's doctor surface reports on it and the extensions that own a dependency format their errors from it. |
33
34
  | [`shell.ts`](./shell.ts) | Shell escape rule shared by the worktree isolation guard and plan mode's read-only git guard: `SHELL_ESCAPABLE`, `isShellEscapable`, `hasShellEscape`. |
34
35
  | [`tool-names.ts`](./tool-names.ts) | Tool names and one parameter name that more than one extension must agree on, so an orchestrating `ctx.executeTool()` call or schema probe breaks the build on a rename. |
35
- | [`tui.ts`](./tui.ts) | Screen chrome: `screenHeader`, `screenHint`, `dockListFrame` (the shared docked-list assembly), `viewportRows`, and `FULL_SCREEN_OVERLAY`, plus `reuseText` (per-render `Text` reuse) and `editorTheme` (inline-editor theme). |
36
- | [`ui.ts`](./ui.ts) | Shared UI vocabulary: `GLYPHS`, `SEPARATORS`, `STATUS_KEYS` (`ctx.ui.setStatus` keys), `EXPAND_KEYBINDING` with `expandKey` / `expandHint`, and the transcript rail `BODY_INDENT` / `GLYPH_GAP`. |
37
36
  | [`web/`](./web/) | The Bytefly web brand system shared by the browser surfaces: [`web/tokens.ts`](./web/tokens.ts) (palette derived from the Bytefly terminal themes, scales, and the `tok-*` map), [`web/base.ts`](./web/base.ts), [`web/components.ts`](./web/components.ts), [`web/layout.ts`](./web/layout.ts), [`web/runtime.ts`](./web/runtime.ts) (`WEB_THEME_STORAGE_KEY`, `THEME_JS`, `THEME_TOGGLE_JS`), [`web/markup.ts`](./web/markup.ts), and the [`web/index.ts`](./web/index.ts) barrel. |
38
37
 
39
38
  ## Conventions
@@ -43,7 +42,12 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
43
42
  - **Inject the host seam.** Modules that need a host capability take a function
44
43
  (`RunGit`, `HttpRunner`) rather than the whole `ExtensionAPI`, so the logic is
45
44
  testable without a runtime.
46
- - **Constants that must agree across both sides live in `ui.ts`** — a glyph and
47
- the `setStatus` key are one contract, not two literals. Tool names shared by
48
- an orchestrating tool and the tool it calls — and parameter names one
49
- extension probes on another — live in `tool-names.ts`.
45
+ - **Constants that must agree across both sides live in `tui/glyphs.ts`** — a
46
+ glyph and the `setStatus` key are one contract, not two literals. Tool names
47
+ shared by an orchestrating tool and the tool it calls — and parameter names
48
+ one extension probes on another — live in `tool-names.ts`.
49
+ - **A color is a role, not a token.** Draw through [`tui/tokens.ts`](./tui/tokens.ts)
50
+ (`paint(theme, role, text)`) so a theme swap re-skins every surface; the
51
+ primitives in `tui/` already do, and a new one should too. The one exception is
52
+ Pi's tool-rendering vocabulary (`toolOutput`, `toolTitle`), which stays direct:
53
+ those tokens style a tool block, and no role maps to them.
package/lib/format.ts CHANGED
@@ -17,6 +17,23 @@ export function formatTokens(count: number): string {
17
17
  return `${Math.round(count / 1000000)}M`;
18
18
  }
19
19
 
20
+ /**
21
+ * An integer grouped with thousands separators (`999`, `8,000`, `42,478`).
22
+ * Hand-rolled rather than `toLocaleString`, so a count is identical under any
23
+ * locale and ICU build.
24
+ */
25
+ export function formatCount(count: number): string {
26
+ if (!Number.isFinite(count)) return "0";
27
+ const sign = count < 0 ? "-" : "";
28
+ const digits = Math.abs(Math.trunc(count)).toString();
29
+ let out = "";
30
+ for (let i = 0; i < digits.length; i++) {
31
+ if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
32
+ out += digits[i];
33
+ }
34
+ return sign + out;
35
+ }
36
+
20
37
  /**
21
38
  * Replace terminal control characters (including ESC and the C1 block) with
22
39
  * spaces, keeping `\n` and `\t` for callers that render multi-line text.
package/lib/notice.ts ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The shared transcript notice: one custom-message type that any extension in
3
+ * the package can post, plus the model-facing content and the renderer-facing
4
+ * details it carries.
5
+ *
6
+ * Pi renders a custom message only when it was sent with `display: true`, and
7
+ * it resolves a renderer by the exact `customType` — there is no wildcard — so
8
+ * the type and the shape behind it are one contract the producing extension and
9
+ * bytefly's renderer must agree on. Keep both here, never re-typed on either
10
+ * side.
11
+ *
12
+ * `content` is what the model reads; `details` is never sent to the model and
13
+ * is the renderer's whole input, so a producer states the facts once and the
14
+ * renderer decides the look. Value-only: constants and pure functions, no
15
+ * state.
16
+ */
17
+
18
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
19
+
20
+ /** The custom-message type every transcript notice is sent under. */
21
+ export const NOTICE_TYPE = "my-pi-agent/notice";
22
+
23
+ /** How a notice, or one of its rows, reads at a glance. */
24
+ export type NoticeSeverity = "info" | "ok" | "warn" | "bad";
25
+
26
+ /** One row under a notice's headline. */
27
+ export interface NoticeItem {
28
+ /** The row's text, e.g. `j1 build`. Sanitized by the renderer. */
29
+ label: string;
30
+ /** Short trailing detail, e.g. `exit 1`. Sanitized by the renderer. */
31
+ detail?: string;
32
+ /** The row's severity; falls back to the notice's own. */
33
+ severity?: NoticeSeverity;
34
+ }
35
+
36
+ /** Everything the renderer draws, and nothing the model sees. */
37
+ export interface NoticeDetails {
38
+ /** Who raised it, e.g. `job` or `watch`. */
39
+ source: string;
40
+ /** One-line headline. */
41
+ title: string;
42
+ /** The headline's severity; a row may override it. */
43
+ severity?: NoticeSeverity;
44
+ /** Rows under the headline, in order. */
45
+ items?: readonly NoticeItem[];
46
+ }
47
+
48
+ export interface NoticeInput {
49
+ /** Model-facing text; the notice is also shown when non-empty. */
50
+ content: string;
51
+ /** Renderer-facing structure. */
52
+ details: NoticeDetails;
53
+ }
54
+
55
+ /** The message shape both send paths accept (`pi.sendMessage`, `before_agent_start`). */
56
+ export interface NoticeMessage {
57
+ customType: string;
58
+ content: string;
59
+ display: true;
60
+ details: NoticeDetails;
61
+ }
62
+
63
+ /** A conversation message narrowed to a notice. */
64
+ export type NoticeEntry = AgentMessage & { customType: string; details?: NoticeDetails };
65
+
66
+ /** Build a visible notice message: shared type, `display: true`, the given content and details. */
67
+ export function noticeMessage(input: NoticeInput): NoticeMessage {
68
+ return { customType: NOTICE_TYPE, content: input.content, display: true, details: input.details };
69
+ }
70
+
71
+ /** Whether a conversation message is a notice. */
72
+ export function isNotice(message: AgentMessage): message is NoticeEntry {
73
+ return message.role === "custom" && (message as { customType?: string }).customType === NOTICE_TYPE;
74
+ }
75
+
76
+ /**
77
+ * Keep only the newest notice of `source` in a transcript, so stale injected
78
+ * context does not accumulate. Notices of other sources and every non-notice
79
+ * are left alone. Returns `undefined` when nothing is stale, so a `context`
80
+ * handler can leave the messages untouched.
81
+ */
82
+ export function pruneStaleNotices(messages: readonly AgentMessage[], source: string): AgentMessage[] | undefined {
83
+ const ofSource = messages.filter((message) => isNotice(message) && message.details?.source === source);
84
+ if (ofSource.length <= 1) return undefined;
85
+ const keep = ofSource[ofSource.length - 1];
86
+ return messages.filter((message) => !(isNotice(message) && message.details?.source === source && message !== keep));
87
+ }
@@ -166,7 +166,7 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
166
166
  optional: [{ kind: "module", name: "unpdf", enables: "PDF text extraction in `web_fetch`", hint: pdfGuidance() }],
167
167
  },
168
168
  "file-browse": {
169
- optional: [gitCheck("the branch chip")],
169
+ optional: [gitCheck("the branch chip and the diff view")],
170
170
  notes: ["The default-browser opener (`open` / `xdg-open` / `cmd`) is best-effort; the URL is printed either way."],
171
171
  },
172
172
  document: {},
@@ -194,7 +194,7 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
194
194
  bytefly: {
195
195
  configFiles: [SHARED_CONFIG_FILE],
196
196
  notes: [
197
- "TUI only for the header, footer, and turn rule; `/bytefly` (and the `/extensions` and `/doctor` aliases) prints a listing and report in RPC, JSON, and print modes.",
197
+ "TUI only for the header, footer, and turn rule; `/bytefly` prints a listing and `/bytefly doctor` the report in RPC, JSON, and print modes.",
198
198
  "The panel writes extension enablement through Pi's own settings; project scope requires a trusted project.",
199
199
  "`/bytefly`'s list screen and the proactive missing-prerequisite warning need an interactive UI.",
200
200
  ],