@gavin-hu/my-pi-agent 0.15.1 → 0.17.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 (189) hide show
  1. package/README.md +22 -13
  2. package/README.zh-CN.md +19 -11
  3. package/extensions/bytefly/README.md +255 -200
  4. package/extensions/bytefly/footer/collect/entries.ts +1 -6
  5. package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
  6. package/extensions/bytefly/footer/price/price.ts +12 -8
  7. package/extensions/bytefly/footer/register.ts +13 -1
  8. package/extensions/bytefly/footer/types.ts +11 -0
  9. package/extensions/bytefly/footer/unpriced.ts +36 -0
  10. package/extensions/bytefly/header/component.ts +15 -29
  11. package/extensions/bytefly/header/identity.ts +71 -0
  12. package/extensions/bytefly/header/register.ts +9 -3
  13. package/extensions/bytefly/index.ts +13 -5
  14. package/extensions/bytefly/message/format.ts +20 -13
  15. package/extensions/bytefly/message/register.ts +8 -7
  16. package/extensions/bytefly/notice/component.ts +6 -4
  17. package/extensions/bytefly/notice/config.ts +16 -2
  18. package/extensions/bytefly/panel/commands.ts +46 -25
  19. package/extensions/bytefly/panel/format.ts +52 -22
  20. package/extensions/bytefly/panel/join.ts +36 -15
  21. package/extensions/bytefly/panel/settings.ts +72 -392
  22. package/extensions/bytefly/panel/skills.ts +51 -0
  23. package/extensions/bytefly/panel/tui/entries.ts +16 -14
  24. package/extensions/bytefly/panel/tui/extensions-view.ts +7 -58
  25. package/extensions/bytefly/panel/tui/frame.ts +11 -3
  26. package/extensions/bytefly/panel/tui/list-view.ts +72 -0
  27. package/extensions/bytefly/panel/tui/nav.ts +71 -3
  28. package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
  29. package/extensions/bytefly/panel/tui/settings-view.ts +8 -12
  30. package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
  31. package/extensions/bytefly/panel/tui.ts +223 -333
  32. package/extensions/bytefly/panel/types.ts +26 -8
  33. package/extensions/bytefly/picker/discovery.ts +32 -27
  34. package/extensions/bytefly/picker/resources.ts +22 -10
  35. package/extensions/bytefly/picker/runtime.ts +62 -34
  36. package/extensions/bytefly/picker/toggle.ts +16 -10
  37. package/extensions/bytefly/picker/types.ts +31 -13
  38. package/extensions/bytefly/update/format.ts +66 -8
  39. package/extensions/bytefly/update/patch.ts +66 -0
  40. package/extensions/bytefly/update/register.ts +4 -11
  41. package/extensions/bytefly/update/release.ts +65 -0
  42. package/extensions/bytefly/update/state.ts +14 -0
  43. package/extensions/bytefly/update/suppress.ts +47 -29
  44. package/extensions/bytefly/update/types.ts +15 -5
  45. package/extensions/bytefly/working/register.ts +2 -8
  46. package/extensions/check/tools.ts +2 -11
  47. package/extensions/check/tui.ts +2 -0
  48. package/extensions/context/README.md +172 -0
  49. package/extensions/context/collect.ts +53 -0
  50. package/extensions/context/commands.ts +100 -0
  51. package/extensions/context/format.ts +84 -0
  52. package/extensions/context/index.ts +16 -0
  53. package/extensions/context/report.ts +332 -0
  54. package/extensions/context/tui.ts +284 -0
  55. package/extensions/context/types.ts +124 -0
  56. package/extensions/file-browse/README.md +67 -19
  57. package/extensions/file-browse/client.ts +16 -463
  58. package/extensions/file-browse/config.ts +8 -0
  59. package/extensions/file-browse/html.ts +9 -7
  60. package/extensions/file-browse/index.ts +17 -2
  61. package/extensions/file-browse/live-client.ts +13 -13
  62. package/extensions/file-browse/live.ts +44 -10
  63. package/extensions/file-browse/markdown.ts +3 -10
  64. package/extensions/file-browse/palette.ts +224 -0
  65. package/extensions/file-browse/raw-html.ts +3 -10
  66. package/extensions/file-browse/router.ts +2 -2
  67. package/extensions/file-browse/server.ts +13 -2
  68. package/extensions/file-browse/styles.ts +8 -3
  69. package/extensions/file-browse/tree.ts +381 -0
  70. package/extensions/file-open/README.md +1 -2
  71. package/extensions/{document → file-read}/README.md +76 -57
  72. package/extensions/{document → file-read}/config.ts +12 -9
  73. package/extensions/{document → file-read}/extract/doc.ts +2 -2
  74. package/extensions/{document → file-read}/extract/docx.ts +3 -3
  75. package/extensions/{document → file-read}/extract/file.ts +9 -9
  76. package/extensions/{document → file-read}/extract/odt.ts +10 -8
  77. package/extensions/{document → file-read}/extract/pdf.ts +3 -3
  78. package/extensions/{document → file-read}/extract/rtf.ts +3 -3
  79. package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
  80. package/extensions/{document → file-read}/formats.ts +11 -11
  81. package/extensions/file-read/index.ts +17 -0
  82. package/extensions/{document → file-read}/paging.ts +15 -13
  83. package/extensions/{document → file-read}/render.ts +17 -13
  84. package/extensions/{document → file-read}/schema.ts +7 -7
  85. package/extensions/{document → file-read}/tool.ts +36 -28
  86. package/extensions/goal/tools.ts +16 -25
  87. package/extensions/job/commands.ts +32 -46
  88. package/extensions/job/index.ts +20 -45
  89. package/extensions/job/runtime.ts +6 -8
  90. package/extensions/job/tools.ts +58 -103
  91. package/extensions/memory/commands.ts +30 -44
  92. package/extensions/memory/tools.ts +10 -20
  93. package/extensions/memory/tui.ts +5 -1
  94. package/extensions/plan/README.md +88 -33
  95. package/extensions/plan/commands.ts +19 -4
  96. package/extensions/plan/config.ts +27 -0
  97. package/extensions/plan/plan-index.ts +3 -1
  98. package/extensions/plan/plans.ts +31 -13
  99. package/extensions/plan/review.ts +34 -14
  100. package/extensions/plan/runtime.ts +3 -1
  101. package/extensions/plan/steps.ts +43 -14
  102. package/extensions/plan/tools/cancel.ts +79 -0
  103. package/extensions/plan/tools/enter.ts +16 -17
  104. package/extensions/plan/tools/exit.ts +60 -50
  105. package/extensions/plan/tools/steps.ts +13 -26
  106. package/extensions/plan/tools/write.ts +30 -38
  107. package/extensions/plan/tools.ts +5 -2
  108. package/extensions/plan/tui.ts +9 -7
  109. package/extensions/plan/types.ts +8 -0
  110. package/extensions/plan/web-chrome.ts +101 -0
  111. package/extensions/plan/web-client.ts +98 -311
  112. package/extensions/plan/web-markdown.ts +222 -0
  113. package/extensions/plan/web-page.ts +28 -2
  114. package/extensions/plan/web-router.ts +2 -2
  115. package/extensions/plan/web.ts +21 -12
  116. package/extensions/question/tools.ts +5 -4
  117. package/extensions/rewind/checklist.ts +4 -0
  118. package/extensions/rewind/review-tui.ts +3 -1
  119. package/extensions/rewind/rewind-tui.ts +4 -0
  120. package/extensions/subagent/README.md +25 -12
  121. package/extensions/subagent/background.ts +1 -7
  122. package/extensions/subagent/format.ts +72 -25
  123. package/extensions/subagent/index.ts +21 -47
  124. package/extensions/subagent/orchestrate.ts +18 -41
  125. package/extensions/subagent/render.ts +29 -42
  126. package/extensions/subagent/stream.ts +24 -18
  127. package/extensions/subagent/types.ts +7 -0
  128. package/extensions/todo/tools.ts +15 -20
  129. package/extensions/todo/tui.ts +5 -1
  130. package/extensions/watch/README.md +5 -5
  131. package/extensions/watch/commands.ts +33 -47
  132. package/extensions/watch/index.ts +10 -35
  133. package/extensions/watch/runtime.ts +5 -7
  134. package/extensions/watch/tools.ts +38 -77
  135. package/extensions/web-access/README.md +3 -3
  136. package/extensions/web-access/search/search.ts +9 -18
  137. package/extensions/wechat/README.md +40 -18
  138. package/extensions/wechat/bridge.ts +38 -40
  139. package/extensions/wechat/client.ts +1 -4
  140. package/extensions/wechat/commands.ts +1 -1
  141. package/extensions/wechat/interaction.ts +25 -4
  142. package/extensions/wechat/login-screen.ts +2 -3
  143. package/extensions/wechat/notices.ts +7 -5
  144. package/extensions/wechat/prompt.ts +19 -4
  145. package/extensions/wechat/qrcode.ts +9 -9
  146. package/extensions/wechat/slash.ts +14 -0
  147. package/extensions/wechat/status-screen.ts +2 -2
  148. package/extensions/wechat/timer.ts +9 -30
  149. package/extensions/wechat/tools.ts +7 -18
  150. package/extensions/worktree/README.md +48 -12
  151. package/extensions/worktree/commands.ts +109 -65
  152. package/extensions/worktree/enter.ts +1 -1
  153. package/extensions/worktree/exit.ts +11 -1
  154. package/extensions/worktree/lifecycle.ts +9 -1
  155. package/extensions/worktree/screen.ts +263 -0
  156. package/extensions/worktree/status.ts +84 -38
  157. package/lib/README.md +15 -11
  158. package/lib/config.ts +12 -0
  159. package/lib/escape.ts +15 -2
  160. package/lib/format.ts +6 -2
  161. package/lib/interaction.ts +18 -1
  162. package/lib/pdf.ts +2 -2
  163. package/lib/report.ts +75 -2
  164. package/lib/requirements.ts +2 -2
  165. package/lib/screen-session.ts +85 -0
  166. package/lib/timer.ts +61 -0
  167. package/lib/tool-result.ts +54 -4
  168. package/lib/tui/README.md +19 -11
  169. package/lib/tui/chrome.ts +27 -6
  170. package/lib/tui/columns.ts +33 -8
  171. package/lib/tui/dock-screen.ts +6 -2
  172. package/lib/tui/gauge.ts +9 -5
  173. package/lib/tui/rows.ts +15 -6
  174. package/lib/tui/rules.ts +4 -8
  175. package/lib/tui/tool.ts +55 -2
  176. package/lib/usage.ts +26 -0
  177. package/lib/web/README.md +131 -0
  178. package/lib/web/base.ts +19 -2
  179. package/lib/web/components.ts +10 -6
  180. package/lib/web/layout.ts +2 -0
  181. package/lib/web/markup.ts +2 -1
  182. package/lib/web/runtime.ts +103 -25
  183. package/lib/web/tokens.ts +23 -7
  184. package/package.json +10 -4
  185. package/skills/changelog/SKILL.md +148 -0
  186. package/skills/next/SKILL.md +184 -0
  187. package/extensions/document/index.ts +0 -17
  188. /package/extensions/{document → file-read}/deadline.ts +0 -0
  189. /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`, `plan`) 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" | "plan";
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,67 +119,12 @@ 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" },
166
125
  ];
167
126
 
168
127
  /** The notify section's delivery methods, labeled by the surface each reaches. */
169
- const METHOD_OPTIONS: readonly SettingOption[] = [
170
- { value: "auto", label: "Auto" },
171
- { value: "osc777", label: "OSC 777" },
172
- { value: "osc99", label: "OSC 99" },
173
- { value: "toast", label: "Windows toast" },
174
- { value: "osascript", label: "macOS" },
175
- { value: "notify-send", label: "Linux" },
176
- ];
177
-
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
128
 
204
129
  /**
205
130
  * A text rule for a money amount inside an inclusive range: a whole number or a
@@ -219,18 +144,16 @@ function amountRule(min: number, max: number): TextRule {
219
144
  };
220
145
  }
221
146
 
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. */
147
+ /**
148
+ * The catalogue, in screen order.
149
+ *
150
+ * Global scope lists all thirteen; Project lists the eleven `bytefly.json` rows,
151
+ * because Pi's setters write the personal `settings.json` only. The list is
152
+ * capped so the screen keeps fitting a 24-row terminal.
153
+ */
230
154
  export const SETTING_ROWS: readonly SettingDescriptor[] = [
231
155
  {
232
156
  id: "byteflyUi",
233
- group: "startup",
234
157
  label: "Bytefly startup TUI",
235
158
  description: "Use bytefly's startup header in place of Pi's startup TUI.",
236
159
  source: "app",
@@ -239,9 +162,19 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
239
162
  json: "boolean",
240
163
  options: ON_OFF,
241
164
  },
165
+ {
166
+ id: "theme",
167
+ label: "Theme",
168
+ description:
169
+ "Pi's color theme, chosen from the palettes this package ships; `system` follows the terminal's colors. Takes effect after reload.",
170
+ source: "app",
171
+ kind: "enum",
172
+ fallback: "system",
173
+ // The host supplies this package's theme names in the snapshot; the static list stays empty.
174
+ options: [],
175
+ },
242
176
  {
243
177
  id: "notify.enabled",
244
- group: "notifications",
245
178
  label: "Desktop notifications",
246
179
  description: "Raise an OS notification when a run or job finishes, or the agent asks for input.",
247
180
  source: "bytefly",
@@ -253,7 +186,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
253
186
  },
254
187
  {
255
188
  id: "notify.turnFinished",
256
- group: "notifications",
257
189
  label: "Turn finished",
258
190
  description: "Announce it when a full agent run settles.",
259
191
  source: "bytefly",
@@ -265,7 +197,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
265
197
  },
266
198
  {
267
199
  id: "notify.needsInput",
268
- group: "notifications",
269
200
  label: "Needs input",
270
201
  description: "Announce it when the agent opens a prompt and waits for an answer.",
271
202
  source: "bytefly",
@@ -277,7 +208,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
277
208
  },
278
209
  {
279
210
  id: "notify.jobFinished",
280
- group: "notifications",
281
211
  label: "Job finished",
282
212
  description: "Announce it when a background job or watcher settles.",
283
213
  source: "bytefly",
@@ -289,7 +219,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
289
219
  },
290
220
  {
291
221
  id: "notify.matchFound",
292
- group: "notifications",
293
222
  label: "Watcher matched",
294
223
  description: "Announce it when a `watch` whose `wake` is on matches while the agent is idle.",
295
224
  source: "bytefly",
@@ -300,34 +229,40 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
300
229
  config: { section: "notify", path: ["matchFound"] },
301
230
  },
302
231
  {
303
- id: "notify.method",
304
- group: "notifications",
305
- label: "Delivery method",
306
- description: "How a notification is delivered; Auto picks from the terminal, then the platform.",
232
+ id: "job.wakeOnFinish",
233
+ label: "Wake when a job finishes",
234
+ description: "Start one agent turn when any background job finishes.",
307
235
  source: "bytefly",
308
- kind: "enum",
309
- fallback: "auto",
310
- options: METHOD_OPTIONS,
311
- config: { section: "notify", path: ["method"] },
236
+ kind: "boolean",
237
+ fallback: "false",
238
+ json: "boolean",
239
+ options: ON_OFF,
240
+ config: { section: "job", path: ["wakeOnFinish"] },
312
241
  },
313
242
  {
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.",
243
+ id: "plan.goalPrompt",
244
+ label: "Offer the session goal",
245
+ description: "Ask before recording an approved plan as the session goal; off records it without asking.",
318
246
  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"] },
247
+ kind: "boolean",
248
+ fallback: "true",
249
+ json: "boolean",
250
+ options: ON_OFF,
251
+ config: { section: "plan", path: ["goalPrompt"] },
252
+ },
253
+ {
254
+ id: "rewind.autoSnapshots",
255
+ label: "Automatic snapshots",
256
+ description: "Take one rewind snapshot per prompt, before the first mutating tool call.",
257
+ source: "bytefly",
258
+ kind: "boolean",
259
+ fallback: "true",
260
+ json: "boolean",
261
+ options: ON_OFF,
262
+ config: { section: "rewind", path: ["autoSnapshots"] },
327
263
  },
328
264
  {
329
265
  id: "doctor.proactive",
330
- group: "diagnostics",
331
266
  label: "Proactive health check",
332
267
  description: "Probe enabled extensions at session start and warn about a missing requirement.",
333
268
  source: "bytefly",
@@ -337,22 +272,8 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
337
272
  options: ON_OFF,
338
273
  config: { section: "doctor", path: ["proactive"] },
339
274
  },
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
275
  {
354
276
  id: "price.currency.code",
355
- group: "pricing",
356
277
  label: "Currency",
357
278
  description:
358
279
  "CNY prices DeepSeek from its yuan rates; USD from its published dollars, falling back to Pi's recorded cost.",
@@ -365,7 +286,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
365
286
  ],
366
287
  config: { section: "price", path: ["currency", "code"] },
367
288
  // Choosing a currency also sets its default precision, so the screen and the
368
- // footer agree; the precision stays overridable afterwards.
289
+ // footer agree; the precision stays overridable in `bytefly.json` afterwards.
369
290
  also: (raw) => {
370
291
  const currency = CURRENCY_DEFAULTS[raw as CurrencyCode];
371
292
  return currency === undefined
@@ -373,21 +294,8 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
373
294
  : [{ section: "price", path: ["currency", "decimals"], value: currency.decimals }];
374
295
  },
375
296
  },
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
297
  {
389
298
  id: "price.budget",
390
- group: "pricing",
391
299
  label: "Session budget",
392
300
  description: "Warn once when the session's cost reaches this amount; 0 is off. It never stops a turn.",
393
301
  source: "bytefly",
@@ -398,210 +306,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
398
306
  text: amountRule(0, MAX_BUDGET),
399
307
  config: { section: "price", path: ["budget"] },
400
308
  },
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
309
  ];
606
310
 
607
311
  /** The one refusal for a Project write while the project is untrusted. */
@@ -615,10 +319,10 @@ export function rowsForScope(scope: WriteScope): readonly SettingDescriptor[] {
615
319
  /**
616
320
  * Why a text row cannot store `raw`, or `undefined` when it can.
617
321
  *
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.
322
+ * The limits are the loaders' own (the budget is clamped to `MAX_BUDGET`) so the
323
+ * editor cannot save a value the next read shortens. A row that is not text is
324
+ * refused here rather than in the caller, so a programmatic `setValue` gets the
325
+ * same reason the screen would show.
622
326
  */
623
327
  export function textError(row: SettingDescriptor, raw: string): string | undefined {
624
328
  const rule = row.text;
@@ -650,16 +354,6 @@ function appValue(snapshot: SettingsSnapshot, row: SettingDescriptor): { value:
650
354
  const quiet = snapshot.app.quietStartup;
651
355
  return { value: quiet === true ? "true" : row.fallback, origin: quiet === undefined ? "default" : "global" };
652
356
  }
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
357
  case "theme": {
664
358
  const theme = snapshot.app.theme;
665
359
  const set = typeof theme === "string" && theme !== "";
@@ -692,15 +386,10 @@ type SectionReader = (
692
386
  path: readonly string[],
693
387
  ) => unknown;
694
388
 
695
- /**
696
- * The four sections bytefly owns, each read through its own normalizer so the
697
- * screen shows the value the loader keeps.
698
- */
389
+ /** The sections bytefly owns, each read through its own normalizer so the screen shows what the loader keeps. */
699
390
  const SECTION_READERS: Partial<Record<ConfigSectionName, SectionReader>> = {
700
391
  notify: (globalRaw, projectRaw, path) =>
701
392
  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
393
  doctor: (globalRaw, projectRaw, path) =>
705
394
  configValueAt(normalizeDoctorConfig(projectRaw, normalizeDoctorConfig(globalRaw, DEFAULT_DOCTOR_CONFIG)), path),
706
395
  price: (globalRaw, projectRaw, path) =>
@@ -772,12 +461,3 @@ export function storedValue(row: SettingDescriptor, raw: string): unknown {
772
461
  return raw;
773
462
  }
774
463
  }
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
- }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Skill metadata for the panel.
3
+ *
4
+ * One `loadSkills` call reads every resolved `SKILL.md` and hands back the
5
+ * declared name, description, and model-invocation flag, keyed by file path so
6
+ * the row model can join it. Pi's loader also reports why it refused a file, and
7
+ * that reason travels with the row so the screen can say `not loaded` instead of
8
+ * silently showing a name. Nothing here renders or writes; the panel reads the
9
+ * index when the Skills section first becomes visible.
10
+ */
11
+
12
+ import { loadSkills } from "@earendil-works/pi-coding-agent";
13
+ import type { Discovery } from "../picker/types.ts";
14
+ import type { SkillInfo } from "./types.ts";
15
+
16
+ /** Loaded skill metadata keyed by the resolved resource path. */
17
+ export type SkillIndex = ReadonlyMap<string, SkillInfo>;
18
+
19
+ /** Every skill file either discovery view resolves, once, for the loader. */
20
+ export function resolvedSkillPaths(discovery: Discovery): string[] {
21
+ const paths = [...discovery.skills.global, ...discovery.skills.effective].map((item) => item.path);
22
+ return [...new Set(paths)];
23
+ }
24
+
25
+ /**
26
+ * Read the metadata for every resolved skill path. A path may be a `SKILL.md`
27
+ * or a standalone Markdown skill; both are what the resolver hands the panel.
28
+ *
29
+ * A diagnostic names the file Pi refused, so it becomes that row's `problem`. A
30
+ * warning about a file that still loaded is dropped: the row's own name and
31
+ * description already come from the same loader, and `not loaded` would be a
32
+ * lie about a skill the model can use.
33
+ */
34
+ export function loadSkillIndex(paths: readonly string[], cwd: string, agentDir: string): SkillIndex {
35
+ if (paths.length === 0) return new Map();
36
+ const { skills, diagnostics } = loadSkills({ cwd, agentDir, skillPaths: [...paths], includeDefaults: false });
37
+ const index = new Map<string, SkillInfo>();
38
+ for (const skill of skills) {
39
+ index.set(skill.filePath, {
40
+ name: skill.name,
41
+ description: skill.description,
42
+ modelHidden: skill.disableModelInvocation,
43
+ });
44
+ }
45
+ for (const diagnostic of diagnostics) {
46
+ const path = diagnostic.path;
47
+ if (path === undefined || index.has(path)) continue;
48
+ index.set(path, { description: "", modelHidden: false, problem: diagnostic.message });
49
+ }
50
+ return index;
51
+ }