@gavin-hu/my-pi-agent 0.13.1 → 0.15.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 (237) hide show
  1. package/README.md +66 -41
  2. package/README.zh-CN.md +53 -36
  3. package/extensions/bytefly/README.md +321 -186
  4. package/extensions/bytefly/block/component.ts +7 -15
  5. package/extensions/bytefly/block/register.ts +6 -1
  6. package/extensions/bytefly/doctor/config.ts +4 -1
  7. package/extensions/bytefly/doctor/register.ts +24 -5
  8. package/extensions/bytefly/doctor/tools.ts +4 -0
  9. package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
  10. package/extensions/bytefly/footer/config.ts +10 -6
  11. package/extensions/bytefly/footer/price/config.ts +3 -2
  12. package/extensions/bytefly/footer/price/price.ts +5 -3
  13. package/extensions/bytefly/footer/price/rates.ts +63 -26
  14. package/extensions/bytefly/footer/render/status.ts +3 -3
  15. package/extensions/bytefly/header/component.ts +33 -10
  16. package/extensions/bytefly/header/config.ts +22 -0
  17. package/extensions/bytefly/header/register.ts +2 -1
  18. package/extensions/bytefly/index.ts +6 -6
  19. package/extensions/bytefly/notice/component.ts +8 -3
  20. package/extensions/bytefly/notify/config.ts +7 -12
  21. package/extensions/bytefly/notify/dispatch.ts +13 -5
  22. package/extensions/bytefly/panel/commands.ts +22 -2
  23. package/extensions/bytefly/panel/format.ts +17 -21
  24. package/extensions/bytefly/panel/settings-runtime.ts +86 -22
  25. package/extensions/bytefly/panel/settings.ts +279 -46
  26. package/extensions/bytefly/panel/themes.ts +32 -0
  27. package/extensions/bytefly/panel/tui/editing.ts +77 -0
  28. package/extensions/bytefly/panel/tui/entries.ts +24 -0
  29. package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
  30. package/extensions/bytefly/panel/tui/frame.ts +121 -4
  31. package/extensions/bytefly/panel/tui/settings-view.ts +98 -13
  32. package/extensions/bytefly/panel/tui.ts +176 -166
  33. package/extensions/bytefly/picker/discovery.ts +1 -0
  34. package/extensions/bytefly/picker/types.ts +2 -0
  35. package/extensions/bytefly/separator/component.ts +17 -6
  36. package/extensions/bytefly/separator/config.ts +15 -0
  37. package/extensions/bytefly/separator/format.ts +60 -6
  38. package/extensions/bytefly/separator/register.ts +68 -9
  39. package/extensions/bytefly/separator/types.ts +8 -0
  40. package/extensions/bytefly/update/cache.ts +58 -19
  41. package/extensions/bytefly/update/check.ts +35 -0
  42. package/extensions/bytefly/update/format.ts +76 -35
  43. package/extensions/bytefly/update/register.ts +144 -55
  44. package/extensions/bytefly/update/state.ts +4 -2
  45. package/extensions/bytefly/update/suppress.ts +66 -0
  46. package/extensions/bytefly/update/types.ts +15 -0
  47. package/extensions/bytefly/working/config.ts +4 -5
  48. package/extensions/bytefly/working/format.ts +24 -7
  49. package/extensions/bytefly/working/register.ts +34 -3
  50. package/extensions/check/README.md +253 -0
  51. package/extensions/check/commands.ts +128 -0
  52. package/extensions/check/config.ts +39 -0
  53. package/extensions/check/detect.ts +269 -0
  54. package/extensions/check/failures.ts +230 -0
  55. package/extensions/check/format.ts +135 -0
  56. package/extensions/check/index.ts +26 -0
  57. package/extensions/check/render.ts +185 -0
  58. package/extensions/check/runner.ts +176 -0
  59. package/extensions/check/runtime.ts +244 -0
  60. package/extensions/check/source.ts +80 -0
  61. package/extensions/check/tools.ts +173 -0
  62. package/extensions/check/tui.ts +195 -0
  63. package/extensions/check/types.ts +99 -0
  64. package/extensions/document/README.md +1 -1
  65. package/extensions/document/extract/pdf.ts +8 -16
  66. package/extensions/document/formats.ts +2 -2
  67. package/extensions/file-browse/README.md +66 -32
  68. package/extensions/file-browse/client.ts +95 -32
  69. package/extensions/file-browse/config.ts +9 -9
  70. package/extensions/file-browse/git.ts +37 -47
  71. package/extensions/file-browse/html.ts +39 -58
  72. package/extensions/file-browse/markdown.ts +110 -92
  73. package/extensions/file-browse/raw-html.ts +478 -0
  74. package/extensions/file-browse/router.ts +32 -70
  75. package/extensions/file-browse/server.ts +18 -56
  76. package/extensions/file-browse/urls.ts +80 -0
  77. package/extensions/goal/README.md +103 -47
  78. package/extensions/goal/clarify-parse.ts +4 -9
  79. package/extensions/goal/clarify.ts +27 -4
  80. package/extensions/goal/commands.ts +24 -11
  81. package/extensions/goal/format.ts +41 -10
  82. package/extensions/goal/index.ts +39 -16
  83. package/extensions/goal/plan-route.ts +51 -0
  84. package/extensions/goal/schema.ts +6 -23
  85. package/extensions/goal/state.ts +39 -57
  86. package/extensions/goal/tools.ts +26 -20
  87. package/extensions/goal/types.ts +11 -1
  88. package/extensions/job/README.md +20 -6
  89. package/extensions/job/format.ts +18 -68
  90. package/extensions/job/index.ts +26 -41
  91. package/extensions/job/log-writer.ts +208 -0
  92. package/extensions/job/logs.ts +3 -2
  93. package/extensions/job/process.ts +6 -99
  94. package/extensions/job/runtime.ts +42 -181
  95. package/extensions/job/schema.ts +1 -4
  96. package/extensions/job/tools.ts +1 -15
  97. package/extensions/job/tui.ts +117 -307
  98. package/extensions/job/ui.ts +7 -33
  99. package/extensions/memory/README.md +3 -1
  100. package/extensions/memory/errors.ts +5 -6
  101. package/extensions/memory/tui.ts +14 -24
  102. package/extensions/plan/README.md +106 -18
  103. package/extensions/plan/commands.ts +9 -5
  104. package/extensions/plan/format.ts +87 -0
  105. package/extensions/plan/index.ts +31 -15
  106. package/extensions/plan/plans.ts +2 -3
  107. package/extensions/plan/policy.ts +0 -6
  108. package/extensions/plan/runtime.ts +4 -2
  109. package/extensions/plan/steps.ts +37 -8
  110. package/extensions/plan/todos.ts +44 -3
  111. package/extensions/plan/tools/enter.ts +94 -0
  112. package/extensions/plan/tools/exit.ts +296 -0
  113. package/extensions/plan/tools/nested.ts +21 -0
  114. package/extensions/plan/tools/steps.ts +136 -0
  115. package/extensions/plan/tools/todo-seed.ts +76 -0
  116. package/extensions/plan/tools/write.ts +127 -0
  117. package/extensions/plan/tools.ts +18 -703
  118. package/extensions/plan/web-client.ts +6 -22
  119. package/extensions/plan/web-page.ts +19 -30
  120. package/extensions/plan/web-router.ts +34 -71
  121. package/extensions/plan/web.ts +13 -40
  122. package/extensions/question/answers.ts +3 -8
  123. package/extensions/question/tui.ts +6 -6
  124. package/extensions/rewind/README.md +95 -22
  125. package/extensions/rewind/checklist.ts +185 -0
  126. package/extensions/rewind/commands.ts +8 -0
  127. package/extensions/rewind/config.ts +3 -11
  128. package/extensions/rewind/format.ts +220 -2
  129. package/extensions/rewind/git.ts +74 -7
  130. package/extensions/rewind/hunk-picker.ts +114 -0
  131. package/extensions/rewind/patch.ts +88 -0
  132. package/extensions/rewind/path-picker.ts +67 -100
  133. package/extensions/rewind/paths.ts +93 -19
  134. package/extensions/rewind/restore.ts +53 -10
  135. package/extensions/rewind/review-tui.ts +320 -0
  136. package/extensions/rewind/review.ts +397 -0
  137. package/extensions/rewind/rewind-tui.ts +4 -3
  138. package/extensions/rewind/runtime.ts +94 -17
  139. package/extensions/subagent/README.md +118 -8
  140. package/extensions/subagent/background-tui.ts +208 -0
  141. package/extensions/subagent/background.ts +470 -0
  142. package/extensions/subagent/commands.ts +80 -0
  143. package/extensions/subagent/config.ts +62 -0
  144. package/extensions/subagent/format.ts +24 -1
  145. package/extensions/subagent/index.ts +116 -1
  146. package/extensions/subagent/orchestrate.ts +16 -3
  147. package/extensions/subagent/render.ts +25 -0
  148. package/extensions/subagent/run.ts +113 -49
  149. package/extensions/subagent/schema.ts +12 -0
  150. package/extensions/subagent/transcripts.ts +202 -0
  151. package/extensions/subagent/types.ts +23 -0
  152. package/extensions/subagent/ui.ts +50 -0
  153. package/extensions/todo/README.md +18 -13
  154. package/extensions/todo/format.ts +3 -2
  155. package/extensions/todo/runtime.ts +3 -2
  156. package/extensions/todo/schema.ts +8 -84
  157. package/extensions/todo/state.ts +5 -46
  158. package/extensions/todo/tools.ts +2 -1
  159. package/extensions/todo/types.ts +6 -15
  160. package/extensions/watch/README.md +16 -6
  161. package/extensions/watch/child.ts +150 -0
  162. package/extensions/watch/format.ts +8 -48
  163. package/extensions/watch/index.ts +42 -58
  164. package/extensions/watch/runtime.ts +49 -128
  165. package/extensions/watch/schema.ts +1 -4
  166. package/extensions/watch/tools.ts +1 -11
  167. package/extensions/watch/tui.ts +103 -273
  168. package/extensions/watch/ui.ts +5 -32
  169. package/extensions/web-access/README.md +1 -1
  170. package/extensions/web-access/fetch/page.ts +1 -1
  171. package/extensions/web-access/fetch/tool.ts +6 -2
  172. package/extensions/web-access/search/search.ts +2 -5
  173. package/extensions/wechat/README.md +3 -1
  174. package/extensions/wechat/client.ts +23 -0
  175. package/extensions/wechat/commands.ts +57 -42
  176. package/extensions/wechat/deliver.ts +5 -9
  177. package/extensions/wechat/errors.ts +3 -6
  178. package/extensions/wechat/inbound.ts +2 -5
  179. package/extensions/wechat/index.ts +2 -8
  180. package/extensions/wechat/outbound.ts +2 -4
  181. package/extensions/wechat/peer-worker.ts +3 -8
  182. package/extensions/wechat/poll.ts +10 -9
  183. package/extensions/wechat/presence.ts +35 -62
  184. package/extensions/wechat/reply-pipeline.ts +3 -1
  185. package/extensions/wechat/status-screen.ts +1 -1
  186. package/extensions/wechat/timer.ts +57 -0
  187. package/extensions/worktree/config.ts +8 -20
  188. package/extensions/worktree/enter.ts +7 -12
  189. package/extensions/worktree/git.ts +38 -69
  190. package/extensions/worktree/index.ts +5 -4
  191. package/extensions/worktree/registry.ts +7 -7
  192. package/extensions/worktree/root-tools.ts +2 -2
  193. package/extensions/worktree/status.ts +3 -2
  194. package/lib/README.md +22 -8
  195. package/lib/config.ts +83 -12
  196. package/lib/errors.ts +12 -0
  197. package/lib/format.ts +56 -0
  198. package/lib/git/read.ts +8 -0
  199. package/lib/goal-state.ts +23 -8
  200. package/lib/http/headers.ts +54 -0
  201. package/lib/http/index.ts +13 -0
  202. package/lib/http/response.ts +46 -0
  203. package/lib/http/router.ts +52 -0
  204. package/lib/http/server.ts +91 -0
  205. package/lib/lines.ts +77 -0
  206. package/lib/pdf.ts +82 -0
  207. package/lib/plan-entry.ts +71 -0
  208. package/lib/plan-state.ts +34 -0
  209. package/lib/report.ts +99 -0
  210. package/lib/requirements.ts +25 -19
  211. package/lib/spawn.ts +170 -0
  212. package/lib/todo-state.ts +110 -23
  213. package/lib/tool-names.ts +7 -0
  214. package/lib/tool-result.ts +33 -0
  215. package/lib/tui/README.md +68 -3
  216. package/lib/tui/chrome.ts +28 -4
  217. package/lib/tui/columns.ts +6 -1
  218. package/lib/tui/confirm.ts +65 -0
  219. package/lib/tui/dock-screen.ts +410 -0
  220. package/lib/tui/glyphs.ts +2 -1
  221. package/lib/tui/indent.ts +1 -1
  222. package/lib/tui/index.ts +2 -0
  223. package/lib/tui/rows.ts +21 -0
  224. package/lib/tui/status-controller.ts +35 -1
  225. package/lib/tui/tool.ts +59 -1
  226. package/lib/web/components.ts +20 -1
  227. package/lib/web/markup.ts +32 -0
  228. package/lib/web/runtime.ts +50 -0
  229. package/package.json +23 -5
  230. package/skills/init/SKILL.md +165 -0
  231. package/skills/init/references/agents-md-quality.md +103 -0
  232. package/skills/init/references/interview-questions.md +81 -0
  233. package/skills/refactor/SKILL.md +141 -0
  234. package/themes/bytefly-dark.json +2 -3
  235. package/themes/bytefly-light.json +2 -3
  236. package/extensions/watch/process.ts +0 -63
  237. package/extensions/web-access/fetch/pdf.ts +0 -45
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Theme } from "@earendil-works/pi-coding-agent";
10
+ import type { ScreenHint } from "../../../../lib/tui/chrome.ts";
10
11
  import { sanitizeLine } from "../../../../lib/format.ts";
11
12
  import { clip } from "../../../../lib/tui/columns.ts";
12
13
  import { checkbox, listRow } from "../../../../lib/tui/rows.ts";
@@ -46,7 +47,7 @@ export function extensionRow(theme: Theme, width: number, row: PanelRow, selecte
46
47
  });
47
48
  }
48
49
 
49
- /** The focused extension's label, findings, and the pending-changes note. */
50
+ /** The focused extension's findings and the pending-changes note. */
50
51
  export function extensionDetail(
51
52
  theme: Theme,
52
53
  width: number,
@@ -57,10 +58,10 @@ export function extensionDetail(
57
58
  ): string[] {
58
59
  const lines: string[] = [];
59
60
  const badge = row ? rowBadge(row) : undefined;
60
- if (row && badge?.text !== undefined) {
61
- const head = `${paint(theme, "meta", sanitizeLine(row.label))} ${paint(theme, "faint", "—")} ${paint(theme, badge.role, badge.text)}`;
62
- lines.push(clip(` ${head}`, width));
63
- } else if (row?.item) {
61
+ // The focused row directly above already carries the entry's name and verdict,
62
+ // so the pane starts with what the row cannot say: the findings, or the
63
+ // resource path when nothing was reported.
64
+ if (badge?.text === undefined && row?.item) {
64
65
  lines.push(clip(` ${paint(theme, "meta", sanitizeLine(row.item.path))}`, width));
65
66
  }
66
67
  if (row?.report && budget > 0) {
@@ -71,12 +72,16 @@ export function extensionDetail(
71
72
  }
72
73
 
73
74
  /** The section's key hints, in the order they drop as the width runs out. */
74
- export function extensionHints(options: { expanded: boolean; refreshing: boolean; projectScope: boolean }): string[] {
75
+ export function extensionHints(options: {
76
+ expanded: boolean;
77
+ refreshing: boolean;
78
+ projectScope: boolean;
79
+ }): ScreenHint[] {
75
80
  return [
76
81
  "Esc close",
77
82
  "←/→",
78
- "Space toggle",
79
- options.expanded ? "Enter collapse" : "Enter detail",
83
+ { text: "Space toggle", color: "accent" },
84
+ { text: options.expanded ? "Enter collapse" : "Enter detail", color: "accent" },
80
85
  "Tab scope",
81
86
  // Only Project scope can hold an override, so the key is not offered elsewhere.
82
87
  ...(options.projectScope ? ["x inherit"] : []),
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * The chrome around the panel's body: the layout constants, the section bar with
3
- * its counts, a group heading, and the hint row that a transient notice replaces.
3
+ * its counts, a group heading, the hint row that a transient notice replaces, and
4
+ * the frame the two sections share.
4
5
  *
5
6
  * Every section draws its body through these, so the two views cannot drift on
6
7
  * spacing, roles, or the width arithmetic.
@@ -8,12 +9,20 @@
8
9
 
9
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
10
11
  import { sanitizeLine } from "../../../../lib/format.ts";
11
- import { screenHint } from "../../../../lib/tui/chrome.ts";
12
+ import {
13
+ detailBlockRows,
14
+ dockListFrame,
15
+ screenHint,
16
+ type ScreenHint,
17
+ type ViewportRowsSource,
18
+ } from "../../../../lib/tui/chrome.ts";
12
19
  import { clip, fitCells, splitRow } from "../../../../lib/tui/columns.ts";
20
+ import { fitRows, formatRange, type ListCursor } from "../../../../lib/tui/cursor.ts";
13
21
  import { BODY_INDENT } from "../../../../lib/tui/indent.ts";
14
22
  import { paint } from "../../../../lib/tui/tokens.ts";
15
23
  import { sectionSegments, type SummarySegment } from "../format.ts";
16
24
  import type { PanelSection } from "../types.ts";
25
+ import { selectableRange, type PanelEntry } from "./entries.ts";
17
26
 
18
27
  /** Body rows when the terminal height is unknown. */
19
28
  export const SCREEN_DEFAULT_ROWS = 16;
@@ -54,8 +63,8 @@ export function groupLine(theme: Theme, width: number, entry: { label: string; c
54
63
  return clip(` ${label} ${count}`, width);
55
64
  }
56
65
 
57
- /** The key-hint row. */
58
- export function hintLine(theme: Theme, width: number, hints: string[]): string {
66
+ /** The key-hint row, or a section's notice that replaces it. */
67
+ export function hintLine(theme: Theme, width: number, hints: ScreenHint[]): string {
59
68
  return screenHint(theme, width, hints);
60
69
  }
61
70
 
@@ -69,3 +78,111 @@ export function savedLine(theme: Theme, width: number, dirty: boolean): string[]
69
78
  if (!dirty) return [];
70
79
  return [clip(`${INDENT}${paint(theme, "warn", "Saved · reload to apply")}`, width)];
71
80
  }
81
+
82
+ /** Terminal rows, or `undefined` when the height is unknown. */
83
+ export function totalRows(source: ViewportRowsSource | undefined): number | undefined {
84
+ const rows = typeof source === "function" ? source() : source;
85
+ return rows !== undefined && Number.isFinite(rows) && rows > 0 ? rows : undefined;
86
+ }
87
+
88
+ /**
89
+ * Detail rows the focused row gets: the fixed pane, or the free height when
90
+ * expanded. Zero when there is no free row for a pane at all, so a short
91
+ * terminal keeps its body; the caller then drops optional rows if needed.
92
+ */
93
+ export function detailRows(expanded: boolean, viewportRows: ViewportRowsSource | undefined): number {
94
+ const total = totalRows(viewportRows);
95
+ if (total === undefined) return expanded ? EXPANDED_DETAIL_FALLBACK : DETAIL_ROWS;
96
+ // One row pays for the blank line the frame puts above the pane.
97
+ const available = total - FRAME_ROWS - MIN_BODY_ROWS - 1;
98
+ if (available < 1) return 0;
99
+ return expanded ? available : Math.min(DETAIL_ROWS, available);
100
+ }
101
+
102
+ /**
103
+ * Drop optional rows until the frame fits the viewport: the range row first,
104
+ * then the project note, then the blank above the detail pane, then the pane.
105
+ * The header, summary, one body row, and the hint always survive; a terminal
106
+ * shorter than those is left to the host.
107
+ */
108
+ export function fitOptionalLines(options: {
109
+ viewportRows: ViewportRowsSource | undefined;
110
+ body: string[];
111
+ note: string | undefined;
112
+ range: string | undefined;
113
+ detail: string[];
114
+ }): { body: string[]; range: string | undefined; detail: string[]; noteShown: boolean; detailBlank: boolean } {
115
+ const total = totalRows(options.viewportRows);
116
+ let { body, range, detail } = options;
117
+ let noteShown = options.note !== undefined && body[0] === options.note;
118
+ if (total === undefined) return { body, range, detail, noteShown, detailBlank: true };
119
+ const fits = (blank: boolean): boolean =>
120
+ FRAME_ROWS + body.length + (range === undefined ? 0 : 1) + detailBlockRows(detail, blank) <= total;
121
+ if (!fits(true)) range = undefined;
122
+ if (!fits(true) && noteShown) {
123
+ body = body.slice(1);
124
+ noteShown = false;
125
+ }
126
+ // The separator goes before the pane does: the focused row's findings are
127
+ // worth more than the blank above them.
128
+ const detailBlank = fits(true) || !fits(false);
129
+ if (!fits(false)) detail = [];
130
+ return { body, range, detail, noteShown, detailBlank };
131
+ }
132
+
133
+ /** What {@link panelFrame} draws: the window, the body, and the chrome around it. */
134
+ export interface PanelFrameInput {
135
+ theme: Theme;
136
+ viewportRows: ViewportRowsSource | undefined;
137
+ cursor: ListCursor;
138
+ width: number;
139
+ header: string;
140
+ summary: string;
141
+ entries: readonly PanelEntry[];
142
+ note?: string;
143
+ detail: string[];
144
+ hint: string;
145
+ draw: (entry: PanelEntry, index: number) => string | undefined;
146
+ }
147
+
148
+ /**
149
+ * The frame both sections share: the cursor window, the visible body rows, the
150
+ * range row, the optional rows that give way on a short terminal, and the
151
+ * chrome around them. Each section supplies its summary, its detail pane, its
152
+ * hint row, and how one entry is drawn.
153
+ */
154
+ export function panelFrame(input: PanelFrameInput): { lines: string[]; noteShown: boolean } {
155
+ const { theme, viewportRows, cursor, width, entries, note, detail, hint, draw } = input;
156
+ cursor.visible = fitRows(
157
+ viewportRows,
158
+ entries.length,
159
+ FRAME_ROWS + detailBlockRows(detail) + (note ? 1 : 0),
160
+ SCREEN_DEFAULT_ROWS,
161
+ );
162
+ cursor.sync(entries.length);
163
+ const { start, end } = cursor.window(entries.length);
164
+
165
+ const body: string[] = [];
166
+ if (note) body.push(note);
167
+ for (let index = start; index < end; index += 1) {
168
+ const line = draw(entries[index], index);
169
+ if (line !== undefined) body.push(line);
170
+ }
171
+
172
+ const range = cursor.clipped(entries.length)
173
+ ? formatRange(theme, width, selectableRange(entries, { start, end }))
174
+ : undefined;
175
+ const fitted = fitOptionalLines({ viewportRows, body, note, range, detail });
176
+ return {
177
+ lines: dockListFrame({
178
+ header: input.header,
179
+ summary: input.summary,
180
+ body: fitted.body,
181
+ range: fitted.range,
182
+ detail: fitted.detail.length > 0 ? fitted.detail : undefined,
183
+ detailBlank: fitted.detailBlank,
184
+ hint,
185
+ }),
186
+ noteShown: fitted.noteShown,
187
+ };
188
+ }
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { join } from "node:path";
10
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
11
- import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
11
+ import { sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
12
12
  import { sanitizeLine } from "../../../../lib/format.ts";
13
13
  import { clip } from "../../../../lib/tui/columns.ts";
14
14
  import { BODY_INDENT } from "../../../../lib/tui/indent.ts";
@@ -23,6 +23,34 @@ import { countsLine, INDENT, savedLine } from "./frame.ts";
23
23
  /** Pi's personal settings file, named in the detail pane. */
24
24
  const PI_SETTINGS_FILE = "settings.json";
25
25
 
26
+ /** Columns the marker and the leading cell take before the label. */
27
+ const ROW_PREFIX_WIDTH = 4;
28
+ /** Columns a value keeps when the row has to choose between the label and the value. */
29
+ const MIN_VALUE_WIDTH = 8;
30
+
31
+ /**
32
+ * The editing cell: the buffer around the caret, scrolled so the caret is always
33
+ * visible, with ellipses where text is off-screen. The cell fits `budget`
34
+ * columns, so a long buffer cannot push the label out of the row.
35
+ */
36
+ function editingCell(buffer: string, caret: number, budget: number): string {
37
+ const width = Math.max(1, budget);
38
+ const before = buffer.slice(0, caret);
39
+ const beforeWidth = visibleWidth(before);
40
+ const caretRoom = width - 1;
41
+ if (beforeWidth > caretRoom) {
42
+ // Reserve one column for the leading ellipsis; the caret sits at the right edge.
43
+ const shownWidth = Math.max(0, caretRoom - 1);
44
+ const prefix = shownWidth > 0 ? "…" : "";
45
+ const shown = shownWidth > 0 ? sliceByColumn(before, beforeWidth - shownWidth, shownWidth, true) : "";
46
+ return `${prefix}${shown}▏`;
47
+ }
48
+ const used = beforeWidth + 1;
49
+ const afterRoom = Math.max(0, width - used);
50
+ const after = afterRoom > 0 ? truncateToWidth(buffer.slice(caret), afterRoom, "…") : "";
51
+ return `${before}▏${after}`;
52
+ }
53
+
26
54
  /** The rows this section shows, and the scope they were read in. */
27
55
  export function settingsSummary(theme: Theme, width: number, scope: PanelScope, count: number): string {
28
56
  return countsLine(theme, width, settingsSummarySegments(scope, count));
@@ -36,17 +64,19 @@ export function settingRow(
36
64
  selected: boolean,
37
65
  editing?: { buffer: string; caret: number },
38
66
  ): string {
39
- // The editor shows the buffer with a caret block, in the accent so the row
40
- // reads as active rather than as a value that happens to contain a bar.
41
- const value = editing
42
- ? `${editing.buffer.slice(0, editing.caret)}▏${editing.buffer.slice(editing.caret)}`
43
- : view.valueLabel;
67
+ const label = sanitizeLine(view.label);
68
+ // Keep the whole label when it fits, and never let a long value or an editing
69
+ // buffer erase it: the value gets what is left, with a floor of eight columns.
70
+ const available = Math.max(1, width - ROW_PREFIX_WIDTH - 1);
71
+ const labelReserve = Math.min(visibleWidth(label), Math.max(0, available - MIN_VALUE_WIDTH));
72
+ const valueBudget = Math.max(1, available - labelReserve);
73
+ const rawValue = editing ? editingCell(editing.buffer, editing.caret, valueBudget) : view.valueLabel;
44
74
  return listRow(theme, width, {
45
75
  selected,
46
76
  leading: view.kind === "boolean" ? checkbox(theme, view.value === "true") : " ",
47
- label: sanitizeLine(view.label),
77
+ label,
48
78
  labelRole: selected ? "accent" : "label",
49
- trailing: sanitizeLine(value),
79
+ trailing: clip(sanitizeLine(rawValue), valueBudget),
50
80
  trailingRole: editing ? "accent" : "meta",
51
81
  });
52
82
  }
@@ -57,9 +87,10 @@ export function projectNote(theme: Theme, width: number): string {
57
87
  }
58
88
 
59
89
  /** Where the focused row's value is written, and which scope supplied it. */
60
- export function originLine(view: SettingView, paths: { scope: WriteScope; cwd: string; agentDir: string }): string {
90
+ function originLine(view: SettingView, paths: { scope: WriteScope; cwd: string; agentDir: string }): string {
91
+ const scope = view.origin === "inherited" ? "global" : paths.scope;
61
92
  const path =
62
- view.source === "app" ? join(paths.agentDir, PI_SETTINGS_FILE) : configPath(paths.scope, paths.cwd, paths.agentDir);
93
+ view.source === "app" ? join(paths.agentDir, PI_SETTINGS_FILE) : configPath(scope, paths.cwd, paths.agentDir);
63
94
  const file = sanitizeLine(path);
64
95
  switch (view.origin) {
65
96
  case "inherited":
@@ -90,7 +121,7 @@ export function settingDetail(options: {
90
121
  // The pending-changes marker survives the collapsed body: on a short terminal
91
122
  // the value still changed, and the screen has to say so.
92
123
  const saved = savedLine(theme, width, dirty);
93
- if (budget === 0) return [headLine, ...saved];
124
+ if (budget === 0) return saved;
94
125
 
95
126
  const lines = [headLine];
96
127
  const descriptionWidth = Math.max(1, width - BODY_INDENT * 2);
@@ -105,8 +136,62 @@ export function settingDetail(options: {
105
136
  }
106
137
 
107
138
  /** The section's key hints, in the order they drop as the width runs out. */
108
- export function settingsHints(): string[] {
109
- return ["Esc close", "←/→", "Space change", "Tab scope", "↑/↓ select"];
139
+ export function settingsHints(json = false): string[] {
140
+ return [
141
+ "Esc close",
142
+ "←/→",
143
+ "Space change",
144
+ "Tab scope",
145
+ "u reset",
146
+ json ? "J hide" : "J JSON",
147
+ "[ ] group",
148
+ "↑/↓ select",
149
+ ];
150
+ }
151
+
152
+ /**
153
+ * The focused row's section as the active scope stores it.
154
+ *
155
+ * This is the only view of the keys the catalogue leaves to JSON, so it names the
156
+ * section and its file rather than pretending to be complete: the pane is clipped
157
+ * to the detail budget and says how many lines it left out. `section` is the row's
158
+ * `bytefly.json` section, which the caller has already established.
159
+ */
160
+ export function sectionJsonPane(options: {
161
+ theme: Theme;
162
+ width: number;
163
+ section: string;
164
+ document: unknown;
165
+ budget: number;
166
+ dirty: boolean;
167
+ paths: { scope: WriteScope; cwd: string; agentDir: string };
168
+ }): string[] {
169
+ const { theme, width, section, document, budget, dirty, paths } = options;
170
+ const saved = savedLine(theme, width, dirty);
171
+ if (budget === 0) return saved;
172
+ const scope = paths.scope === "global" ? "Global" : "Project";
173
+ const file = sanitizeLine(configPath(paths.scope, paths.cwd, paths.agentDir));
174
+ const source = paint(theme, "faint", `${scope} · ${file}`);
175
+ const head = `${paint(theme, "meta", sanitizeLine(section))} ${paint(theme, "faint", "—")} ${source}`;
176
+ const lines = [clip(`${INDENT}${head}`, width)];
177
+ const room = Math.max(0, budget - lines.length - saved.length);
178
+ const body = document === undefined ? [] : JSON.stringify(document, null, 2).split("\n");
179
+
180
+ if (body.length === 0) {
181
+ if (room > 0) lines.push(clip(`${INDENT}${paint(theme, "faint", "Not set in this scope.")}`, width));
182
+ lines.push(...saved);
183
+ return lines;
184
+ }
185
+
186
+ const shown = body.slice(0, room);
187
+ if (body.length > room && shown.length > 0) {
188
+ // The marker replaces one already-shown line, so the hidden count is one
189
+ // larger than the number of lines the slice left out.
190
+ shown[shown.length - 1] = `… ${body.length - room + 1} more lines`;
191
+ }
192
+ for (const line of shown) lines.push(clip(`${INDENT}${paint(theme, "faint", sanitizeLine(line))}`, width));
193
+ lines.push(...saved);
194
+ return lines;
110
195
  }
111
196
 
112
197
  /** The editor's key hints: `Esc` cancels the edit, it does not close the screen. */