@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
@@ -22,15 +22,16 @@ import { Type, type Static } from "typebox";
22
22
  import { sanitizeLine } from "../../lib/format.ts";
23
23
  import { askHuman } from "../../lib/interaction.ts";
24
24
  import { TODO_TOOL } from "../../lib/tool-names.ts";
25
- import { FULL_SCREEN_OVERLAY, reuseText, toolDetails } from "../../lib/tui.ts";
26
- import { expandHint } from "../../lib/ui.ts";
25
+ import { FULL_SCREEN_OVERLAY, reuseText, toolDetails } from "../../lib/tui/chrome.ts";
26
+ import { paint } from "../../lib/tui/tokens.ts";
27
+ import { expandHint } from "../../lib/tui/glyphs.ts";
27
28
  import { READ_ONLY_SUMMARY } from "./policy.ts";
28
- import { feedbackSummary } from "./feedback.ts";
29
+ import { feedbackSummary, type PlanAnnotation } from "./feedback.ts";
29
30
  import { ENTER_TOOL, EXIT_TOOL, SET_PLAN_STEPS_TOOL, WRITE_PLAN_TOOL, type PlanRuntime } from "./runtime.ts";
30
31
  import { PLAN_OUTLINE, extractPlanSteps, type PlanStep, type StepUpdate } from "./steps.ts";
31
32
  import { PlanViewComponent, sanitizePlanText, type PlanViewAction } from "./tui.ts";
32
33
  import type { EnterPlanModeDetails, ExitPlanModeDetails, SetPlanStepsDetails, WritePlanDetails } from "./types.ts";
33
- import { createPlanWebReview, type PlanWebReview } from "./web.ts";
34
+ import { createPlanWebReview, type PlanReviewResult, type PlanReviewSession, type PlanWebReview } from "./web.ts";
34
35
 
35
36
  const ExitPlanModeParams = Type.Object({
36
37
  plan_path: Type.String({
@@ -238,7 +239,7 @@ export function registerTools(
238
239
 
239
240
  renderCall(_args, theme, context) {
240
241
  const text = reuseText(context?.lastComponent);
241
- text.setText(theme.fg("toolTitle", theme.bold(`${ENTER_TOOL} `)) + theme.fg("muted", "requested plan mode"));
242
+ text.setText(theme.fg("toolTitle", theme.bold(`${ENTER_TOOL} `)) + paint(theme, "meta", "requested plan mode"));
242
243
  return text;
243
244
  },
244
245
 
@@ -250,10 +251,10 @@ export function registerTools(
250
251
  text.setText(first?.type === "text" ? first.text : "");
251
252
  return text;
252
253
  }
253
- if (details.unavailable) text.setText(theme.fg("warning", "No UI to confirm plan mode"));
254
- else if (details.already) text.setText(theme.fg("muted", "Already in plan mode"));
255
- else if (details.entered) text.setText(theme.fg("success", "✓ Plan mode enabled"));
256
- else text.setText(theme.fg("muted", "Stayed in normal mode"));
254
+ if (details.unavailable) text.setText(paint(theme, "warn", "No UI to confirm plan mode"));
255
+ else if (details.already) text.setText(paint(theme, "meta", "Already in plan mode"));
256
+ else if (details.entered) text.setText(paint(theme, "ok", "✓ Plan mode enabled"));
257
+ else text.setText(paint(theme, "meta", "Stayed in normal mode"));
257
258
  return text;
258
259
  },
259
260
  });
@@ -315,7 +316,7 @@ export function registerTools(
315
316
  renderCall(args, theme, context) {
316
317
  const text = reuseText(context?.lastComponent);
317
318
  const title = (args as Partial<WritePlanArgs>).title;
318
- const suffix = title ? theme.fg("muted", sanitizeLine(title)) : theme.fg("muted", "plan");
319
+ const suffix = title ? paint(theme, "meta", sanitizeLine(title)) : paint(theme, "meta", "plan");
319
320
  text.setText(theme.fg("toolTitle", theme.bold(`${WRITE_PLAN_TOOL} `)) + suffix);
320
321
  return text;
321
322
  },
@@ -328,7 +329,7 @@ export function registerTools(
328
329
  text.setText(first?.type === "text" ? first.text : "");
329
330
  return text;
330
331
  }
331
- text.setText(theme.fg("success", "✓ Saved plan ") + theme.fg("muted", details.relativePath));
332
+ text.setText(paint(theme, "ok", "✓ Saved plan ") + paint(theme, "meta", details.relativePath));
332
333
  return text;
333
334
  },
334
335
  });
@@ -393,6 +394,8 @@ export function registerTools(
393
394
  }
394
395
 
395
396
  let outcome = await reviewPlan(ctx, file);
397
+ // Comments carried by a browser approval, reported alongside the approval.
398
+ let browserComments: PlanAnnotation[] = [];
396
399
  // Review, refine, and browser review all loop back here until the user
397
400
  // approves, keeps planning, or a refinement is handed to the model. The
398
401
  // dialog-based refine editor carries no text from the review screen, so an
@@ -400,15 +403,25 @@ export function registerTools(
400
403
  // as "not approved".
401
404
  for (;;) {
402
405
  if (outcome.action === "browser") {
403
- const session = await web.start(ctx.cwd, file).catch(() => undefined);
406
+ const session: PlanReviewSession | undefined = await web
407
+ .start(ctx.cwd, file, { canApprove: true })
408
+ .catch(() => undefined);
404
409
  if (!session) {
405
410
  ctx.ui.notify("Plan review: could not start the browser review server.", "error");
406
411
  outcome = await reviewPlan(ctx, file);
407
412
  continue;
408
413
  }
409
414
  ctx.ui.notify(`Plan review: opened ${session.url}`, "info");
410
- const result = await session.done;
415
+ const result: PlanReviewResult = await session.done;
411
416
  await session.close();
417
+ if (result.status === "approved") {
418
+ // Approve from the browser is the TUI's approve: keep the comments it
419
+ // carried and fall through to the shared approval path below.
420
+ file = result.plan;
421
+ browserComments = result.annotations;
422
+ outcome = { action: "approve" };
423
+ continue;
424
+ }
412
425
  if (result.status === "saved") {
413
426
  // The comments are already in the file; hand them back as a refinement so
414
427
  // the model revises the same plan and calls exit_plan_mode again.
@@ -490,11 +503,14 @@ export function registerTools(
490
503
  : steps.length > 0
491
504
  ? ` Could not record the steps in the todo list${error ? `: ${error}` : ""}. Keep these steps in mind and tick them off with ${SET_PLAN_STEPS_TOOL} as you go.`
492
505
  : " If the todo tool is available, record the steps with it before you start.";
506
+ const browserNote = browserComments.length
507
+ ? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(browserComments)}`
508
+ : "";
493
509
  return {
494
510
  content: [
495
511
  {
496
512
  type: "text",
497
- text: `Plan approved. Plan mode is off and write access is restored. The plan file ${file.relativePath} is the source of truth; follow its steps.${tail}${listing}`,
513
+ text: `Plan approved. Plan mode is off and write access is restored. The plan file ${file.relativePath} is the source of truth; follow its steps.${browserNote}${tail}${listing}`,
498
514
  },
499
515
  ],
500
516
  details: {
@@ -508,7 +524,7 @@ export function registerTools(
508
524
 
509
525
  renderCall(_args, theme, context) {
510
526
  const text = reuseText(context?.lastComponent);
511
- text.setText(theme.fg("toolTitle", theme.bold(`${EXIT_TOOL} `)) + theme.fg("muted", "submitted a plan"));
527
+ text.setText(theme.fg("toolTitle", theme.bold(`${EXIT_TOOL} `)) + paint(theme, "meta", "submitted a plan"));
512
528
  return text;
513
529
  },
514
530
 
@@ -521,25 +537,25 @@ export function registerTools(
521
537
  return text;
522
538
  }
523
539
  const heading = details.unavailable
524
- ? theme.fg("warning", "No UI to approve the plan")
540
+ ? paint(theme, "warn", "No UI to approve the plan")
525
541
  : details.approved
526
- ? theme.fg("success", "✓ Plan approved")
542
+ ? paint(theme, "ok", "✓ Plan approved")
527
543
  : details.refined
528
- ? theme.fg("accent", "Refining the plan")
529
- : theme.fg("warning", "Plan not approved");
544
+ ? paint(theme, "accent", "Refining the plan")
545
+ : paint(theme, "warn", "Plan not approved");
530
546
  const extra =
531
547
  details.refined && details.refinement
532
- ? `\n${theme.fg("muted", `↳ ${sanitizePlanText(details.refinement)}`)}`
548
+ ? `\n${paint(theme, "meta", `↳ ${sanitizePlanText(details.refinement)}`)}`
533
549
  : "";
534
550
  const shown = details.relativePath ?? details.planPath;
535
- const path = shown ? `\n${theme.fg("muted", shown)}` : "";
551
+ const path = shown ? `\n${paint(theme, "meta", shown)}` : "";
536
552
  const planText = sanitizePlanText(details.plan);
537
553
  let out = `${heading}${extra}${path}`;
538
554
  if (planText) {
539
555
  const { body, more } = expanded ? { body: planText, more: 0 } : preview(planText);
540
556
  // A blank line separates the header block from the plan body.
541
557
  out += `\n\n${theme.fg("toolOutput", body)}`;
542
- if (more > 0) out += `\n${theme.fg("dim", `… ${more} more lines`)} ${expandHint(theme)}`;
558
+ if (more > 0) out += `\n${paint(theme, "faint", `… ${more} more lines`)} ${expandHint(theme)}`;
543
559
  }
544
560
  text.setText(out);
545
561
  return text;
@@ -609,7 +625,7 @@ export function registerTools(
609
625
  : updates && updates.length > 1
610
626
  ? `${updates.length} steps`
611
627
  : "steps";
612
- text.setText(theme.fg("toolTitle", theme.bold(`${SET_PLAN_STEPS_TOOL} `)) + theme.fg("muted", label));
628
+ text.setText(theme.fg("toolTitle", theme.bold(`${SET_PLAN_STEPS_TOOL} `)) + paint(theme, "meta", label));
613
629
  return text;
614
630
  },
615
631
 
@@ -625,10 +641,10 @@ export function registerTools(
625
641
  const pending = details.updates.filter((update) => update.status === "pending").map((u) => u.step);
626
642
  const heading =
627
643
  completed.length > 0
628
- ? theme.fg("success", `✓ ${stepLabel(completed)} done`) +
629
- (pending.length > 0 ? theme.fg("muted", ` · ${stepLabel(pending)} reopened`) : "")
630
- : theme.fg("muted", `${stepLabel(pending)} reopened`);
631
- text.setText(heading + theme.fg("dim", ` · ${details.done}/${details.total}`));
644
+ ? paint(theme, "ok", `✓ ${stepLabel(completed)} done`) +
645
+ (pending.length > 0 ? paint(theme, "meta", ` · ${stepLabel(pending)} reopened`) : "")
646
+ : paint(theme, "meta", `${stepLabel(pending)} reopened`);
647
+ text.setText(heading + paint(theme, "faint", ` · ${details.done}/${details.total}`));
632
648
  return text;
633
649
  },
634
650
  });
@@ -33,13 +33,17 @@ import {
33
33
  import { stripControlChars } from "../../lib/format.ts";
34
34
  import {
35
35
  editorTheme,
36
- screenHeader,
37
36
  screenHint,
38
37
  viewportRows,
39
38
  type ScreenHint,
40
39
  type ViewportRowsSource,
41
- } from "../../lib/tui.ts";
42
- import { GLYPHS } from "../../lib/ui.ts";
40
+ } from "../../lib/tui/chrome.ts";
41
+ import { isCloseKey } from "../../lib/tui/cursor.ts";
42
+ import { gaugeBar } from "../../lib/tui/gauge.ts";
43
+ import { GLYPHS } from "../../lib/tui/glyphs.ts";
44
+ import { titleRule } from "../../lib/tui/rules.ts";
45
+ import { emptyState, checkbox } from "../../lib/tui/rows.ts";
46
+ import { paint } from "../../lib/tui/tokens.ts";
43
47
  import { planTitle, type StoredPlan } from "./plans.ts";
44
48
  import { extractPlanSections } from "./steps.ts";
45
49
 
@@ -110,14 +114,12 @@ export function sanitizePlanText(text: string): string {
110
114
 
111
115
  /** A task checkbox in a list marker: `- [x] `, `1. [ ] `. */
112
116
  const TASK_MARKER = /^([-*+]|\d+[.)])(\s+)(\[([ xX])\])(\s*)$/;
113
- /** Task-checkbox markers; three columns wide so list wrapping stays aligned. */
114
- const TASK_BOX = { done: `[${GLYPHS.check}]`, pending: "[ ]" } as const;
115
117
 
116
118
  /**
117
- * Wrap the Markdown theme so task checkboxes read at a glance: done steps get a
118
- * `success` check, pending steps a `dim` box. The replacement keeps the marker's
119
- * visible width (`[✓]` and `[ ]` are both three columns), so wrapped list
120
- * continuation lines stay aligned.
119
+ * Wrap the Markdown theme so task checkboxes read at a glance with the shared
120
+ * `◼`/`◻` vocabulary, the same box the bytefly screen and the question dialog
121
+ * draw. The replacement is one column wide instead of the bracket's three, so a
122
+ * wrapped list continuation shifts left by two.
121
123
  */
122
124
  function planMarkdownTheme(theme: Theme, base: MarkdownTheme): MarkdownTheme {
123
125
  return {
@@ -126,27 +128,19 @@ function planMarkdownTheme(theme: Theme, base: MarkdownTheme): MarkdownTheme {
126
128
  const match = TASK_MARKER.exec(text);
127
129
  if (!match) return base.listBullet(text);
128
130
  const [, bullet, gap, , mark, trail] = match;
129
- const box = mark.toLowerCase() === "x" ? theme.fg("success", TASK_BOX.done) : theme.fg("dim", TASK_BOX.pending);
130
- return `${base.listBullet(`${bullet}${gap}`)}${box}${trail}`;
131
+ return `${base.listBullet(`${bullet}${gap}`)}${checkbox(theme, mark.toLowerCase() === "x")}${trail}`;
131
132
  },
132
133
  };
133
134
  }
134
135
 
135
- /** A fixed-width gauge: `fillSegments` (rounded) in `fill`, the rest dim. */
136
- function gauge(theme: Theme, fillSegments: number, fill: "accent" | "success"): string {
137
- const count = Math.min(GAUGE_SEGMENTS, Math.max(0, Math.round(fillSegments)));
138
- return (
139
- theme.fg(fill, GLYPHS.gaugeFull.repeat(count)) + theme.fg("dim", GLYPHS.gaugeEmpty.repeat(GAUGE_SEGMENTS - count))
140
- );
141
- }
142
-
143
136
  /** Scroll position below the body: a gauge, the percent, and the visible range. */
144
137
  function scrollRow(theme: Theme, width: number, offset: number, end: number, total: number, section?: string): string {
145
138
  const maxOffset = Math.max(0, total - (end - offset));
146
139
  const percent = maxOffset > 0 ? Math.round((offset / maxOffset) * 100) : 100;
147
- const head = `${gauge(theme, (percent / 100) * GAUGE_SEGMENTS, "accent")} ${theme.fg("accent", `${percent}%`)}`;
148
- const range = theme.fg("dim", `lines ${offset + 1}–${end} of ${total}`);
149
- const label = section ? theme.fg("muted", `${GLYPHS.plan} ${section}`) : "";
140
+ const bar = gaugeBar(theme, { percent, blocks: GAUGE_SEGMENTS, role: "accent", emptyRole: "faint" });
141
+ const head = `${bar} ${paint(theme, "accent", `${percent}%`)}`;
142
+ const range = paint(theme, "faint", `lines ${offset + 1}–${end} of ${total}`);
143
+ const label = section ? paint(theme, "meta", `${GLYPHS.plan} ${section}`) : "";
150
144
  // Keep the gauge and percent; drop the range first, then the section label, on a
151
145
  // narrow terminal.
152
146
  const candidates = [
@@ -295,7 +289,7 @@ export class PlanViewComponent implements Component {
295
289
 
296
290
  handleInput(data: string): void {
297
291
  if (this.editing) {
298
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
292
+ if (isCloseKey(data)) {
299
293
  this.cancelRefine();
300
294
  return;
301
295
  }
@@ -303,7 +297,7 @@ export class PlanViewComponent implements Component {
303
297
  this.options.requestRender();
304
298
  return;
305
299
  }
306
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
300
+ if (isCloseKey(data)) {
307
301
  this.options.onClose("keep");
308
302
  return;
309
303
  }
@@ -361,19 +355,19 @@ export class PlanViewComponent implements Component {
361
355
  this.offset = Math.min(Math.max(0, this.offset), maxOffset);
362
356
  const sectionLabel = this.currentSection(sections)?.title;
363
357
 
364
- const lines: string[] = [screenHeader(theme, w, truncateToWidth(`Plan Review · ${title}`, Math.max(1, w - 6)))];
358
+ const lines: string[] = [titleRule(theme, w, truncateToWidth(`Plan Review · ${title}`, Math.max(1, w - 6)))];
365
359
  lines.push("");
366
360
 
367
361
  const end = Math.min(body.length, this.offset + visible);
368
362
  for (let i = this.offset; i < end; i++) lines.push(body[i]);
369
- if (body.length === 0) lines.push(truncateToWidth(` ${theme.fg("dim", "This plan is empty.")}`, w));
363
+ if (body.length === 0) lines.push(...emptyState(theme, w, { label: "This plan is empty." }));
370
364
 
371
365
  if (this.editing) {
372
366
  // Keep the scroll position visible while editing: the body above is still
373
367
  // clipped, so the reader needs to know where they are.
374
368
  if (this.maxOffset > 0) lines.push(scrollRow(theme, w, this.offset, end, body.length, sectionLabel));
375
369
  else lines.push("");
376
- lines.push(truncateToWidth(` ${theme.fg("muted", "Refine the plan:")}`, w));
370
+ lines.push(truncateToWidth(` ${paint(theme, "meta", "Refine the plan:")}`, w));
377
371
  for (const line of editorLines) lines.push(truncateToWidth(` ${line}`, w));
378
372
  lines.push("");
379
373
  lines.push(screenHint(theme, w, ["Esc back to plan", { text: "Enter submit", color: "success" }]));