@gavin-hu/my-pi-agent 0.21.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 (254) hide show
  1. package/README.md +46 -31
  2. package/README.zh-CN.md +40 -26
  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 -24
  9. package/docs/extensions/board.md +361 -147
  10. package/docs/extensions/bytefly.md +152 -111
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +30 -31
  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 +166 -126
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +34 -18
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +72 -33
  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 +56 -15
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +5 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +4 -40
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +13 -11
  38. package/extensions/board/result.ts +30 -16
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +42 -10
  41. package/extensions/board/snapshot.ts +2 -1
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +131 -44
  44. package/extensions/board/types.ts +36 -32
  45. package/extensions/board/view/card.ts +229 -0
  46. package/extensions/board/view/client.ts +33 -15
  47. package/extensions/board/view/dialog.ts +169 -6
  48. package/extensions/board/view/empty.ts +47 -0
  49. package/extensions/board/view/keys.ts +13 -2
  50. package/extensions/board/view/live.ts +89 -21
  51. package/extensions/board/view/names.ts +13 -4
  52. package/extensions/board/view/page.ts +23 -9
  53. package/extensions/board/view/render.ts +208 -271
  54. package/extensions/board/view/styles.ts +123 -31
  55. package/extensions/board/view/team.ts +297 -76
  56. package/extensions/bytefly/block/fallback.ts +1 -1
  57. package/extensions/bytefly/doctor/runtime.ts +5 -2
  58. package/extensions/bytefly/footer/collect/label.ts +60 -0
  59. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  60. package/extensions/bytefly/footer/config.ts +4 -2
  61. package/extensions/bytefly/footer/register.ts +33 -4
  62. package/extensions/bytefly/footer/render/lines.ts +7 -4
  63. package/extensions/bytefly/footer/render/segments.ts +37 -10
  64. package/extensions/bytefly/footer/render/status.ts +0 -11
  65. package/extensions/bytefly/footer/types.ts +7 -0
  66. package/extensions/bytefly/header/config.ts +1 -1
  67. package/extensions/bytefly/index.ts +32 -6
  68. package/extensions/bytefly/mode/editor.ts +115 -0
  69. package/extensions/bytefly/panel/commands.ts +210 -120
  70. package/extensions/bytefly/panel/format.ts +4 -4
  71. package/extensions/bytefly/panel/settings.ts +73 -6
  72. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  73. package/extensions/bytefly/update/cache.ts +1 -1
  74. package/extensions/bytefly/update/register.ts +4 -4
  75. package/extensions/bytefly/working/config.ts +5 -7
  76. package/extensions/bytefly/working/format.ts +10 -13
  77. package/extensions/bytefly/working/register.ts +22 -27
  78. package/extensions/check/commands.ts +3 -2
  79. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  80. package/extensions/context/commands.ts +2 -2
  81. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  82. package/extensions/file-browse/index.ts +67 -36
  83. package/extensions/file-read/render.ts +4 -4
  84. package/extensions/goal/plan-route.ts +2 -2
  85. package/extensions/job/child.ts +527 -0
  86. package/extensions/job/clock.ts +142 -0
  87. package/extensions/job/commands.ts +4 -2
  88. package/extensions/job/config.ts +8 -1
  89. package/extensions/job/format.ts +22 -1
  90. package/extensions/job/log-writer.ts +13 -2
  91. package/extensions/job/registry.ts +63 -7
  92. package/extensions/job/render.ts +34 -0
  93. package/extensions/job/runtime.ts +296 -655
  94. package/extensions/job/session.ts +132 -19
  95. package/extensions/job/store.ts +7 -7
  96. package/extensions/job/tools.ts +3 -3
  97. package/extensions/job/types.ts +47 -0
  98. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
  99. package/extensions/job/waiters.ts +44 -1
  100. package/extensions/memory/commands.ts +4 -2
  101. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  102. package/extensions/plan/commands.ts +12 -9
  103. package/extensions/plan/format.ts +8 -2
  104. package/extensions/plan/git-bash.ts +115 -16
  105. package/extensions/plan/index.ts +53 -52
  106. package/extensions/plan/plan-index.ts +4 -2
  107. package/extensions/plan/plans.ts +5 -54
  108. package/extensions/plan/policy.ts +8 -10
  109. package/extensions/plan/review-lazy.ts +85 -0
  110. package/extensions/plan/review.ts +2 -2
  111. package/extensions/plan/runtime.ts +49 -39
  112. package/extensions/plan/sections.ts +1 -1
  113. package/extensions/plan/steps.ts +3 -3
  114. package/extensions/plan/todos.ts +1 -1
  115. package/extensions/plan/tools/cancel.ts +27 -62
  116. package/extensions/plan/tools/enter.ts +55 -81
  117. package/extensions/plan/tools/exit.ts +221 -238
  118. package/extensions/plan/tools/refusal.ts +18 -0
  119. package/extensions/plan/tools/steps.ts +75 -92
  120. package/extensions/plan/tools/write.ts +77 -120
  121. package/extensions/plan/tools.ts +188 -30
  122. package/extensions/plan/tui.ts +5 -5
  123. package/extensions/plan/types.ts +48 -5
  124. package/extensions/plan/web-client.ts +3 -0
  125. package/extensions/plan/web-lazy.ts +53 -0
  126. package/extensions/plan/web-markdown.ts +9 -67
  127. package/extensions/plan/web-payload.ts +3 -3
  128. package/extensions/plan/web-router.ts +18 -9
  129. package/extensions/plan/web.ts +1 -12
  130. package/extensions/question/tools.ts +6 -2
  131. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  132. package/extensions/rewind/commands/commands.ts +6 -3
  133. package/extensions/rewind/state/runtime.ts +1 -1
  134. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  135. package/extensions/rewind/view/review-tui.ts +4 -4
  136. package/extensions/serve/config.ts +10 -8
  137. package/extensions/serve/index.ts +17 -3
  138. package/extensions/serve/registry.ts +26 -17
  139. package/extensions/session-bus/commands.ts +11 -6
  140. package/extensions/session-bus/join-request.ts +2 -1
  141. package/extensions/session-bus/mail-watch.ts +26 -17
  142. package/extensions/session-bus/protocol.ts +15 -3
  143. package/extensions/session-bus/resolve.ts +5 -7
  144. package/extensions/session-bus/runtime.ts +40 -12
  145. package/extensions/session-bus/store.ts +101 -19
  146. package/extensions/session-bus/types.ts +19 -0
  147. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  148. package/extensions/subagent/agents.ts +1 -1
  149. package/extensions/subagent/commands.ts +4 -2
  150. package/extensions/subagent/format.ts +15 -15
  151. package/extensions/subagent/index.ts +1 -1
  152. package/extensions/subagent/render.ts +3 -3
  153. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  154. package/extensions/team/commands.ts +24 -7
  155. package/extensions/team/dispatch.ts +18 -4
  156. package/extensions/team/format.ts +17 -2
  157. package/extensions/team/index.ts +9 -8
  158. package/extensions/team/schema.ts +6 -5
  159. package/extensions/team/tools.ts +107 -45
  160. package/extensions/team/types.ts +3 -2
  161. package/extensions/todo/commands.ts +4 -2
  162. package/extensions/todo/format.ts +11 -0
  163. package/extensions/todo/render.ts +57 -0
  164. package/extensions/todo/runtime.ts +1 -1
  165. package/extensions/todo/tools.ts +1 -1
  166. package/extensions/todo/view/todo-screen.ts +122 -0
  167. package/extensions/todo/widget.ts +47 -0
  168. package/extensions/watch/commands.ts +4 -2
  169. package/extensions/watch/format.ts +36 -7
  170. package/extensions/watch/render.ts +57 -0
  171. package/extensions/watch/runtime.ts +27 -22
  172. package/extensions/watch/tools.ts +4 -4
  173. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  174. package/extensions/web-access/fetch/tool.ts +12 -4
  175. package/extensions/wechat/commands/commands.ts +18 -14
  176. package/extensions/wechat/index.ts +72 -37
  177. package/extensions/wechat/state/credentials.ts +1 -1
  178. package/extensions/wechat/state/state.ts +1 -1
  179. package/extensions/wechat/text/status.ts +112 -0
  180. package/extensions/wechat/types.ts +2 -2
  181. package/extensions/wechat/view/status-screen.ts +4 -103
  182. package/extensions/worktree/commands.ts +11 -9
  183. package/extensions/worktree/enter.ts +2 -2
  184. package/extensions/worktree/exit.ts +2 -2
  185. package/extensions/worktree/index.ts +5 -5
  186. package/extensions/worktree/screen.ts +3 -3
  187. package/extensions/worktree/status.ts +2 -2
  188. package/extensions/worktree/tools.ts +113 -116
  189. package/lib/README.md +27 -13
  190. package/lib/board/actions.ts +111 -0
  191. package/lib/board/artifact.ts +149 -0
  192. package/lib/board/card.ts +163 -52
  193. package/lib/board/config.ts +58 -0
  194. package/lib/board/contract.ts +113 -0
  195. package/lib/board/join.ts +38 -0
  196. package/lib/board/lanes.ts +182 -0
  197. package/lib/board/lead.ts +130 -0
  198. package/lib/board/limits.ts +78 -0
  199. package/lib/board/links.ts +31 -10
  200. package/lib/board/page-pointer.ts +1 -1
  201. package/lib/board/read.ts +83 -0
  202. package/lib/board/reason.ts +102 -0
  203. package/lib/board/round-text.ts +68 -6
  204. package/lib/board/round.ts +31 -15
  205. package/lib/board/rules.ts +151 -307
  206. package/lib/board/seat.ts +35 -0
  207. package/lib/board/session.ts +136 -54
  208. package/lib/board/skipped.ts +22 -0
  209. package/lib/board/steps.ts +487 -0
  210. package/lib/board/store.ts +87 -30
  211. package/lib/board/tone.ts +92 -0
  212. package/lib/board/view.ts +70 -58
  213. package/{extensions/watch → lib}/child.ts +40 -21
  214. package/lib/env.ts +65 -7
  215. package/lib/format.ts +13 -0
  216. package/lib/http/index.ts +14 -12
  217. package/lib/http/port.ts +15 -10
  218. package/lib/lazy.ts +45 -0
  219. package/lib/plan-entry.ts +39 -10
  220. package/lib/plans/paths.ts +82 -0
  221. package/lib/policy.ts +9 -6
  222. package/lib/report.ts +3 -2
  223. package/lib/requirements.ts +8 -8
  224. package/lib/session-label.ts +111 -5
  225. package/lib/session-roster.ts +43 -1
  226. package/lib/session-tool.ts +5 -4
  227. package/lib/stray-fields.ts +7 -2
  228. package/lib/tool-names.ts +5 -10
  229. package/lib/tui/README.md +23 -8
  230. package/lib/tui/chrome.ts +16 -15
  231. package/lib/tui/cursor.ts +18 -3
  232. package/lib/tui/glyphs.ts +9 -7
  233. package/lib/tui/tokens.ts +33 -2
  234. package/lib/tui/tool.ts +3 -3
  235. package/lib/web/README.md +21 -0
  236. package/lib/web/components.ts +5 -1
  237. package/lib/web/index.ts +1 -0
  238. package/lib/web/markdown.ts +83 -0
  239. package/lib/web/markup.ts +2 -1
  240. package/lib/web/runtime.ts +9 -10
  241. package/package.json +6 -1
  242. package/skills/init/SKILL.md +5 -5
  243. package/skills/next/SKILL.md +3 -5
  244. package/extensions/board/seat.ts +0 -28
  245. package/extensions/todo/tui.ts +0 -212
  246. package/lib/tui/index.ts +0 -34
  247. package/skills/project-docs/SKILL.md +0 -133
  248. package/skills/project-docs/references/audit.md +0 -131
  249. package/skills/project-docs/references/changelog.md +0 -105
  250. package/skills/project-docs/references/discovery.md +0 -127
  251. package/skills/project-docs/references/readme.md +0 -80
  252. package/skills/project-docs/references/release.md +0 -84
  253. package/skills/project-docs/references/roadmap.md +0 -93
  254. package/skills/project-docs/scripts/state.ts +0 -316
@@ -1,20 +1,20 @@
1
1
  /**
2
2
  * Pure text for the live working line.
3
3
  *
4
- * Before the first tool starts it is a plain count (`Working… 42s`). After that
5
- * it names the last top-level tool and how many this run has started, then the
6
- * elapsed time (`edit · 3 tools · 42s`). Whole seconds, because the tick
7
- * advances once a second; negative and non-finite input reads as `0s`.
4
+ * A plain count while the run has started nothing (`working · 42s`), then how
5
+ * many top-level tools this run has started and the elapsed time
6
+ * (`working · 3 tools · 42s`). The line no longer names the tool: it is one word
7
+ * a reader can skim, and the transcript already draws each call as its own block.
8
+ * Whole seconds, because the tick advances once a second; negative and
9
+ * non-finite input reads as `0s`.
8
10
  */
9
11
 
10
- import { sanitizeLine, formatToolCount } from "../../../lib/format.ts";
12
+ import { formatToolCount } from "../../../lib/format.ts";
11
13
  import { SEPARATORS } from "../../../lib/tui/glyphs.ts";
12
14
  import { WORKING_CONFIG } from "./config.ts";
13
15
 
14
16
  /** What the current run has done so far, for the live line. */
15
17
  export interface WorkingRun {
16
- /** The last top-level tool started, or `null` before the first one. */
17
- readonly tool: string | null;
18
18
  /** Top-level tool calls started in this run. */
19
19
  readonly tools: number;
20
20
  }
@@ -30,13 +30,10 @@ export function formatElapsed(ms: number): string {
30
30
  return `${seconds}s`;
31
31
  }
32
32
 
33
- /** The working-row message: `Working… 4s` until a tool starts, then the run line. */
33
+ /** The working line's message: `working · 4s`, then `working · 3 tools · 4s`. */
34
34
  export function workingCounter(ms: number, run?: WorkingRun): string {
35
35
  const elapsed = formatElapsed(ms);
36
36
  const tools = run?.tools ?? 0;
37
- const tool = run?.tool ? sanitizeLine(run.tool) : "";
38
- if (tools > 0) {
39
- return [tool, formatToolCount(tools), elapsed].filter((part) => part !== "").join(SEPARATORS.item);
40
- }
41
- return `${WORKING_CONFIG.working}${WORKING_CONFIG.ellipsis} ${elapsed}`;
37
+ const count = tools > 0 ? formatToolCount(tools) : "";
38
+ return [WORKING_CONFIG.working, count, elapsed].filter((part) => part !== "").join(SEPARATORS.item);
42
39
  }
@@ -1,31 +1,30 @@
1
1
  /**
2
2
  * bytefly's live working line.
3
3
  *
4
- * Pi's working row shows a static `Working`, so a long run reads the same as a
5
- * short one. This surface advances the message once a second while the row is
6
- * up, and hands it back to Pi when the row comes down. Once a top-level tool
7
- * starts, the message also names that tool and how many this run has started,
8
- * turning the row into a live run line (`edit · 3 tools · 12s`).
4
+ * Pi's working indicator shows a static `Working`, so a long run reads the same as
5
+ * a short one. This surface advances the message once a second while the indicator
6
+ * is up, and hands it back to Pi when it comes down. Once a top-level tool starts,
7
+ * the message also counts this run's tools, turning the line on the composer's top
8
+ * border into `working · 3 tools · 12s`.
9
9
  *
10
- * The timer is tied to the row's visibility. The host shows the working
11
- * indicator in its own `turn_start` handler and clears it at `agent_end`
12
- * (`interactive-mode.js`), so the tick starts at `turn_start` and stops at
13
- * `agent_end` — the two moments the row appears and disappears. Turns inside
14
- * one run do not restart it: the host leaves the same indicator up across
15
- * turns, and `start` is a no-op while it is counting.
10
+ * The timer is tied to the indicator's visibility. The host shows it in its own
11
+ * `turn_start` handler and clears it at `agent_end` (`interactive-mode.js`), so the
12
+ * tick starts at `turn_start` and stops at `agent_end` — the two moments it appears
13
+ * and disappears. Turns inside one run do not restart it: the host leaves the same
14
+ * indicator up across turns, and `start` is a no-op while it is counting.
16
15
  *
17
16
  * A nested tool call (one carrying `parentToolCallId`, such as a codemode
18
- * script's inner call) is not counted and does not take over the line, so the
19
- * number matches the tool blocks the transcript shows. The tool events repaint
20
- * the row immediately; the one-second tick is still the only timer.
17
+ * script's inner call) is not counted, so the number matches the tool blocks the
18
+ * transcript shows. The tool events repaint the line immediately; the one-second
19
+ * tick is still the only timer.
21
20
  *
22
- * Compaction and auto-retry also replace the row, but both run outside this
21
+ * Compaction and auto-retry also replace the indicator, but both run outside this
23
22
  * window (`auto_retry_start` follows `agent_end`; the before-settle compaction
24
23
  * runs after it), and neither is an extension event, so there is nothing to
25
24
  * hook and nothing to undo.
26
25
  *
27
- * The handle is global (`setWorkingMessage` drives the single working row), so
28
- * the tick is cancelled and the message reset on stop and, belt and braces, on
26
+ * The handle is global (`setWorkingMessage` drives the single indicator), so the
27
+ * tick is cancelled and the message reset on stop and, belt and braces, on
29
28
  * `session_shutdown` — the factory itself only registers.
30
29
  */
31
30
 
@@ -50,14 +49,13 @@ export function registerWorkingTimer(pi: ExtensionAPI, deps: WorkingDeps = {}):
50
49
  let startedAt = 0;
51
50
  // A boolean, not `startedAt === 0`: an injected clock is allowed to start at 0.
52
51
  let active = false;
53
- // The last top-level tool and how many this run has started. Reset with the run.
54
- let tool: string | null = null;
52
+ // How many top-level tools this run has started. Reset with the run.
55
53
  let tools = 0;
56
54
 
57
- /** The current row text, from the run state and the elapsed time. */
58
- const message = (): string => workingCounter(now() - startedAt, { tool, tools });
55
+ /** The current line's text, from the run state and the elapsed time. */
56
+ const message = (): string => workingCounter(now() - startedAt, { tools });
59
57
 
60
- /** Repaint an active row now, so a tool start does not wait for the next tick. */
58
+ /** Repaint an active line now, so a tool start does not wait for the next tick. */
61
59
  const refresh = (ctx: ExtensionContext): void => {
62
60
  if (active) ctx.ui.setWorkingMessage(message());
63
61
  };
@@ -68,7 +66,6 @@ export function registerWorkingTimer(pi: ExtensionAPI, deps: WorkingDeps = {}):
68
66
  cancel = undefined;
69
67
  if (!active) return;
70
68
  active = false;
71
- tool = null;
72
69
  tools = 0;
73
70
  ctx.ui.setWorkingMessage(undefined);
74
71
  };
@@ -78,7 +75,6 @@ export function registerWorkingTimer(pi: ExtensionAPI, deps: WorkingDeps = {}):
78
75
  if (active) return;
79
76
  startedAt = now();
80
77
  active = true;
81
- tool = null;
82
78
  tools = 0;
83
79
  const tick = (): void => {
84
80
  ctx.ui.setWorkingMessage(message());
@@ -88,17 +84,16 @@ export function registerWorkingTimer(pi: ExtensionAPI, deps: WorkingDeps = {}):
88
84
  };
89
85
 
90
86
  // The host shows the working indicator on its own `turn_start` handler, so
91
- // this is the first frame the row is up.
87
+ // this is the first frame the indicator is up.
92
88
  pi.on("turn_start", (_event, ctx) => {
93
89
  if (ctx.mode !== "tui") return;
94
90
  start(ctx);
95
91
  });
96
92
 
97
93
  // A nested tool is an implementation detail of its parent run, not a
98
- // transcript block, so it neither counts nor takes over the line.
94
+ // transcript block, so it does not count toward the line.
99
95
  pi.on("tool_execution_start", (event, ctx) => {
100
96
  if (ctx.mode !== "tui" || !active || event.parentToolCallId) return;
101
- tool = event.toolName;
102
97
  tools += 1;
103
98
  refresh(ctx);
104
99
  });
@@ -12,10 +12,8 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
12
12
  import { resolveEffectiveCwd } from "../../lib/env.ts";
13
13
  import { planModeRefusal } from "../../lib/plan-entry.ts";
14
14
  import { isPlanModeEnabled } from "../../lib/plan-state.ts";
15
- import { withRailsSuppressed } from "../../lib/rails.ts";
16
15
  import { formatCheckReport, formatCheckSummary } from "./format.ts";
17
16
  import { CheckError, type CheckRuntime } from "./runtime.ts";
18
- import { CheckResultComponent, CheckRunComponent } from "./tui.ts";
19
17
  import type { CheckResult, CheckTarget } from "./types.ts";
20
18
 
21
19
  /** The accepted targets and their completion descriptions. */
@@ -42,6 +40,8 @@ function runLabel(commands: readonly string[]): string {
42
40
 
43
41
  /** The TUI loop: run, then list/detail, with rerun. */
44
42
  async function runTui(runtime: CheckRuntime, ctx: ExtensionCommandContext, target: CheckTarget): Promise<void> {
43
+ // The screens are the TUI's: a text run above and registration load neither.
44
+ const { CheckResultComponent, CheckRunComponent } = await import("./view/check-screens.ts");
45
45
  while (true) {
46
46
  let active: ReturnType<CheckRuntime["start"]>;
47
47
  try {
@@ -123,6 +123,7 @@ export function registerCheckCommand(pi: ExtensionAPI, runtime: CheckRuntime): v
123
123
  return;
124
124
  }
125
125
 
126
+ const { withRailsSuppressed } = await import("../../lib/rails.ts");
126
127
  await withRailsSuppressed(pi, () => runTui(runtime, ctx, target));
127
128
  },
128
129
  });
@@ -11,19 +11,19 @@
11
11
 
12
12
  import type { Theme } from "@earendil-works/pi-coding-agent";
13
13
  import { type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
14
- import { formatDuration, sanitizeLine } from "../../lib/format.ts";
15
- import { dockListFrame, screenHint, type ScreenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
16
- import { clip } from "../../lib/tui/columns.ts";
17
- import { fitRows, formatRange, isCloseKey, wheelDelta } from "../../lib/tui/cursor.ts";
18
- import { DockListScreen, type DetailContents } from "../../lib/tui/dock-screen.ts";
19
- import { SEPARATORS } from "../../lib/tui/glyphs.ts";
20
- import { LinePane } from "../../lib/tui/line-pane.ts";
21
- import { emptyState, listRow } from "../../lib/tui/rows.ts";
22
- import { titleRule } from "../../lib/tui/rules.ts";
23
- import { paint } from "../../lib/tui/tokens.ts";
24
- import { formatFailureDetail, formatFailureLocation, formatResultSummary } from "./format.ts";
25
- import { readSourceExcerpt } from "./source.ts";
26
- import type { CheckResult, Failure } from "./types.ts";
14
+ import { formatDuration, sanitizeLine } from "../../../lib/format.ts";
15
+ import { dockListFrame, screenHint, type ScreenHint, type ViewportRowsSource } from "../../../lib/tui/chrome.ts";
16
+ import { clip } from "../../../lib/tui/columns.ts";
17
+ import { fitRows, formatRange, isCloseKey, wheelDelta } from "../../../lib/tui/cursor.ts";
18
+ import { DockListScreen, type DetailContents } from "../../../lib/tui/dock-screen.ts";
19
+ import { SEPARATORS } from "../../../lib/tui/glyphs.ts";
20
+ import { LinePane } from "../../../lib/tui/line-pane.ts";
21
+ import { emptyState, listRow } from "../../../lib/tui/rows.ts";
22
+ import { titleRule } from "../../../lib/tui/rules.ts";
23
+ import { paint } from "../../../lib/tui/tokens.ts";
24
+ import { formatFailureDetail, formatFailureLocation, formatResultSummary } from "../format.ts";
25
+ import { readSourceExcerpt } from "../source.ts";
26
+ import type { CheckResult, Failure } from "../types.ts";
27
27
 
28
28
  const RUN_DEFAULT_ROWS = 20;
29
29
  const RUN_CHROME_ROWS = 5;
@@ -166,12 +166,7 @@ export class CheckResultComponent extends DockListScreen<Failure> {
166
166
  protected listHints(rows: readonly Failure[]): ScreenHint[] {
167
167
  // An empty list has nothing to open, so it offers only the two ways out.
168
168
  if (rows.length === 0) return ["Esc close", { text: "r rerun", color: "accent" }];
169
- return [
170
- "Esc close",
171
- { text: "Enter detail", color: "accent" },
172
- { text: "r rerun", color: "warning" },
173
- "↑/↓ select",
174
- ];
169
+ return ["Esc close", { text: "Enter detail", color: "accent" }, { text: "r rerun", color: "warn" }, "↑/↓ select"];
175
170
  }
176
171
 
177
172
  protected detailHints(): ScreenHint[] {
@@ -10,12 +10,10 @@
10
10
 
11
11
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
12
12
  import { formatTokens } from "../../lib/format.ts";
13
- import { withRailsSuppressed } from "../../lib/rails.ts";
14
13
  import { messageOf } from "../../lib/errors.ts";
15
14
  import { collectInputs } from "./collect.ts";
16
15
  import { formatReport } from "./format.ts";
17
16
  import { buildReport } from "./report.ts";
18
- import { ContextComponent } from "./tui.ts";
19
17
 
20
18
  /** The command's argument surface, shared by the description and the usage warning. */
21
19
  const CONTEXT_USAGE = "/context [compact [instructions]]";
@@ -37,6 +35,8 @@ async function openReport(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promi
37
35
  }
38
36
 
39
37
  // The dock screen owns the editor slot; hide the rails for its lifetime.
38
+ const { withRailsSuppressed } = await import("../../lib/rails.ts");
39
+ const { ContextComponent } = await import("./view/context-report.ts");
40
40
  await withRailsSuppressed(pi, () =>
41
41
  ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
42
42
  return new ContextComponent({
@@ -14,17 +14,17 @@
14
14
 
15
15
  import type { CompactionResult, Theme } from "@earendil-works/pi-coding-agent";
16
16
  import { truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
17
- import { messageOf } from "../../lib/errors.ts";
18
- import { formatTokens } from "../../lib/format.ts";
19
- import { screenFooter, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
20
- import { ConfirmState } from "../../lib/tui/confirm.ts";
21
- import { clampScroll, fitRows, formatRange, isCloseKey, navigateScroll } from "../../lib/tui/cursor.ts";
22
- import { gaugeBar, gaugeRole } from "../../lib/tui/gauge.ts";
23
- import { BODY_INDENT } from "../../lib/tui/indent.ts";
24
- import { titleRule } from "../../lib/tui/rules.ts";
25
- import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
26
- import { formatBudget, formatCompaction, formatDrift } from "./format.ts";
27
- import type { ContextBucket, ContextReport } from "./types.ts";
17
+ import { messageOf } from "../../../lib/errors.ts";
18
+ import { formatTokens } from "../../../lib/format.ts";
19
+ import { screenFooter, screenHint, type ViewportRowsSource } from "../../../lib/tui/chrome.ts";
20
+ import { ConfirmState } from "../../../lib/tui/confirm.ts";
21
+ import { clampScroll, fitRows, formatRange, isCloseKey, navigateScroll } from "../../../lib/tui/cursor.ts";
22
+ import { gaugeBar, gaugeRole } from "../../../lib/tui/gauge.ts";
23
+ import { BODY_INDENT } from "../../../lib/tui/indent.ts";
24
+ import { titleRule } from "../../../lib/tui/rules.ts";
25
+ import { paint, type TuiRole } from "../../../lib/tui/tokens.ts";
26
+ import { formatBudget, formatCompaction, formatDrift } from "../format.ts";
27
+ import type { ContextBucket, ContextReport } from "../types.ts";
28
28
 
29
29
  /** Fixed rows around the scrollable body: title, gauge, budget, blank, blank, footer, closing rule. */
30
30
  const CHROME_ROWS = 7;
@@ -71,6 +71,34 @@ function shareCell(share: number): string {
71
71
  return `${share.toFixed(1)}%`.padStart(6);
72
72
  }
73
73
 
74
+ /**
75
+ * The hue that identifies a context category, keyed by the bucket ids
76
+ * `report.ts` builds.
77
+ *
78
+ * Categories are what the screen tells apart at a glance; a bucket with no
79
+ * entry here — injected context and its children, summaries, shell output, a
80
+ * type this build has never seen — reads neutral, so the catch-all rows stay
81
+ * quiet and the coloured ones mean something. The same table answers for the
82
+ * largest-message list, so a tool result is one colour in both sections.
83
+ */
84
+ const CATEGORY_HUES: Readonly<Record<string, TuiRole>> = {
85
+ system: "hue1",
86
+ tools: "hue2",
87
+ user: "hue3",
88
+ assistant: "hue4",
89
+ toolResults: "hue5",
90
+ };
91
+
92
+ /** The hue for a bucket id, or `undefined` when the category is neutral. */
93
+ export function hueOf(id: string): TuiRole | undefined {
94
+ return CATEGORY_HUES[id];
95
+ }
96
+
97
+ /** The hue for a message role word; `tool result` is the bucket's own category. */
98
+ export function hueForRole(role: string): TuiRole | undefined {
99
+ return CATEGORY_HUES[role === "tool result" ? "toolResults" : role];
100
+ }
101
+
74
102
  /** Pad a label to its column with spaces, measured in terminal columns. */
75
103
  function padCell(text: string, width: number): string {
76
104
  const clipped = truncateToWidth(text, width, "…");
@@ -151,7 +179,7 @@ export class ContextComponent implements Component {
151
179
  private compositionLine(width: number): string {
152
180
  const label = paint(this.theme, "label", "Composition");
153
181
  const total = paint(this.theme, "meta", `${formatTokens(this.report.estimate)} estimate`);
154
- const full = ` ${label} · ${total}`;
182
+ const full = ` ${label}${paint(this.theme, "faint", " · ")}${total}`;
155
183
  return visibleWidth(full) <= width ? full : truncateToWidth(` ${label}`, width);
156
184
  }
157
185
 
@@ -161,12 +189,15 @@ export class ContextComponent implements Component {
161
189
  const labelWidth = bucket.depth > 0 ? 20 : 22;
162
190
  const label = padCell(bucket.label, labelWidth);
163
191
  const tokens = formatTokens(bucket.tokens).padStart(6);
164
- const share = shareCell(bucket.share);
165
- const note = bucket.note ? ` ${bucket.note}` : "";
192
+ // The row's hue carries the category; the label, the count, and the printed
193
+ // share still say everything the colour repeats.
194
+ const hue = hueOf(bucket.id) ?? "meta";
195
+ const share = paint(this.theme, hue, shareCell(bucket.share));
196
+ const note = bucket.note ? paint(this.theme, "faint", ` ${bucket.note}`) : "";
166
197
  const bar = gaugeBar(this.theme, {
167
198
  percent: bucket.share,
168
199
  blocks: BUCKET_BLOCKS,
169
- role: "meta",
200
+ role: hue,
170
201
  emptyRole: "faint",
171
202
  });
172
203
  const head = `${indent}${paint(this.theme, "label", label)}${paint(this.theme, "meta", tokens)}`;
@@ -204,7 +235,8 @@ export class ContextComponent implements Component {
204
235
  for (const block of report.largest) {
205
236
  const tokens = formatTokens(block.tokens).padStart(6);
206
237
  const role = padCell(block.role, 12);
207
- const candidate = ` ${paint(this.theme, "meta", tokens)} ${paint(this.theme, "label", role)}${block.label}`;
238
+ const hue = hueForRole(block.role) ?? "meta";
239
+ const candidate = ` ${paint(this.theme, "meta", tokens)} ${paint(this.theme, hue, role)}${paint(this.theme, "label", block.label)}`;
208
240
  lines.push(truncateToWidth(candidate, width));
209
241
  }
210
242
  }
@@ -221,9 +253,9 @@ export class ContextComponent implements Component {
221
253
  width,
222
254
  [
223
255
  { text: "c compact", color: "accent" },
224
- { text: "r refresh", color: "dim" },
225
- { text: "↑↓ scroll", color: "dim" },
226
- { text: "Esc close", color: "dim" },
256
+ { text: "r refresh", color: "faint" },
257
+ { text: "↑↓ scroll", color: "faint" },
258
+ { text: "Esc close", color: "faint" },
227
259
  ],
228
260
  BODY_INDENT,
229
261
  );
@@ -14,16 +14,13 @@ import { realpathSync } from "node:fs";
14
14
  import { stat } from "node:fs/promises";
15
15
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
16
  import { isExtensionEnabled, resolveEffectiveCwd } from "../../lib/env.ts";
17
- import { requestMount } from "../../lib/http/index.ts";
18
- import { loadConfig } from "./config/config.ts";
19
- import { createGitDiff, createGitStatus } from "./git/git.ts";
20
- import { openInBrowser } from "../../lib/browser.ts";
21
- import { HttpError, resolveRequestPath } from "./files/paths.ts";
22
- import { createFileServer, type FileServer } from "./transport/server.ts";
17
+ import type { openInBrowser } from "../../lib/browser.ts";
18
+ import type { ServeConfig } from "./config/config.ts";
19
+ import type { FileServer } from "./transport/server.ts";
23
20
 
24
21
  export interface FileBrowserDeps {
25
22
  /** Override the server factory (tests). */
26
- createServer?: typeof createFileServer;
23
+ createServer?: typeof import("./transport/server.ts").createFileServer;
27
24
  /** Override the browser opener (tests). */
28
25
  open?: typeof openInBrowser;
29
26
  }
@@ -58,51 +55,49 @@ export default function fileBrowser(pi: ExtensionAPI, deps: FileBrowserDeps = {}
58
55
  if (await stop()) current.ctx.ui.notify("browse: stopped — the last page was closed", "info");
59
56
  };
60
57
 
61
- const start = async (args: string, ctx: ExtensionContext): Promise<void> => {
62
- const target = args.trim();
63
- if (target === "status") {
64
- ctx.ui.notify(
65
- running ? `browse: ${running.server.root} at ${running.server.url}` : "browse: not running",
66
- "info",
67
- );
68
- return;
69
- }
70
- if (target === "stop") {
71
- ctx.ui.notify((await stop()) ? "browse: stopped" : "browse: not running", "info");
72
- return;
73
- }
58
+ type ResolvedRoot = { root: string; config: ServeConfig };
74
59
 
75
- const cwd = resolveEffectiveCwd(ctx.cwd);
60
+ /** The folder `/browse [path]` names, or undefined after telling the user why. */
61
+ const resolveRoot = async (target: string, cwd: string, ctx: ExtensionContext): Promise<ResolvedRoot | undefined> => {
62
+ const { loadConfig } = await import("./config/config.ts");
76
63
  const config = loadConfig(cwd);
77
64
  let root = cwd;
78
65
  if (target) {
66
+ const { HttpError, resolveRequestPath } = await import("./files/paths.ts");
79
67
  try {
80
68
  root = await resolveRequestPath(cwd, target);
81
69
  } catch (error) {
82
70
  const message = error instanceof HttpError ? error.message : String(error);
83
71
  ctx.ui.notify(`browse: ${message}`, "error");
84
- return;
72
+ return undefined;
85
73
  }
86
74
  const info = await stat(root).catch(() => undefined);
87
75
  if (!info?.isDirectory()) {
88
76
  ctx.ui.notify(`browse: ${target} is not a folder inside the working directory`, "error");
89
- return;
77
+ return undefined;
90
78
  }
91
79
  }
80
+ return { root, config };
81
+ };
92
82
 
93
- let realRoot = root;
94
- try {
95
- realRoot = realpathSync.native(root);
96
- } catch {
97
- // Keep the unresolved root; the server factory reports a real failure.
98
- }
99
- if (running && running.server.root === realRoot) {
100
- ctx.ui.notify(`browse: ${running.server.url} (already running)`, "info");
101
- return;
102
- }
83
+ /**
84
+ * Put the page in front of the user, unless the project asked to stay headless.
85
+ *
86
+ * Every `/browse` opens it, including one that finds the page already served: the
87
+ * command's job is showing the page, and a URL printed over a running server leaves
88
+ * the user to paste it.
89
+ */
90
+ const openPage = async (url: string, config: ServeConfig): Promise<void> => {
91
+ if (!config.autoOpen) return;
92
+ const open = deps.open ?? (await import("../../lib/browser.ts")).openInBrowser;
93
+ open(url);
94
+ };
103
95
 
104
- await stop();
105
- const create = deps.createServer ?? createFileServer;
96
+ /** Open the page's server; the server, git, HTTP, and browser modules live here. */
97
+ const openServer = async (root: string, config: ServeConfig, ctx: ExtensionContext): Promise<void> => {
98
+ const create = deps.createServer ?? (await import("./transport/server.ts")).createFileServer;
99
+ const { createGitDiff, createGitStatus } = await import("./git/git.ts");
100
+ const { requestMount } = await import("../../lib/http/index.ts");
106
101
  try {
107
102
  const outcome = await create({
108
103
  root,
@@ -125,12 +120,48 @@ export default function fileBrowser(pi: ExtensionAPI, deps: FileBrowserDeps = {}
125
120
  const server = outcome.server;
126
121
  running = { server, ctx };
127
122
  ctx.ui.notify(`browse: ${server.root} at ${server.url}`, "info");
128
- if (config.autoOpen) (deps.open ?? openInBrowser)(server.url);
123
+ await openPage(server.url, config);
129
124
  } catch (error) {
130
125
  ctx.ui.notify(`browse: ${error instanceof Error ? error.message : String(error)}`, "error");
131
126
  }
132
127
  };
133
128
 
129
+ const start = async (args: string, ctx: ExtensionContext): Promise<void> => {
130
+ const target = args.trim();
131
+ if (target === "status") {
132
+ ctx.ui.notify(
133
+ running ? `browse: ${running.server.root} at ${running.server.url}` : "browse: not running",
134
+ "info",
135
+ );
136
+ return;
137
+ }
138
+ if (target === "stop") {
139
+ ctx.ui.notify((await stop()) ? "browse: stopped" : "browse: not running", "info");
140
+ return;
141
+ }
142
+
143
+ const cwd = resolveEffectiveCwd(ctx.cwd);
144
+ const resolved = await resolveRoot(target, cwd, ctx);
145
+ if (resolved === undefined) return;
146
+ const { root, config } = resolved;
147
+
148
+ let realRoot = root;
149
+ try {
150
+ realRoot = realpathSync.native(root);
151
+ } catch {
152
+ // Keep the unresolved root; the server factory reports a real failure.
153
+ }
154
+ if (running && running.server.root === realRoot) {
155
+ ctx.ui.notify(`browse: ${running.server.url} (already running)`, "info");
156
+ // The page is already served, so this is an open and not a restart.
157
+ await openPage(running.server.url, config);
158
+ return;
159
+ }
160
+
161
+ await stop();
162
+ await openServer(root, config, ctx);
163
+ };
164
+
134
165
  pi.registerCommand("browse", {
135
166
  description: "Browse the working directory in a local browser (read-only): /browse [path | status | stop]",
136
167
  getArgumentCompletions: (prefix) => {
@@ -25,10 +25,10 @@ export const PREVIEW_LINES = 20;
25
25
 
26
26
  /** Render the argument path: body colour, `~`-shortened, OSC-8 linked when supported. */
27
27
  export function renderReadPath(rawPath: string | undefined, theme: Theme, cwd: string, argsComplete = true): string {
28
- if (rawPath === undefined && !argsComplete) return theme.fg("toolOutput", "...");
28
+ if (rawPath === undefined && !argsComplete) return paint(theme, "toolOutput", "...");
29
29
  if (typeof rawPath !== "string") return paint(theme, "bad", "[invalid arg]");
30
30
  const value = sanitizeLine(rawPath);
31
- if (!value) return theme.fg("toolOutput", "...");
31
+ if (!value) return paint(theme, "toolOutput", "...");
32
32
  const styled = paint(theme, "label", shortenPath(value));
33
33
  if (!getCapabilities().hyperlinks) return styled;
34
34
  const abs = isAbsolute(value) ? value : resolve(cwd, value);
@@ -93,14 +93,14 @@ export function formatReadResult(result: ReadRenderResult, options: ReadRenderOp
93
93
 
94
94
  const summary = summarizeRead(details);
95
95
  const sep = paint(theme, "faint", SEPARATORS.item);
96
- let text = paint(theme, "meta", summary.format) + sep + theme.fg("toolOutput", summary.detail);
96
+ let text = paint(theme, "meta", summary.format) + sep + paint(theme, "toolOutput", summary.detail);
97
97
  if (summary.note) text += paint(theme, summary.truncated ? "accent" : "faint", SEPARATORS.item + summary.note);
98
98
 
99
99
  if (details.text.length > 0) {
100
100
  if (options.expanded) {
101
101
  const { body, moreLines } = previewText(details.text, PREVIEW_LINES);
102
102
  // Blank line before the body, matching the built-ins' header/output gap.
103
- text += `\n\n${theme.fg("toolOutput", body)}`;
103
+ text += `\n\n${paint(theme, "toolOutput", body)}`;
104
104
  if (moreLines > 0) text += `\n${paint(theme, "meta", `... (${moreLines} more lines)`)}`;
105
105
  } else {
106
106
  const hint = expandKey(theme);
@@ -22,7 +22,7 @@ import { isPursuedGoal } from "../../lib/goal-state.ts";
22
22
  import { isRemoteTurn, remoteLanguage } from "../../lib/interaction.ts";
23
23
  import { PLAN_CONSENT, PLAN_CONSENT_SHORT, PLAN_CONSENT_SHORT_ZH, requestPlanMode } from "../../lib/plan-entry.ts";
24
24
  import { isPlanModeEnabled } from "../../lib/plan-state.ts";
25
- import { PLAN_ENTER_TOOL } from "../../lib/tool-names.ts";
25
+ import { PLAN_TOOL } from "../../lib/tool-names.ts";
26
26
  import type { Goal, GoalStatus } from "./types.ts";
27
27
 
28
28
  /** Dialog title: the question the phase answers. */
@@ -45,7 +45,7 @@ export async function goalPhase(
45
45
  if (isPlanModeEnabled(ctx.sessionManager.getBranch())) return "planning";
46
46
  if (!options.ask || !ctx.hasUI) return "active";
47
47
  // Do not ask about a phase this session cannot enter.
48
- if (!pi.getAllTools().some((tool) => tool.name === PLAN_ENTER_TOOL)) return "active";
48
+ if (!pi.getAllTools().some((tool) => tool.name === PLAN_TOOL)) return "active";
49
49
  const consent = isRemoteTurn(ctx)
50
50
  ? remoteLanguage(ctx) === "zh"
51
51
  ? PLAN_CONSENT_SHORT_ZH