@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.
- package/README.md +29 -15
- package/README.zh-CN.md +27 -15
- package/extensions/bytefly/README.md +195 -115
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +29 -39
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +89 -22
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +74 -459
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +26 -152
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +27 -6
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +10 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +35 -52
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/README.md +3 -3
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +333 -84
- package/extensions/plan/commands.ts +73 -7
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +184 -58
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +95 -95
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +123 -107
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +17 -12
- package/extensions/plan/tools/exit.ts +148 -59
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +29 -31
- package/extensions/plan/tools.ts +6 -3
- package/extensions/plan/tui.ts +60 -15
- package/extensions/plan/types.ts +45 -3
- package/extensions/plan/web-chrome.ts +102 -0
- package/extensions/plan/web-client.ts +177 -446
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +131 -19
- package/extensions/plan/web-router.ts +148 -32
- package/extensions/plan/web-share-session.ts +197 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +26 -13
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +9 -5
- package/extensions/watch/index.ts +25 -44
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +45 -36
- package/extensions/wechat/bridge.ts +46 -48
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +3 -3
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +54 -18
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +9 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +53 -23
- package/extensions/wechat/reply-pipeline.ts +12 -4
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -40
- package/lib/README.md +12 -7
- package/lib/claims/overlap.ts +43 -0
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +28 -1
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +117 -25
- package/lib/requirements.ts +1 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +30 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +91 -19
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +5 -2
- package/skills/changelog/SKILL.md +57 -45
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +209 -0
- package/skills/parallel-dev/SKILL.md +150 -0
- 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
|
|
6
|
-
*
|
|
7
|
-
* renders no health,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
7
|
-
* are built through a small factory
|
|
8
|
-
* instead of touching real settings
|
|
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,
|
|
14
|
+
import type { Discovery, ResolvedResources, ResourceResolver, SettingsStore } from "./types.ts";
|
|
19
15
|
|
|
20
|
-
/** One settings view: the store to write through and its resolved
|
|
16
|
+
/** One settings view: the store to write through and its resolved resources. */
|
|
21
17
|
interface View {
|
|
22
18
|
store: SettingsStore;
|
|
23
|
-
resolve(): Promise<
|
|
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
|
|
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
|
-
):
|
|
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 () =>
|
|
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
|
|
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
|
|
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())
|
|
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())
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
137
|
-
|
|
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)
|
|
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
|
|
2
|
+
* Stateful core of the resource picker, independent of rendering.
|
|
3
3
|
*
|
|
4
|
-
* Owns the active write scope and the live copies of both
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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[] =>
|
|
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
|
-
|
|
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()
|
|
83
|
-
|
|
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()
|
|
125
|
+
const paths = ops.read(store.projectSettings());
|
|
98
126
|
const next = clearProjectTopLevelList(paths, item, cwd, agentDir);
|
|
99
127
|
if (next === paths) return false;
|
|
100
|
-
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
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
|
|
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,
|
|
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
|
|
2
|
+
* Shared types and host seams for the resource picker.
|
|
3
3
|
*
|
|
4
|
-
* The manager
|
|
5
|
-
* host capability it needs is behind a small
|
|
6
|
-
* can be tested without a Pi runtime:
|
|
7
|
-
* `
|
|
8
|
-
* views (global and effective)
|
|
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
|
|
20
|
+
/** One resolved resource and where it came from. */
|
|
20
21
|
export type ResourceItem = ResolvedResource;
|
|
21
22
|
|
|
22
|
-
/**
|
|
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
|
|
48
|
-
export interface
|
|
49
|
-
resolve(): Promise<
|
|
59
|
+
/** Resolves every configured package into its resource lists. */
|
|
60
|
+
export interface ResourceResolver {
|
|
61
|
+
resolve(): Promise<ResolvedResources>;
|
|
50
62
|
}
|
|
51
63
|
|
|
52
|
-
/**
|
|
53
|
-
export interface
|
|
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;
|