@gavin-hu/my-pi-agent 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -33
- package/README.zh-CN.md +44 -27
- package/extensions/bytefly/README.md +193 -140
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +3 -2
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +6 -11
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +21 -1
- package/extensions/bytefly/panel/format.ts +5 -2
- package/extensions/bytefly/panel/settings-runtime.ts +26 -6
- package/extensions/bytefly/panel/settings.ts +41 -4
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
- package/extensions/bytefly/panel/tui.ts +94 -152
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +14 -4
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +94 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +61 -6
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +4 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +91 -2
- package/extensions/subagent/render.ts +13 -0
- package/extensions/subagent/run.ts +17 -16
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/types.ts +11 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +1 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +50 -3
- package/lib/errors.ts +12 -0
- package/lib/format.ts +50 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +22 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
readJson,
|
|
19
19
|
SHARED_CONFIG_FILE,
|
|
20
20
|
unsetConfigValueAt,
|
|
21
|
-
|
|
21
|
+
writeConfigValuesAt,
|
|
22
22
|
} from "../../../lib/config.ts";
|
|
23
23
|
import type { SettingsStore, WriteScope } from "../picker/types.ts";
|
|
24
24
|
import {
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
settingViews,
|
|
32
32
|
storedValue,
|
|
33
33
|
textError,
|
|
34
|
+
UNTRUSTED_SETTINGS_NOTE,
|
|
34
35
|
} from "./settings.ts";
|
|
35
36
|
|
|
36
37
|
/** Host seams for the Settings runtime. */
|
|
@@ -42,6 +43,8 @@ export interface SettingsRuntimeDeps {
|
|
|
42
43
|
trusted: boolean;
|
|
43
44
|
/** Report a refused or failed write to the screen. */
|
|
44
45
|
onError?: (message: string) => void;
|
|
46
|
+
/** Theme names for the Theme row, already narrowed to this package's palettes; empty outside the TUI. */
|
|
47
|
+
themes?: readonly string[];
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
/** What the panel needs from the runtime. */
|
|
@@ -74,6 +77,7 @@ function readSnapshot(deps: SettingsRuntimeDeps): SettingsSnapshot {
|
|
|
74
77
|
app: deps.stores.global.globalSettings(),
|
|
75
78
|
global: readJson(configPath("global", deps.cwd, deps.agentDir)) ?? {},
|
|
76
79
|
project: deps.trusted ? (readJson(configPath("project", deps.cwd, deps.agentDir)) ?? {}) : {},
|
|
80
|
+
themes: deps.themes ?? [],
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -90,6 +94,9 @@ function applyAppSetting(store: SettingsStore, row: SettingDescriptor, raw: stri
|
|
|
90
94
|
case "showHardwareCursor":
|
|
91
95
|
store.setShowHardwareCursor(value as boolean);
|
|
92
96
|
return;
|
|
97
|
+
case "theme":
|
|
98
|
+
store.setTheme(value as string);
|
|
99
|
+
return;
|
|
93
100
|
default:
|
|
94
101
|
throw new Error(`Unknown app setting ${row.id}`);
|
|
95
102
|
}
|
|
@@ -98,7 +105,7 @@ function applyAppSetting(store: SettingsStore, row: SettingDescriptor, raw: stri
|
|
|
98
105
|
/** Why `row` cannot be written in `scope`, or `undefined` when it can. */
|
|
99
106
|
function refusal(row: SettingDescriptor, scope: WriteScope, trusted: boolean): string | undefined {
|
|
100
107
|
if (scope === "project" && !trusted) {
|
|
101
|
-
return
|
|
108
|
+
return UNTRUSTED_SETTINGS_NOTE;
|
|
102
109
|
}
|
|
103
110
|
if (row.source === "app" && scope !== "global") return `${row.label} is a Global setting.`;
|
|
104
111
|
return undefined;
|
|
@@ -130,8 +137,12 @@ export function createSettingsRuntime(deps: SettingsRuntimeDeps): SettingsRuntim
|
|
|
130
137
|
const config = row.config;
|
|
131
138
|
if (!config) throw new Error(`Setting ${row.id} has no config section`);
|
|
132
139
|
const path = configPath(scope, deps.cwd, deps.agentDir);
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
// One document pass for the row and every companion leaf it declares, so a
|
|
141
|
+
// failure cannot leave the row written and its companion not.
|
|
142
|
+
writeConfigValuesAt(path, [
|
|
143
|
+
{ section: config.section, path: config.path, value: storedValue(row, raw) },
|
|
144
|
+
...(row.also?.(raw) ?? []),
|
|
145
|
+
]);
|
|
135
146
|
};
|
|
136
147
|
|
|
137
148
|
/** Take a completed write: re-read the snapshot, mark it dirty, repaint. */
|
|
@@ -159,7 +170,9 @@ export function createSettingsRuntime(deps: SettingsRuntimeDeps): SettingsRuntim
|
|
|
159
170
|
}
|
|
160
171
|
const current = settingViews(snapshot, scope).find((entry) => entry.id === id);
|
|
161
172
|
if (!current) return;
|
|
162
|
-
|
|
173
|
+
// Use the view's options: the Theme row's list comes from the host, not
|
|
174
|
+
// from the static descriptor.
|
|
175
|
+
const next = nextValue(current, current.value);
|
|
163
176
|
try {
|
|
164
177
|
await write(scope, row, next.value);
|
|
165
178
|
} catch (error) {
|
|
@@ -212,7 +225,14 @@ export function createSettingsRuntime(deps: SettingsRuntimeDeps): SettingsRuntim
|
|
|
212
225
|
if (!config) return false;
|
|
213
226
|
const document = scope === "global" ? snapshot.global : snapshot.project;
|
|
214
227
|
if (configValueAt(configSection(document, config.section), config.path) === undefined) {
|
|
215
|
-
|
|
228
|
+
const inherited =
|
|
229
|
+
scope === "project" &&
|
|
230
|
+
configValueAt(configSection(snapshot.global, config.section), config.path) !== undefined;
|
|
231
|
+
deps.onError?.(
|
|
232
|
+
inherited
|
|
233
|
+
? `${row.label} is inherited from Global; there is nothing to reset in Project.`
|
|
234
|
+
: `${row.label} is already at its default.`,
|
|
235
|
+
);
|
|
216
236
|
return false;
|
|
217
237
|
}
|
|
218
238
|
try {
|
|
@@ -135,6 +135,8 @@ export interface SettingsSnapshot {
|
|
|
135
135
|
app: Settings;
|
|
136
136
|
global: Record<string, unknown>;
|
|
137
137
|
project: Record<string, unknown>;
|
|
138
|
+
/** Theme names the Theme row offers, narrowed to this package's palettes by the caller. */
|
|
139
|
+
themes: readonly string[];
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
/** One rendered group: its heading and the rows under it. */
|
|
@@ -436,6 +438,18 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
436
438
|
{ value: "1", label: "1" },
|
|
437
439
|
],
|
|
438
440
|
},
|
|
441
|
+
{
|
|
442
|
+
id: "theme",
|
|
443
|
+
group: "terminal",
|
|
444
|
+
label: "Theme",
|
|
445
|
+
description:
|
|
446
|
+
"Pi's color theme, chosen from the palettes this package ships; `system` follows the terminal's colors. Takes effect after reload.",
|
|
447
|
+
source: "app",
|
|
448
|
+
kind: "enum",
|
|
449
|
+
fallback: "system",
|
|
450
|
+
// The host supplies this package's theme names in the snapshot; the static list stays empty.
|
|
451
|
+
options: [],
|
|
452
|
+
},
|
|
439
453
|
{
|
|
440
454
|
id: "showHardwareCursor",
|
|
441
455
|
group: "terminal",
|
|
@@ -519,7 +533,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
519
533
|
id: "rewind.max",
|
|
520
534
|
group: "snapshots",
|
|
521
535
|
label: "Snapshots kept",
|
|
522
|
-
description: "
|
|
536
|
+
description: "Snapshot refs kept per repository, including pre-restore safety snapshots; 0 keeps none.",
|
|
523
537
|
source: "bytefly",
|
|
524
538
|
kind: "text",
|
|
525
539
|
fallback: "20",
|
|
@@ -590,6 +604,9 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
590
604
|
},
|
|
591
605
|
];
|
|
592
606
|
|
|
607
|
+
/** The one refusal for a Project write while the project is untrusted. */
|
|
608
|
+
export const UNTRUSTED_SETTINGS_NOTE = "Project is not trusted; settings can only be changed in Global scope.";
|
|
609
|
+
|
|
593
610
|
/** The rows a scope can write: app rows are personal-file only. */
|
|
594
611
|
export function rowsForScope(scope: WriteScope): readonly SettingDescriptor[] {
|
|
595
612
|
return scope === "global" ? SETTING_ROWS : SETTING_ROWS.filter((row) => row.source === "bytefly");
|
|
@@ -612,9 +629,18 @@ export function textError(row: SettingDescriptor, raw: string): string | undefin
|
|
|
612
629
|
return rule.validate?.(raw);
|
|
613
630
|
}
|
|
614
631
|
|
|
632
|
+
/** The Theme row's values: `system` first, then the names the caller offers. */
|
|
633
|
+
function themeOptions(themes: readonly string[]): readonly SettingOption[] {
|
|
634
|
+
const names = ["system", ...new Set(themes.filter((name) => name !== "" && name !== "system"))];
|
|
635
|
+
return names.map((name) => ({ value: name, label: name }));
|
|
636
|
+
}
|
|
637
|
+
|
|
615
638
|
/** The display label for `raw`, or the raw value when it is a valid custom one. */
|
|
616
639
|
function labelFor(row: SettingDescriptor, raw: string): string {
|
|
617
|
-
|
|
640
|
+
const option = row.options.find((entry) => entry.value === raw);
|
|
641
|
+
if (option) return option.label;
|
|
642
|
+
// A stored light/dark pair is Pi's automatic mode, not a missing theme.
|
|
643
|
+
return row.id === "theme" && raw.includes("/") ? `${raw} (automatic)` : raw;
|
|
618
644
|
}
|
|
619
645
|
|
|
620
646
|
/** Read an app preference and where it came from. */
|
|
@@ -634,6 +660,11 @@ function appValue(snapshot: SettingsSnapshot, row: SettingDescriptor): { value:
|
|
|
634
660
|
value: snapshot.app.showHardwareCursor === true ? "true" : row.fallback,
|
|
635
661
|
origin: snapshot.app.showHardwareCursor === undefined ? "default" : "global",
|
|
636
662
|
};
|
|
663
|
+
case "theme": {
|
|
664
|
+
const theme = snapshot.app.theme;
|
|
665
|
+
const set = typeof theme === "string" && theme !== "";
|
|
666
|
+
return { value: set ? theme : row.fallback, origin: set ? "global" : "default" };
|
|
667
|
+
}
|
|
637
668
|
default:
|
|
638
669
|
throw new Error(`Unknown app setting ${row.id}`);
|
|
639
670
|
}
|
|
@@ -646,7 +677,11 @@ function statesConfig(config: SettingConfig, document: Record<string, unknown>):
|
|
|
646
677
|
|
|
647
678
|
/** Where the effective value came from: the project file, the personal one, or nothing. */
|
|
648
679
|
function originFor(config: SettingConfig, snapshot: SettingsSnapshot, scope: WriteScope): SettingOrigin {
|
|
649
|
-
if (scope === "project")
|
|
680
|
+
if (scope === "project") {
|
|
681
|
+
if (statesConfig(config, snapshot.project)) return "project";
|
|
682
|
+
if (statesConfig(config, snapshot.global)) return "inherited";
|
|
683
|
+
return "default";
|
|
684
|
+
}
|
|
650
685
|
return statesConfig(config, snapshot.global) ? "global" : "default";
|
|
651
686
|
}
|
|
652
687
|
|
|
@@ -711,8 +746,10 @@ function byteflyValue(
|
|
|
711
746
|
/** Build the rows for `scope` from a snapshot. */
|
|
712
747
|
export function settingViews(snapshot: SettingsSnapshot, scope: WriteScope): SettingView[] {
|
|
713
748
|
return rowsForScope(scope).map((row) => {
|
|
749
|
+
const options = row.id === "theme" ? themeOptions(snapshot.themes) : row.options;
|
|
714
750
|
const { value, origin } = row.source === "app" ? appValue(snapshot, row) : byteflyValue(snapshot, row, scope);
|
|
715
|
-
|
|
751
|
+
const view = { ...row, options, value };
|
|
752
|
+
return { ...view, valueLabel: labelFor(view, value), origin };
|
|
716
753
|
});
|
|
717
754
|
}
|
|
718
755
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which themes the Settings **Theme** row offers: this package's own two, not
|
|
3
|
+
* every theme in play.
|
|
4
|
+
*
|
|
5
|
+
* The host resolves Pi's built-ins, this package's palettes, and any user or
|
|
6
|
+
* project theme into one list, but the row writes Pi's single `theme`
|
|
7
|
+
* preference from a bytefly screen, so it offers only the palettes this package
|
|
8
|
+
* ships and can vouch for. The file's directory identifies a shipped theme, not
|
|
9
|
+
* its name: `themes/` is the manifest's own resource glob, so a renamed palette
|
|
10
|
+
* or a look-alike name cannot drift the list away from what the package loads.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { dirname, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
/** This package's `themes/` directory. */
|
|
17
|
+
export const SHIPPED_THEMES_DIR = resolve(fileURLToPath(new URL("../../../themes", import.meta.url)));
|
|
18
|
+
|
|
19
|
+
/** One theme as the host reports it. */
|
|
20
|
+
export interface HostTheme {
|
|
21
|
+
name: string;
|
|
22
|
+
path?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The shipped theme names in the host's order; a pathless or foreign theme is dropped. */
|
|
26
|
+
export function shippedThemeNames(themes: readonly HostTheme[]): string[] {
|
|
27
|
+
return themes.filter(isShipped).map((theme) => theme.name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isShipped(theme: HostTheme): boolean {
|
|
31
|
+
return theme.path !== undefined && dirname(resolve(theme.path)) === SHIPPED_THEMES_DIR;
|
|
32
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The panel's text-editing state machine.
|
|
3
|
+
*
|
|
4
|
+
* One key in, the next editing state out; the component owns the settings write,
|
|
5
|
+
* the write chain, and the repaint. Esc cancels the edit instead of closing the
|
|
6
|
+
* screen, Enter asks the caller to commit, and everything else the screen binds
|
|
7
|
+
* is inert until the editor closes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
11
|
+
import { isCloseKey } from "../../../../lib/tui/cursor.ts";
|
|
12
|
+
|
|
13
|
+
/** Bracketed-paste markers a terminal wraps around pasted text. */
|
|
14
|
+
const PASTE_START = "\x1b[200~";
|
|
15
|
+
const PASTE_END = "\x1b[201~";
|
|
16
|
+
|
|
17
|
+
/** The text row being edited: its id, the buffer, and the caret offset. */
|
|
18
|
+
export interface EditingState {
|
|
19
|
+
id: string;
|
|
20
|
+
buffer: string;
|
|
21
|
+
caret: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** What one editing key asked for. */
|
|
25
|
+
export type EditAction =
|
|
26
|
+
| { kind: "cancel" }
|
|
27
|
+
| { kind: "commit" }
|
|
28
|
+
| { kind: "update"; state: EditingState; changed: boolean };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Apply one key to the editing state.
|
|
32
|
+
*
|
|
33
|
+
* `maxLength` comes from the setting's text rule; an undefined limit means the
|
|
34
|
+
* row has no text rule, so typed characters are refused rather than stored
|
|
35
|
+
* unvalidated. A key that changes nothing (Backspace at the start, a refused
|
|
36
|
+
* character) reports `changed: false`, so the caller does not ask for a frame.
|
|
37
|
+
*/
|
|
38
|
+
export function applyEditKey(data: string, state: EditingState, maxLength: number | undefined): EditAction {
|
|
39
|
+
if (isCloseKey(data)) return { kind: "cancel" };
|
|
40
|
+
if (matchesKey(data, Key.enter)) return { kind: "commit" };
|
|
41
|
+
|
|
42
|
+
const { buffer, caret } = state;
|
|
43
|
+
let next = state;
|
|
44
|
+
if (matchesKey(data, Key.backspace)) {
|
|
45
|
+
if (caret > 0) next = { ...state, buffer: buffer.slice(0, caret - 1) + buffer.slice(caret), caret: caret - 1 };
|
|
46
|
+
} else if (matchesKey(data, Key.delete)) {
|
|
47
|
+
if (caret < buffer.length) next = { ...state, buffer: buffer.slice(0, caret) + buffer.slice(caret + 1) };
|
|
48
|
+
} else if (matchesKey(data, Key.left)) {
|
|
49
|
+
next = { ...state, caret: Math.max(0, caret - 1) };
|
|
50
|
+
} else if (matchesKey(data, Key.right)) {
|
|
51
|
+
next = { ...state, caret: Math.min(buffer.length, caret + 1) };
|
|
52
|
+
// `Ctrl+A`/`Ctrl+E` are the emacs spellings of Home/End, which pi-tui's
|
|
53
|
+
// `Key.home`/`Key.end` do not match; both are offered because a terminal
|
|
54
|
+
// may send either.
|
|
55
|
+
} else if (matchesKey(data, Key.home) || matchesKey(data, "ctrl+a")) {
|
|
56
|
+
next = { ...state, caret: 0 };
|
|
57
|
+
} else if (matchesKey(data, Key.end) || matchesKey(data, "ctrl+e")) {
|
|
58
|
+
next = { ...state, caret: buffer.length };
|
|
59
|
+
} else if (matchesKey(data, "ctrl+u")) {
|
|
60
|
+
next = { ...state, buffer: "", caret: 0 };
|
|
61
|
+
} else {
|
|
62
|
+
// A pasted chunk arrives wrapped in bracketed-paste markers. Its ESC is
|
|
63
|
+
// dropped by the filter below, which would leave `[200~` in the value, so
|
|
64
|
+
// strip the markers first, the way Pi's own Input and Editor do.
|
|
65
|
+
const text = data.replaceAll(PASTE_START, "").replaceAll(PASTE_END, "");
|
|
66
|
+
// Anything else must be printable; control sequences are not text.
|
|
67
|
+
const typed = [...text].filter((char) => char >= " " && char !== "\u007f").join("");
|
|
68
|
+
if (typed && maxLength !== undefined && buffer.length + typed.length <= maxLength) {
|
|
69
|
+
next = {
|
|
70
|
+
...state,
|
|
71
|
+
buffer: buffer.slice(0, caret) + typed + buffer.slice(caret),
|
|
72
|
+
caret: caret + typed.length,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { kind: "update", state: next, changed: next.buffer !== buffer || next.caret !== caret };
|
|
77
|
+
}
|
|
@@ -27,6 +27,30 @@ export function isSelectable(entry: PanelEntry | undefined): boolean {
|
|
|
27
27
|
return entryKey(entry) !== undefined;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* One display window as ordinals over the selectable rows it contains.
|
|
32
|
+
*
|
|
33
|
+
* The range row counts rows, not entries: a list of twenty-nine settings in ten
|
|
34
|
+
* groups spans thirty-nine entries, but `showing 3–5 of 29` is what the user can
|
|
35
|
+
* focus. `formatRange` prints `start + 1` through `end`, so the ordinals here are
|
|
36
|
+
* zero-based and `end` is exclusive.
|
|
37
|
+
*/
|
|
38
|
+
export function selectableRange(
|
|
39
|
+
entries: readonly PanelEntry[],
|
|
40
|
+
window: { start: number; end: number },
|
|
41
|
+
): { start: number; end: number; total: number } {
|
|
42
|
+
let before = 0;
|
|
43
|
+
let shown = 0;
|
|
44
|
+
let total = 0;
|
|
45
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
46
|
+
if (!isSelectable(entries[index])) continue;
|
|
47
|
+
total += 1;
|
|
48
|
+
if (index < window.start) before += 1;
|
|
49
|
+
else if (index < window.end) shown += 1;
|
|
50
|
+
}
|
|
51
|
+
return { start: before, end: before + shown, total };
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
/** The next selectable entry from `from` in `direction`, or `undefined`. */
|
|
31
55
|
export function nextRow(entries: PanelEntry[], from: number, direction: -1 | 1): number | undefined {
|
|
32
56
|
for (let index = from + direction; index >= 0 && index < entries.length; index += direction) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type { ScreenHint } from "../../../../lib/tui/chrome.ts";
|
|
10
11
|
import { sanitizeLine } from "../../../../lib/format.ts";
|
|
11
12
|
import { clip } from "../../../../lib/tui/columns.ts";
|
|
12
13
|
import { checkbox, listRow } from "../../../../lib/tui/rows.ts";
|
|
@@ -46,7 +47,7 @@ export function extensionRow(theme: Theme, width: number, row: PanelRow, selecte
|
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
/** The focused extension's
|
|
50
|
+
/** The focused extension's findings and the pending-changes note. */
|
|
50
51
|
export function extensionDetail(
|
|
51
52
|
theme: Theme,
|
|
52
53
|
width: number,
|
|
@@ -57,10 +58,10 @@ export function extensionDetail(
|
|
|
57
58
|
): string[] {
|
|
58
59
|
const lines: string[] = [];
|
|
59
60
|
const badge = row ? rowBadge(row) : undefined;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// The focused row directly above already carries the entry's name and verdict,
|
|
62
|
+
// so the pane starts with what the row cannot say: the findings, or the
|
|
63
|
+
// resource path when nothing was reported.
|
|
64
|
+
if (badge?.text === undefined && row?.item) {
|
|
64
65
|
lines.push(clip(` ${paint(theme, "meta", sanitizeLine(row.item.path))}`, width));
|
|
65
66
|
}
|
|
66
67
|
if (row?.report && budget > 0) {
|
|
@@ -71,12 +72,16 @@ export function extensionDetail(
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
/** The section's key hints, in the order they drop as the width runs out. */
|
|
74
|
-
export function extensionHints(options: {
|
|
75
|
+
export function extensionHints(options: {
|
|
76
|
+
expanded: boolean;
|
|
77
|
+
refreshing: boolean;
|
|
78
|
+
projectScope: boolean;
|
|
79
|
+
}): ScreenHint[] {
|
|
75
80
|
return [
|
|
76
81
|
"Esc close",
|
|
77
82
|
"←/→",
|
|
78
|
-
"Space toggle",
|
|
79
|
-
options.expanded ? "Enter collapse" : "Enter detail",
|
|
83
|
+
{ text: "Space toggle", color: "accent" },
|
|
84
|
+
{ text: options.expanded ? "Enter collapse" : "Enter detail", color: "accent" },
|
|
80
85
|
"Tab scope",
|
|
81
86
|
// Only Project scope can hold an override, so the key is not offered elsewhere.
|
|
82
87
|
...(options.projectScope ? ["x inherit"] : []),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The chrome around the panel's body: the layout constants, the section bar with
|
|
3
|
-
* its counts, a group heading,
|
|
3
|
+
* its counts, a group heading, the hint row that a transient notice replaces, and
|
|
4
|
+
* the frame the two sections share.
|
|
4
5
|
*
|
|
5
6
|
* Every section draws its body through these, so the two views cannot drift on
|
|
6
7
|
* spacing, roles, or the width arithmetic.
|
|
@@ -8,12 +9,20 @@
|
|
|
8
9
|
|
|
9
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import { sanitizeLine } from "../../../../lib/format.ts";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
detailBlockRows,
|
|
14
|
+
dockListFrame,
|
|
15
|
+
screenHint,
|
|
16
|
+
type ScreenHint,
|
|
17
|
+
type ViewportRowsSource,
|
|
18
|
+
} from "../../../../lib/tui/chrome.ts";
|
|
12
19
|
import { clip, fitCells, splitRow } from "../../../../lib/tui/columns.ts";
|
|
20
|
+
import { fitRows, formatRange, type ListCursor } from "../../../../lib/tui/cursor.ts";
|
|
13
21
|
import { BODY_INDENT } from "../../../../lib/tui/indent.ts";
|
|
14
22
|
import { paint } from "../../../../lib/tui/tokens.ts";
|
|
15
23
|
import { sectionSegments, type SummarySegment } from "../format.ts";
|
|
16
24
|
import type { PanelSection } from "../types.ts";
|
|
25
|
+
import { selectableRange, type PanelEntry } from "./entries.ts";
|
|
17
26
|
|
|
18
27
|
/** Body rows when the terminal height is unknown. */
|
|
19
28
|
export const SCREEN_DEFAULT_ROWS = 16;
|
|
@@ -54,8 +63,8 @@ export function groupLine(theme: Theme, width: number, entry: { label: string; c
|
|
|
54
63
|
return clip(` ${label} ${count}`, width);
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
/** The key-hint row. */
|
|
58
|
-
export function hintLine(theme: Theme, width: number, hints:
|
|
66
|
+
/** The key-hint row, or a section's notice that replaces it. */
|
|
67
|
+
export function hintLine(theme: Theme, width: number, hints: ScreenHint[]): string {
|
|
59
68
|
return screenHint(theme, width, hints);
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -69,3 +78,111 @@ export function savedLine(theme: Theme, width: number, dirty: boolean): string[]
|
|
|
69
78
|
if (!dirty) return [];
|
|
70
79
|
return [clip(`${INDENT}${paint(theme, "warn", "Saved · reload to apply")}`, width)];
|
|
71
80
|
}
|
|
81
|
+
|
|
82
|
+
/** Terminal rows, or `undefined` when the height is unknown. */
|
|
83
|
+
export function totalRows(source: ViewportRowsSource | undefined): number | undefined {
|
|
84
|
+
const rows = typeof source === "function" ? source() : source;
|
|
85
|
+
return rows !== undefined && Number.isFinite(rows) && rows > 0 ? rows : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Detail rows the focused row gets: the fixed pane, or the free height when
|
|
90
|
+
* expanded. Zero when there is no free row for a pane at all, so a short
|
|
91
|
+
* terminal keeps its body; the caller then drops optional rows if needed.
|
|
92
|
+
*/
|
|
93
|
+
export function detailRows(expanded: boolean, viewportRows: ViewportRowsSource | undefined): number {
|
|
94
|
+
const total = totalRows(viewportRows);
|
|
95
|
+
if (total === undefined) return expanded ? EXPANDED_DETAIL_FALLBACK : DETAIL_ROWS;
|
|
96
|
+
// One row pays for the blank line the frame puts above the pane.
|
|
97
|
+
const available = total - FRAME_ROWS - MIN_BODY_ROWS - 1;
|
|
98
|
+
if (available < 1) return 0;
|
|
99
|
+
return expanded ? available : Math.min(DETAIL_ROWS, available);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Drop optional rows until the frame fits the viewport: the range row first,
|
|
104
|
+
* then the project note, then the blank above the detail pane, then the pane.
|
|
105
|
+
* The header, summary, one body row, and the hint always survive; a terminal
|
|
106
|
+
* shorter than those is left to the host.
|
|
107
|
+
*/
|
|
108
|
+
export function fitOptionalLines(options: {
|
|
109
|
+
viewportRows: ViewportRowsSource | undefined;
|
|
110
|
+
body: string[];
|
|
111
|
+
note: string | undefined;
|
|
112
|
+
range: string | undefined;
|
|
113
|
+
detail: string[];
|
|
114
|
+
}): { body: string[]; range: string | undefined; detail: string[]; noteShown: boolean; detailBlank: boolean } {
|
|
115
|
+
const total = totalRows(options.viewportRows);
|
|
116
|
+
let { body, range, detail } = options;
|
|
117
|
+
let noteShown = options.note !== undefined && body[0] === options.note;
|
|
118
|
+
if (total === undefined) return { body, range, detail, noteShown, detailBlank: true };
|
|
119
|
+
const fits = (blank: boolean): boolean =>
|
|
120
|
+
FRAME_ROWS + body.length + (range === undefined ? 0 : 1) + detailBlockRows(detail, blank) <= total;
|
|
121
|
+
if (!fits(true)) range = undefined;
|
|
122
|
+
if (!fits(true) && noteShown) {
|
|
123
|
+
body = body.slice(1);
|
|
124
|
+
noteShown = false;
|
|
125
|
+
}
|
|
126
|
+
// The separator goes before the pane does: the focused row's findings are
|
|
127
|
+
// worth more than the blank above them.
|
|
128
|
+
const detailBlank = fits(true) || !fits(false);
|
|
129
|
+
if (!fits(false)) detail = [];
|
|
130
|
+
return { body, range, detail, noteShown, detailBlank };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** What {@link panelFrame} draws: the window, the body, and the chrome around it. */
|
|
134
|
+
export interface PanelFrameInput {
|
|
135
|
+
theme: Theme;
|
|
136
|
+
viewportRows: ViewportRowsSource | undefined;
|
|
137
|
+
cursor: ListCursor;
|
|
138
|
+
width: number;
|
|
139
|
+
header: string;
|
|
140
|
+
summary: string;
|
|
141
|
+
entries: readonly PanelEntry[];
|
|
142
|
+
note?: string;
|
|
143
|
+
detail: string[];
|
|
144
|
+
hint: string;
|
|
145
|
+
draw: (entry: PanelEntry, index: number) => string | undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The frame both sections share: the cursor window, the visible body rows, the
|
|
150
|
+
* range row, the optional rows that give way on a short terminal, and the
|
|
151
|
+
* chrome around them. Each section supplies its summary, its detail pane, its
|
|
152
|
+
* hint row, and how one entry is drawn.
|
|
153
|
+
*/
|
|
154
|
+
export function panelFrame(input: PanelFrameInput): { lines: string[]; noteShown: boolean } {
|
|
155
|
+
const { theme, viewportRows, cursor, width, entries, note, detail, hint, draw } = input;
|
|
156
|
+
cursor.visible = fitRows(
|
|
157
|
+
viewportRows,
|
|
158
|
+
entries.length,
|
|
159
|
+
FRAME_ROWS + detailBlockRows(detail) + (note ? 1 : 0),
|
|
160
|
+
SCREEN_DEFAULT_ROWS,
|
|
161
|
+
);
|
|
162
|
+
cursor.sync(entries.length);
|
|
163
|
+
const { start, end } = cursor.window(entries.length);
|
|
164
|
+
|
|
165
|
+
const body: string[] = [];
|
|
166
|
+
if (note) body.push(note);
|
|
167
|
+
for (let index = start; index < end; index += 1) {
|
|
168
|
+
const line = draw(entries[index], index);
|
|
169
|
+
if (line !== undefined) body.push(line);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const range = cursor.clipped(entries.length)
|
|
173
|
+
? formatRange(theme, width, selectableRange(entries, { start, end }))
|
|
174
|
+
: undefined;
|
|
175
|
+
const fitted = fitOptionalLines({ viewportRows, body, note, range, detail });
|
|
176
|
+
return {
|
|
177
|
+
lines: dockListFrame({
|
|
178
|
+
header: input.header,
|
|
179
|
+
summary: input.summary,
|
|
180
|
+
body: fitted.body,
|
|
181
|
+
range: fitted.range,
|
|
182
|
+
detail: fitted.detail.length > 0 ? fitted.detail : undefined,
|
|
183
|
+
detailBlank: fitted.detailBlank,
|
|
184
|
+
hint,
|
|
185
|
+
}),
|
|
186
|
+
noteShown: fitted.noteShown,
|
|
187
|
+
};
|
|
188
|
+
}
|