@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
@@ -78,7 +78,9 @@ export function formatResultText(details: MemoryDetails, expanded = false): stri
78
78
  ].join("\n");
79
79
  case "add":
80
80
  if (details.changed) {
81
- return `Added to ${scopeLabel(details.scope)} memory: ${details.entry ?? ""}\nNow ${countLine(entries)}.`;
81
+ // The call line already names the note, so the result confirms the write
82
+ // and the new counts rather than repeating it.
83
+ return `Added to ${scopeLabel(details.scope)} memory\nNow ${countLine(entries)}.`;
82
84
  }
83
85
  if (details.existing !== undefined && details.existing !== details.entry) {
84
86
  return `Already in ${scopeLabel(details.scope)} memory (near-duplicate): ${details.existing}`;
@@ -86,10 +88,10 @@ export function formatResultText(details: MemoryDetails, expanded = false): stri
86
88
  return `Already in ${scopeLabel(details.scope)} memory: ${details.existing ?? details.entry ?? ""}`;
87
89
  case "update":
88
90
  return details.changed
89
- ? `Updated ${scopeLabel(details.scope)} memory: ${details.entry ?? ""} → ${details.to ?? ""}\nNow ${countLine(entries)}.`
91
+ ? `Updated ${scopeLabel(details.scope)} memory\nNow ${countLine(entries)}.`
90
92
  : `Already set in ${scopeLabel(details.scope)} memory: ${details.entry ?? ""}`;
91
93
  case "forget":
92
- return `Removed from ${scopeLabel(details.scope)} memory: ${details.entry ?? ""}\nNow ${countLine(entries)}.`;
94
+ return `Removed from ${scopeLabel(details.scope)} memory\nNow ${countLine(entries)}.`;
93
95
  }
94
96
  }
95
97
 
@@ -11,7 +11,8 @@
11
11
  import type { JsonValue } from "@earendil-works/pi-ai";
12
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
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 { messageOf } from "./errors.ts";
16
17
  import { formatCallText, formatResultText, scopeLabel } from "./format.ts";
17
18
  import type { MemoryRuntime } from "./runtime.ts";
@@ -142,7 +143,7 @@ export function registerTools(pi: ExtensionAPI, runtime: MemoryRuntime): void {
142
143
  args as { action?: unknown; text?: unknown; to?: unknown; scope?: unknown },
143
144
  context.argsComplete,
144
145
  );
145
- text.setText(theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + theme.fg("muted", body));
146
+ text.setText(theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "meta", body));
146
147
  return text;
147
148
  },
148
149
 
@@ -155,7 +156,7 @@ export function registerTools(pi: ExtensionAPI, runtime: MemoryRuntime): void {
155
156
  return text;
156
157
  }
157
158
  if (details.error) {
158
- text.setText(theme.fg("error", `Error: ${sanitizeLine(details.error)}`));
159
+ text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
159
160
  return text;
160
161
  }
161
162
  text.setText(formatResultText(details, expanded));
@@ -5,7 +5,7 @@
5
5
  * every render (`entries()` refreshes from disk first) and asks for a repaint
6
6
  * after an action settles, so an external edit or a mutation is reflected
7
7
  * without the component owning any authoritative state. Navigation and scroll
8
- * math come from `lib/list-cursor.ts` and the screen chrome from `lib/tui.ts`;
8
+ * math come from `lib/tui/cursor.ts` and the screen chrome from `lib/tui/chrome.ts`;
9
9
  * the inline note editor is the shared `Editor` from `@earendil-works/pi-tui`.
10
10
  */
11
11
 
@@ -14,23 +14,18 @@ import {
14
14
  Editor,
15
15
  Key,
16
16
  matchesKey,
17
- truncateToWidth,
18
17
  type Component,
19
18
  type TUI,
20
19
  type TuiMouseEvent,
21
20
  type TuiMouseEventResult,
22
21
  } from "@earendil-works/pi-tui";
23
22
  import { sanitizeLine } from "../../lib/format.ts";
24
- import {
25
- fitRows,
26
- formatRange,
27
- isCloseKey,
28
- ListCursor,
29
- navigateList,
30
- scrollList,
31
- selectionMarker,
32
- } from "../../lib/list-cursor.ts";
33
- import { dockListFrame, editorTheme, screenHint, type ViewportRowsSource } from "../../lib/tui.ts";
23
+ import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../lib/tui/cursor.ts";
24
+ import { dockListFrame, editorTheme, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
25
+ import { clip } from "../../lib/tui/columns.ts";
26
+ import { emptyState, listRow } from "../../lib/tui/rows.ts";
27
+ import { titleRule } from "../../lib/tui/rules.ts";
28
+ import { paint } from "../../lib/tui/tokens.ts";
34
29
  import { messageOf } from "./errors.ts";
35
30
  import { scopeLabel, type MemoryEntries } from "./format.ts";
36
31
  import type { MemoryScope } from "./types.ts";
@@ -301,6 +296,13 @@ export class MemoryPickerComponent implements Component {
301
296
  this.requestRender();
302
297
  return;
303
298
  }
299
+ // The hint advertises `g`/`G`, so bind them to the ends.
300
+ if (data === "g" || data === "G") {
301
+ this.cursor.set(data === "g" ? 0 : notes.length - 1, notes.length);
302
+ this.selectedKey = notes[this.cursor.selected] ? noteKey(notes[this.cursor.selected]) : undefined;
303
+ this.requestRender();
304
+ return;
305
+ }
304
306
 
305
307
  const focused = notes[this.cursor.selected];
306
308
  if (matchesKey(data, Key.tab) || data === "s") this.cycleFilter();
@@ -344,22 +346,21 @@ export class MemoryPickerComponent implements Component {
344
346
  private summary(width: number): string {
345
347
  const entries = this.callbacks.entries();
346
348
  const filter = this.effectiveFilter();
347
- let text = this.theme.fg("accent", filter === "all" ? "All" : scopeLabel(filter));
348
- text += this.theme.fg("muted", ` · ${entries.project.length} project, ${entries.global.length} global`);
349
- if (!this.callbacks.trusted()) text += this.theme.fg("warning", " · project untrusted");
350
- return truncateToWidth(` ${text}`, width, "…");
349
+ let text = paint(this.theme, "accent", filter === "all" ? "All" : scopeLabel(filter));
350
+ text += paint(this.theme, "meta", ` · ${entries.project.length} project, ${entries.global.length} global`);
351
+ if (!this.callbacks.trusted()) text += paint(this.theme, "warn", " · project untrusted");
352
+ return clip(` ${text}`, width);
351
353
  }
352
354
 
353
355
  /** One note row: selection marker, scope tag, and the sanitized note text. */
354
356
  private noteRow(note: Note, selected: boolean, width: number): string {
355
- const scope = this.theme.fg("muted", scopeLabel(note.scope));
356
- const text = sanitizeLine(note.text);
357
- const label = selected ? this.theme.fg("accent", text) : this.theme.fg("text", text);
358
- return truncateToWidth(
359
- `${selectionMarker(this.theme, selected)}${scope} ${this.theme.fg("dim", "·")} ${label}`,
360
- width,
361
- "…",
362
- );
357
+ const tag = `${paint(this.theme, "meta", scopeLabel(note.scope))} ${paint(this.theme, "faint", "·")}`;
358
+ return listRow(this.theme, width, {
359
+ selected,
360
+ leading: tag,
361
+ label: sanitizeLine(note.text),
362
+ labelRole: selected ? "accent" : "label",
363
+ });
363
364
  }
364
365
 
365
366
  private confirmPrompt(): string {
@@ -377,13 +378,10 @@ export class MemoryPickerComponent implements Component {
377
378
  const body: string[] = [];
378
379
  for (const line of this.editor.render(Math.max(1, width - 2))) body.push(`${pad}${line}`);
379
380
  return dockListFrame({
380
- theme: this.theme,
381
- width,
382
- title: `Memory · ${verb} ${scope}`,
383
- summary: truncateToWidth(
384
- ` ${this.theme.fg("muted", this.mode === "edit" ? "Editing the focused note" : `New ${scope} note`)}`,
381
+ header: titleRule(this.theme, width, `Memory · ${verb} ${scope}`),
382
+ summary: clip(
383
+ ` ${paint(this.theme, "meta", this.mode === "edit" ? "Editing the focused note" : `New ${scope} note`)}`,
385
384
  width,
386
- "…",
387
385
  ),
388
386
  body,
389
387
  hint: screenHint(this.theme, width, ["Enter save", "Esc cancel"]),
@@ -401,7 +399,7 @@ export class MemoryPickerComponent implements Component {
401
399
 
402
400
  const body: string[] = [];
403
401
  if (notes.length === 0) {
404
- body.push(truncateToWidth(` ${this.theme.fg("dim", "No notes yet. Press a to add one.")}`, w));
402
+ body.push(...emptyState(this.theme, w, { label: "No notes yet. Press a to add one." }));
405
403
  } else {
406
404
  const { start, end } = this.cursor.window(notes.length);
407
405
  for (let index = start; index < end; index++) {
@@ -410,15 +408,13 @@ export class MemoryPickerComponent implements Component {
410
408
  }
411
409
 
412
410
  const hint = this.confirm
413
- ? truncateToWidth(` ${this.theme.fg("warning", this.confirmPrompt())}`, w)
411
+ ? clip(` ${paint(this.theme, "warn", this.confirmPrompt())}`, w)
414
412
  : this.notice !== undefined
415
- ? truncateToWidth(` ${this.theme.fg("muted", this.notice)}`, w)
413
+ ? clip(` ${paint(this.theme, "meta", this.notice)}`, w)
416
414
  : screenHint(this.theme, w, ["Esc close", "Enter/e edit", "a add", "d forget", "s/Tab scope", "↑/↓", "g/G"]);
417
415
 
418
416
  return dockListFrame({
419
- theme: this.theme,
420
- width: w,
421
- title: "Memory",
417
+ header: titleRule(this.theme, w, "Memory"),
422
418
  summary: this.summary(w),
423
419
  body,
424
420
  range: this.cursor.clipped(notes.length)
@@ -12,6 +12,14 @@ pi -e . # load the whole @gavin-hu/my-pi-agent packag
12
12
  pi --plan # start in plan mode
13
13
  ```
14
14
 
15
+ ## Tool rendering
16
+
17
+ How the `write_plan` and `exit_plan_mode` calls and their results look in the transcript — the call line as a headline and the body indented under it:
18
+
19
+ <img src="../../assets/tools/write_plan.png" alt="The write_plan tool's call and result: the call line and an indented body with the saved plan path and byte count" width="100%">
20
+
21
+ <img src="../../assets/tools/exit_plan_mode.png" alt="The exit_plan_mode tool's call and result: the call line and an indented body with the approval line and the approved plan preview" width="100%">
22
+
15
23
  ## What it does
16
24
 
17
25
  - **Read-only gating, with one exception.** Tools that are not read-only are
@@ -48,8 +56,11 @@ pi --plan # start in plan mode
48
56
  - **Reviewable plan.** `exit_plan_mode` reads the plan file back and presents
49
57
  it for approval. The user approves, keeps planning, or asks for a refinement.
50
58
  - **Browser review on demand.** Press `w` on the review screen (or run
51
- `/plan review [path]`) to open the plan in a local browser, select text to
52
- leave inline comments, and save them back into the plan file itself as
59
+ `/plan review [path]`) to open the plan in a local browser: a reading-first
60
+ page with the plan's H1 as its title, GitHub's Markdown content column, a
61
+ comment rail in
62
+ the left margin, and live section and step progress. Select text to leave an
63
+ inline comment, and save them back into the plan file itself as
53
64
  `> [!REVIEW]` lines. Saved comments return to the model as a refinement
54
65
  request.
55
66
  - **The file is the source of truth.** On approval the model follows the plan
@@ -107,9 +118,9 @@ The review screen is TUI-only. `exit_plan_mode` opens `PlanViewComponent`: a
107
118
  scrollable, width-safe view of the plan with approve/refine/keep in the footer.
108
119
  The body is rendered with Pi's own `Markdown` component and `getMarkdownTheme()`,
109
120
  so headings, lists, inline code, code blocks, links, and quotes carry the active
110
- theme's colors; task items get a `success` `[✓]` (done) or `dim` `[ ]` (pending)
111
- marker. The footer colors the primary actions (`a approve & run` in `success`,
112
- `r refine` in `accent`).
121
+ theme's colors; task items get the shared `◼` (done) or `◻` (pending) marker,
122
+ like the bytefly screen and the question dialog. The footer colors the primary
123
+ actions (`a approve & run` in `success`, `r refine` in `accent`).
113
124
  `a` ends plan mode, restores write access, and seeds `todo`; `r` keeps the plan
114
125
  mounted and opens an inline editor beneath it (`Enter` submits, an empty buffer
115
126
  does nothing, `Esc` returns to the plan); `Esc` keeps planning and tells the
@@ -118,7 +129,8 @@ model the plan was not approved. Scroll with `↑`/`↓` or `j`/`k`, `space`/`b`
118
129
  the next or previous heading; the scroll row names the current section when the
119
130
  plan has more than one. When the plan overflows,
120
131
  a gauge row above the hints shows the scroll position, the percent, and the
121
- visible line range (the range drops first on a narrow terminal); the gauge stays
132
+ visible line range (the range drops first on a narrow terminal); the gauge comes
133
+ from the design system's `gaugeBar` and stays
122
134
  visible while the refine editor is open, since the body above is still clipped.
123
135
  Hints are
124
136
  dropped whole as the terminal narrows, and each footer leads with its safe exit
@@ -138,7 +150,9 @@ the user.
138
150
 
139
151
  Browser review is a local-turn feature: the server binds `127.0.0.1`, so a remote
140
152
  (WeChat) turn cannot reach it and `/plan review` there refuses with a notice
141
- instead of opening a server nobody can use.
153
+ instead of opening a server nobody can use. The browser's **Approve & run** is
154
+ offered only for the review `exit_plan_mode` opens, since only that flow owns
155
+ leaving plan mode; `/plan review` stays comment-only.
142
156
 
143
157
  ### Transcript
144
158
 
@@ -189,8 +203,8 @@ command. The read-only guarantee is layered:
189
203
  strips control characters from the plan body before rendering it in the
190
204
  transcript and in non-TUI notices; a plan file cannot drive the terminal.
191
205
  - **Loopback browser review.** The review server binds `127.0.0.1` and requires a
192
- random per-session token in the URL, checks the `Host` header, serves only
193
- `GET`/`POST`, and caps the request body; the page renders plan and comment text
206
+ random per-session token in the URL, rejects a missing or foreign `Host`
207
+ header, serves only `GET`/`POST`, and caps the request body; the page renders plan and comment text
194
208
  with DOM text nodes, never `innerHTML`, so a plan cannot script the reviewer.
195
209
 
196
210
  ## Read-only git shell
@@ -250,26 +264,52 @@ agent file that overrides it.
250
264
 
251
265
  ## Browser review
252
266
 
253
- <img src="../../assets/plan-review.png" alt="The browser-based plan review, showing the plan's sections, a rendered code block, and an inline comment box" width="100%">
267
+ <img src="../../assets/plan-review.png" alt="The browser-based plan review: a sticky header with the plan title, a status chip, and the Approve & run, Save feedback, and Back to Pi actions, a side panel with step progress and a section outline, and the plan as one measured column with a comment rail and an inline review thread" width="100%">
254
268
 
255
269
  Press `w` on the review screen — or run `/plan review [path]` — to open the plan
256
- in the default browser on `127.0.0.1`. The page is a review surface: a sticky
257
- header with the plan title, theme toggle, and Save/Close; a sticky **section
258
- outline** on the left; and the plan with each comment rendered **inline under
259
- the block it anchors to**. Every block has a **+ Comment** action; select text
260
- first to quote it. **Save feedback** writes the comments into the plan file and
261
- returns them to the model.
270
+ in the default browser on `127.0.0.1`. The page is a reading-first review
271
+ surface: a sticky header carrying the plan's H1 (falling back to the file-name
272
+ slug), a live status chip (connection, comment count, and unsaved changes), the
273
+ theme toggle, and the review actions, over a hairline reading-progress bar. A
274
+ sticky side panel shows the plan's step progress and comment count above a
275
+ **section outline**, and the document is GitHub's Markdown content column — one centred
276
+ measure of at most 1012px with a comment rail in its left margin. Every block
277
+ reveals a **+ Comment** action on hover; select
278
+ text first to quote it. **Save feedback** writes the comments into the plan
279
+ file and returns them to the model.
262
280
 
263
281
  - **Shared theme.** The page uses the same Bytefly tokens, base rules, and
264
282
  syntax-token colors as `file-browse` (`lib/web/`), follows the system light/dark
265
283
  preference until the header toggle sets an explicit choice, and remembers that
266
284
  choice under the shared `pi-web-theme` key.
267
- - **Section outline.** Headings become a sticky outline, labelled in the panel,
268
- with jump links and an active-section highlight; the panel collapses into a
269
- disclosure on narrow screens.
285
+ - **One content column, one rail.** The document is GitHub's Markdown content
286
+ column (a centred measure of at most 1012px), so a plan reads the way the same
287
+ file does on GitHub; each block carries its `+ Comment` button and
288
+ comment-count badge
289
+ in a rail in the left margin, so annotating never reflows the text.
290
+ - **Section outline and facts.** Headings become a sticky outline with jump
291
+ links, an active-section highlight, and a per-section comment count; the panel
292
+ above it reports the plan's checkbox steps (`n/m`) and total comments.
270
293
  - **Inline threads.** A comment renders directly under the block it anchors to,
271
- indented as a thread, so it reads in place and the plan keeps one measure at
272
- every width. There is no separate rail or grid to reflow.
294
+ labelled and indented as a thread, so it reads in place and the plan keeps one
295
+ measure at every width. There is no separate column to reflow.
296
+ - **Keyboard first.** `j`/`k` move to the next or previous block, `c` comments
297
+ on the current block, `a` approves (the approval flow only), `s` saves, `Esc`
298
+ closes the composer or the sheet and then the review, and `?` opens a shortcut
299
+ sheet; the header's `?` button opens the same sheet.
300
+ - **Decision in place.** A review opened for approval (`w` on the review screen)
301
+ shows **Approve & run** beside Save feedback: it persists the current comments,
302
+ ends plan mode, restores write access, seeds `todo`, and hands control back to
303
+ the model. **Save feedback** keeps plan mode and returns the comments as a
304
+ refinement; **Back to Pi** closes the review with no decision. Each action
305
+ settles the page on a terminal state — `Approved`, `Saved`, or `Closed` — so
306
+ the tab never falls to Offline. `/plan review` is comment-only: it renders no
307
+ approve action and the server refuses one.
308
+ - **Markdown that reads.** Headings, lists, task items, blockquotes, fenced
309
+ code, GFM tables, `---` rules, and bare-URL autolinks render; a list item's
310
+ continuation lines hang under the item's text, inline code is a soft fill
311
+ rather than a bordered chip, tables honour the alignment row and scroll
312
+ horizontally when narrow.
273
313
  - **Highlighted code.** Fenced code is tokenized by the shared
274
314
  `lib/highlight.ts`; the client copies only text and `tok-*` spans through
275
315
  `DOMParser`, so a code line is never parsed as markup.
@@ -277,18 +317,24 @@ returns them to the model.
277
317
  - **The file is the store.** Each comment is written as
278
318
  `> [!REVIEW] <comment>` on the line after the block it anchors to. Saving is
279
319
  idempotent: the previous markers are stripped first, so a second save replaces
280
- rather than appends. The browser keeps no copy of its own.
320
+ rather than appends. Stripping is fence-aware: a `> [!REVIEW]` line inside a
321
+ code block is documentation, not feedback. The browser keeps no copy of its own.
281
322
  - **Anchored to source.** `blocks.ts` splits the plan (fence-aware) into
282
323
  source-line ranges; a comment is placed after the whole anchored block, so a
283
324
  fenced code block is never split. A save re-checks the file against what the
284
- page rendered and answers `409` (reload) if it changed underneath.
325
+ page rendered and answers `409`; the page then offers a **Reload** action,
326
+ because only a reload can re-anchor the comments to the new text.
285
327
  - **Back to the model.** A save from the review screen returns `refined: true`
286
328
  with the comments, so the model stays in plan mode and revises the same file.
287
329
  `/plan review` sends them as a user message (a plan-mode revision or a plain
288
330
  update, depending on the mode).
289
331
  - **Loopback and tokenized.** The server binds `127.0.0.1`, carries a random
290
- per-session token in the URL, checks the `Host` header, serves only `GET` and
291
- `POST`, and caps the request body. It is closed on save, cancel, tab close, or
332
+ per-session token in the URL, rejects a missing or foreign `Host` header,
333
+ serves only `GET` and `POST`, and caps the request body. A save or cancel
334
+ resolves the session only after its HTTP response is delivered, so the browser
335
+ never sees a false "Failed to fetch". The last bound port is remembered per
336
+ plan, so reviewing the same plan again reuses the origin (and the browser's
337
+ saved theme with it). It is closed on save, cancel, tab close, or
292
338
  `session_shutdown`.
293
339
 
294
340
  | Route | Result |
@@ -312,6 +358,9 @@ returns them to the model.
312
358
  turn cannot drive it.
313
359
  - A browser that never connects leaves the review waiting until the user
314
360
  cancels, closes the tab, or aborts the turn.
361
+ - Feedback stripping follows fenced code blocks; an unterminated fence makes the
362
+ rest of the file code, so a marker-looking line after it is treated as content
363
+ rather than stored feedback.
315
364
 
316
365
  ## Non-goals
317
366
 
@@ -386,12 +435,13 @@ returns them to the model.
386
435
  existing heading-only split cannot, and the page renders text with DOM nodes
387
436
  so review never widens the plan's trust surface. The browser opener is shared
388
437
  through `lib/browser.ts` rather than imported from `file-browse`.
389
- - **Comments render in the document flow, not a rail.** The anchor is already a
390
- source line, so the browser needs no paired rail cell: a thread is appended
391
- after its block and the plan keeps one measure at every width. That removes
392
- the two-column grid, the `bf-review--no-rail` collapse, and the breakpoint
393
- ladder, and matches the TUI, where neither review header carries a progress
394
- counter.
438
+ - **Comments render in the document flow, not a second column.** The anchor is
439
+ already a source line, so the browser needs no paired column: a thread is
440
+ appended after its block and the plan keeps one measure at every width. The
441
+ per-block affordance lives in a narrow rail inside the measure's left margin,
442
+ so annotating never moves the text. That removes the two-column grid, the
443
+ `bf-review--no-rail` collapse, and the breakpoint ladder, and matches the TUI,
444
+ where neither review header carries a progress counter.
395
445
 
396
446
  ## Files
397
447
 
@@ -407,15 +457,15 @@ returns them to the model.
407
457
  | `tools.ts` | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps`. |
408
458
  | `commands.ts` | `/plan` (enter, optional task) and the `/plan review [path]` browser subcommand. |
409
459
  | `tui.ts` | `PlanViewComponent` (sectioned Markdown body, section jump, scroll gauge, approve/refine/browser/keep). |
410
- | `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, blanks). |
460
+ | `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, tables, rules, blanks). |
411
461
  | `feedback.ts` | Browser-comment codec: `stripFeedback`, `parseFeedback`, `formatFeedback`, `feedbackSummary`, and payload validation. |
412
- | `web.ts` | `createPlanWebReview`: the loopback server, the section outline and code highlighting, and the save/cancel/abort session. |
462
+ | `web.ts` | `createPlanWebReview`: the loopback server, the section outline and code highlighting, and the save/approve/cancel/abort session. |
413
463
  | `web-router.ts` | Pure request handler: token/`Host`/method checks and the review routes. |
414
- | `web-page.ts` | The review page shell (header, section outline), its data island, and inline CSS. |
415
- | `web-client.ts` | The `/client.js` source (block/thread rendering, outline, inline comments, selection quotes, highlighted code, save/cancel). |
464
+ | `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet), its data island, and inline CSS. |
465
+ | `web-client.ts` | The `/client.js` source (block/thread rendering, the comment rail, outline and facts, inline comments, selection quotes, tables, highlighted code, keyboard nav, save/approve/back). |
416
466
  | `path-guard.ts` | Recursive path-argument detection for the read-only backstop. |
417
467
  | [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform default-browser opener (shared with `file-browse`). |
418
468
  | [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer (shared with `file-browse`). |
419
469
  | [`../../lib/web/index.ts`](../../lib/web/index.ts) | Shared Bytefly web brand system: tokens, base rules, `tok-*` colors, layout, the bootstrap, the toggle wiring, and shared markup. |
420
470
  | `../../lib/policy.ts` | Shared read-only capability policy (default-deny + `readOnlyHint`). |
421
- | `../../lib/tui.ts` | Shared screen header, viewport-row helper, and styled key-hint footer. |
471
+ | `../../lib/tui/chrome.ts` | Shared screen header, viewport-row helper, and styled key-hint footer. |
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  /** How a block is rendered and grouped. */
18
- export type PlanBlockKind = "heading" | "paragraph" | "list" | "code" | "quote" | "blank";
18
+ export type PlanBlockKind = "heading" | "paragraph" | "list" | "code" | "quote" | "table" | "hr" | "blank";
19
19
 
20
20
  /** A contiguous, source-line-anchored chunk of a plan. */
21
21
  export interface PlanBlock {
@@ -36,6 +36,25 @@ const FENCE = /^ {0,3}(`{3,}|~{3,})/;
36
36
  const LIST_ITEM = /^( *)([-*+]|\d+[.)])(\s+)/;
37
37
  /** A blockquote line. */
38
38
  const QUOTE = /^ {0,3}>/;
39
+ /** A thematic break (`---`, `***`, `___`, spaced or not). */
40
+ const THEMATIC_BREAK = /^ {0,3}(?:(?:\* *){3,}|(?:- *){3,}|(?:_ *){3,})$/;
41
+ /** A line that can be a table row: it carries at least one pipe. */
42
+ const TABLE_ROW = /\|/;
43
+ /** A GFM alignment row (`| --- | :--: |`), with or without edge pipes. */
44
+ const TABLE_DELIM = /^ {0,3}\|? *:?-+:? *(?:\| *:?-+:? *)*\|? *$/;
45
+
46
+ /** Whether a table starts at `index`: a pipe row followed by an alignment row. */
47
+ function startsTable(lines: string[], index: number): boolean {
48
+ const header = lines[index];
49
+ const delim = lines[index + 1];
50
+ return (
51
+ header !== undefined &&
52
+ delim !== undefined &&
53
+ TABLE_ROW.test(header) &&
54
+ delim.includes("-") &&
55
+ TABLE_DELIM.test(delim)
56
+ );
57
+ }
39
58
 
40
59
  /** Leading spaces and tabs of a line, in columns (tab = 4). */
41
60
  function indentOf(line: string): number {
@@ -102,6 +121,21 @@ export function planBlocks(plan: string): PlanBlock[] {
102
121
  continue;
103
122
  }
104
123
 
124
+ if (startsTable(lines, i)) {
125
+ let end = i + 1;
126
+ // The header and alignment rows, then every contiguous pipe row.
127
+ while (end + 1 < lines.length && lines[end + 1].trim() !== "" && TABLE_ROW.test(lines[end + 1])) end += 1;
128
+ push(i, end, "table");
129
+ i = end + 1;
130
+ continue;
131
+ }
132
+
133
+ if (THEMATIC_BREAK.test(line)) {
134
+ push(i, i, "hr");
135
+ i += 1;
136
+ continue;
137
+ }
138
+
105
139
  const item = line.match(LIST_ITEM);
106
140
  if (item) {
107
141
  const indent = item[1].length;
@@ -123,7 +157,15 @@ export function planBlocks(plan: string): PlanBlock[] {
123
157
  let end = i;
124
158
  while (end + 1 < lines.length) {
125
159
  const next = lines[end + 1];
126
- if (next.trim() === "" || FENCE.test(next) || HEADING.test(next) || QUOTE.test(next) || LIST_ITEM.test(next)) {
160
+ if (
161
+ next.trim() === "" ||
162
+ FENCE.test(next) ||
163
+ HEADING.test(next) ||
164
+ QUOTE.test(next) ||
165
+ LIST_ITEM.test(next) ||
166
+ THEMATIC_BREAK.test(next) ||
167
+ startsTable(lines, end + 1)
168
+ ) {
127
169
  break;
128
170
  }
129
171
  end += 1;
@@ -40,11 +40,25 @@ export const MAX_QUOTE_LENGTH = 500;
40
40
  /** A stored feedback line, capturing its comment. */
41
41
  const MARKER_LINE = /^\s*>\s*\[!REVIEW\]\s?(.*)$/;
42
42
 
43
+ /**
44
+ * Source lines inside a fenced code block. A marker-looking line there is
45
+ * documentation, not stored feedback, so it is never stripped or parsed.
46
+ */
47
+ function fencedLines(plan: string): Set<number> {
48
+ const fenced = new Set<number>();
49
+ for (const block of planBlocks(plan)) {
50
+ if (block.kind !== "code") continue;
51
+ for (let line = block.start; line <= block.end; line++) fenced.add(line);
52
+ }
53
+ return fenced;
54
+ }
55
+
43
56
  /** Remove every stored feedback line, leaving the plan body as authored. */
44
57
  export function stripFeedback(plan: string): string {
58
+ const fenced = fencedLines(plan);
45
59
  return plan
46
60
  .split("\n")
47
- .filter((line) => !MARKER_LINE.test(line))
61
+ .filter((line, index) => fenced.has(index) || !MARKER_LINE.test(line))
48
62
  .join("\n");
49
63
  }
50
64
 
@@ -53,17 +67,18 @@ export function stripFeedback(plan: string): string {
53
67
  * marker in the stripped plan, so a comment keeps its place across reloads.
54
68
  */
55
69
  export function parseFeedback(plan: string): PlanAnnotation[] {
70
+ const fenced = fencedLines(plan);
56
71
  const clean: string[] = [];
57
72
  const markers: Array<{ after: number; comment: string }> = [];
58
- for (const line of plan.split("\n")) {
59
- const match = MARKER_LINE.exec(line);
73
+ plan.split("\n").forEach((line, index) => {
74
+ const match = fenced.has(index) ? null : MARKER_LINE.exec(line);
60
75
  if (match) {
61
76
  const comment = match[1].trim();
62
77
  if (comment) markers.push({ after: clean.length, comment });
63
- continue;
78
+ return;
64
79
  }
65
80
  clean.push(line);
66
- }
81
+ });
67
82
  if (markers.length === 0) return [];
68
83
 
69
84
  const blocks = planBlocks(clean.join("\n"));
@@ -22,7 +22,7 @@ import { fileURLToPath } from "node:url";
22
22
  import { hasPathInput } from "./path-guard.ts";
23
23
  import { isExtensionEnabled } from "../../lib/env.ts";
24
24
  import { ASK_USER_QUESTION_TOOL, SUBAGENT_READ_ONLY_PARAM, SUBAGENT_TOOL } from "../../lib/tool-names.ts";
25
- import { STATUS_KEYS } from "../../lib/ui.ts";
25
+ import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
26
26
  import { planModeNotice, registerCommands } from "./commands.ts";
27
27
  import { checkReadOnlyGit } from "./git-bash.ts";
28
28
  import {
@@ -20,7 +20,8 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
20
20
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
21
21
  import { sanitizeLine } from "../../lib/format.ts";
22
22
  import type { ReadOnlyPolicy } from "../../lib/policy.ts";
23
- import { GLYPHS, STATUS_KEYS } from "../../lib/ui.ts";
23
+ import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
24
+ import { paint } from "../../lib/tui/tokens.ts";
24
25
  import { createPlanStore, type PlanStore } from "./plans.ts";
25
26
  import type { PlanModeEntry } from "./types.ts";
26
27
 
@@ -143,7 +144,7 @@ export function createPlanRuntime(
143
144
  lastPlanPath ? `${GLYPHS.plan} plan · ${statusName(lastPlanPath)}` : `${GLYPHS.plan} plan`;
144
145
 
145
146
  const setStatus = (ctx: ExtensionContext): void => {
146
- ctx.ui.setStatus(STATUS_KEYS.planMode, enabled ? ctx.ui.theme.fg("warning", statusText()) : undefined);
147
+ ctx.ui.setStatus(STATUS_KEYS.planMode, enabled ? paint(ctx.ui.theme, "warn", statusText()) : undefined);
147
148
  };
148
149
 
149
150
  const setEnabled = (next: boolean, ctx: ExtensionContext | undefined, persist: boolean): void => {