@gavin-hu/my-pi-agent 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -8
- package/README.zh-CN.md +14 -9
- package/extensions/bytefly/README.md +174 -101
- 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/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 +10 -27
- 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 +29 -1
- package/extensions/file-browse/client.ts +16 -463
- package/extensions/file-browse/html.ts +7 -5
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/router.ts +2 -2
- package/extensions/file-browse/styles.ts +8 -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/tools.ts +6 -11
- package/extensions/job/index.ts +20 -45
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +88 -33
- package/extensions/plan/commands.ts +19 -4
- package/extensions/plan/config.ts +27 -0
- package/extensions/plan/plan-index.ts +3 -1
- package/extensions/plan/plans.ts +31 -13
- package/extensions/plan/review.ts +34 -14
- package/extensions/plan/runtime.ts +3 -1
- package/extensions/plan/steps.ts +43 -14
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +10 -10
- package/extensions/plan/tools/exit.ts +54 -43
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +22 -29
- package/extensions/plan/tools.ts +5 -2
- package/extensions/plan/tui.ts +9 -7
- package/extensions/plan/types.ts +8 -0
- package/extensions/plan/web-chrome.ts +101 -0
- package/extensions/plan/web-client.ts +98 -311
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +28 -2
- package/extensions/plan/web-router.ts +2 -2
- 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/subagent/README.md +25 -12
- 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/index.ts +10 -35
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +40 -18
- package/extensions/wechat/bridge.ts +38 -40
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +1 -1
- package/extensions/wechat/interaction.ts +25 -4
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +7 -5
- package/extensions/wechat/prompt.ts +19 -4
- package/extensions/wechat/qrcode.ts +9 -9
- 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/lifecycle.ts +9 -1
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -38
- package/lib/README.md +7 -6
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/interaction.ts +18 -1
- package/lib/report.ts +75 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +16 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +6 -2
- package/lib/tui/gauge.ts +9 -5
- 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 +3 -1
- package/skills/changelog/SKILL.md +57 -45
- package/skills/next/SKILL.md +184 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `/bytefly` surface: one command, `/bytefly [doctor] [settings] [update] [extension] [refresh]`.
|
|
2
|
+
* The `/bytefly` surface: one command, `/bytefly [doctor] [settings] [skills] [update] [extension] [refresh]`.
|
|
3
3
|
*
|
|
4
|
-
* Without
|
|
4
|
+
* Without a section word it opens the extension screen: the dock in `tui`, the
|
|
5
5
|
* grouped listing elsewhere. `settings` opens the Settings section, or prints
|
|
6
|
-
* its grouped listing outside `tui` without resolving packages. `
|
|
6
|
+
* its grouped listing outside `tui` without resolving packages. `skills` opens
|
|
7
|
+
* the Skills section and lists the resolved skills in text modes. `update`
|
|
7
8
|
* re-runs the published-version check and reports what it found, which is the
|
|
8
9
|
* one place a request is made on demand. With `doctor` it produces the doctor
|
|
9
10
|
* report — the same text `run_doctor` returns — for the whole set or one named
|
|
@@ -23,12 +24,13 @@ import { formatExtension, formatReport, hasProblems } from "../doctor/format.ts"
|
|
|
23
24
|
import type { DoctorRuntime } from "../doctor/runtime.ts";
|
|
24
25
|
import { updateOutcomeText } from "../update/format.ts";
|
|
25
26
|
import type { UpdateHandle } from "../update/register.ts";
|
|
26
|
-
import { createSettingsStores,
|
|
27
|
-
import {
|
|
27
|
+
import { createSettingsStores, discoverResources, type DiscoveryDeps } from "../picker/discovery.ts";
|
|
28
|
+
import { createPickerRuntime, type PickerRuntime } from "../picker/runtime.ts";
|
|
28
29
|
import type { Discovery } from "../picker/types.ts";
|
|
29
|
-
import { formatListing, formatSettingsListing } from "./format.ts";
|
|
30
|
-
import { buildPanelModel, extensionName } from "./join.ts";
|
|
30
|
+
import { formatListing, formatSettingsListing, SKILL_NOUN } from "./format.ts";
|
|
31
|
+
import { buildPanelModel, buildSkillModel, extensionName } from "./join.ts";
|
|
31
32
|
import { createSettingsRuntime, type SettingsRuntime, type SettingsRuntimeDeps } from "./settings-runtime.ts";
|
|
33
|
+
import { loadSkillIndex, resolvedSkillPaths, type SkillIndex } from "./skills.ts";
|
|
32
34
|
import { shippedThemeNames } from "./themes.ts";
|
|
33
35
|
import { PanelComponent } from "./tui.ts";
|
|
34
36
|
import type { PanelSection } from "./types.ts";
|
|
@@ -43,6 +45,8 @@ export interface PanelCommandDeps {
|
|
|
43
45
|
discover?: (deps: DiscoveryDeps) => Promise<Discovery>;
|
|
44
46
|
/** The Settings runtime seam. */
|
|
45
47
|
settings?: (deps: SettingsRuntimeDeps) => SettingsRuntime;
|
|
48
|
+
/** The skill-metadata seam; `loadSkillIndex` unless a test supplies one. */
|
|
49
|
+
skills?: (paths: readonly string[], cwd: string, agentDir: string) => SkillIndex;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
@@ -52,19 +56,21 @@ export interface PanelCommandDeps {
|
|
|
52
56
|
*/
|
|
53
57
|
function selectableNames(discovery: Discovery): Set<string> {
|
|
54
58
|
const names = new Set<string>(Object.keys(REQUIREMENTS));
|
|
55
|
-
for (const item of [...discovery.global, ...discovery.effective])
|
|
59
|
+
for (const item of [...discovery.extensions.global, ...discovery.extensions.effective]) {
|
|
60
|
+
names.add(extensionName(item.path));
|
|
61
|
+
}
|
|
56
62
|
return names;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
/** The values a bare `/bytefly <tab>` offers, in screen order. */
|
|
60
|
-
const FIRST_TOKENS = ["doctor", "settings", "update", ...Object.keys(REQUIREMENTS), "refresh"] as const;
|
|
66
|
+
const FIRST_TOKENS = ["doctor", "settings", "skills", "update", ...Object.keys(REQUIREMENTS), "refresh"] as const;
|
|
61
67
|
|
|
62
68
|
/**
|
|
63
69
|
* Argument completions. The host replaces the whole argument text with the
|
|
64
70
|
* returned `value`, so a second token returns the full `doctor <name>` or
|
|
65
|
-
* `<name> refresh` argument rather than just the word. `/bytefly settings
|
|
66
|
-
* `/bytefly update` take no name, and `refresh` is never
|
|
67
|
-
* token.
|
|
71
|
+
* `<name> refresh` argument rather than just the word. `/bytefly settings`,
|
|
72
|
+
* `/bytefly skills`, and `/bytefly update` take no name, and `refresh` is never
|
|
73
|
+
* followed by another token.
|
|
68
74
|
*/
|
|
69
75
|
function completions(prefix: string): Array<{ value: string; label: string }> | null {
|
|
70
76
|
const [first = "", second, ...rest] = prefix.trimStart().split(/\s+/);
|
|
@@ -73,7 +79,7 @@ function completions(prefix: string): Array<{ value: string; label: string }> |
|
|
|
73
79
|
const options = FIRST_TOKENS.filter((option) => option.startsWith(first));
|
|
74
80
|
return options.length > 0 ? options.map((option) => ({ value: `${option} `, label: option })) : null;
|
|
75
81
|
}
|
|
76
|
-
if (first === "settings" || first === "refresh" || first === "update") return null;
|
|
82
|
+
if (first === "settings" || first === "skills" || first === "refresh" || first === "update") return null;
|
|
77
83
|
const options = (first === "doctor" ? [...Object.keys(REQUIREMENTS), "refresh"] : ["refresh"]).filter((option) =>
|
|
78
84
|
option.startsWith(second),
|
|
79
85
|
);
|
|
@@ -94,7 +100,7 @@ interface PanelArgs {
|
|
|
94
100
|
refresh: boolean;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
/** Parse `/bytefly [doctor] [settings] [update] [extension] [refresh]`; extra tokens are ignored. */
|
|
103
|
+
/** Parse `/bytefly [doctor] [settings] [skills] [update] [extension] [refresh]`; extra tokens are ignored. */
|
|
98
104
|
function parseArgs(args: string): PanelArgs {
|
|
99
105
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
100
106
|
const refresh = parts.includes("refresh");
|
|
@@ -104,6 +110,7 @@ function parseArgs(args: string): PanelArgs {
|
|
|
104
110
|
// beside it is accepted and ignored rather than refused.
|
|
105
111
|
if (rest[0] === "update") return { mode: "update", section: "extensions", refresh };
|
|
106
112
|
if (rest[0] === "settings") return { mode: "screen", section: "settings", name: rest[1], refresh };
|
|
113
|
+
if (rest[0] === "skills") return { mode: "screen", section: "skills", name: rest[1], refresh };
|
|
107
114
|
return { mode: "screen", section: "extensions", name: rest[0], refresh };
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -115,14 +122,14 @@ function notifyExtension(
|
|
|
115
122
|
ctx: ExtensionCommandContext,
|
|
116
123
|
name: string,
|
|
117
124
|
reports: readonly ExtensionReport[],
|
|
118
|
-
runtime:
|
|
125
|
+
runtime: PickerRuntime,
|
|
119
126
|
): void {
|
|
120
127
|
const report = reports.find((entry) => entry.extension === name);
|
|
121
128
|
if (report) {
|
|
122
129
|
ctx.ui.notify(formatExtension(report), hasProblems([report]) ? "warning" : "info");
|
|
123
130
|
return;
|
|
124
131
|
}
|
|
125
|
-
const item = runtime.items().find((entry) => extensionName(entry.path) === name);
|
|
132
|
+
const item = runtime.items("extensions").find((entry) => extensionName(entry.path) === name);
|
|
126
133
|
ctx.ui.notify(
|
|
127
134
|
item
|
|
128
135
|
? `bytefly: ${sanitizeLine(item.path)} — no doctor report`
|
|
@@ -146,15 +153,16 @@ function settingsThemeNames(ctx: ExtensionCommandContext): string[] {
|
|
|
146
153
|
|
|
147
154
|
/** Register the single `/bytefly` command. */
|
|
148
155
|
export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
149
|
-
const discover = deps.discover ??
|
|
156
|
+
const discover = deps.discover ?? discoverResources;
|
|
150
157
|
const makeSettings = deps.settings ?? createSettingsRuntime;
|
|
158
|
+
const loadSkills = deps.skills ?? loadSkillIndex;
|
|
151
159
|
|
|
152
160
|
const handler = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
153
161
|
const { mode, section, name, refresh } = parseArgs(args);
|
|
154
|
-
//
|
|
155
|
-
// a filter, so say so rather than ignoring it.
|
|
156
|
-
if (section
|
|
157
|
-
ctx.ui.notify(`bytefly: /bytefly
|
|
162
|
+
// Only the Extensions section takes an extension argument; a stray word is a
|
|
163
|
+
// typo, not a filter, so say so rather than ignoring it.
|
|
164
|
+
if (section !== "extensions" && name !== undefined) {
|
|
165
|
+
ctx.ui.notify(`bytefly: /bytefly ${section} takes no extension name (got "${sanitizeLine(name)}").`, "warning");
|
|
158
166
|
return;
|
|
159
167
|
}
|
|
160
168
|
const cwd = ctx.cwd ?? process.cwd();
|
|
@@ -195,7 +203,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
195
203
|
try {
|
|
196
204
|
discovery = await discover({ cwd, agentDir, trusted });
|
|
197
205
|
} catch (error) {
|
|
198
|
-
ctx.ui.notify(`Could not list
|
|
206
|
+
ctx.ui.notify(`Could not list resources: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
199
207
|
return;
|
|
200
208
|
}
|
|
201
209
|
|
|
@@ -208,7 +216,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
208
216
|
onError: settingsNotice,
|
|
209
217
|
});
|
|
210
218
|
|
|
211
|
-
const runtime =
|
|
219
|
+
const runtime = createPickerRuntime({
|
|
212
220
|
discovery,
|
|
213
221
|
cwd,
|
|
214
222
|
agentDir,
|
|
@@ -222,6 +230,18 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
222
230
|
return;
|
|
223
231
|
}
|
|
224
232
|
|
|
233
|
+
// Skills outside `tui` print the same grouped listing, from the metadata Pi's
|
|
234
|
+
// loader reads; there is no report to probe for and no package to resolve.
|
|
235
|
+
if (section === "skills" && ctx.mode !== "tui") {
|
|
236
|
+
const index = loadSkills(resolvedSkillPaths(discovery), cwd, agentDir);
|
|
237
|
+
const model = buildSkillModel({ items: runtime.items("skills"), index, agentDir });
|
|
238
|
+
ctx.ui.notify(
|
|
239
|
+
formatListing(model, runtime.scope(), { noun: SKILL_NOUN, emptyLabel: "No skills found." }),
|
|
240
|
+
"info",
|
|
241
|
+
);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
225
245
|
// Text modes print the report, so they probe now. The dock screen opens on
|
|
226
246
|
// the session cache and probes on its first Extensions view instead, which
|
|
227
247
|
// keeps `/bytefly` and `/bytefly settings` instant.
|
|
@@ -243,7 +263,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
243
263
|
ctx.ui.notify(formatReport(reports), hasProblems(reports) ? "warning" : "info");
|
|
244
264
|
return;
|
|
245
265
|
}
|
|
246
|
-
const model = buildPanelModel({ items: runtime.items(), reports, agentDir });
|
|
266
|
+
const model = buildPanelModel({ items: runtime.items("extensions"), reports, agentDir });
|
|
247
267
|
ctx.ui.notify(formatListing(model, runtime.scope()), "info");
|
|
248
268
|
return;
|
|
249
269
|
}
|
|
@@ -269,6 +289,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
269
289
|
initialRefresh: refresh,
|
|
270
290
|
viewportRows: () => tui.terminal?.rows,
|
|
271
291
|
requestRender: () => tui.requestRender(),
|
|
292
|
+
skills: () => loadSkills(resolvedSkillPaths(discovery), cwd, agentDir),
|
|
272
293
|
refresh: async (force) => {
|
|
273
294
|
reports = await deps.doctor.probeAll({ refresh: force === true });
|
|
274
295
|
},
|
|
@@ -293,7 +314,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
293
314
|
|
|
294
315
|
pi.registerCommand("bytefly", {
|
|
295
316
|
description:
|
|
296
|
-
"Open the bytefly control center: /bytefly [extension] [refresh] lists extensions, /bytefly settings opens settings; /bytefly doctor [extension] [refresh] prints the doctor report",
|
|
317
|
+
"Open the bytefly control center: /bytefly [extension] [refresh] lists extensions, /bytefly skills lists skills, /bytefly settings opens settings; /bytefly doctor [extension] [refresh] prints the doctor report",
|
|
297
318
|
getArgumentCompletions: completions,
|
|
298
319
|
handler,
|
|
299
320
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Every mode other than `tui` gets the same facts the screen shows, as plain
|
|
5
5
|
* text: a headline with the scope and the counts, then the picker's grouped
|
|
6
|
-
* listing with a
|
|
6
|
+
* listing with a verdict suffix on each row. The Settings listing uses the same
|
|
7
7
|
* headline and groups with `Label: Value` rows. A single named extension prints
|
|
8
8
|
* its full findings instead. Nothing here touches the host, and every string
|
|
9
9
|
* from a probe or a settings file is sanitized before it is printed.
|
|
@@ -32,17 +32,35 @@ export interface SummarySegment {
|
|
|
32
32
|
|
|
33
33
|
/** Row and problem counts over a model. */
|
|
34
34
|
interface PanelCounts {
|
|
35
|
-
|
|
35
|
+
rows: number;
|
|
36
36
|
/** Rows switched off in settings (the picker's checkbox), not the env flag. */
|
|
37
37
|
off: number;
|
|
38
38
|
missing: number;
|
|
39
39
|
degraded: number;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** How one row is counted in the headline, singular, plural, and abbreviated. */
|
|
43
|
+
export interface CountNoun {
|
|
44
|
+
one: string;
|
|
45
|
+
many: string;
|
|
46
|
+
/** The form a narrow terminal uses. */
|
|
47
|
+
compact: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Extensions are the default noun; Skills pass their own. */
|
|
51
|
+
export const EXTENSION_NOUN: CountNoun = { one: "extension", many: "extensions", compact: "ext" };
|
|
52
|
+
|
|
53
|
+
/** The Skills section's noun: `4 skills`, and the same word when narrowed. */
|
|
54
|
+
export const SKILL_NOUN: CountNoun = { one: "skill", many: "skills", compact: "skills" };
|
|
55
|
+
|
|
42
56
|
/** Options for the plain-text headline and listing. */
|
|
43
57
|
export interface PanelTextOptions {
|
|
44
58
|
/** Abbreviate counts (`15 ext · 3 off`) for a narrow terminal. */
|
|
45
59
|
compact?: boolean;
|
|
60
|
+
/** What one row is called; extensions by default. */
|
|
61
|
+
noun?: CountNoun;
|
|
62
|
+
/** What the listing says when it has no rows. */
|
|
63
|
+
emptyLabel?: string;
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
/** Options for the TUI headline, which can also name a narrowed extension. */
|
|
@@ -72,7 +90,7 @@ export function panelCounts(model: PanelModel): PanelCounts {
|
|
|
72
90
|
missing += report.findings.filter((finding) => finding.severity === "missing").length;
|
|
73
91
|
degraded += report.findings.filter((finding) => finding.severity === "degraded").length;
|
|
74
92
|
}
|
|
75
|
-
return {
|
|
93
|
+
return { rows: model.rows.length, off, missing, degraded };
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
/**
|
|
@@ -81,12 +99,13 @@ export function panelCounts(model: PanelModel): PanelCounts {
|
|
|
81
99
|
*/
|
|
82
100
|
export function summarySegments(model: PanelModel, scope: PanelScope, options: SummaryOptions = {}): SummarySegment[] {
|
|
83
101
|
const counts = panelCounts(model);
|
|
102
|
+
const forms = options.noun ?? EXTENSION_NOUN;
|
|
84
103
|
const segments: SummarySegment[] = [{ text: scope === "global" ? "Global" : "Project", color: "accent" }];
|
|
85
104
|
if (options.filter !== undefined) {
|
|
86
105
|
segments.push({ text: `${options.filter} only`, color: "meta" });
|
|
87
106
|
} else {
|
|
88
|
-
const noun = options.compact ?
|
|
89
|
-
segments.push({ text: `${counts.
|
|
107
|
+
const noun = options.compact ? forms.compact : counts.rows === 1 ? forms.one : forms.many;
|
|
108
|
+
segments.push({ text: `${counts.rows} ${noun}`, color: "meta" });
|
|
90
109
|
if (counts.off > 0) segments.push({ text: `${counts.off} off`, color: "meta" });
|
|
91
110
|
}
|
|
92
111
|
const problems: string[] = [];
|
|
@@ -98,15 +117,25 @@ export function summarySegments(model: PanelModel, scope: PanelScope, options: S
|
|
|
98
117
|
return segments;
|
|
99
118
|
}
|
|
100
119
|
|
|
120
|
+
/** The sections left to right, as the bar reads them. */
|
|
121
|
+
export const SECTION_ORDER: readonly PanelSection[] = ["extensions", "skills", "settings"];
|
|
122
|
+
|
|
123
|
+
/** One section's name in the bar. */
|
|
124
|
+
const SECTION_LABELS: Record<PanelSection, string> = {
|
|
125
|
+
extensions: "Extensions",
|
|
126
|
+
skills: "Skills",
|
|
127
|
+
settings: "Settings",
|
|
128
|
+
};
|
|
129
|
+
|
|
101
130
|
/**
|
|
102
|
-
* The section bar's
|
|
103
|
-
* accent and the
|
|
131
|
+
* The section bar's segments, in screen order. The active section takes the
|
|
132
|
+
* accent and the others recede, so the bar reads as a left-to-right switch.
|
|
104
133
|
*/
|
|
105
134
|
export function sectionSegments(active: PanelSection): SummarySegment[] {
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
135
|
+
return SECTION_ORDER.map((section) => ({
|
|
136
|
+
text: SECTION_LABELS[section],
|
|
137
|
+
color: active === section ? "accent" : "faint",
|
|
138
|
+
}));
|
|
110
139
|
}
|
|
111
140
|
|
|
112
141
|
/** The Settings section's counts: the scope and how many rows it shows. */
|
|
@@ -151,15 +180,21 @@ interface RowBadge {
|
|
|
151
180
|
|
|
152
181
|
/**
|
|
153
182
|
* The row's badge. Settings is the enablement axis the checkbox already shows,
|
|
154
|
-
* so an off
|
|
183
|
+
* so an off row gets one verdict (`off`) instead of a health word that
|
|
155
184
|
* contradicts it. `PI_DISABLED_EXTENSIONS` is a different axis, so it is named
|
|
156
|
-
* rather than sharing the word: `env disabled`.
|
|
185
|
+
* rather than sharing the word: `env disabled`. A skill has no probed report, so
|
|
186
|
+
* its own file supplies the verdict: `not loaded` when Pi refused it, `hidden`
|
|
187
|
+
* when it is reachable only through `/skill:<name>`. The glyph marks what needs
|
|
157
188
|
* attention — a warning, a miss, a disable — so a row with nothing to report
|
|
158
189
|
* carries no glyph at all, and the column reads as one shape instead of a
|
|
159
190
|
* checkmark that comes and goes.
|
|
160
191
|
*/
|
|
161
192
|
export function rowBadge(row: PanelRow): RowBadge {
|
|
162
193
|
if (row.item && !row.item.enabled) return { glyph: GLYPHS.disabled, role: "faint", text: "off" };
|
|
194
|
+
// A skill's verdict comes from its own file, not from a probed report: a file
|
|
195
|
+
// Pi refused is a warning, and one hidden from the model still loads by name.
|
|
196
|
+
if (row.skill?.problem !== undefined) return { glyph: GLYPHS.warn, role: "warn", text: "not loaded" };
|
|
197
|
+
if (row.skill?.modelHidden === true) return { role: "faint", text: "hidden" };
|
|
163
198
|
const report = row.report;
|
|
164
199
|
if (!report) return { role: "faint" };
|
|
165
200
|
if (!report.enabled) return { glyph: GLYPHS.disabled, role: "faint", text: "env disabled" };
|
|
@@ -185,7 +220,7 @@ export function formatRow(row: PanelRow): string {
|
|
|
185
220
|
export function formatListing(model: PanelModel, scope: PanelScope, options: PanelTextOptions = {}): string {
|
|
186
221
|
const lines = [formatHeadline(model, scope, options)];
|
|
187
222
|
if (model.rows.length === 0) {
|
|
188
|
-
lines.push("", "No extensions found.");
|
|
223
|
+
lines.push("", options.emptyLabel ?? "No extensions found.");
|
|
189
224
|
return lines.join("\n");
|
|
190
225
|
}
|
|
191
226
|
for (const group of model.groups) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The merge behind the unified panel: one row per
|
|
3
|
-
*
|
|
2
|
+
* The merge behind the unified panel: one row per resolved resource, joining the
|
|
3
|
+
* picker's items to whatever describes them — the doctor's report for an
|
|
4
|
+
* extension, the loaded `SKILL.md` metadata for a skill.
|
|
4
5
|
*
|
|
5
|
-
* The join key
|
|
6
|
-
* display name: a subfolder resource is shown as `web-access/index.ts` but its
|
|
6
|
+
* The join key for an extension is its directory (`basename(dirname(path))`), not
|
|
7
|
+
* the display name: a subfolder resource is shown as `web-access/index.ts` but its
|
|
7
8
|
* report is named `web-access`. A directory name is only usable when it maps to
|
|
8
9
|
* exactly one unique resource path — two unrelated extensions can share a
|
|
9
10
|
* directory name (a user `plan` beside the package's `plan`) and every flat file
|
|
@@ -15,14 +16,15 @@
|
|
|
15
16
|
* for a report with no item) so the cursor survives a rebuild even when two
|
|
16
17
|
* rows share a directory name. Items keep the picker's grouping and order, and
|
|
17
18
|
* a row never invents health: an item without a report carries no glyph and no
|
|
18
|
-
* summary.
|
|
19
|
+
* summary. Skills skip the report half entirely and take their label and detail
|
|
20
|
+
* text from the skill index instead.
|
|
19
21
|
*/
|
|
20
22
|
|
|
21
23
|
import { basename, dirname } from "node:path";
|
|
22
24
|
import type { ExtensionReport } from "../../../lib/requirements.ts";
|
|
23
25
|
import { buildEntries, resourceKey } from "../picker/resources.ts";
|
|
24
|
-
import type { ResourceItem } from "../picker/types.ts";
|
|
25
|
-
import type { PanelGroup, PanelModel, PanelRow } from "./types.ts";
|
|
26
|
+
import type { ResourceItem, ResourceKind } from "../picker/types.ts";
|
|
27
|
+
import type { PanelGroup, PanelModel, PanelRow, SkillInfo } from "./types.ts";
|
|
26
28
|
|
|
27
29
|
/** The extension directory name that joins a resolved item to its report. */
|
|
28
30
|
export function extensionName(path: string): string {
|
|
@@ -44,11 +46,13 @@ export interface PanelJoinOptions {
|
|
|
44
46
|
agentDir: string;
|
|
45
47
|
/** Narrow to one extension directory or report name, or `undefined` for the full list. */
|
|
46
48
|
filter?: string;
|
|
49
|
+
/** Which resource kind the list is, for labels and grouping. */
|
|
50
|
+
kind?: ResourceKind;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/** Build the merged model for the current items and reports. */
|
|
50
54
|
export function buildPanelModel(options: PanelJoinOptions): PanelModel {
|
|
51
|
-
const { items, reports, agentDir, filter } = options;
|
|
55
|
+
const { items, reports, agentDir, filter, kind = "extensions" } = options;
|
|
52
56
|
const byName = new Map(reports.map((report) => [report.extension, report]));
|
|
53
57
|
|
|
54
58
|
// A directory name may claim a report only when exactly one unique resource
|
|
@@ -68,7 +72,7 @@ export function buildPanelModel(options: PanelJoinOptions): PanelModel {
|
|
|
68
72
|
const rows: PanelRow[] = [];
|
|
69
73
|
let current: PanelGroup | undefined;
|
|
70
74
|
|
|
71
|
-
for (const entry of buildEntries([...items], agentDir)) {
|
|
75
|
+
for (const entry of buildEntries([...items], agentDir, kind)) {
|
|
72
76
|
if (entry.kind === "group") {
|
|
73
77
|
current = { key: entry.key, label: entry.label, count: 0 };
|
|
74
78
|
groups.push(current);
|
|
@@ -96,3 +100,25 @@ export function buildPanelModel(options: PanelJoinOptions): PanelModel {
|
|
|
96
100
|
|
|
97
101
|
return { groups: groups.filter((group) => group.count > 0), rows };
|
|
98
102
|
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The Skills model: the grouped list an extension screen shows, labelled by the
|
|
106
|
+
* skill's declared name and carrying what its detail pane needs. There is no
|
|
107
|
+
* doctor report for a skill, so the only join is to the loaded metadata; a file
|
|
108
|
+
* Pi refused keeps the name its path gives it and carries the reason.
|
|
109
|
+
*/
|
|
110
|
+
export function buildSkillModel(options: {
|
|
111
|
+
items: readonly ResourceItem[];
|
|
112
|
+
index: ReadonlyMap<string, SkillInfo>;
|
|
113
|
+
agentDir: string;
|
|
114
|
+
}): PanelModel {
|
|
115
|
+
const model = buildPanelModel({ items: options.items, reports: [], agentDir: options.agentDir, kind: "skills" });
|
|
116
|
+
return {
|
|
117
|
+
groups: model.groups,
|
|
118
|
+
rows: model.rows.map((row) => {
|
|
119
|
+
const skill = row.item ? options.index.get(row.item.path) : undefined;
|
|
120
|
+
if (skill === undefined) return row;
|
|
121
|
+
return { ...row, label: skill.name ?? row.label, skill };
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Three sections have a normalizer here, so the screen shows what the notifier,
|
|
20
20
|
* the doctor, and the footer keep. The three owned by other extensions (`rewind`,
|
|
21
|
-
* `job`, `
|
|
21
|
+
* `job`, `plan`) are read as the file states them, because importing their
|
|
22
22
|
* normalizer would be one extension importing another's module; each such row
|
|
23
23
|
* declares the loader's default, and `test/settings-coverage.test.ts` holds the
|
|
24
24
|
* pair together.
|
|
@@ -61,7 +61,7 @@ interface TextRule {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** The `bytefly.json` sections a row still reaches. */
|
|
64
|
-
export type ConfigSectionName = "notify" | "doctor" | "price" | "rewind" | "job" | "
|
|
64
|
+
export type ConfigSectionName = "notify" | "doctor" | "price" | "rewind" | "job" | "plan";
|
|
65
65
|
|
|
66
66
|
/** Where a `bytefly.json` row lives: a section, and the path to its leaf. */
|
|
67
67
|
interface SettingConfig {
|
|
@@ -125,14 +125,6 @@ const ON_OFF: readonly SettingOption[] = [
|
|
|
125
125
|
];
|
|
126
126
|
|
|
127
127
|
/** The notify section's delivery methods, labeled by the surface each reaches. */
|
|
128
|
-
const METHOD_OPTIONS: readonly SettingOption[] = [
|
|
129
|
-
{ value: "auto", label: "Auto" },
|
|
130
|
-
{ value: "osc777", label: "OSC 777" },
|
|
131
|
-
{ value: "osc99", label: "OSC 99" },
|
|
132
|
-
{ value: "toast", label: "Windows toast" },
|
|
133
|
-
{ value: "osascript", label: "macOS" },
|
|
134
|
-
{ value: "notify-send", label: "Linux" },
|
|
135
|
-
];
|
|
136
128
|
|
|
137
129
|
/**
|
|
138
130
|
* A text rule for a money amount inside an inclusive range: a whole number or a
|
|
@@ -155,8 +147,9 @@ function amountRule(min: number, max: number): TextRule {
|
|
|
155
147
|
/**
|
|
156
148
|
* The catalogue, in screen order.
|
|
157
149
|
*
|
|
158
|
-
* Global scope lists all
|
|
159
|
-
* because Pi's setters write the personal `settings.json` only.
|
|
150
|
+
* Global scope lists all thirteen; Project lists the eleven `bytefly.json` rows,
|
|
151
|
+
* because Pi's setters write the personal `settings.json` only. The list is
|
|
152
|
+
* capped so the screen keeps fitting a 24-row terminal.
|
|
160
153
|
*/
|
|
161
154
|
export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
162
155
|
{
|
|
@@ -235,16 +228,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
235
228
|
options: ON_OFF,
|
|
236
229
|
config: { section: "notify", path: ["matchFound"] },
|
|
237
230
|
},
|
|
238
|
-
{
|
|
239
|
-
id: "notify.method",
|
|
240
|
-
label: "Delivery method",
|
|
241
|
-
description: "How a notification is delivered; Auto picks from the terminal, then the platform.",
|
|
242
|
-
source: "bytefly",
|
|
243
|
-
kind: "enum",
|
|
244
|
-
fallback: "auto",
|
|
245
|
-
options: METHOD_OPTIONS,
|
|
246
|
-
config: { section: "notify", path: ["method"] },
|
|
247
|
-
},
|
|
248
231
|
{
|
|
249
232
|
id: "job.wakeOnFinish",
|
|
250
233
|
label: "Wake when a job finishes",
|
|
@@ -257,15 +240,15 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
257
240
|
config: { section: "job", path: ["wakeOnFinish"] },
|
|
258
241
|
},
|
|
259
242
|
{
|
|
260
|
-
id: "
|
|
261
|
-
label: "
|
|
262
|
-
description: "
|
|
243
|
+
id: "plan.goalPrompt",
|
|
244
|
+
label: "Offer the session goal",
|
|
245
|
+
description: "Ask before recording an approved plan as the session goal; off records it without asking.",
|
|
263
246
|
source: "bytefly",
|
|
264
247
|
kind: "boolean",
|
|
265
|
-
fallback: "
|
|
248
|
+
fallback: "true",
|
|
266
249
|
json: "boolean",
|
|
267
250
|
options: ON_OFF,
|
|
268
|
-
config: { section: "
|
|
251
|
+
config: { section: "plan", path: ["goalPrompt"] },
|
|
269
252
|
},
|
|
270
253
|
{
|
|
271
254
|
id: "rewind.autoSnapshots",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill metadata for the panel.
|
|
3
|
+
*
|
|
4
|
+
* One `loadSkills` call reads every resolved `SKILL.md` and hands back the
|
|
5
|
+
* declared name, description, and model-invocation flag, keyed by file path so
|
|
6
|
+
* the row model can join it. Pi's loader also reports why it refused a file, and
|
|
7
|
+
* that reason travels with the row so the screen can say `not loaded` instead of
|
|
8
|
+
* silently showing a name. Nothing here renders or writes; the panel reads the
|
|
9
|
+
* index when the Skills section first becomes visible.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { loadSkills } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import type { Discovery } from "../picker/types.ts";
|
|
14
|
+
import type { SkillInfo } from "./types.ts";
|
|
15
|
+
|
|
16
|
+
/** Loaded skill metadata keyed by the resolved resource path. */
|
|
17
|
+
export type SkillIndex = ReadonlyMap<string, SkillInfo>;
|
|
18
|
+
|
|
19
|
+
/** Every skill file either discovery view resolves, once, for the loader. */
|
|
20
|
+
export function resolvedSkillPaths(discovery: Discovery): string[] {
|
|
21
|
+
const paths = [...discovery.skills.global, ...discovery.skills.effective].map((item) => item.path);
|
|
22
|
+
return [...new Set(paths)];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read the metadata for every resolved skill path. A path may be a `SKILL.md`
|
|
27
|
+
* or a standalone Markdown skill; both are what the resolver hands the panel.
|
|
28
|
+
*
|
|
29
|
+
* A diagnostic names the file Pi refused, so it becomes that row's `problem`. A
|
|
30
|
+
* warning about a file that still loaded is dropped: the row's own name and
|
|
31
|
+
* description already come from the same loader, and `not loaded` would be a
|
|
32
|
+
* lie about a skill the model can use.
|
|
33
|
+
*/
|
|
34
|
+
export function loadSkillIndex(paths: readonly string[], cwd: string, agentDir: string): SkillIndex {
|
|
35
|
+
if (paths.length === 0) return new Map();
|
|
36
|
+
const { skills, diagnostics } = loadSkills({ cwd, agentDir, skillPaths: [...paths], includeDefaults: false });
|
|
37
|
+
const index = new Map<string, SkillInfo>();
|
|
38
|
+
for (const skill of skills) {
|
|
39
|
+
index.set(skill.filePath, {
|
|
40
|
+
name: skill.name,
|
|
41
|
+
description: skill.description,
|
|
42
|
+
modelHidden: skill.disableModelInvocation,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
for (const diagnostic of diagnostics) {
|
|
46
|
+
const path = diagnostic.path;
|
|
47
|
+
if (path === undefined || index.has(path)) continue;
|
|
48
|
+
index.set(path, { description: "", modelHidden: false, problem: diagnostic.message });
|
|
49
|
+
}
|
|
50
|
+
return index;
|
|
51
|
+
}
|
|
@@ -1,54 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Extensions section
|
|
3
|
-
* the detail pane for the focused row.
|
|
2
|
+
* The Extensions section's focused-row pane.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The row, the summary, and the key hints are the shared list grammar; what is
|
|
5
|
+
* specific here is the doctor's findings, including the resource path when a row
|
|
6
|
+
* has no verdict.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import type { ScreenHint } from "../../../../lib/tui/chrome.ts";
|
|
11
10
|
import { sanitizeLine } from "../../../../lib/format.ts";
|
|
12
11
|
import { clip } from "../../../../lib/tui/columns.ts";
|
|
13
|
-
import { checkbox, listRow } from "../../../../lib/tui/rows.ts";
|
|
14
12
|
import { paint } from "../../../../lib/tui/tokens.ts";
|
|
15
13
|
import { detailLines as reportDetailLines } from "../../doctor/format.ts";
|
|
16
|
-
import { rowBadge
|
|
17
|
-
import type {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
/** The section's hits, its scope, and how many need attention. */
|
|
21
|
-
export function extensionSummary(
|
|
22
|
-
theme: Theme,
|
|
23
|
-
width: number,
|
|
24
|
-
model: PanelModel,
|
|
25
|
-
scope: PanelScope,
|
|
26
|
-
filter: string | undefined,
|
|
27
|
-
): string {
|
|
28
|
-
return countsLine(theme, width, summarySegments(model, scope, { filter, compact: width < 72 }));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** One extension row: its checkbox, health glyph, name, and verdict. */
|
|
32
|
-
export function extensionRow(theme: Theme, width: number, row: PanelRow, selected: boolean): string {
|
|
33
|
-
const badge = rowBadge(row);
|
|
34
|
-
return listRow(theme, width, {
|
|
35
|
-
selected,
|
|
36
|
-
// A reported-only row has no enable pattern, but still reserves the box
|
|
37
|
-
// column so the list stays aligned.
|
|
38
|
-
leading: row.item ? checkbox(theme, row.item.enabled) : " ",
|
|
39
|
-
glyph: badge.glyph ?? " ",
|
|
40
|
-
glyphRole: badge.role,
|
|
41
|
-
label: sanitizeLine(row.label),
|
|
42
|
-
labelRole: selected ? "accent" : row.item && !row.item.enabled ? "faint" : "label",
|
|
43
|
-
trailing: badge.text,
|
|
44
|
-
// The glyph carries the status colour; the word stays quiet unless it is a
|
|
45
|
-
// problem, so a clean row does not shout.
|
|
46
|
-
trailingRole: badge.role === "bad" || badge.role === "warn" ? badge.role : "faint",
|
|
47
|
-
// The verdict sits beside the name instead of at the far edge of a wide
|
|
48
|
-
// terminal, where a two-word verdict would be unreadable.
|
|
49
|
-
align: "inline",
|
|
50
|
-
});
|
|
51
|
-
}
|
|
14
|
+
import { rowBadge } from "../format.ts";
|
|
15
|
+
import type { PanelRow } from "../types.ts";
|
|
16
|
+
import { savedLine } from "./frame.ts";
|
|
52
17
|
|
|
53
18
|
/** The focused extension's findings and the pending-changes note. */
|
|
54
19
|
export function extensionDetail(
|
|
@@ -73,22 +38,3 @@ export function extensionDetail(
|
|
|
73
38
|
lines.push(...savedLine(theme, width, dirty));
|
|
74
39
|
return lines;
|
|
75
40
|
}
|
|
76
|
-
|
|
77
|
-
/** The section's key hints, in the order they drop as the width runs out. */
|
|
78
|
-
export function extensionHints(options: {
|
|
79
|
-
expanded: boolean;
|
|
80
|
-
refreshing: boolean;
|
|
81
|
-
projectScope: boolean;
|
|
82
|
-
}): ScreenHint[] {
|
|
83
|
-
return [
|
|
84
|
-
"Esc close",
|
|
85
|
-
"←/→",
|
|
86
|
-
{ text: "Space toggle", color: "accent" },
|
|
87
|
-
{ text: options.expanded ? "Enter collapse" : "Enter detail", color: "accent" },
|
|
88
|
-
"Tab scope",
|
|
89
|
-
// Only Project scope can hold an override, so the key is not offered elsewhere.
|
|
90
|
-
...(options.projectScope ? ["x inherit"] : []),
|
|
91
|
-
options.refreshing ? "refreshing…" : "r refresh",
|
|
92
|
-
"↑/↓ select",
|
|
93
|
-
];
|
|
94
|
-
}
|