@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
@@ -14,11 +14,11 @@
14
14
  * flags then narrow what is announced.
15
15
  */
16
16
 
17
- import { cleanString, loadConfigSection } from "../../../lib/config.ts";
17
+ import { asBoolean, cleanString, loadConfigSection } from "../../../lib/config.ts";
18
18
  import { NOTIFY_METHODS, type NotifyConfig, type NotifyMethod } from "./types.ts";
19
19
 
20
20
  /** A notification title longer than this is clamped; it is a label, not a message. */
21
- const MAX_TITLE_LENGTH = 40;
21
+ export const MAX_TITLE_LENGTH = 40;
22
22
 
23
23
  export const DEFAULT_NOTIFY_CONFIG: NotifyConfig = {
24
24
  enabled: false,
@@ -30,11 +30,6 @@ export const DEFAULT_NOTIFY_CONFIG: NotifyConfig = {
30
30
  title: "Pi",
31
31
  };
32
32
 
33
- /** A boolean, or the fallback. */
34
- function bool(value: unknown, fallback: boolean): boolean {
35
- return typeof value === "boolean" ? value : fallback;
36
- }
37
-
38
33
  /** One of the known methods, or the fallback. */
39
34
  function method(value: unknown, fallback: NotifyMethod): NotifyMethod {
40
35
  return typeof value === "string" && (NOTIFY_METHODS as readonly string[]).includes(value)
@@ -46,11 +41,11 @@ function method(value: unknown, fallback: NotifyMethod): NotifyMethod {
46
41
  export function normalizeNotifyConfig(raw: Record<string, unknown> | undefined, base: NotifyConfig): NotifyConfig {
47
42
  if (!raw) return base;
48
43
  return {
49
- enabled: bool(raw.enabled, base.enabled),
50
- turnFinished: bool(raw.turnFinished, base.turnFinished),
51
- needsInput: bool(raw.needsInput, base.needsInput),
52
- jobFinished: bool(raw.jobFinished, base.jobFinished),
53
- matchFound: bool(raw.matchFound, base.matchFound),
44
+ enabled: asBoolean(raw.enabled, base.enabled),
45
+ turnFinished: asBoolean(raw.turnFinished, base.turnFinished),
46
+ needsInput: asBoolean(raw.needsInput, base.needsInput),
47
+ jobFinished: asBoolean(raw.jobFinished, base.jobFinished),
48
+ matchFound: asBoolean(raw.matchFound, base.matchFound),
54
49
  method: method(raw.method, base.method),
55
50
  title: cleanString(raw.title, base.title).slice(0, MAX_TITLE_LENGTH),
56
51
  };
@@ -14,7 +14,7 @@
14
14
  * to smuggle a control character into an escape sequence or a shell argument.
15
15
  */
16
16
 
17
- import { execFile } from "node:child_process";
17
+ import { spawn } from "node:child_process";
18
18
  import { sanitizeLine } from "../../../lib/format.ts";
19
19
  import type { NotifyConfig, NotifyMethod } from "./types.ts";
20
20
 
@@ -30,7 +30,7 @@ export interface NotifySeams {
30
30
  run(command: string, args: string[]): void;
31
31
  }
32
32
 
33
- /** The production seams: this process's platform, environment, stdout, and `execFile`. */
33
+ /** The production seams: this process's platform, environment, stdout, and a detached notifier. */
34
34
  export function defaultSeams(): NotifySeams {
35
35
  return {
36
36
  platform: process.platform,
@@ -38,9 +38,14 @@ export function defaultSeams(): NotifySeams {
38
38
  stdout: process.stdout,
39
39
  run: (command, args) => {
40
40
  try {
41
- execFile(command, args, { windowsHide: true }, () => {});
41
+ // No pipes and unref'd: the notifier is best-effort and must not keep
42
+ // Pi alive, which `/quit` right after a turn would otherwise wait out
43
+ // (PowerShell's cold start alone is a second).
44
+ const child = spawn(command, args, { windowsHide: true, stdio: "ignore" });
45
+ child.on("error", () => {});
46
+ child.unref();
42
47
  } catch {
43
- // The notifier is best-effort; a missing command must not break a turn.
48
+ // A missing command must not break a turn.
44
49
  }
45
50
  },
46
51
  };
@@ -89,7 +94,10 @@ function windowsToastScript(title: string, body: string): string {
89
94
  return [
90
95
  `${mgr} > $null`,
91
96
  `$xml = [${type}.ToastNotificationManager]::GetTemplateContent(${template})`,
92
- `$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${powershellLiteral(body)}')) > $null`,
97
+ `$nodes = $xml.GetElementsByTagName('text')`,
98
+ // PowerShell 5.1 cannot index the live node list with `[0]`; `.Item(0)`
99
+ // reads the single node without enumerating a collection AppendChild mutates.
100
+ `$nodes.Item(0).AppendChild($xml.CreateTextNode('${powershellLiteral(body)}')) > $null`,
93
101
  `[${type}.ToastNotificationManager]::CreateToastNotifier('${powershellLiteral(title)}').Show(${toast})`,
94
102
  ].join("; ");
95
103
  }
@@ -29,6 +29,7 @@ import type { Discovery } from "../picker/types.ts";
29
29
  import { formatListing, formatSettingsListing } from "./format.ts";
30
30
  import { buildPanelModel, extensionName } from "./join.ts";
31
31
  import { createSettingsRuntime, type SettingsRuntime, type SettingsRuntimeDeps } from "./settings-runtime.ts";
32
+ import { shippedThemeNames } from "./themes.ts";
32
33
  import { PanelComponent } from "./tui.ts";
33
34
  import type { PanelSection } from "./types.ts";
34
35
 
@@ -130,6 +131,19 @@ function notifyExtension(
130
131
  );
131
132
  }
132
133
 
134
+ /**
135
+ * Theme names the row offers: the host's resolved list, narrowed to this
136
+ * package's own palettes. The list is empty outside the TUI (RPC, print, JSON),
137
+ * where the row still shows the stored value.
138
+ */
139
+ function settingsThemeNames(ctx: ExtensionCommandContext): string[] {
140
+ try {
141
+ return shippedThemeNames(ctx.ui.getAllThemes());
142
+ } catch {
143
+ return [];
144
+ }
145
+ }
146
+
133
147
  /** Register the single `/bytefly` command. */
134
148
  export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
135
149
  const discover = deps.discover ?? discoverExtensions;
@@ -146,6 +160,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
146
160
  const cwd = ctx.cwd ?? process.cwd();
147
161
  const agentDir = getAgentDir();
148
162
  const trusted = ctx.isProjectTrusted();
163
+ const themes = settingsThemeNames(ctx);
149
164
  // The mounted screen owns its feedback: a refusal or a failed write belongs in
150
165
  // the frame, not appended to the transcript behind it. Before the screen
151
166
  // exists (the non-TUI settings path, a pre-mount failure) the transcript is
@@ -171,8 +186,8 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
171
186
  // packages or probing the doctor.
172
187
  if (section === "settings" && ctx.mode !== "tui") {
173
188
  const stores = createSettingsStores(cwd, agentDir, trusted);
174
- const settings = makeSettings({ stores, cwd, agentDir, trusted, onError: settingsNotice });
175
- ctx.ui.notify(formatSettingsListing(settings.views("global"), "global"), "info");
189
+ const settings = makeSettings({ stores, cwd, agentDir, trusted, themes, onError: settingsNotice });
190
+ ctx.ui.notify(formatSettingsListing(settings.views("global")), "info");
176
191
  return;
177
192
  }
178
193
 
@@ -189,6 +204,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
189
204
  cwd,
190
205
  agentDir,
191
206
  trusted: discovery.trusted,
207
+ themes,
192
208
  onError: settingsNotice,
193
209
  });
194
210
 
@@ -236,6 +252,9 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
236
252
  await withRailsSuppressed(pi, () =>
237
253
  ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
238
254
  const unsubscribe = runtime.onChange(() => tui.requestRender());
255
+ // The settings runtime re-reads its snapshot after a write; without a
256
+ // subscriber that repaint depends on every write chain's own callback.
257
+ const unsubscribeSettings = settings.onChange(() => tui.requestRender());
239
258
  const component = new PanelComponent({
240
259
  runtime,
241
260
  reports: () => reports,
@@ -257,6 +276,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
257
276
  // A late error falls back to the transcript, never a disposed screen.
258
277
  panel = undefined;
259
278
  unsubscribe();
279
+ unsubscribeSettings();
260
280
  done();
261
281
  },
262
282
  });
@@ -31,7 +31,7 @@ export interface SummarySegment {
31
31
  }
32
32
 
33
33
  /** Row and problem counts over a model. */
34
- export interface PanelCounts {
34
+ interface PanelCounts {
35
35
  extensions: number;
36
36
  /** Rows switched off in settings (the picker's checkbox), not the env flag. */
37
37
  off: number;
@@ -113,28 +113,21 @@ export function sectionSegments(active: PanelSection): SummarySegment[] {
113
113
  export function settingsSummarySegments(scope: PanelScope, count: number): SummarySegment[] {
114
114
  return [
115
115
  { text: scope === "global" ? "Global" : "Project", color: "accent" },
116
- { text: `${count} ${count === 1 ? "setting" : "settings"}`, color: "meta" },
116
+ { text: `${count} settings`, color: "meta" },
117
117
  ];
118
118
  }
119
119
 
120
- /** The Settings headline for non-TUI modes, e.g. `bytefly: Settings · Global · 5 settings`. */
121
- export function formatSettingsHeadline(scope: PanelScope, count: number): string {
122
- return `bytefly: Settings · ${settingsSummarySegments(scope, count)
123
- .map((segment) => segment.text)
124
- .join(" · ")}`;
125
- }
126
-
127
120
  /**
128
- * The grouped Settings listing for non-TUI modes. The project form names the
129
- * missing app rows rather than leaving their absence unexplained.
121
+ * The Settings headline for non-TUI modes, e.g. `bytefly: Settings · Global · 16 settings`.
122
+ * The listing only ever reads the personal document, so the scope is not a parameter.
130
123
  */
131
- export function formatSettingsListing(views: readonly SettingView[], scope: PanelScope): string {
132
- const lines = [formatSettingsHeadline(scope, views.length)];
133
- if (scope === "project") lines.push("", " App settings are Global only.");
134
- if (views.length === 0) {
135
- lines.push("", "No settings found.");
136
- return lines.join("\n");
137
- }
124
+ export function formatSettingsHeadline(count: number): string {
125
+ return `bytefly: Settings · Global · ${count} settings`;
126
+ }
127
+
128
+ /** The grouped Settings listing for non-TUI modes. */
129
+ export function formatSettingsListing(views: readonly SettingView[]): string {
130
+ const lines = [formatSettingsHeadline(views.length)];
138
131
  for (const group of groupViews(views)) {
139
132
  lines.push("", `${group.label} (${group.views.length})`);
140
133
  for (const view of group.views) {
@@ -152,7 +145,7 @@ export function formatHeadline(model: PanelModel, scope: PanelScope, options: Pa
152
145
  }
153
146
 
154
147
  /** A row's glyph and verdict, drawn from its enablement and health axes. */
155
- export interface RowBadge {
148
+ interface RowBadge {
156
149
  /** The status glyph, or `undefined` when the row has nothing to say. */
157
150
  glyph?: string;
158
151
  /** The role the glyph is painted in. */
@@ -165,8 +158,10 @@ export interface RowBadge {
165
158
  * The row's badge. Settings is the enablement axis the checkbox already shows,
166
159
  * so an off extension gets one verdict (`off`) instead of a health word that
167
160
  * contradicts it. `PI_DISABLED_EXTENSIONS` is a different axis, so it is named
168
- * rather than sharing the word: `env disabled`. A row without a report leaves
169
- * the glyph and the word blank instead of claiming `ok`.
161
+ * rather than sharing the word: `env disabled`. The glyph marks what needs
162
+ * attention — a warning, a miss, a disable — so a row with nothing to report
163
+ * carries no glyph at all, and the column reads as one shape instead of a
164
+ * checkmark that comes and goes.
170
165
  */
171
166
  export function rowBadge(row: PanelRow): RowBadge {
172
167
  if (row.item && !row.item.enabled) return { glyph: GLYPHS.disabled, role: "faint", text: "off" };
@@ -174,6 +169,7 @@ export function rowBadge(row: PanelRow): RowBadge {
174
169
  if (!report) return { role: "faint" };
175
170
  if (!report.enabled) return { glyph: GLYPHS.disabled, role: "faint", text: "env disabled" };
176
171
  const severity = reportSeverity(report);
172
+ if (severity === "ok") return { role: "faint", text: reportSummary(report) };
177
173
  return { glyph: severityGlyphChar(severity), role: severityRole(severity), text: reportSummary(report) };
178
174
  }
179
175
 
@@ -1,20 +1,28 @@
1
1
  /**
2
2
  * The stateful Settings section: one snapshot of the two config documents and
3
- * the personal settings, the dirty flag, and the two write paths.
3
+ * the personal settings, the dirty flag, and the writes it offers.
4
4
  *
5
5
  * App rows are written through Pi's `SettingsStore` (personal file only), and
6
- * `bytefly.json` rows through the per-key writer, so an edit cannot disturb a
7
- * sibling key or another extension's section. A failed write leaves the
8
- * snapshot untouched and reports through `onError`; the screen never shows a
9
- * value the file did not accept.
6
+ * `bytefly.json` rows through the per-key writers, so an edit cannot disturb a
7
+ * sibling key or another extension's section. A reset deletes one leaf through the
8
+ * same path, and the JSON pane reads the section the row belongs to. A failed
9
+ * write leaves the snapshot untouched and reports through `onError`; the screen
10
+ * never shows a value the file did not accept.
10
11
  */
11
12
 
12
13
  import { join } from "node:path";
13
14
  import { CONFIG_DIR_NAME, type QuietStartup } from "@earendil-works/pi-coding-agent";
14
- import { readJson, SHARED_CONFIG_FILE, writeConfigValueAt } from "../../../lib/config.ts";
15
+ import {
16
+ configSection,
17
+ configValueAt,
18
+ readJson,
19
+ SHARED_CONFIG_FILE,
20
+ unsetConfigValueAt,
21
+ writeConfigValuesAt,
22
+ } from "../../../lib/config.ts";
15
23
  import type { SettingsStore, WriteScope } from "../picker/types.ts";
16
24
  import {
17
- countSettings,
25
+ type ConfigSectionName,
18
26
  nextValue,
19
27
  type SettingDescriptor,
20
28
  type SettingsSnapshot,
@@ -23,7 +31,7 @@ import {
23
31
  settingViews,
24
32
  storedValue,
25
33
  textError,
26
- validValue,
34
+ UNTRUSTED_SETTINGS_NOTE,
27
35
  } from "./settings.ts";
28
36
 
29
37
  /** Host seams for the Settings runtime. */
@@ -35,16 +43,21 @@ export interface SettingsRuntimeDeps {
35
43
  trusted: boolean;
36
44
  /** Report a refused or failed write to the screen. */
37
45
  onError?: (message: string) => void;
46
+ /** Theme names for the Theme row, already narrowed to this package's palettes; empty outside the TUI. */
47
+ themes?: readonly string[];
38
48
  }
39
49
 
40
50
  /** What the panel needs from the runtime. */
41
51
  export interface SettingsSurface {
42
52
  views(scope: WriteScope): SettingView[];
43
- count(scope: WriteScope): number;
44
53
  /** Cycle an enum or toggle a boolean. */
45
54
  change(scope: WriteScope, id: string): Promise<void>;
46
55
  /** Store a value the user typed; `false` when the row refused it. */
47
56
  setValue(scope: WriteScope, id: string, raw: string): Promise<boolean>;
57
+ /** Delete the focused row's leaf so its default applies; `false` when nothing changed. */
58
+ reset(scope: WriteScope, id: string): Promise<boolean>;
59
+ /** The raw `bytefly.json` section for `scope`, for the JSON pane. */
60
+ sectionDocument(scope: WriteScope, section: ConfigSectionName): unknown;
48
61
  isDirty(): boolean;
49
62
  }
50
63
 
@@ -62,8 +75,9 @@ export function configPath(scope: WriteScope, cwd: string, agentDir: string): st
62
75
  function readSnapshot(deps: SettingsRuntimeDeps): SettingsSnapshot {
63
76
  return {
64
77
  app: deps.stores.global.globalSettings(),
65
- global: readJson(join(deps.agentDir, SHARED_CONFIG_FILE)) ?? {},
66
- project: deps.trusted ? (readJson(join(deps.cwd, CONFIG_DIR_NAME, SHARED_CONFIG_FILE)) ?? {}) : {},
78
+ global: readJson(configPath("global", deps.cwd, deps.agentDir)) ?? {},
79
+ project: deps.trusted ? (readJson(configPath("project", deps.cwd, deps.agentDir)) ?? {}) : {},
80
+ themes: deps.themes ?? [],
67
81
  };
68
82
  }
69
83
 
@@ -80,6 +94,9 @@ function applyAppSetting(store: SettingsStore, row: SettingDescriptor, raw: stri
80
94
  case "showHardwareCursor":
81
95
  store.setShowHardwareCursor(value as boolean);
82
96
  return;
97
+ case "theme":
98
+ store.setTheme(value as string);
99
+ return;
83
100
  default:
84
101
  throw new Error(`Unknown app setting ${row.id}`);
85
102
  }
@@ -88,7 +105,7 @@ function applyAppSetting(store: SettingsStore, row: SettingDescriptor, raw: stri
88
105
  /** Why `row` cannot be written in `scope`, or `undefined` when it can. */
89
106
  function refusal(row: SettingDescriptor, scope: WriteScope, trusted: boolean): string | undefined {
90
107
  if (scope === "project" && !trusted) {
91
- return "Project is not trusted; settings can only be changed in Global scope.";
108
+ return UNTRUSTED_SETTINGS_NOTE;
92
109
  }
93
110
  if (row.source === "app" && scope !== "global") return `${row.label} is a Global setting.`;
94
111
  return undefined;
@@ -120,8 +137,12 @@ export function createSettingsRuntime(deps: SettingsRuntimeDeps): SettingsRuntim
120
137
  const config = row.config;
121
138
  if (!config) throw new Error(`Setting ${row.id} has no config section`);
122
139
  const path = configPath(scope, deps.cwd, deps.agentDir);
123
- writeConfigValueAt(path, config.section, config.path, storedValue(row, raw));
124
- for (const leaf of row.also?.(raw) ?? []) writeConfigValueAt(path, leaf.section, leaf.path, leaf.value);
140
+ // One document pass for the row and every companion leaf it declares, so a
141
+ // failure cannot leave the row written and its companion not.
142
+ writeConfigValuesAt(path, [
143
+ { section: config.section, path: config.path, value: storedValue(row, raw) },
144
+ ...(row.also?.(raw) ?? []),
145
+ ]);
125
146
  };
126
147
 
127
148
  /** Take a completed write: re-read the snapshot, mark it dirty, repaint. */
@@ -133,10 +154,15 @@ export function createSettingsRuntime(deps: SettingsRuntimeDeps): SettingsRuntim
133
154
 
134
155
  return {
135
156
  views: (scope) => settingViews(snapshot, scope),
136
- count: (scope) => countSettings(scope),
137
157
  change: async (scope, id) => {
138
158
  const row = SETTING_ROWS.find((entry) => entry.id === id);
139
159
  if (!row) return;
160
+ // Only a boolean or an enum cycles; a text row is stored from its editor,
161
+ // so refusing here keeps `nextValue`'s cycle contract honest.
162
+ if (row.kind === "text") {
163
+ deps.onError?.(`${row.label} is edited as text, not cycled.`);
164
+ return;
165
+ }
140
166
  const refused = refusal(row, scope, deps.trusted);
141
167
  if (refused !== undefined) {
142
168
  deps.onError?.(refused);
@@ -144,11 +170,9 @@ export function createSettingsRuntime(deps: SettingsRuntimeDeps): SettingsRuntim
144
170
  }
145
171
  const current = settingViews(snapshot, scope).find((entry) => entry.id === id);
146
172
  if (!current) return;
147
- const next = nextValue(row, current.value);
148
- if (!validValue(row, next.value)) {
149
- deps.onError?.(`Cannot save ${row.label}: unknown value.`);
150
- return;
151
- }
173
+ // Use the view's options: the Theme row's list comes from the host, not
174
+ // from the static descriptor.
175
+ const next = nextValue(current, current.value);
152
176
  try {
153
177
  await write(scope, row, next.value);
154
178
  } catch (error) {
@@ -159,8 +183,8 @@ export function createSettingsRuntime(deps: SettingsRuntimeDeps): SettingsRuntim
159
183
  },
160
184
  async setValue(scope, id, raw) {
161
185
  const row = SETTING_ROWS.find((entry) => entry.id === id);
162
- if (!row || row.kind !== "text") {
163
- deps.onError?.(`${row?.label ?? id} cannot be edited as text.`);
186
+ if (!row) {
187
+ deps.onError?.(`${id} is not a setting.`);
164
188
  return false;
165
189
  }
166
190
  const refused = refusal(row, scope, deps.trusted);
@@ -182,6 +206,46 @@ export function createSettingsRuntime(deps: SettingsRuntimeDeps): SettingsRuntim
182
206
  committed();
183
207
  return true;
184
208
  },
209
+ async reset(scope, id) {
210
+ const row = SETTING_ROWS.find((entry) => entry.id === id);
211
+ if (!row) {
212
+ deps.onError?.(`${id} is not a setting.`);
213
+ return false;
214
+ }
215
+ if (row.source === "app") {
216
+ deps.onError?.(`${row.label} is a Pi setting; Pi has no unset, so set the value instead.`);
217
+ return false;
218
+ }
219
+ const refused = refusal(row, scope, deps.trusted);
220
+ if (refused !== undefined) {
221
+ deps.onError?.(refused);
222
+ return false;
223
+ }
224
+ const config = row.config;
225
+ if (!config) return false;
226
+ const document = scope === "global" ? snapshot.global : snapshot.project;
227
+ if (configValueAt(configSection(document, config.section), config.path) === undefined) {
228
+ const inherited =
229
+ scope === "project" &&
230
+ configValueAt(configSection(snapshot.global, config.section), config.path) !== undefined;
231
+ deps.onError?.(
232
+ inherited
233
+ ? `${row.label} is inherited from Global; there is nothing to reset in Project.`
234
+ : `${row.label} is already at its default.`,
235
+ );
236
+ return false;
237
+ }
238
+ try {
239
+ unsetConfigValueAt(configPath(scope, deps.cwd, deps.agentDir), config.section, config.path);
240
+ } catch (error) {
241
+ deps.onError?.(`Could not reset ${row.label}: ${error instanceof Error ? error.message : String(error)}`);
242
+ return false;
243
+ }
244
+ committed();
245
+ return true;
246
+ },
247
+ sectionDocument: (scope, section) =>
248
+ configSection(scope === "global" ? snapshot.global : snapshot.project, section),
185
249
  isDirty: () => dirty,
186
250
  onChange: (listener) => {
187
251
  listeners.add(listener);