@gavin-hu/my-pi-agent 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -13
- package/README.zh-CN.md +19 -11
- package/extensions/bytefly/README.md +255 -200
- 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/notice/component.ts +6 -4
- package/extensions/bytefly/notice/config.ts +16 -2
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +52 -22
- package/extensions/bytefly/panel/join.ts +36 -15
- package/extensions/bytefly/panel/settings.ts +72 -392
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/entries.ts +16 -14
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -58
- package/extensions/bytefly/panel/tui/frame.ts +11 -3
- 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/settings-view.ts +8 -12
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -333
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +22 -10
- 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/bytefly/working/register.ts +2 -8
- package/extensions/check/tools.ts +2 -11
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +172 -0
- package/extensions/context/collect.ts +53 -0
- package/extensions/context/commands.ts +100 -0
- package/extensions/context/format.ts +84 -0
- package/extensions/context/index.ts +16 -0
- package/extensions/context/report.ts +332 -0
- package/extensions/context/tui.ts +284 -0
- package/extensions/context/types.ts +124 -0
- package/extensions/file-browse/README.md +67 -19
- package/extensions/file-browse/client.ts +16 -463
- package/extensions/file-browse/config.ts +8 -0
- package/extensions/file-browse/html.ts +9 -7
- package/extensions/file-browse/index.ts +17 -2
- package/extensions/file-browse/live-client.ts +13 -13
- package/extensions/file-browse/live.ts +44 -10
- package/extensions/file-browse/markdown.ts +3 -10
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/raw-html.ts +3 -10
- package/extensions/file-browse/router.ts +2 -2
- package/extensions/file-browse/server.ts +13 -2
- package/extensions/file-browse/styles.ts +8 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-open/README.md +1 -2
- package/extensions/{document → file-read}/README.md +76 -57
- package/extensions/{document → file-read}/config.ts +12 -9
- package/extensions/{document → file-read}/extract/doc.ts +2 -2
- package/extensions/{document → file-read}/extract/docx.ts +3 -3
- package/extensions/{document → file-read}/extract/file.ts +9 -9
- package/extensions/{document → file-read}/extract/odt.ts +10 -8
- package/extensions/{document → file-read}/extract/pdf.ts +3 -3
- package/extensions/{document → file-read}/extract/rtf.ts +3 -3
- package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
- package/extensions/{document → file-read}/formats.ts +11 -11
- package/extensions/file-read/index.ts +17 -0
- package/extensions/{document → file-read}/paging.ts +15 -13
- package/extensions/{document → file-read}/render.ts +17 -13
- package/extensions/{document → file-read}/schema.ts +7 -7
- package/extensions/{document → file-read}/tool.ts +36 -28
- package/extensions/goal/tools.ts +16 -25
- package/extensions/job/commands.ts +32 -46
- package/extensions/job/index.ts +20 -45
- package/extensions/job/runtime.ts +6 -8
- package/extensions/job/tools.ts +58 -103
- package/extensions/memory/commands.ts +30 -44
- package/extensions/memory/tools.ts +10 -20
- 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 +16 -17
- package/extensions/plan/tools/exit.ts +60 -50
- package/extensions/plan/tools/steps.ts +13 -26
- package/extensions/plan/tools/write.ts +30 -38
- 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/plan/web.ts +21 -12
- package/extensions/question/tools.ts +5 -4
- 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/background.ts +1 -7
- 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 +15 -20
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +5 -5
- package/extensions/watch/commands.ts +33 -47
- package/extensions/watch/index.ts +10 -35
- package/extensions/watch/runtime.ts +5 -7
- package/extensions/watch/tools.ts +38 -77
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/search/search.ts +9 -18
- 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/timer.ts +9 -30
- package/extensions/wechat/tools.ts +7 -18
- 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 +15 -11
- package/lib/config.ts +12 -0
- package/lib/escape.ts +15 -2
- package/lib/format.ts +6 -2
- package/lib/interaction.ts +18 -1
- package/lib/pdf.ts +2 -2
- package/lib/report.ts +75 -2
- package/lib/requirements.ts +2 -2
- package/lib/screen-session.ts +85 -0
- package/lib/timer.ts +61 -0
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +19 -11
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/columns.ts +33 -8
- package/lib/tui/dock-screen.ts +6 -2
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/rows.ts +15 -6
- package/lib/tui/rules.ts +4 -8
- package/lib/tui/tool.ts +55 -2
- 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 +10 -4
- package/skills/changelog/SKILL.md +148 -0
- package/skills/next/SKILL.md +184 -0
- package/extensions/document/index.ts +0 -17
- /package/extensions/{document → file-read}/deadline.ts +0 -0
- /package/extensions/{document → file-read}/extract/vendor.d.ts +0 -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.
|
|
@@ -18,7 +18,7 @@ import { sanitizeLine } from "../../../lib/format.ts";
|
|
|
18
18
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
19
19
|
import type { TuiRole } from "../../../lib/tui/tokens.ts";
|
|
20
20
|
import { reportSeverity, reportSummary, severityGlyphChar, severityRole } from "../doctor/format.ts";
|
|
21
|
-
import {
|
|
21
|
+
import type { SettingView } from "./settings.ts";
|
|
22
22
|
import type { PanelModel, PanelRow, PanelSection } from "./types.ts";
|
|
23
23
|
|
|
24
24
|
/** The settings scope the screen shows. */
|
|
@@ -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. */
|
|
@@ -125,15 +154,10 @@ export function formatSettingsHeadline(count: number): string {
|
|
|
125
154
|
return `bytefly: Settings · Global · ${count} settings`;
|
|
126
155
|
}
|
|
127
156
|
|
|
128
|
-
/** The
|
|
157
|
+
/** The Settings listing for non-TUI modes, one row per line, in screen order. */
|
|
129
158
|
export function formatSettingsListing(views: readonly SettingView[]): string {
|
|
130
159
|
const lines = [formatSettingsHeadline(views.length)];
|
|
131
|
-
for (const
|
|
132
|
-
lines.push("", `${group.label} (${group.views.length})`);
|
|
133
|
-
for (const view of group.views) {
|
|
134
|
-
lines.push(` ${sanitizeLine(view.label)}: ${sanitizeLine(view.valueLabel)}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
160
|
+
for (const view of views) lines.push(` ${sanitizeLine(view.label)}: ${sanitizeLine(view.valueLabel)}`);
|
|
137
161
|
return lines.join("\n");
|
|
138
162
|
}
|
|
139
163
|
|
|
@@ -156,15 +180,21 @@ interface RowBadge {
|
|
|
156
180
|
|
|
157
181
|
/**
|
|
158
182
|
* The row's badge. Settings is the enablement axis the checkbox already shows,
|
|
159
|
-
* so an off
|
|
183
|
+
* so an off row gets one verdict (`off`) instead of a health word that
|
|
160
184
|
* contradicts it. `PI_DISABLED_EXTENSIONS` is a different axis, so it is named
|
|
161
|
-
* 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
|
|
162
188
|
* attention — a warning, a miss, a disable — so a row with nothing to report
|
|
163
189
|
* carries no glyph at all, and the column reads as one shape instead of a
|
|
164
190
|
* checkmark that comes and goes.
|
|
165
191
|
*/
|
|
166
192
|
export function rowBadge(row: PanelRow): RowBadge {
|
|
167
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" };
|
|
168
198
|
const report = row.report;
|
|
169
199
|
if (!report) return { role: "faint" };
|
|
170
200
|
if (!report.enabled) return { glyph: GLYPHS.disabled, role: "faint", text: "env disabled" };
|
|
@@ -190,7 +220,7 @@ export function formatRow(row: PanelRow): string {
|
|
|
190
220
|
export function formatListing(model: PanelModel, scope: PanelScope, options: PanelTextOptions = {}): string {
|
|
191
221
|
const lines = [formatHeadline(model, scope, options)];
|
|
192
222
|
if (model.rows.length === 0) {
|
|
193
|
-
lines.push("", "No extensions found.");
|
|
223
|
+
lines.push("", options.emptyLabel ?? "No extensions found.");
|
|
194
224
|
return lines.join("\n");
|
|
195
225
|
}
|
|
196
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,25 +16,21 @@
|
|
|
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
|
-
import { buildEntries } from "../picker/resources.ts";
|
|
24
|
-
import type { ResourceItem } from "../picker/types.ts";
|
|
25
|
-
import type { PanelGroup, PanelModel, PanelRow } from "./types.ts";
|
|
25
|
+
import { buildEntries, resourceKey } from "../picker/resources.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 {
|
|
29
31
|
return basename(dirname(path));
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
/** Unique identity of a resolved resource, independent of the directory name. */
|
|
33
|
-
export function resourceKey(item: ResourceItem): string {
|
|
34
|
-
return `${item.metadata.source}:${item.path}`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
34
|
/** Row key for a report that has no resolved extension item. */
|
|
38
35
|
function reportKey(name: string): string {
|
|
39
36
|
return `report:${name}`;
|
|
@@ -49,11 +46,13 @@ export interface PanelJoinOptions {
|
|
|
49
46
|
agentDir: string;
|
|
50
47
|
/** Narrow to one extension directory or report name, or `undefined` for the full list. */
|
|
51
48
|
filter?: string;
|
|
49
|
+
/** Which resource kind the list is, for labels and grouping. */
|
|
50
|
+
kind?: ResourceKind;
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
/** Build the merged model for the current items and reports. */
|
|
55
54
|
export function buildPanelModel(options: PanelJoinOptions): PanelModel {
|
|
56
|
-
const { items, reports, agentDir, filter } = options;
|
|
55
|
+
const { items, reports, agentDir, filter, kind = "extensions" } = options;
|
|
57
56
|
const byName = new Map(reports.map((report) => [report.extension, report]));
|
|
58
57
|
|
|
59
58
|
// A directory name may claim a report only when exactly one unique resource
|
|
@@ -73,7 +72,7 @@ export function buildPanelModel(options: PanelJoinOptions): PanelModel {
|
|
|
73
72
|
const rows: PanelRow[] = [];
|
|
74
73
|
let current: PanelGroup | undefined;
|
|
75
74
|
|
|
76
|
-
for (const entry of buildEntries([...items], agentDir)) {
|
|
75
|
+
for (const entry of buildEntries([...items], agentDir, kind)) {
|
|
77
76
|
if (entry.kind === "group") {
|
|
78
77
|
current = { key: entry.key, label: entry.label, count: 0 };
|
|
79
78
|
groups.push(current);
|
|
@@ -101,3 +100,25 @@ export function buildPanelModel(options: PanelJoinOptions): PanelModel {
|
|
|
101
100
|
|
|
102
101
|
return { groups: groups.filter((group) => group.count > 0), rows };
|
|
103
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
|
+
}
|