@gavin-hu/my-pi-agent 0.15.0 → 0.16.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 (106) hide show
  1. package/README.md +12 -11
  2. package/README.zh-CN.md +9 -7
  3. package/extensions/bytefly/README.md +98 -116
  4. package/extensions/bytefly/notice/component.ts +6 -4
  5. package/extensions/bytefly/notice/config.ts +16 -2
  6. package/extensions/bytefly/panel/format.ts +3 -8
  7. package/extensions/bytefly/panel/join.ts +1 -6
  8. package/extensions/bytefly/panel/settings.ts +74 -377
  9. package/extensions/bytefly/panel/tui/entries.ts +16 -14
  10. package/extensions/bytefly/panel/tui/extensions-view.ts +3 -0
  11. package/extensions/bytefly/panel/tui/frame.ts +2 -2
  12. package/extensions/bytefly/panel/tui/settings-view.ts +8 -12
  13. package/extensions/bytefly/panel/tui.ts +4 -6
  14. package/extensions/bytefly/picker/resources.ts +7 -3
  15. package/extensions/bytefly/working/register.ts +2 -8
  16. package/extensions/check/tools.ts +2 -11
  17. package/extensions/context/README.md +162 -0
  18. package/extensions/context/collect.ts +53 -0
  19. package/extensions/context/commands.ts +100 -0
  20. package/extensions/context/format.ts +82 -0
  21. package/extensions/context/index.ts +16 -0
  22. package/extensions/context/report.ts +319 -0
  23. package/extensions/context/tui.ts +268 -0
  24. package/extensions/context/types.ts +122 -0
  25. package/extensions/file-browse/README.md +83 -8
  26. package/extensions/file-browse/client.ts +3 -1
  27. package/extensions/file-browse/config.ts +23 -0
  28. package/extensions/file-browse/files.ts +183 -48
  29. package/extensions/file-browse/fuzzy.ts +65 -11
  30. package/extensions/file-browse/html.ts +78 -133
  31. package/extensions/file-browse/index.ts +17 -2
  32. package/extensions/file-browse/live-client.ts +158 -0
  33. package/extensions/file-browse/live.ts +256 -0
  34. package/extensions/file-browse/markdown.ts +3 -10
  35. package/extensions/file-browse/raw-html.ts +3 -10
  36. package/extensions/file-browse/router.ts +33 -8
  37. package/extensions/file-browse/server.ts +78 -13
  38. package/extensions/file-browse/styles.ts +124 -0
  39. package/extensions/file-open/README.md +1 -2
  40. package/extensions/{document → file-read}/README.md +72 -56
  41. package/extensions/{document → file-read}/config.ts +12 -9
  42. package/extensions/{document → file-read}/extract/doc.ts +2 -2
  43. package/extensions/{document → file-read}/extract/docx.ts +3 -3
  44. package/extensions/{document → file-read}/extract/file.ts +9 -9
  45. package/extensions/{document → file-read}/extract/odt.ts +3 -3
  46. package/extensions/{document → file-read}/extract/pdf.ts +3 -3
  47. package/extensions/{document → file-read}/extract/rtf.ts +3 -3
  48. package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
  49. package/extensions/{document → file-read}/formats.ts +11 -11
  50. package/extensions/file-read/index.ts +17 -0
  51. package/extensions/{document → file-read}/paging.ts +15 -13
  52. package/extensions/{document → file-read}/render.ts +17 -13
  53. package/extensions/{document → file-read}/schema.ts +7 -7
  54. package/extensions/{document → file-read}/tool.ts +36 -28
  55. package/extensions/goal/tools.ts +10 -14
  56. package/extensions/job/commands.ts +32 -46
  57. package/extensions/job/runtime.ts +6 -8
  58. package/extensions/job/tools.ts +7 -26
  59. package/extensions/memory/commands.ts +30 -44
  60. package/extensions/memory/tools.ts +6 -14
  61. package/extensions/plan/tools/enter.ts +6 -7
  62. package/extensions/plan/tools/exit.ts +6 -7
  63. package/extensions/plan/tools/steps.ts +6 -7
  64. package/extensions/plan/tools/write.ts +8 -9
  65. package/extensions/plan/web-router.ts +2 -12
  66. package/extensions/plan/web.ts +30 -15
  67. package/extensions/question/tools.ts +5 -4
  68. package/extensions/rewind/checklist.ts +10 -3
  69. package/extensions/rewind/git.ts +27 -14
  70. package/extensions/rewind/hunk-picker.ts +1 -1
  71. package/extensions/rewind/path-picker.ts +1 -1
  72. package/extensions/rewind/review-tui.ts +3 -3
  73. package/extensions/rewind/snapshot.ts +13 -4
  74. package/extensions/subagent/background-tui.ts +1 -1
  75. package/extensions/subagent/background.ts +1 -7
  76. package/extensions/todo/tools.ts +10 -14
  77. package/extensions/watch/README.md +5 -5
  78. package/extensions/watch/commands.ts +33 -47
  79. package/extensions/watch/runtime.ts +5 -7
  80. package/extensions/watch/tools.ts +7 -18
  81. package/extensions/web-access/README.md +3 -3
  82. package/extensions/web-access/search/search.ts +9 -18
  83. package/extensions/wechat/timer.ts +9 -30
  84. package/extensions/wechat/tools.ts +4 -7
  85. package/extensions/worktree/status.ts +14 -9
  86. package/lib/README.md +12 -8
  87. package/lib/escape.ts +15 -2
  88. package/lib/git/read.ts +20 -0
  89. package/lib/http/headers.ts +20 -0
  90. package/lib/http/server.ts +22 -4
  91. package/lib/pdf.ts +2 -2
  92. package/lib/pool.ts +62 -0
  93. package/lib/requirements.ts +2 -2
  94. package/lib/screen-session.ts +85 -0
  95. package/lib/timer.ts +61 -0
  96. package/lib/tui/README.md +3 -3
  97. package/lib/tui/columns.ts +33 -8
  98. package/lib/tui/dock-screen.ts +12 -4
  99. package/lib/tui/rows.ts +15 -6
  100. package/lib/tui/rules.ts +4 -8
  101. package/lib/tui/tool.ts +55 -2
  102. package/package.json +8 -4
  103. package/skills/changelog/SKILL.md +136 -0
  104. package/extensions/document/index.ts +0 -17
  105. /package/extensions/{document → file-read}/deadline.ts +0 -0
  106. /package/extensions/{document → file-read}/extract/vendor.d.ts +0 -0
@@ -3,40 +3,38 @@
3
3
  *
4
4
  * A row is one preference: an app setting from Pi's `settings.json`, or a leaf
5
5
  * of a `bytefly.json` section. This module is pure — descriptors, a snapshot to
6
- * views, cycling, validation, and grouping — so the screen and the runtime share
7
- * one definition and the tests need no host.
6
+ * views, cycling, and validation — so the screen, the text listing, and the
7
+ * runtime share one definition and the tests need no host.
8
8
  *
9
- * The catalogue is curated rather than exhaustive: it holds the preferences a
10
- * person changes, across every extension of the package, and each row states its
11
- * own rule so a value the screen accepts is a value the loader keeps. The rest of
12
- * the document stays JSON, reachable from the screen's JSON pane: a list
13
- * (`doctor.skip`), a table (`price.rates`), and a schedule (`price.peak`) are not
14
- * one row.
9
+ * The catalogue is the shortlist a session changes, not the whole document: every
10
+ * row is one a workflow differs on, and each states its own rule so a value the
11
+ * screen accepts is a value the loader keeps. The catalogue order is the screen's
12
+ * order, and it is capped so the section fits a 24-row terminal (`settings.test.ts`
13
+ * holds the ceiling); a new row has to displace one. Everything else stays
14
+ * `bytefly.json` — a list (`doctor.skip`), a table (`price.rates`), a schedule
15
+ * (`price.peak`), and the tuning numbers and labels this screen deliberately does
16
+ * not carry — documented in `docs/configuration.md` and reachable from the JSON
17
+ * pane of any row whose section is still on the list.
15
18
  *
16
- * Four sections have a normalizer here, so the screen shows what the footer, the
17
- * notifier, and the doctor keep. The six owned by other extensions are read as the
18
- * file states them, because importing their normalizer would be one extension
19
- * importing another's module; each such row declares the loader's default, and
20
- * `test/settings-coverage.test.ts` holds the pair together.
19
+ * Three sections have a normalizer here, so the screen shows what the notifier,
20
+ * the doctor, and the footer keep. The three owned by other extensions (`rewind`,
21
+ * `job`, `watch`) are read as the file states them, because importing their
22
+ * normalizer would be one extension importing another's module; each such row
23
+ * declares the loader's default, and `test/settings-coverage.test.ts` holds the
24
+ * pair together.
21
25
  */
22
26
 
23
27
  import { configSection, configValueAt } from "../../../lib/config.ts";
24
- import { DEFAULT_CHROME_CONFIG, isRailGlyph, normalizeChromeConfig } from "../chrome.ts";
25
- import {
26
- DEFAULT_CONFIG as DEFAULT_DOCTOR_CONFIG,
27
- normalizeConfig as normalizeDoctorConfig,
28
- TIMEOUT_MS_RANGE,
29
- } from "../doctor/config.ts";
28
+ import { DEFAULT_CONFIG as DEFAULT_DOCTOR_CONFIG, normalizeConfig as normalizeDoctorConfig } from "../doctor/config.ts";
30
29
  import {
31
30
  CURRENCY_DEFAULTS,
32
31
  DEFAULT_BYTEFLY_CONFIG,
33
32
  DEFAULT_CURRENCY,
34
33
  MAX_BUDGET,
35
- MAX_DECIMALS,
36
34
  normalizeByteflyConfig,
37
35
  } from "../footer/price/config.ts";
38
36
  import type { CurrencyCode } from "../footer/price/types.ts";
39
- import { DEFAULT_NOTIFY_CONFIG, MAX_TITLE_LENGTH, normalizeNotifyConfig } from "../notify/config.ts";
37
+ import { DEFAULT_NOTIFY_CONFIG, normalizeNotifyConfig } from "../notify/config.ts";
40
38
  import type { Settings, WriteScope } from "../picker/types.ts";
41
39
 
42
40
  /** Where a preference is stored. */
@@ -62,24 +60,8 @@ interface TextRule {
62
60
  validate?: (raw: string) => string | undefined;
63
61
  }
64
62
 
65
- /** One heading in the Settings list. */
66
- interface SettingGroup {
67
- key: string;
68
- label: string;
69
- }
70
-
71
- /** The `bytefly.json` sections the Settings section can read and write. */
72
- export type ConfigSectionName =
73
- | "notify"
74
- | "chrome"
75
- | "doctor"
76
- | "price"
77
- | "worktree"
78
- | "rewind"
79
- | "job"
80
- | "watch"
81
- | "memory"
82
- | "goal";
63
+ /** The `bytefly.json` sections a row still reaches. */
64
+ export type ConfigSectionName = "notify" | "doctor" | "price" | "rewind" | "job" | "watch";
83
65
 
84
66
  /** Where a `bytefly.json` row lives: a section, and the path to its leaf. */
85
67
  interface SettingConfig {
@@ -98,8 +80,6 @@ interface SettingLeaf {
98
80
  export interface SettingDescriptor {
99
81
  /** Stable identity, also the focus key. */
100
82
  id: string;
101
- /** Group key from {@link SETTING_GROUPS}. */
102
- group: string;
103
83
  label: string;
104
84
  description: string;
105
85
  source: SettingSource;
@@ -139,27 +119,6 @@ export interface SettingsSnapshot {
139
119
  themes: readonly string[];
140
120
  }
141
121
 
142
- /** One rendered group: its heading and the rows under it. */
143
- interface GroupedSettings {
144
- key: string;
145
- label: string;
146
- views: SettingView[];
147
- }
148
-
149
- /** Group order on screen. */
150
- export const SETTING_GROUPS: readonly SettingGroup[] = [
151
- { key: "startup", label: "Startup" },
152
- { key: "notifications", label: "Notifications" },
153
- { key: "diagnostics", label: "Diagnostics" },
154
- { key: "pricing", label: "Pricing" },
155
- { key: "transcript", label: "Transcript" },
156
- { key: "terminal", label: "Terminal" },
157
- { key: "worktree", label: "Worktree" },
158
- { key: "snapshots", label: "Snapshots" },
159
- { key: "jobs", label: "Jobs and watchers" },
160
- { key: "memory", label: "Memory" },
161
- ];
162
-
163
122
  const ON_OFF: readonly SettingOption[] = [
164
123
  { value: "false", label: "Off" },
165
124
  { value: "true", label: "On" },
@@ -175,32 +134,6 @@ const METHOD_OPTIONS: readonly SettingOption[] = [
175
134
  { value: "notify-send", label: "Linux" },
176
135
  ];
177
136
 
178
- /** Cost precision 0 through the loader's ceiling, the range `price` clamps `decimals` to. */
179
- const DECIMALS_OPTIONS: readonly SettingOption[] = Array.from({ length: MAX_DECIMALS + 1 }, (_, value) => ({
180
- value: String(value),
181
- label: String(value),
182
- }));
183
-
184
- /**
185
- * A text rule for a whole number inside an inclusive range.
186
- *
187
- * The bounds are the loader's own (`doctor.timeoutMs` is clamped to 100-30000),
188
- * so an accepted value is never silently clamped on the next read. `maxLength`
189
- * only stops a runaway buffer; `validate` is the rule.
190
- */
191
- function integerRule(min: number, max: number): TextRule {
192
- return {
193
- maxLength: String(max).length + 2,
194
- validate(raw) {
195
- const text = raw.trim();
196
- if (!/^\d+$/.test(text)) return `Use a whole number, ${min} to ${max}.`;
197
- const value = Number(text);
198
- if (value < min || value > max) return `Out of range: ${min} to ${max}.`;
199
- return undefined;
200
- },
201
- };
202
- }
203
-
204
137
  /**
205
138
  * A text rule for a money amount inside an inclusive range: a whole number or a
206
139
  * decimal with up to four places, matching the loader's clamp for `price.budget`.
@@ -219,18 +152,15 @@ function amountRule(min: number, max: number): TextRule {
219
152
  };
220
153
  }
221
154
 
222
- /** The rail's rule: the loader drops anything that is not one column. */
223
- const RAIL_RULE: TextRule = {
224
- maxLength: 4,
225
- validate: (raw) =>
226
- isRailGlyph(raw) ? undefined : "Use exactly one column: a single glyph, with no spaces or control characters.",
227
- };
228
-
229
- /** The v1 catalogue, in screen order. */
155
+ /**
156
+ * The catalogue, in screen order.
157
+ *
158
+ * Global scope lists all fourteen; Project lists the twelve `bytefly.json` rows,
159
+ * because Pi's setters write the personal `settings.json` only.
160
+ */
230
161
  export const SETTING_ROWS: readonly SettingDescriptor[] = [
231
162
  {
232
163
  id: "byteflyUi",
233
- group: "startup",
234
164
  label: "Bytefly startup TUI",
235
165
  description: "Use bytefly's startup header in place of Pi's startup TUI.",
236
166
  source: "app",
@@ -239,9 +169,19 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
239
169
  json: "boolean",
240
170
  options: ON_OFF,
241
171
  },
172
+ {
173
+ id: "theme",
174
+ label: "Theme",
175
+ description:
176
+ "Pi's color theme, chosen from the palettes this package ships; `system` follows the terminal's colors. Takes effect after reload.",
177
+ source: "app",
178
+ kind: "enum",
179
+ fallback: "system",
180
+ // The host supplies this package's theme names in the snapshot; the static list stays empty.
181
+ options: [],
182
+ },
242
183
  {
243
184
  id: "notify.enabled",
244
- group: "notifications",
245
185
  label: "Desktop notifications",
246
186
  description: "Raise an OS notification when a run or job finishes, or the agent asks for input.",
247
187
  source: "bytefly",
@@ -253,7 +193,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
253
193
  },
254
194
  {
255
195
  id: "notify.turnFinished",
256
- group: "notifications",
257
196
  label: "Turn finished",
258
197
  description: "Announce it when a full agent run settles.",
259
198
  source: "bytefly",
@@ -265,7 +204,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
265
204
  },
266
205
  {
267
206
  id: "notify.needsInput",
268
- group: "notifications",
269
207
  label: "Needs input",
270
208
  description: "Announce it when the agent opens a prompt and waits for an answer.",
271
209
  source: "bytefly",
@@ -277,7 +215,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
277
215
  },
278
216
  {
279
217
  id: "notify.jobFinished",
280
- group: "notifications",
281
218
  label: "Job finished",
282
219
  description: "Announce it when a background job or watcher settles.",
283
220
  source: "bytefly",
@@ -289,7 +226,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
289
226
  },
290
227
  {
291
228
  id: "notify.matchFound",
292
- group: "notifications",
293
229
  label: "Watcher matched",
294
230
  description: "Announce it when a `watch` whose `wake` is on matches while the agent is idle.",
295
231
  source: "bytefly",
@@ -301,7 +237,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
301
237
  },
302
238
  {
303
239
  id: "notify.method",
304
- group: "notifications",
305
240
  label: "Delivery method",
306
241
  description: "How a notification is delivered; Auto picks from the terminal, then the platform.",
307
242
  source: "bytefly",
@@ -311,23 +246,40 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
311
246
  config: { section: "notify", path: ["method"] },
312
247
  },
313
248
  {
314
- id: "notify.title",
315
- group: "notifications",
316
- label: "Notification title",
317
- description: "Shown as the title of every OS notification; up to 40 characters.",
249
+ id: "job.wakeOnFinish",
250
+ label: "Wake when a job finishes",
251
+ description: "Start one agent turn when any background job finishes.",
318
252
  source: "bytefly",
319
- kind: "text",
320
- fallback: "Pi",
321
- options: [],
322
- text: {
323
- maxLength: MAX_TITLE_LENGTH,
324
- validate: (raw) => (raw.trim() === "" ? "A title is required." : undefined),
325
- },
326
- config: { section: "notify", path: ["title"] },
253
+ kind: "boolean",
254
+ fallback: "false",
255
+ json: "boolean",
256
+ options: ON_OFF,
257
+ config: { section: "job", path: ["wakeOnFinish"] },
258
+ },
259
+ {
260
+ id: "watch.wakeOnMatch",
261
+ label: "Wake on a match",
262
+ description: "Start one agent turn when a watcher matches while the agent is idle.",
263
+ source: "bytefly",
264
+ kind: "boolean",
265
+ fallback: "false",
266
+ json: "boolean",
267
+ options: ON_OFF,
268
+ config: { section: "watch", path: ["wakeOnMatch"] },
269
+ },
270
+ {
271
+ id: "rewind.autoSnapshots",
272
+ label: "Automatic snapshots",
273
+ description: "Take one rewind snapshot per prompt, before the first mutating tool call.",
274
+ source: "bytefly",
275
+ kind: "boolean",
276
+ fallback: "true",
277
+ json: "boolean",
278
+ options: ON_OFF,
279
+ config: { section: "rewind", path: ["autoSnapshots"] },
327
280
  },
328
281
  {
329
282
  id: "doctor.proactive",
330
- group: "diagnostics",
331
283
  label: "Proactive health check",
332
284
  description: "Probe enabled extensions at session start and warn about a missing requirement.",
333
285
  source: "bytefly",
@@ -337,22 +289,8 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
337
289
  options: ON_OFF,
338
290
  config: { section: "doctor", path: ["proactive"] },
339
291
  },
340
- {
341
- id: "doctor.timeoutMs",
342
- group: "diagnostics",
343
- label: "Probe timeout",
344
- description: `How long one doctor probe may take, in milliseconds; ${TIMEOUT_MS_RANGE.min} to ${TIMEOUT_MS_RANGE.max}.`,
345
- source: "bytefly",
346
- kind: "text",
347
- fallback: String(DEFAULT_DOCTOR_CONFIG.timeoutMs),
348
- json: "number",
349
- options: [],
350
- text: integerRule(TIMEOUT_MS_RANGE.min, TIMEOUT_MS_RANGE.max),
351
- config: { section: "doctor", path: ["timeoutMs"] },
352
- },
353
292
  {
354
293
  id: "price.currency.code",
355
- group: "pricing",
356
294
  label: "Currency",
357
295
  description:
358
296
  "CNY prices DeepSeek from its yuan rates; USD from its published dollars, falling back to Pi's recorded cost.",
@@ -365,7 +303,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
365
303
  ],
366
304
  config: { section: "price", path: ["currency", "code"] },
367
305
  // Choosing a currency also sets its default precision, so the screen and the
368
- // footer agree; the precision stays overridable afterwards.
306
+ // footer agree; the precision stays overridable in `bytefly.json` afterwards.
369
307
  also: (raw) => {
370
308
  const currency = CURRENCY_DEFAULTS[raw as CurrencyCode];
371
309
  return currency === undefined
@@ -373,21 +311,8 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
373
311
  : [{ section: "price", path: ["currency", "decimals"], value: currency.decimals }];
374
312
  },
375
313
  },
376
- {
377
- id: "price.currency.decimals",
378
- group: "pricing",
379
- label: "Cost decimals",
380
- description: "Digits shown after the decimal point in the footer's cost total.",
381
- source: "bytefly",
382
- kind: "enum",
383
- fallback: "2",
384
- json: "number",
385
- options: DECIMALS_OPTIONS,
386
- config: { section: "price", path: ["currency", "decimals"] },
387
- },
388
314
  {
389
315
  id: "price.budget",
390
- group: "pricing",
391
316
  label: "Session budget",
392
317
  description: "Warn once when the session's cost reaches this amount; 0 is off. It never stops a turn.",
393
318
  source: "bytefly",
@@ -398,210 +323,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
398
323
  text: amountRule(0, MAX_BUDGET),
399
324
  config: { section: "price", path: ["budget"] },
400
325
  },
401
- {
402
- id: "chrome.rail",
403
- group: "transcript",
404
- label: "Rail glyph",
405
- description: "The single-column glyph that leads a tool result or notice body, e.g. ╎ │ ┃ ▏.",
406
- source: "bytefly",
407
- kind: "text",
408
- fallback: DEFAULT_CHROME_CONFIG.rail,
409
- options: [],
410
- text: RAIL_RULE,
411
- config: { section: "chrome", path: ["rail"] },
412
- },
413
- {
414
- id: "goal.achieved",
415
- group: "transcript",
416
- label: "Achieved goal",
417
- description: "How a completed goal renders: the dimmed rail line, or nothing.",
418
- source: "bytefly",
419
- kind: "enum",
420
- fallback: "hide",
421
- options: [
422
- { value: "show", label: "Show" },
423
- { value: "hide", label: "Hide" },
424
- ],
425
- config: { section: "goal", path: ["achieved"] },
426
- },
427
- {
428
- id: "outputPad",
429
- group: "transcript",
430
- label: "Output padding",
431
- description: "Pi's transcript padding; bytefly's header and blocks follow it.",
432
- source: "app",
433
- kind: "enum",
434
- fallback: "1",
435
- json: "number",
436
- options: [
437
- { value: "0", label: "0" },
438
- { value: "1", label: "1" },
439
- ],
440
- },
441
- {
442
- id: "theme",
443
- group: "terminal",
444
- label: "Theme",
445
- description:
446
- "Pi's color theme, chosen from the palettes this package ships; `system` follows the terminal's colors. Takes effect after reload.",
447
- source: "app",
448
- kind: "enum",
449
- fallback: "system",
450
- // The host supplies this package's theme names in the snapshot; the static list stays empty.
451
- options: [],
452
- },
453
- {
454
- id: "showHardwareCursor",
455
- group: "terminal",
456
- label: "Hardware cursor",
457
- description: "Use the terminal cursor instead of Pi's drawn cursor.",
458
- source: "app",
459
- kind: "boolean",
460
- fallback: "false",
461
- json: "boolean",
462
- options: ON_OFF,
463
- },
464
- {
465
- id: "worktree.baseRef",
466
- group: "worktree",
467
- label: "Base ref",
468
- description: "Where a new worktree branches from: the remote default branch, or the current local HEAD.",
469
- source: "bytefly",
470
- kind: "enum",
471
- fallback: "fresh",
472
- options: [
473
- { value: "fresh", label: "Fresh" },
474
- { value: "head", label: "Head" },
475
- ],
476
- config: { section: "worktree", path: ["baseRef"] },
477
- },
478
- {
479
- id: "worktree.branchPrefix",
480
- group: "worktree",
481
- label: "Branch prefix",
482
- description: "Prepended to every branch a managed worktree creates.",
483
- source: "bytefly",
484
- kind: "text",
485
- fallback: "worktree-",
486
- options: [],
487
- text: {
488
- // The loader trims and rejects an empty prefix but sets no ceiling; this
489
- // only stops a runaway buffer.
490
- maxLength: 80,
491
- validate: (raw) => (raw.trim() === "" ? "A prefix is required." : undefined),
492
- },
493
- config: { section: "worktree", path: ["branchPrefix"] },
494
- },
495
- {
496
- id: "worktree.fetchRemote",
497
- group: "worktree",
498
- label: "Fetch remote",
499
- description: "Fetch the remote before branching, so a new worktree starts from its latest commit.",
500
- source: "bytefly",
501
- kind: "boolean",
502
- fallback: "true",
503
- json: "boolean",
504
- options: ON_OFF,
505
- config: { section: "worktree", path: ["fetchRemote"] },
506
- },
507
- {
508
- id: "worktree.pruneAfterDays",
509
- group: "worktree",
510
- label: "Prune after (days)",
511
- description: "Age at which `/worktree prune` removes a clean worktree nobody is using.",
512
- source: "bytefly",
513
- kind: "text",
514
- fallback: "7",
515
- json: "number",
516
- options: [],
517
- text: integerRule(0, 3650),
518
- config: { section: "worktree", path: ["pruneAfterDays"] },
519
- },
520
- {
521
- id: "rewind.autoSnapshots",
522
- group: "snapshots",
523
- label: "Automatic snapshots",
524
- description: "Take one rewind snapshot per prompt, before the first mutating tool call.",
525
- source: "bytefly",
526
- kind: "boolean",
527
- fallback: "true",
528
- json: "boolean",
529
- options: ON_OFF,
530
- config: { section: "rewind", path: ["autoSnapshots"] },
531
- },
532
- {
533
- id: "rewind.max",
534
- group: "snapshots",
535
- label: "Snapshots kept",
536
- description: "Snapshot refs kept per repository, including pre-restore safety snapshots; 0 keeps none.",
537
- source: "bytefly",
538
- kind: "text",
539
- fallback: "20",
540
- json: "number",
541
- options: [],
542
- text: integerRule(0, 1000),
543
- config: { section: "rewind", path: ["max"] },
544
- },
545
- {
546
- id: "rewind.safetySnapshot",
547
- group: "snapshots",
548
- label: "Safety snapshot",
549
- description: "Snapshot the current state before a restore, so a rewind can itself be undone.",
550
- source: "bytefly",
551
- kind: "boolean",
552
- fallback: "true",
553
- json: "boolean",
554
- options: ON_OFF,
555
- config: { section: "rewind", path: ["safetySnapshot"] },
556
- },
557
- {
558
- id: "job.wakeOnFinish",
559
- group: "jobs",
560
- label: "Wake when a job finishes",
561
- description: "Start one agent turn when any background job finishes.",
562
- source: "bytefly",
563
- kind: "boolean",
564
- fallback: "false",
565
- json: "boolean",
566
- options: ON_OFF,
567
- config: { section: "job", path: ["wakeOnFinish"] },
568
- },
569
- {
570
- id: "job.detachedByDefault",
571
- group: "jobs",
572
- label: "Keep jobs after exit",
573
- description: "Leave jobs running when the session ends, unless the job opts out.",
574
- source: "bytefly",
575
- kind: "boolean",
576
- fallback: "false",
577
- json: "boolean",
578
- options: ON_OFF,
579
- config: { section: "job", path: ["detachedByDefault"] },
580
- },
581
- {
582
- id: "watch.wakeOnMatch",
583
- group: "jobs",
584
- label: "Wake on a match",
585
- description: "Start one agent turn when a watcher matches while the agent is idle.",
586
- source: "bytefly",
587
- kind: "boolean",
588
- fallback: "false",
589
- json: "boolean",
590
- options: ON_OFF,
591
- config: { section: "watch", path: ["wakeOnMatch"] },
592
- },
593
- {
594
- id: "memory.inject",
595
- group: "memory",
596
- label: "Inject notes",
597
- description: "Put stored memory notes in context before each run.",
598
- source: "bytefly",
599
- kind: "boolean",
600
- fallback: "true",
601
- json: "boolean",
602
- options: ON_OFF,
603
- config: { section: "memory", path: ["inject"] },
604
- },
605
326
  ];
606
327
 
607
328
  /** The one refusal for a Project write while the project is untrusted. */
@@ -615,10 +336,10 @@ export function rowsForScope(scope: WriteScope): readonly SettingDescriptor[] {
615
336
  /**
616
337
  * Why a text row cannot store `raw`, or `undefined` when it can.
617
338
  *
618
- * The limits are the loaders' own (`notify.title` is sliced to `MAX_TITLE_LENGTH`,
619
- * the rail to one column) so the editor cannot save a value the next read shortens.
620
- * A row that is not text is refused here rather than in the caller, so a programmatic
621
- * `setValue` gets the same reason the screen would show.
339
+ * The limits are the loaders' own (the budget is clamped to `MAX_BUDGET`) so the
340
+ * editor cannot save a value the next read shortens. A row that is not text is
341
+ * refused here rather than in the caller, so a programmatic `setValue` gets the
342
+ * same reason the screen would show.
622
343
  */
623
344
  export function textError(row: SettingDescriptor, raw: string): string | undefined {
624
345
  const rule = row.text;
@@ -650,16 +371,6 @@ function appValue(snapshot: SettingsSnapshot, row: SettingDescriptor): { value:
650
371
  const quiet = snapshot.app.quietStartup;
651
372
  return { value: quiet === true ? "true" : row.fallback, origin: quiet === undefined ? "default" : "global" };
652
373
  }
653
- case "outputPad":
654
- return {
655
- value: snapshot.app.outputPad === 0 ? "0" : row.fallback,
656
- origin: snapshot.app.outputPad === undefined ? "default" : "global",
657
- };
658
- case "showHardwareCursor":
659
- return {
660
- value: snapshot.app.showHardwareCursor === true ? "true" : row.fallback,
661
- origin: snapshot.app.showHardwareCursor === undefined ? "default" : "global",
662
- };
663
374
  case "theme": {
664
375
  const theme = snapshot.app.theme;
665
376
  const set = typeof theme === "string" && theme !== "";
@@ -692,15 +403,10 @@ type SectionReader = (
692
403
  path: readonly string[],
693
404
  ) => unknown;
694
405
 
695
- /**
696
- * The four sections bytefly owns, each read through its own normalizer so the
697
- * screen shows the value the loader keeps.
698
- */
406
+ /** The sections bytefly owns, each read through its own normalizer so the screen shows what the loader keeps. */
699
407
  const SECTION_READERS: Partial<Record<ConfigSectionName, SectionReader>> = {
700
408
  notify: (globalRaw, projectRaw, path) =>
701
409
  configValueAt(normalizeNotifyConfig(projectRaw, normalizeNotifyConfig(globalRaw, DEFAULT_NOTIFY_CONFIG)), path),
702
- chrome: (globalRaw, projectRaw, path) =>
703
- configValueAt(normalizeChromeConfig(projectRaw, normalizeChromeConfig(globalRaw, DEFAULT_CHROME_CONFIG)), path),
704
410
  doctor: (globalRaw, projectRaw, path) =>
705
411
  configValueAt(normalizeDoctorConfig(projectRaw, normalizeDoctorConfig(globalRaw, DEFAULT_DOCTOR_CONFIG)), path),
706
412
  price: (globalRaw, projectRaw, path) =>
@@ -772,12 +478,3 @@ export function storedValue(row: SettingDescriptor, raw: string): unknown {
772
478
  return raw;
773
479
  }
774
480
  }
775
-
776
- /** Group views in screen order, dropping empty groups. */
777
- export function groupViews(views: readonly SettingView[]): GroupedSettings[] {
778
- return SETTING_GROUPS.map((group) => ({
779
- key: group.key,
780
- label: group.label,
781
- views: views.filter((view) => view.group === group.key),
782
- })).filter((group) => group.views.length > 0);
783
- }
@@ -1,12 +1,16 @@
1
1
  /**
2
- * The panel's entry model: a flat list where group headers and selectable rows
3
- * interleave, shared by both sections.
2
+ * The panel's entry model: one flat list of selectable rows per section.
3
+ *
4
+ * Extensions interleaves group headings with rows — a source group says where a
5
+ * resource came from, which the row cannot — while Settings is flat, in catalogue
6
+ * order. Both are built from a model or a set of views, so the component only has
7
+ * to keep one cursor per section.
4
8
  *
5
9
  * Pure: the list is rebuilt from a model or a set of views, and navigation is
6
- * index arithmetic, so the component only has to keep one cursor per section.
10
+ * index arithmetic.
7
11
  */
8
12
 
9
- import { groupViews, type SettingView } from "../settings.ts";
13
+ import type { SettingView } from "../settings.ts";
10
14
  import type { PanelModel, PanelRow } from "../types.ts";
11
15
 
12
16
  /** A flat display entry: a group header or a selectable row. */
@@ -30,9 +34,9 @@ export function isSelectable(entry: PanelEntry | undefined): boolean {
30
34
  /**
31
35
  * One display window as ordinals over the selectable rows it contains.
32
36
  *
33
- * The range row counts rows, not entries: a list of twenty-nine settings in ten
34
- * groups spans thirty-nine entries, but `showing 3–5 of 29` is what the user can
35
- * focus. `formatRange` prints `start + 1` through `end`, so the ordinals here are
37
+ * The range row counts rows, not entries: an extension list's group headings are
38
+ * not focusable, so `showing 3–5 of 12` is what the user can land on.
39
+ * `formatRange` prints `start + 1` through `end`, so the ordinals here are
36
40
  * zero-based and `end` is exclusive.
37
41
  */
38
42
  export function selectableRange(
@@ -71,12 +75,10 @@ export function entriesOf(model: PanelModel): PanelEntry[] {
71
75
  return entries;
72
76
  }
73
77
 
74
- /** Interleave the settings views into the same group/row display list. */
78
+ /**
79
+ * The Settings entries: one row per view, in catalogue order, with no group
80
+ * headings — the list is short enough to read whole.
81
+ */
75
82
  export function settingEntries(views: readonly SettingView[]): PanelEntry[] {
76
- const entries: PanelEntry[] = [];
77
- for (const group of groupViews(views)) {
78
- entries.push({ kind: "group", key: group.key, label: group.label, count: group.views.length });
79
- for (const view of group.views) entries.push({ kind: "setting", setting: view });
80
- }
81
- return entries;
83
+ return views.map((setting) => ({ kind: "setting", setting }));
82
84
  }
@@ -44,6 +44,9 @@ export function extensionRow(theme: Theme, width: number, row: PanelRow, selecte
44
44
  // The glyph carries the status colour; the word stays quiet unless it is a
45
45
  // problem, so a clean row does not shout.
46
46
  trailingRole: badge.role === "bad" || badge.role === "warn" ? badge.role : "faint",
47
+ // The verdict sits beside the name instead of at the far edge of a wide
48
+ // terminal, where a two-word verdict would be unreadable.
49
+ align: "inline",
47
50
  });
48
51
  }
49
52