@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 (200) hide show
  1. package/README.md +29 -15
  2. package/README.zh-CN.md +27 -15
  3. package/extensions/bytefly/README.md +195 -115
  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/notify/types.ts +1 -1
  17. package/extensions/bytefly/panel/commands.ts +46 -25
  18. package/extensions/bytefly/panel/format.ts +49 -14
  19. package/extensions/bytefly/panel/join.ts +35 -9
  20. package/extensions/bytefly/panel/settings.ts +29 -39
  21. package/extensions/bytefly/panel/skills.ts +51 -0
  22. package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
  23. package/extensions/bytefly/panel/tui/frame.ts +9 -1
  24. package/extensions/bytefly/panel/tui/list-view.ts +72 -0
  25. package/extensions/bytefly/panel/tui/nav.ts +71 -3
  26. package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
  27. package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
  28. package/extensions/bytefly/panel/tui.ts +223 -331
  29. package/extensions/bytefly/panel/types.ts +26 -8
  30. package/extensions/bytefly/picker/discovery.ts +32 -27
  31. package/extensions/bytefly/picker/resources.ts +16 -8
  32. package/extensions/bytefly/picker/runtime.ts +62 -34
  33. package/extensions/bytefly/picker/toggle.ts +16 -10
  34. package/extensions/bytefly/picker/types.ts +31 -13
  35. package/extensions/bytefly/update/format.ts +66 -8
  36. package/extensions/bytefly/update/patch.ts +66 -0
  37. package/extensions/bytefly/update/register.ts +4 -11
  38. package/extensions/bytefly/update/release.ts +65 -0
  39. package/extensions/bytefly/update/state.ts +14 -0
  40. package/extensions/bytefly/update/suppress.ts +47 -29
  41. package/extensions/bytefly/update/types.ts +15 -5
  42. package/extensions/check/tui.ts +2 -0
  43. package/extensions/context/README.md +21 -11
  44. package/extensions/context/format.ts +10 -8
  45. package/extensions/context/report.ts +21 -8
  46. package/extensions/context/tui.ts +26 -10
  47. package/extensions/context/types.ts +5 -3
  48. package/extensions/file-browse/README.md +89 -22
  49. package/extensions/file-browse/chrome.ts +83 -0
  50. package/extensions/file-browse/client.ts +74 -459
  51. package/extensions/file-browse/config.ts +12 -3
  52. package/extensions/file-browse/diff-html.ts +94 -0
  53. package/extensions/file-browse/html.ts +26 -152
  54. package/extensions/file-browse/live-client.ts +47 -1
  55. package/extensions/file-browse/live.ts +35 -14
  56. package/extensions/file-browse/palette.ts +224 -0
  57. package/extensions/file-browse/rev.ts +42 -0
  58. package/extensions/file-browse/router.ts +27 -6
  59. package/extensions/file-browse/server.ts +7 -0
  60. package/extensions/file-browse/styles.ts +10 -3
  61. package/extensions/file-browse/tree.ts +381 -0
  62. package/extensions/file-read/README.md +4 -1
  63. package/extensions/file-read/extract/odt.ts +7 -5
  64. package/extensions/goal/plan-route.ts +8 -2
  65. package/extensions/goal/tools.ts +6 -11
  66. package/extensions/job/README.md +7 -5
  67. package/extensions/job/index.ts +35 -52
  68. package/extensions/job/tools.ts +51 -77
  69. package/extensions/memory/README.md +3 -3
  70. package/extensions/memory/tools.ts +4 -6
  71. package/extensions/memory/tui.ts +5 -1
  72. package/extensions/plan/README.md +333 -84
  73. package/extensions/plan/commands.ts +73 -7
  74. package/extensions/plan/feedback.ts +300 -63
  75. package/extensions/plan/index.ts +1 -1
  76. package/extensions/plan/plan-index.ts +4 -2
  77. package/extensions/plan/plan-text.ts +18 -1
  78. package/extensions/plan/plans.ts +184 -58
  79. package/extensions/plan/review-prompt.ts +93 -0
  80. package/extensions/plan/review.ts +95 -95
  81. package/extensions/plan/runtime.ts +4 -2
  82. package/extensions/plan/split.ts +87 -0
  83. package/extensions/plan/steps.ts +123 -107
  84. package/extensions/plan/tools/cancel.ts +79 -0
  85. package/extensions/plan/tools/enter.ts +17 -12
  86. package/extensions/plan/tools/exit.ts +148 -59
  87. package/extensions/plan/tools/steps.ts +7 -19
  88. package/extensions/plan/tools/write.ts +29 -31
  89. package/extensions/plan/tools.ts +6 -3
  90. package/extensions/plan/tui.ts +60 -15
  91. package/extensions/plan/types.ts +45 -3
  92. package/extensions/plan/web-chrome.ts +102 -0
  93. package/extensions/plan/web-client.ts +177 -446
  94. package/extensions/plan/web-collab.ts +207 -0
  95. package/extensions/plan/web-comments.ts +269 -0
  96. package/extensions/plan/web-markdown.ts +222 -0
  97. package/extensions/plan/web-page.ts +131 -19
  98. package/extensions/plan/web-router.ts +148 -32
  99. package/extensions/plan/web-share-session.ts +197 -0
  100. package/extensions/plan/web-share.ts +169 -0
  101. package/extensions/plan/web-streams.ts +124 -0
  102. package/extensions/plan/web-tokens.ts +63 -0
  103. package/extensions/plan/web.ts +193 -45
  104. package/extensions/question/README.md +3 -0
  105. package/extensions/question/dialogs.ts +91 -1
  106. package/extensions/question/tools.ts +2 -1
  107. package/extensions/rewind/checklist.ts +4 -0
  108. package/extensions/rewind/review-tui.ts +3 -1
  109. package/extensions/rewind/rewind-tui.ts +4 -0
  110. package/extensions/session-bus/README.md +247 -0
  111. package/extensions/session-bus/commands.ts +165 -0
  112. package/extensions/session-bus/format.ts +124 -0
  113. package/extensions/session-bus/index.ts +69 -0
  114. package/extensions/session-bus/notice.ts +58 -0
  115. package/extensions/session-bus/protocol.ts +134 -0
  116. package/extensions/session-bus/result.ts +243 -0
  117. package/extensions/session-bus/rows.ts +169 -0
  118. package/extensions/session-bus/runtime.ts +575 -0
  119. package/extensions/session-bus/store.ts +329 -0
  120. package/extensions/session-bus/team.ts +102 -0
  121. package/extensions/session-bus/tools.ts +227 -0
  122. package/extensions/session-bus/tui.ts +394 -0
  123. package/extensions/session-bus/types.ts +98 -0
  124. package/extensions/subagent/README.md +26 -13
  125. package/extensions/subagent/format.ts +72 -25
  126. package/extensions/subagent/index.ts +21 -47
  127. package/extensions/subagent/orchestrate.ts +18 -41
  128. package/extensions/subagent/render.ts +29 -42
  129. package/extensions/subagent/stream.ts +24 -18
  130. package/extensions/subagent/types.ts +7 -0
  131. package/extensions/todo/tools.ts +5 -6
  132. package/extensions/todo/tui.ts +5 -1
  133. package/extensions/watch/README.md +9 -5
  134. package/extensions/watch/index.ts +25 -44
  135. package/extensions/watch/tools.ts +31 -59
  136. package/extensions/wechat/README.md +45 -36
  137. package/extensions/wechat/bridge.ts +46 -48
  138. package/extensions/wechat/client.ts +1 -4
  139. package/extensions/wechat/commands.ts +3 -3
  140. package/extensions/wechat/deliver.ts +57 -37
  141. package/extensions/wechat/interaction.ts +54 -18
  142. package/extensions/wechat/login-screen.ts +2 -3
  143. package/extensions/wechat/notices.ts +9 -7
  144. package/extensions/wechat/peer-worker.ts +19 -34
  145. package/extensions/wechat/presence.ts +3 -4
  146. package/extensions/wechat/prompt.ts +53 -23
  147. package/extensions/wechat/reply-pipeline.ts +12 -4
  148. package/extensions/wechat/slash.ts +14 -0
  149. package/extensions/wechat/status-screen.ts +2 -2
  150. package/extensions/wechat/tools.ts +3 -11
  151. package/extensions/worktree/README.md +48 -12
  152. package/extensions/worktree/commands.ts +109 -65
  153. package/extensions/worktree/enter.ts +1 -1
  154. package/extensions/worktree/exit.ts +11 -1
  155. package/extensions/worktree/git.ts +5 -0
  156. package/extensions/worktree/lifecycle.ts +9 -1
  157. package/extensions/worktree/registry.ts +11 -5
  158. package/extensions/worktree/screen.ts +263 -0
  159. package/extensions/worktree/status.ts +84 -40
  160. package/lib/README.md +12 -7
  161. package/lib/claims/overlap.ts +43 -0
  162. package/lib/config.ts +12 -0
  163. package/lib/format.ts +6 -2
  164. package/lib/http/router.ts +40 -13
  165. package/lib/http/server.ts +27 -13
  166. package/lib/interaction.ts +28 -1
  167. package/lib/notice.ts +29 -2
  168. package/lib/plan-entry.ts +7 -2
  169. package/lib/plans/sections.ts +172 -0
  170. package/lib/plans/setting.ts +52 -0
  171. package/lib/plans/split.ts +215 -0
  172. package/lib/plans/store.ts +19 -0
  173. package/lib/plans/team.ts +151 -0
  174. package/lib/qr.ts +64 -0
  175. package/lib/repo-key.ts +74 -0
  176. package/lib/report.ts +117 -25
  177. package/lib/requirements.ts +1 -2
  178. package/lib/tool-result.ts +54 -4
  179. package/lib/tui/README.md +30 -8
  180. package/lib/tui/chrome.ts +27 -6
  181. package/lib/tui/dock-screen.ts +91 -19
  182. package/lib/tui/gauge.ts +9 -5
  183. package/lib/tui/line-pane.ts +15 -4
  184. package/lib/tui/tool.ts +23 -4
  185. package/lib/usage.ts +26 -0
  186. package/lib/web/README.md +131 -0
  187. package/lib/web/base.ts +19 -2
  188. package/lib/web/components.ts +10 -6
  189. package/lib/web/layout.ts +2 -0
  190. package/lib/web/markup.ts +2 -1
  191. package/lib/web/runtime.ts +103 -25
  192. package/lib/web/tokens.ts +23 -7
  193. package/package.json +5 -2
  194. package/skills/changelog/SKILL.md +57 -45
  195. package/skills/init/SKILL.md +80 -45
  196. package/skills/init/references/agents-md-quality.md +26 -2
  197. package/skills/init/references/interview-questions.md +36 -14
  198. package/skills/next/SKILL.md +209 -0
  199. package/skills/parallel-dev/SKILL.md +150 -0
  200. package/extensions/wechat/qrcode.ts +0 -46
@@ -2,16 +2,32 @@
2
2
  * Types for the unified bytefly panel: the merged row model the formatter and
3
3
  * the screen both read.
4
4
  *
5
- * A row is the union of one picker item and its doctor report, joined on the
6
- * extension directory name. Either side may be absent: an item without a report
7
- * renders no health, and a report without an item has no checkbox.
5
+ * A row is one picker item, optionally joined to its doctor report (extensions)
6
+ * or to what its `SKILL.md` declares (skills). Either side may be absent: an item
7
+ * without a report renders no health, a report without an item has no checkbox,
8
+ * and a skill Pi refused to load carries the reason instead.
8
9
  */
9
10
 
10
11
  import type { ExtensionReport } from "../../../lib/requirements.ts";
11
12
  import type { ResourceItem } from "../picker/types.ts";
12
13
 
13
- /** The control center's surfaces: the extension list and the settings list. */
14
- export type PanelSection = "extensions" | "settings";
14
+ /** The control center's sections: the two resource lists and the settings list. */
15
+ export type PanelSection = "extensions" | "skills" | "settings";
16
+
17
+ /**
18
+ * What a skill's `SKILL.md` declares, as Pi loaded it. A file Pi refused carries
19
+ * `problem` and no name, so the row keeps the name its path gives it.
20
+ */
21
+ export interface SkillInfo {
22
+ /** Frontmatter name; absent when Pi did not load the file. */
23
+ name?: string;
24
+ /** Frontmatter description, empty when Pi did not load the file. */
25
+ description: string;
26
+ /** `disable-model-invocation`: reachable only through `/skill:<name>`. */
27
+ modelHidden: boolean;
28
+ /** Why Pi did not load the file, when it did not. */
29
+ problem?: string;
30
+ }
15
31
 
16
32
  /** One group header in the merged model. */
17
33
  export interface PanelGroup {
@@ -23,18 +39,20 @@ export interface PanelGroup {
23
39
  count: number;
24
40
  }
25
41
 
26
- /** One extension row: a picker item, a doctor report, or both. */
42
+ /** One resource row: a picker item, a doctor report, or both. */
27
43
  export interface PanelRow {
28
44
  /**
29
45
  * Unique row identity: `source:path` for an item, `report:<name>` for a
30
46
  * report-only row. Never the directory name, which two rows can share.
31
47
  */
32
48
  key: string;
33
- /** The resolved extension item, when the picker has one. */
49
+ /** The resolved resource item, when the picker has one. */
34
50
  item?: ResourceItem;
35
51
  /** The doctor report, when one exists for this directory. */
36
52
  report?: ExtensionReport;
37
- /** Picker display name (`folder/file.ts`), or the report name when no item. */
53
+ /** What the skill's `SKILL.md` declares, on a Skills row. */
54
+ skill?: SkillInfo;
55
+ /** Picker display name (`folder/file.ts`, a skill name), or the report name when no item. */
38
56
  label: string;
39
57
  /** Key of the group this row belongs to. */
40
58
  group: string;
@@ -3,24 +3,20 @@
3
3
  *
4
4
  * Builds the two settings views the screen shows — personal (`global`) and the
5
5
  * effective one the session sees (`effective`) — from Pi's public
6
- * `SettingsManager` and `DefaultPackageManager`. Both managers and resolvers
7
- * are built through a small factory seam so tests can supply in-memory doubles
8
- * instead of touching real settings files or installed packages.
6
+ * `SettingsManager` and `DefaultPackageManager`, for both resource kinds the
7
+ * screen lists. Both managers and resolvers are built through a small factory
8
+ * seam so tests can supply in-memory doubles instead of touching real settings
9
+ * files or installed packages.
9
10
  */
10
11
 
11
- import {
12
- DefaultPackageManager,
13
- getAgentDir,
14
- type ResolvedResource,
15
- SettingsManager,
16
- } from "@earendil-works/pi-coding-agent";
12
+ import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
17
13
  import { isSelf } from "./resources.ts";
18
- import type { Discovery, ExtensionResolver, SettingsStore } from "./types.ts";
14
+ import type { Discovery, ResolvedResources, ResourceResolver, SettingsStore } from "./types.ts";
19
15
 
20
- /** One settings view: the store to write through and its resolved extensions. */
16
+ /** One settings view: the store to write through and its resolved resources. */
21
17
  interface View {
22
18
  store: SettingsStore;
23
- resolve(): Promise<ResolvedResource[]>;
19
+ resolve(): Promise<ResolvedResources>;
24
20
  }
25
21
 
26
22
  /** Builds isolated global and project views. Injected by tests. */
@@ -44,8 +40,10 @@ export function createSettingsStore(manager: SettingsManager): SettingsStore {
44
40
  projectSettings: () => manager.getProjectSettings(),
45
41
  packages: () => manager.getPackages(),
46
42
  setExtensionPaths: (paths) => manager.setExtensionPaths(paths),
43
+ setSkillPaths: (paths) => manager.setSkillPaths(paths),
47
44
  setPackages: (packages) => manager.setPackages(packages),
48
45
  setProjectExtensionPaths: (paths) => manager.setProjectExtensionPaths(paths),
46
+ setProjectSkillPaths: (paths) => manager.setProjectSkillPaths(paths),
49
47
  setProjectPackages: (packages) => manager.setProjectPackages(packages),
50
48
  setQuietStartup: (quiet) => manager.setQuietStartup(quiet),
51
49
  setOutputPad: (padding) => manager.setOutputPad(padding),
@@ -76,18 +74,16 @@ export function createSettingsStores(
76
74
 
77
75
  /** The slice of `DefaultPackageManager` the resolver uses. */
78
76
  export interface ResolverManager {
79
- resolve(
80
- onMissing?: (source: string) => Promise<"install" | "skip" | "error">,
81
- ): Promise<{ extensions: ResolvedResource[] }>;
77
+ resolve(onMissing?: (source: string) => Promise<"install" | "skip" | "error">): Promise<ResolvedResources>;
82
78
  }
83
79
 
84
- /** Resolve a settings view's packages into extension resources, never installing. */
80
+ /** Resolve a settings view's packages into resources, never installing. */
85
81
  export function createResolver(
86
82
  settingsManager: SettingsManager,
87
83
  cwd: string,
88
84
  agentDir: string,
89
85
  manager?: ResolverManager,
90
- ): ExtensionResolver {
86
+ ): ResourceResolver {
91
87
  const packages =
92
88
  manager ??
93
89
  new DefaultPackageManager({
@@ -99,7 +95,10 @@ export function createResolver(
99
95
  builtinExtensions: [],
100
96
  });
101
97
  return {
102
- resolve: async () => (await packages.resolve(async () => "skip")).extensions,
98
+ resolve: async () => {
99
+ const resolved = await packages.resolve(async () => "skip");
100
+ return { extensions: resolved.extensions, skills: resolved.skills };
101
+ },
103
102
  };
104
103
  }
105
104
 
@@ -119,31 +118,37 @@ export function createDiscoveryFactory(cwd: string, agentDir: string): Discovery
119
118
  * can read or write project configuration.
120
119
  *
121
120
  * `bytefly`'s entrypoint — the extension that hosts this picker — is dropped
122
- * from every view, so the picker does not list itself and the row set, the
123
- * disabled count, and the text listing all agree.
121
+ * from the extension views, so the picker does not list itself and the row set,
122
+ * the disabled count, and the text listing all agree. Skills have no such self
123
+ * entry, so their lists are passed through untouched.
124
124
  */
125
- export async function discoverExtensions(deps: DiscoveryDeps): Promise<Discovery> {
125
+ export async function discoverResources(deps: DiscoveryDeps): Promise<Discovery> {
126
126
  const agentDir = deps.agentDir ?? getAgentDir();
127
127
  const factory = deps.factory ?? createDiscoveryFactory(deps.cwd, agentDir);
128
128
 
129
129
  const globalView = factory.global();
130
- const global = (await globalView.resolve()).filter((item) => !isSelf(item));
130
+ const global = withoutSelf(await globalView.resolve());
131
131
 
132
132
  if (!deps.trusted) {
133
133
  return {
134
- global,
135
- effective: global,
134
+ extensions: { global: global.extensions, effective: global.extensions },
135
+ skills: { global: global.skills, effective: global.skills },
136
136
  stores: { global: globalView.store, project: globalView.store },
137
137
  trusted: false,
138
138
  };
139
139
  }
140
140
 
141
141
  const projectView = factory.project();
142
- const effective = (await projectView.resolve()).filter((item) => !isSelf(item));
142
+ const effective = withoutSelf(await projectView.resolve());
143
143
  return {
144
- global,
145
- effective,
144
+ extensions: { global: global.extensions, effective: effective.extensions },
145
+ skills: { global: global.skills, effective: effective.skills },
146
146
  stores: { global: globalView.store, project: projectView.store },
147
147
  trusted: true,
148
148
  };
149
149
  }
150
+
151
+ /** Drop this extension's own entrypoint from the extension list, and only that. */
152
+ function withoutSelf(resources: ResolvedResources): ResolvedResources {
153
+ return { extensions: resources.extensions.filter((item) => !isSelf(item)), skills: resources.skills };
154
+ }
@@ -4,14 +4,14 @@
4
4
  *
5
5
  * The pattern functions are a trimmed port of Pi's own config selector
6
6
  * (`dist/modes/interactive/components/config-selector.js`) restricted to the
7
- * `extensions` resource type. Keeping them here, pure and separately tested,
8
- * makes a future drift from the built-in easy to diff.
7
+ * two resource types the screen toggles. Keeping them here, pure and separately
8
+ * tested, makes a future drift from the built-in easy to diff.
9
9
  */
10
10
 
11
11
  import { homedir } from "node:os";
12
12
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
13
13
  import { CONFIG_DIR_NAME, type PathMetadata } from "@earendil-works/pi-coding-agent";
14
- import type { Entry, ResourceItem } from "./types.ts";
14
+ import type { Entry, ResourceItem, ResourceKind } from "./types.ts";
15
15
 
16
16
  /** Settings scope of a single resource, in the config selector's vocabulary. */
17
17
  type ItemScope = "user" | "project";
@@ -133,11 +133,18 @@ export function isSelf(item: ResourceItem): boolean {
133
133
  return basename(dirname(item.path)) === SELF_EXTENSION_DIR;
134
134
  }
135
135
 
136
- /** Short name for one extension: `folder/file.ts` when it sits in a subfolder. */
137
- export function getDisplayName(item: ResourceItem): string {
136
+ /** The file name that makes a directory a skill root. */
137
+ const SKILL_FILE = "SKILL.md";
138
+
139
+ /**
140
+ * Short name for one resource: `folder/file.ts` for a subfolder extension, the
141
+ * frontmatter-free folder name for a skill, and a standalone skill's file stem.
142
+ */
143
+ export function getDisplayName(item: ResourceItem, kind: ResourceKind = "extensions"): string {
138
144
  const fileName = basename(item.path);
139
145
  const parentFolder = basename(dirname(item.path));
140
146
  if (item.metadata.source === "builtin") return fileName;
147
+ if (kind === "skills") return fileName === SKILL_FILE ? parentFolder : fileName.replace(/\.md$/i, "");
141
148
  return parentFolder === "extensions" ? fileName : `${parentFolder}/${fileName}`;
142
149
  }
143
150
 
@@ -164,7 +171,7 @@ interface Group {
164
171
  * Flatten resources into group headers and item rows, sorted packages-first,
165
172
  * then user-before-project, then by source; items sort by display name.
166
173
  */
167
- export function buildEntries(items: ResourceItem[], agentDir: string): Entry[] {
174
+ export function buildEntries(items: ResourceItem[], agentDir: string, kind: ResourceKind = "extensions"): Entry[] {
168
175
  const groups = new Map<string, Group>();
169
176
  for (const item of items) {
170
177
  const { metadata } = item;
@@ -192,9 +199,10 @@ export function buildEntries(items: ResourceItem[], agentDir: string): Entry[] {
192
199
 
193
200
  const entries: Entry[] = [];
194
201
  for (const group of sorted) {
195
- const items = [...group.items].sort((a, b) => getDisplayName(a).localeCompare(getDisplayName(b)));
202
+ const items = [...group.items].sort((a, b) => getDisplayName(a, kind).localeCompare(getDisplayName(b, kind)));
196
203
  entries.push({ kind: "group", key: group.key, label: group.label, count: items.length });
197
- for (const item of items) entries.push({ kind: "item", key: resourceKey(item), item, label: getDisplayName(item) });
204
+ for (const item of items)
205
+ entries.push({ kind: "item", key: resourceKey(item), item, label: getDisplayName(item, kind) });
198
206
  }
199
207
  return entries;
200
208
  }
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Stateful core of the extensions picker, independent of rendering.
2
+ * Stateful core of the resource picker, independent of rendering.
3
3
  *
4
- * Owns the active write scope and the live copies of both resource views,
4
+ * Owns the active write scope and the live copies of both kinds' views,
5
5
  * computes toggles through the pure `toggle.ts` helpers, and persists them
6
6
  * through the scope's `SettingsStore`. The TUI subscribes with `onChange` and
7
- * reads `items()` on every render, so it stays a derived view of this state.
7
+ * reads `items(kind)` on every render, so it stays a derived view of this state.
8
8
  */
9
9
 
10
10
  import { getDisplayName, getPackageResourcePattern, getResourcePattern } from "./resources.ts";
@@ -16,7 +16,7 @@ import {
16
16
  nextProjectTopLevelList,
17
17
  nextTopLevelList,
18
18
  } from "./toggle.ts";
19
- import type { Discovery, ResourceItem, SettingsStore, WriteScope } from "./types.ts";
19
+ import type { Discovery, ResourceItem, ResourceKind, Settings, SettingsStore, WriteScope } from "./types.ts";
20
20
 
21
21
  export interface RuntimeDeps {
22
22
  discovery: Discovery;
@@ -25,16 +25,16 @@ export interface RuntimeDeps {
25
25
  onError?: (message: string) => void;
26
26
  }
27
27
 
28
- export interface ExtensionsPickerRuntime {
28
+ export interface PickerRuntime {
29
29
  scope(): WriteScope;
30
30
  setScope(scope: WriteScope): void;
31
- items(): ResourceItem[];
32
- disabledCount(): number;
31
+ items(kind: ResourceKind): ResourceItem[];
32
+ disabledCount(kind: ResourceKind): number;
33
33
  projectAvailable(): boolean;
34
34
  isDirty(): boolean;
35
- toggle(item: ResourceItem): Promise<void>;
35
+ toggle(kind: ResourceKind, item: ResourceItem): Promise<void>;
36
36
  /** Drop `item`'s project override in Project scope; a no-op when it has none. */
37
- clearOverride(item: ResourceItem): Promise<void>;
37
+ clearOverride(kind: ResourceKind, item: ResourceItem): Promise<void>;
38
38
  onChange(listener: () => void): () => void;
39
39
  }
40
40
 
@@ -48,42 +48,69 @@ function sameResource(left: ResourceItem, right: ResourceItem): boolean {
48
48
  return left.metadata.source === right.metadata.source && left.path === right.path;
49
49
  }
50
50
 
51
- export function createExtensionsPickerRuntime(deps: RuntimeDeps): ExtensionsPickerRuntime {
51
+ /** How one resource kind is stored: its settings list and the writers beside it. */
52
+ interface ResourceOps {
53
+ read(settings: Settings): string[];
54
+ write(store: SettingsStore, paths: string[]): void;
55
+ writeProject(store: SettingsStore, paths: string[]): void;
56
+ }
57
+
58
+ /** The two settings keys the picker toggles, and their setters. */
59
+ const RESOURCE_OPS: Record<ResourceKind, ResourceOps> = {
60
+ extensions: {
61
+ read: (settings) => settings.extensions ?? [],
62
+ write: (store, paths) => store.setExtensionPaths(paths),
63
+ writeProject: (store, paths) => store.setProjectExtensionPaths(paths),
64
+ },
65
+ skills: {
66
+ read: (settings) => settings.skills ?? [],
67
+ write: (store, paths) => store.setSkillPaths(paths),
68
+ writeProject: (store, paths) => store.setProjectSkillPaths(paths),
69
+ },
70
+ };
71
+
72
+ /** A fresh copy of one list, so a toggle mutates this runtime's view. */
73
+ function copies(items: readonly ResourceItem[]): ResourceItem[] {
74
+ return items.map((item) => ({ ...item }));
75
+ }
76
+
77
+ export function createPickerRuntime(deps: RuntimeDeps): PickerRuntime {
52
78
  const { discovery, cwd, agentDir } = deps;
53
79
  let scope: WriteScope = "global";
54
80
  let dirty = false;
55
81
  const listeners = new Set<() => void>();
56
- // Copies so a toggle mutates this runtime's view, not the discovery result.
57
- const views: Record<WriteScope, ResourceItem[]> = {
58
- global: discovery.global.map((item) => ({ ...item })),
59
- project: discovery.effective.map((item) => ({ ...item })),
82
+ const views: Record<WriteScope, Record<ResourceKind, ResourceItem[]>> = {
83
+ global: { extensions: copies(discovery.extensions.global), skills: copies(discovery.skills.global) },
84
+ project: { extensions: copies(discovery.extensions.effective), skills: copies(discovery.skills.effective) },
60
85
  };
61
86
 
62
- const current = (): ResourceItem[] => (scope === "global" ? views.global : views.project);
87
+ const current = (kind: ResourceKind): ResourceItem[] => views[scope][kind];
63
88
 
64
89
  const emit = (): void => {
65
90
  for (const listener of listeners) listener();
66
91
  };
67
92
 
68
- const persistGlobal = (item: ResourceItem, enabled: boolean): void => {
93
+ const persistGlobal = (kind: ResourceKind, item: ResourceItem, enabled: boolean): void => {
69
94
  const store = storeFor(discovery, "global");
95
+ const ops = RESOURCE_OPS[kind];
70
96
  if (item.metadata.origin === "top-level") {
71
97
  const pattern = getResourcePattern(item, cwd, agentDir);
72
- store.setExtensionPaths(nextTopLevelList(store.globalSettings().extensions ?? [], pattern, enabled));
98
+ ops.write(store, nextTopLevelList(ops.read(store.globalSettings()), pattern, enabled));
73
99
  return;
74
100
  }
75
101
  const pattern = getPackageResourcePattern(item);
76
- store.setPackages(nextGlobalPackages(store.packages(), item.metadata.source, pattern, enabled));
102
+ store.setPackages(nextGlobalPackages(store.packages(), item.metadata.source, pattern, enabled, kind));
77
103
  };
78
104
 
79
- const persistProject = (item: ResourceItem, enabled: boolean): void => {
105
+ const persistProject = (kind: ResourceKind, item: ResourceItem, enabled: boolean): void => {
80
106
  const store = storeFor(discovery, "project");
107
+ const ops = RESOURCE_OPS[kind];
81
108
  if (item.metadata.origin === "top-level") {
82
- const currentPaths = store.projectSettings().extensions ?? [];
83
- store.setProjectExtensionPaths(nextProjectTopLevelList(currentPaths, item, enabled, cwd, agentDir));
109
+ const currentPaths = ops.read(store.projectSettings());
110
+ ops.writeProject(store, nextProjectTopLevelList(currentPaths, item, enabled, cwd, agentDir));
84
111
  return;
85
112
  }
86
- store.setProjectPackages(nextProjectPackages(store.packages(), item, enabled, cwd, agentDir));
113
+ store.setProjectPackages(nextProjectPackages(store.packages(), item, enabled, cwd, agentDir, kind));
87
114
  };
88
115
 
89
116
  /**
@@ -91,17 +118,18 @@ export function createExtensionsPickerRuntime(deps: RuntimeDeps): ExtensionsPick
91
118
  * absent override returns `false` without touching the store, so a stray key
92
119
  * press cannot dirty the file.
93
120
  */
94
- const clearFromStore = (item: ResourceItem): boolean => {
121
+ const clearFromStore = (kind: ResourceKind, item: ResourceItem): boolean => {
95
122
  const store = storeFor(discovery, "project");
123
+ const ops = RESOURCE_OPS[kind];
96
124
  if (item.metadata.origin === "top-level") {
97
- const paths = store.projectSettings().extensions ?? [];
125
+ const paths = ops.read(store.projectSettings());
98
126
  const next = clearProjectTopLevelList(paths, item, cwd, agentDir);
99
127
  if (next === paths) return false;
100
- store.setProjectExtensionPaths(next);
128
+ ops.writeProject(store, next);
101
129
  return true;
102
130
  }
103
131
  const packages = store.packages();
104
- const next = clearProjectPackages(packages, item, cwd, agentDir);
132
+ const next = clearProjectPackages(packages, item, cwd, agentDir, kind);
105
133
  if (next === packages) return false;
106
134
  store.setProjectPackages(next);
107
135
  return true;
@@ -116,14 +144,14 @@ export function createExtensionsPickerRuntime(deps: RuntimeDeps): ExtensionsPick
116
144
  emit();
117
145
  },
118
146
  items: current,
119
- disabledCount: () => current().filter((item) => !item.enabled).length,
147
+ disabledCount: (kind) => current(kind).filter((item) => !item.enabled).length,
120
148
  projectAvailable: () => discovery.trusted,
121
149
  isDirty: () => dirty,
122
- toggle: async (item) => {
150
+ toggle: async (kind, item) => {
123
151
  const enabled = !item.enabled;
124
152
  try {
125
- if (scope === "global") persistGlobal(item, enabled);
126
- else persistProject(item, enabled);
153
+ if (scope === "global") persistGlobal(kind, item, enabled);
154
+ else persistProject(kind, item, enabled);
127
155
  const store = storeFor(discovery, scope);
128
156
  await store.flush();
129
157
  for (const error of store.drainErrors()) deps.onError?.(error.error.message);
@@ -135,15 +163,15 @@ export function createExtensionsPickerRuntime(deps: RuntimeDeps): ExtensionsPick
135
163
  dirty = true;
136
164
  emit();
137
165
  },
138
- clearOverride: async (item) => {
166
+ clearOverride: async (kind, item) => {
139
167
  if (scope !== "project") {
140
168
  deps.onError?.("Overrides are cleared in Project scope; switch with Tab.");
141
169
  return;
142
170
  }
143
171
  const store = storeFor(discovery, "project");
144
172
  try {
145
- if (!clearFromStore(item)) {
146
- deps.onError?.(`No project override for ${getDisplayName(item)}.`);
173
+ if (!clearFromStore(kind, item)) {
174
+ deps.onError?.(`No project override for ${getDisplayName(item, kind)}.`);
147
175
  return;
148
176
  }
149
177
  await store.flush();
@@ -154,7 +182,7 @@ export function createExtensionsPickerRuntime(deps: RuntimeDeps): ExtensionsPick
154
182
  }
155
183
  // With the override gone the row shows the inherited state again; a
156
184
  // project-only resource has no global twin and keeps its value.
157
- const inherited = views.global.find((entry) => sameResource(entry, item));
185
+ const inherited = views.global[kind].find((entry) => sameResource(entry, item));
158
186
  if (inherited) item.enabled = inherited.enabled;
159
187
  dirty = true;
160
188
  emit();
@@ -20,7 +20,7 @@ import {
20
20
  isLocalPath,
21
21
  resolveLocalSource,
22
22
  } from "./resources.ts";
23
- import type { ResourceItem } from "./types.ts";
23
+ import type { ResourceItem, ResourceKind } from "./types.ts";
24
24
 
25
25
  /** Replace any prior pattern for `pattern` and append `+pattern` / `-pattern`. */
26
26
  export function nextTopLevelList(current: string[], pattern: string, enabled: boolean): string[] {
@@ -30,15 +30,17 @@ export function nextTopLevelList(current: string[], pattern: string, enabled: bo
30
30
  }
31
31
 
32
32
  /**
33
- * Toggle one extension inside a package entry, converting a string source to
33
+ * Toggle one resource inside a package entry, converting a string source to
34
34
  * object form when needed and collapsing it back when no filters remain.
35
- * Packages not present in `packages` are returned unchanged.
35
+ * Packages not present in `packages` are returned unchanged. `kind` names which
36
+ * filter list on the entry the pattern lands in.
36
37
  */
37
38
  export function nextGlobalPackages(
38
39
  packages: PackageSource[],
39
40
  source: string,
40
41
  pattern: string,
41
42
  enabled: boolean,
43
+ kind: ResourceKind,
42
44
  ): PackageSource[] {
43
45
  const updated = [...packages];
44
46
  const index = updated.findIndex((entry) => (typeof entry === "string" ? entry : entry.source) === source);
@@ -49,8 +51,9 @@ export function nextGlobalPackages(
49
51
  pkg = { source: pkg };
50
52
  updated[index] = pkg;
51
53
  }
52
- pkg.extensions = nextTopLevelList(pkg.extensions ?? [], pattern, enabled);
53
- updated[index] = pkg.extensions.length > 0 ? pkg : pkg.source;
54
+ const filters = nextTopLevelList(pkg[kind] ?? [], pattern, enabled);
55
+ pkg[kind] = filters;
56
+ updated[index] = filters.length > 0 ? pkg : pkg.source;
54
57
  return updated;
55
58
  }
56
59
 
@@ -107,7 +110,7 @@ export function nextProjectTopLevelList(
107
110
  }
108
111
 
109
112
  /**
110
- * Toggle one extension inside a package in project settings. A missing project
113
+ * Toggle one resource inside a package in project settings. A missing project
111
114
  * entry is created with `autoload: false` so it filters the personal entry
112
115
  * rather than replacing it; an entry left with no filters is removed.
113
116
  */
@@ -117,6 +120,7 @@ export function nextProjectPackages(
117
120
  enabled: boolean,
118
121
  cwd: string,
119
122
  agentDir: string,
123
+ kind: ResourceKind,
120
124
  ): PackageSource[] {
121
125
  const updated = [...packages];
122
126
  let index = updated.findIndex((entry) =>
@@ -133,8 +137,9 @@ export function nextProjectPackages(
133
137
  updated[index] = pkg;
134
138
  }
135
139
  const pattern = getPackageResourcePattern(item);
136
- pkg.extensions = nextTopLevelList(pkg.extensions ?? [], pattern, enabled);
137
- if (pkg.extensions.length > 0) {
140
+ const filters = nextTopLevelList(pkg[kind] ?? [], pattern, enabled);
141
+ pkg[kind] = filters;
142
+ if (filters.length > 0) {
138
143
  updated[index] = pkg;
139
144
  } else if (pkg.autoload === false) {
140
145
  updated.splice(index, 1);
@@ -181,6 +186,7 @@ export function clearProjectPackages(
181
186
  item: ResourceItem,
182
187
  cwd: string,
183
188
  agentDir: string,
189
+ kind: ResourceKind,
184
190
  ): PackageSource[] {
185
191
  const index = packages.findIndex((entry) =>
186
192
  packageSourceMatches(item, typeof entry === "string" ? entry : entry.source, "project", cwd, agentDir),
@@ -189,12 +195,12 @@ export function clearProjectPackages(
189
195
  if (entry === undefined || typeof entry === "string") return packages;
190
196
 
191
197
  const pattern = getPackageResourcePattern(item);
192
- const filters = entry.extensions ?? [];
198
+ const filters = entry[kind] ?? [];
193
199
  if (!filters.some((filter) => getPatternEntryTarget(filter) === pattern)) return packages;
194
200
 
195
201
  const updated = [...packages];
196
202
  const remaining = filters.filter((filter) => getPatternEntryTarget(filter) !== pattern);
197
- if (remaining.length > 0) updated[index] = { ...entry, extensions: remaining };
203
+ if (remaining.length > 0) updated[index] = { ...entry, [kind]: remaining };
198
204
  else if (entry.autoload === false) updated.splice(index, 1);
199
205
  else updated[index] = entry.source;
200
206
  return updated;
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Shared types and host seams for the extensions picker.
2
+ * Shared types and host seams for the resource picker.
3
3
  *
4
- * The manager only lists and toggles the `extensions` resource type. Every
5
- * host capability it needs is behind a small interface here so the pure logic
6
- * can be tested without a Pi runtime: `SettingsStore` wraps the settings file,
7
- * `ExtensionResolver` wraps package resolution, and `Discovery` is the pair of
8
- * views (global and effective) the screen shows.
4
+ * The manager lists and toggles the two resource types the screen offers:
5
+ * extensions and skills. Every host capability it needs is behind a small
6
+ * interface here so the pure logic can be tested without a Pi runtime:
7
+ * `SettingsStore` wraps the settings file, `ResourceResolver` wraps package
8
+ * resolution, and `Discovery` is the pair of views (global and effective) each
9
+ * kind is shown from.
9
10
  */
10
11
 
11
12
  import type { PackageSource, QuietStartup, ResolvedResource, SettingsManager } from "@earendil-works/pi-coding-agent";
@@ -16,10 +17,19 @@ export type Settings = ReturnType<SettingsManager["getSettings"]>;
16
17
  /** Which settings file a toggle is written to. */
17
18
  export type WriteScope = "global" | "project";
18
19
 
19
- /** One resolved extension and where it came from. */
20
+ /** One resolved resource and where it came from. */
20
21
  export type ResourceItem = ResolvedResource;
21
22
 
22
- /** A flattened list row: a source-group header or an extension item. */
23
+ /** The resource types the picker can list and toggle. */
24
+ export type ResourceKind = "extensions" | "skills";
25
+
26
+ /** Both resource arrays Pi resolves in one pass. */
27
+ export interface ResolvedResources {
28
+ extensions: ResourceItem[];
29
+ skills: ResourceItem[];
30
+ }
31
+
32
+ /** A flattened list row: a source-group header or a resource item. */
23
33
  export type Entry =
24
34
  | { kind: "group"; key: string; label: string; count: number }
25
35
  | { kind: "item"; key: string; item: ResourceItem; label: string };
@@ -30,8 +40,10 @@ export interface SettingsStore {
30
40
  projectSettings(): Settings;
31
41
  packages(): PackageSource[];
32
42
  setExtensionPaths(paths: string[]): void;
43
+ setSkillPaths(paths: string[]): void;
33
44
  setPackages(packages: PackageSource[]): void;
34
45
  setProjectExtensionPaths(paths: string[]): void;
46
+ setProjectSkillPaths(paths: string[]): void;
35
47
  setProjectPackages(packages: PackageSource[]): void;
36
48
  /** App preferences are written to the personal settings file. */
37
49
  setQuietStartup(quiet: QuietStartup): void;
@@ -44,17 +56,23 @@ export interface SettingsStore {
44
56
  drainErrors(): Array<{ error: Error }>;
45
57
  }
46
58
 
47
- /** Resolves every configured package into its extension resources. */
48
- export interface ExtensionResolver {
49
- resolve(): Promise<ResolvedResource[]>;
59
+ /** Resolves every configured package into its resource lists. */
60
+ export interface ResourceResolver {
61
+ resolve(): Promise<ResolvedResources>;
50
62
  }
51
63
 
52
- /** Both settings views plus the stores a toggle writes through. */
53
- export interface Discovery {
64
+ /** One resource kind's two views: personal settings, and what the session sees. */
65
+ export interface ResourceViews {
54
66
  /** Resources as configured by personal settings only. */
55
67
  global: ResourceItem[];
56
68
  /** Resources as the session actually sees them (project applied when trusted). */
57
69
  effective: ResourceItem[];
70
+ }
71
+
72
+ /** Both settings views plus the stores a toggle writes through. */
73
+ export interface Discovery {
74
+ extensions: ResourceViews;
75
+ skills: ResourceViews;
58
76
  stores: { global: SettingsStore; project: SettingsStore };
59
77
  /** Whether the project settings file may be read and written. */
60
78
  trusted: boolean;