@gavin-hu/my-pi-agent 0.11.3 → 0.12.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 +13 -8
- package/README.zh-CN.md +5 -3
- package/assets/chrome.png +0 -0
- package/assets/plan-review.png +0 -0
- package/extensions/bytefly/README.md +291 -207
- package/extensions/bytefly/doctor/runtime.ts +16 -0
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/segments.ts +3 -2
- package/extensions/bytefly/footer/types.ts +1 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +13 -3
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +201 -38
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +32 -0
- package/extensions/bytefly/update/register.ts +72 -0
- package/extensions/bytefly/update/state.ts +28 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +2 -2
- package/extensions/document/tool.ts +1 -3
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +39 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/memory/README.md +1 -1
- package/extensions/plan/README.md +24 -8
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +18 -12
- package/extensions/plan/plans.ts +1 -1
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +243 -0
- package/extensions/plan/runtime.ts +4 -0
- package/extensions/plan/steps.ts +34 -23
- package/extensions/plan/todos.ts +5 -58
- package/extensions/plan/tools.ts +66 -222
- package/extensions/plan/tui.ts +17 -9
- package/extensions/plan/types.ts +3 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +5 -0
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +100 -36
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/subagent/index.ts +0 -2
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/fetch/tool.ts +1 -3
- package/extensions/web-access/search/tool.ts +1 -3
- package/extensions/wechat/README.md +22 -11
- package/extensions/wechat/bridge.ts +90 -22
- package/extensions/wechat/commands.ts +45 -1
- package/extensions/wechat/doctor.ts +1 -0
- package/extensions/wechat/index.ts +3 -1
- package/extensions/wechat/status-screen.ts +32 -2
- package/extensions/wechat/tools.ts +0 -2
- package/lib/README.md +2 -1
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/requirements.ts +76 -2
- package/lib/todo-state.ts +62 -0
- package/lib/tui/glyphs.ts +1 -0
- package/package.json +1 -1
- package/assets/bytefly.schema.json +0 -785
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { CONFIG_DIR_NAME, type QuietStartup } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { readJson, SHARED_CONFIG_FILE,
|
|
14
|
+
import { readJson, SHARED_CONFIG_FILE, writeConfigValueAt } from "../../../lib/config.ts";
|
|
15
15
|
import type { SettingsStore, WriteScope } from "../picker/types.ts";
|
|
16
16
|
import {
|
|
17
17
|
countSettings,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
SETTING_ROWS,
|
|
23
23
|
settingViews,
|
|
24
24
|
storedValue,
|
|
25
|
+
textError,
|
|
25
26
|
validValue,
|
|
26
27
|
} from "./settings.ts";
|
|
27
28
|
|
|
@@ -40,7 +41,10 @@ export interface SettingsRuntimeDeps {
|
|
|
40
41
|
export interface SettingsSurface {
|
|
41
42
|
views(scope: WriteScope): SettingView[];
|
|
42
43
|
count(scope: WriteScope): number;
|
|
44
|
+
/** Cycle an enum or toggle a boolean. */
|
|
43
45
|
change(scope: WriteScope, id: string): Promise<void>;
|
|
46
|
+
/** Store a value the user typed; `false` when the row refused it. */
|
|
47
|
+
setValue(scope: WriteScope, id: string, raw: string): Promise<boolean>;
|
|
44
48
|
isDirty(): boolean;
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -81,6 +85,15 @@ function applyAppSetting(store: SettingsStore, row: SettingDescriptor, raw: stri
|
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
/** Why `row` cannot be written in `scope`, or `undefined` when it can. */
|
|
89
|
+
function refusal(row: SettingDescriptor, scope: WriteScope, trusted: boolean): string | undefined {
|
|
90
|
+
if (scope === "project" && !trusted) {
|
|
91
|
+
return "Project is not trusted; settings can only be changed in Global scope.";
|
|
92
|
+
}
|
|
93
|
+
if (row.source === "app" && scope !== "global") return `${row.label} is a Global setting.`;
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
84
97
|
/** Create the Settings runtime for one screen invocation. */
|
|
85
98
|
export function createSettingsRuntime(deps: SettingsRuntimeDeps): SettingsRuntime {
|
|
86
99
|
let snapshot = readSnapshot(deps);
|
|
@@ -91,18 +104,42 @@ export function createSettingsRuntime(deps: SettingsRuntimeDeps): SettingsRuntim
|
|
|
91
104
|
for (const listener of listeners) listener();
|
|
92
105
|
};
|
|
93
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Write one row's value, including the extra leaves the row declares. Throws
|
|
109
|
+
* with the reason a write failed; the caller formats it and leaves the
|
|
110
|
+
* displayed value alone.
|
|
111
|
+
*/
|
|
112
|
+
const write = async (scope: WriteScope, row: SettingDescriptor, raw: string): Promise<void> => {
|
|
113
|
+
if (row.source === "app") {
|
|
114
|
+
applyAppSetting(deps.stores.global, row, raw);
|
|
115
|
+
await deps.stores.global.flush();
|
|
116
|
+
const first = deps.stores.global.drainErrors()[0];
|
|
117
|
+
if (first) throw first.error;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const config = row.config;
|
|
121
|
+
if (!config) throw new Error(`Setting ${row.id} has no config section`);
|
|
122
|
+
const path = configPath(scope, deps.cwd, deps.agentDir);
|
|
123
|
+
writeConfigValueAt(path, config.section, config.path, storedValue(row, raw));
|
|
124
|
+
for (const leaf of row.also?.(raw) ?? []) writeConfigValueAt(path, leaf.section, leaf.path, leaf.value);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Take a completed write: re-read the snapshot, mark it dirty, repaint. */
|
|
128
|
+
const committed = (): void => {
|
|
129
|
+
snapshot = readSnapshot(deps);
|
|
130
|
+
dirty = true;
|
|
131
|
+
emit();
|
|
132
|
+
};
|
|
133
|
+
|
|
94
134
|
return {
|
|
95
135
|
views: (scope) => settingViews(snapshot, scope),
|
|
96
136
|
count: (scope) => countSettings(scope),
|
|
97
137
|
change: async (scope, id) => {
|
|
98
138
|
const row = SETTING_ROWS.find((entry) => entry.id === id);
|
|
99
139
|
if (!row) return;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
if (row.source === "app" && scope !== "global") {
|
|
105
|
-
deps.onError?.(`${row.label} is a Global setting.`);
|
|
140
|
+
const refused = refusal(row, scope, deps.trusted);
|
|
141
|
+
if (refused !== undefined) {
|
|
142
|
+
deps.onError?.(refused);
|
|
106
143
|
return;
|
|
107
144
|
}
|
|
108
145
|
const current = settingViews(snapshot, scope).find((entry) => entry.id === id);
|
|
@@ -112,32 +149,38 @@ export function createSettingsRuntime(deps: SettingsRuntimeDeps): SettingsRuntim
|
|
|
112
149
|
deps.onError?.(`Cannot save ${row.label}: unknown value.`);
|
|
113
150
|
return;
|
|
114
151
|
}
|
|
115
|
-
|
|
116
152
|
try {
|
|
117
|
-
|
|
118
|
-
applyAppSetting(deps.stores.global, row, next.value);
|
|
119
|
-
await deps.stores.global.flush();
|
|
120
|
-
const errors = deps.stores.global.drainErrors();
|
|
121
|
-
const first = errors[0];
|
|
122
|
-
if (first) throw first.error;
|
|
123
|
-
} else {
|
|
124
|
-
const config = row.config;
|
|
125
|
-
if (!config) throw new Error(`Setting ${row.id} has no config section`);
|
|
126
|
-
writeConfigValue(
|
|
127
|
-
configPath(scope, deps.cwd, deps.agentDir),
|
|
128
|
-
config.section,
|
|
129
|
-
config.key,
|
|
130
|
-
storedValue(row, next.value),
|
|
131
|
-
);
|
|
132
|
-
}
|
|
153
|
+
await write(scope, row, next.value);
|
|
133
154
|
} catch (error) {
|
|
134
155
|
deps.onError?.(`Could not save ${row.label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
135
156
|
return;
|
|
136
157
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
158
|
+
committed();
|
|
159
|
+
},
|
|
160
|
+
async setValue(scope, id, raw) {
|
|
161
|
+
const row = SETTING_ROWS.find((entry) => entry.id === id);
|
|
162
|
+
if (!row || row.kind !== "text") {
|
|
163
|
+
deps.onError?.(`${row?.label ?? id} cannot be edited as text.`);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const refused = refusal(row, scope, deps.trusted);
|
|
167
|
+
if (refused !== undefined) {
|
|
168
|
+
deps.onError?.(refused);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const invalid = textError(row, raw);
|
|
172
|
+
if (invalid !== undefined) {
|
|
173
|
+
deps.onError?.(invalid);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
await write(scope, row, raw);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
deps.onError?.(`Could not save ${row.label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
committed();
|
|
183
|
+
return true;
|
|
141
184
|
},
|
|
142
185
|
isDirty: () => dirty,
|
|
143
186
|
onChange: (listener) => {
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Settings section's catalogue and value arithmetic.
|
|
3
3
|
*
|
|
4
|
-
* A row is one preference: an app setting from Pi's `settings.json`, or a
|
|
5
|
-
*
|
|
4
|
+
* A row is one preference: an app setting from Pi's `settings.json`, or a leaf
|
|
5
|
+
* of a `bytefly.json` section. This module is pure — descriptors, a snapshot to
|
|
6
6
|
* views, cycling, validation, and grouping — so the screen and the runtime share
|
|
7
7
|
* one definition and the tests need no host.
|
|
8
8
|
*
|
|
9
|
-
* The catalogue is deliberately curated
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The catalogue is deliberately curated: one proven row family at a time. It
|
|
10
|
+
* covers scalars (a boolean checkbox, an enum cycle, a short string with its own
|
|
11
|
+
* rule), both scopes, both write paths, and — with `price.currency.decimals` —
|
|
12
|
+
* one nested key. Arrays, nested tariffs, and credentials are still JSON edits.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
import { configSection } from "../../../lib/config.ts";
|
|
15
|
+
import { configSection, configValueAt } from "../../../lib/config.ts";
|
|
15
16
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
16
17
|
import { DEFAULT_CHROME_CONFIG, normalizeChromeConfig } from "../chrome.ts";
|
|
18
|
+
import { DEFAULT_CONFIG as DEFAULT_DOCTOR_CONFIG, normalizeConfig as normalizeDoctorConfig } from "../doctor/config.ts";
|
|
19
|
+
import {
|
|
20
|
+
CURRENCY_DEFAULTS,
|
|
21
|
+
DEFAULT_BYTEFLY_CONFIG,
|
|
22
|
+
DEFAULT_CURRENCY,
|
|
23
|
+
normalizeByteflyConfig,
|
|
24
|
+
} from "../footer/price/config.ts";
|
|
25
|
+
import type { CurrencyCode } from "../footer/price/types.ts";
|
|
17
26
|
import { DEFAULT_NOTIFY_CONFIG, normalizeNotifyConfig } from "../notify/config.ts";
|
|
18
27
|
import type { Settings, WriteScope } from "../picker/types.ts";
|
|
19
28
|
|
|
@@ -21,7 +30,7 @@ import type { Settings, WriteScope } from "../picker/types.ts";
|
|
|
21
30
|
export type SettingSource = "app" | "bytefly";
|
|
22
31
|
|
|
23
32
|
/** The row control a preference uses. */
|
|
24
|
-
export type SettingKind = "boolean" | "enum";
|
|
33
|
+
export type SettingKind = "boolean" | "enum" | "text";
|
|
25
34
|
|
|
26
35
|
/** Where the effective value came from. */
|
|
27
36
|
export type SettingOrigin = "default" | "global" | "project" | "inherited";
|
|
@@ -32,14 +41,35 @@ export interface SettingOption {
|
|
|
32
41
|
label: string;
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
/** What a text row accepts, checked before a write so the file cannot hold less. */
|
|
45
|
+
export interface TextRule {
|
|
46
|
+
/** Longest accepted value, in the units the loader slices with. */
|
|
47
|
+
maxLength: number;
|
|
48
|
+
/** Why `raw` cannot be stored, or `undefined` when it can. */
|
|
49
|
+
validate?: (raw: string) => string | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
/** One heading in the Settings list. */
|
|
36
53
|
export interface SettingGroup {
|
|
37
54
|
key: string;
|
|
38
55
|
label: string;
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
/**
|
|
42
|
-
type
|
|
58
|
+
/** The `bytefly.json` sections the Settings section can read and write. */
|
|
59
|
+
export type ConfigSectionName = "notify" | "chrome" | "doctor" | "price";
|
|
60
|
+
|
|
61
|
+
/** Where a `bytefly.json` row lives: a section, and the path to its leaf. */
|
|
62
|
+
export interface SettingConfig {
|
|
63
|
+
section: ConfigSectionName;
|
|
64
|
+
path: readonly string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One `bytefly.json` leaf a row writes alongside its own. */
|
|
68
|
+
export interface SettingLeaf {
|
|
69
|
+
section: ConfigSectionName;
|
|
70
|
+
path: readonly string[];
|
|
71
|
+
value: unknown;
|
|
72
|
+
}
|
|
43
73
|
|
|
44
74
|
/** The static definition of one preference. */
|
|
45
75
|
export interface SettingDescriptor {
|
|
@@ -51,12 +81,21 @@ export interface SettingDescriptor {
|
|
|
51
81
|
description: string;
|
|
52
82
|
source: SettingSource;
|
|
53
83
|
kind: SettingKind;
|
|
54
|
-
/** Curated values, in cycle order. */
|
|
84
|
+
/** Curated values, in cycle order; empty for a text row. */
|
|
55
85
|
options: readonly SettingOption[];
|
|
86
|
+
/** For `kind: "text"`: the length limit and any rule the value must satisfy. */
|
|
87
|
+
text?: TextRule;
|
|
56
88
|
/** The value shown when neither file sets the key. */
|
|
57
89
|
fallback: string;
|
|
58
|
-
/**
|
|
90
|
+
/** The JSON type the row stores; string when omitted. */
|
|
91
|
+
json?: "boolean" | "number";
|
|
92
|
+
/** For `bytefly` rows: the section and leaf path this row edits. */
|
|
59
93
|
config?: SettingConfig;
|
|
94
|
+
/**
|
|
95
|
+
* Extra leaves written with this row's own, for a value that implies another.
|
|
96
|
+
* Kept on the row so the writer stays generic and the rule is stated once.
|
|
97
|
+
*/
|
|
98
|
+
also?: (raw: string) => readonly SettingLeaf[];
|
|
60
99
|
}
|
|
61
100
|
|
|
62
101
|
/** A row with the value read from one scope. */
|
|
@@ -86,6 +125,8 @@ export interface GroupedSettings {
|
|
|
86
125
|
export const SETTING_GROUPS: readonly SettingGroup[] = [
|
|
87
126
|
{ key: "startup", label: "Startup" },
|
|
88
127
|
{ key: "notifications", label: "Notifications" },
|
|
128
|
+
{ key: "diagnostics", label: "Diagnostics" },
|
|
129
|
+
{ key: "pricing", label: "Pricing" },
|
|
89
130
|
{ key: "transcript", label: "Transcript" },
|
|
90
131
|
{ key: "terminal", label: "Terminal" },
|
|
91
132
|
];
|
|
@@ -103,6 +144,22 @@ const RAIL_OPTIONS: readonly SettingOption[] = [
|
|
|
103
144
|
{ value: "▏", label: "▏" },
|
|
104
145
|
];
|
|
105
146
|
|
|
147
|
+
/** The notify section's delivery methods, labeled by the surface each reaches. */
|
|
148
|
+
const METHOD_OPTIONS: readonly SettingOption[] = [
|
|
149
|
+
{ value: "auto", label: "Auto" },
|
|
150
|
+
{ value: "osc777", label: "OSC 777" },
|
|
151
|
+
{ value: "osc99", label: "OSC 99" },
|
|
152
|
+
{ value: "toast", label: "Windows toast" },
|
|
153
|
+
{ value: "osascript", label: "macOS" },
|
|
154
|
+
{ value: "notify-send", label: "Linux" },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
/** Cost precision 0-4, the range the `price` section clamps `decimals` to. */
|
|
158
|
+
const DECIMALS_OPTIONS: readonly SettingOption[] = [0, 1, 2, 3, 4].map((value) => ({
|
|
159
|
+
value: String(value),
|
|
160
|
+
label: String(value),
|
|
161
|
+
}));
|
|
162
|
+
|
|
106
163
|
/** The v1 catalogue, in screen order. */
|
|
107
164
|
export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
108
165
|
{
|
|
@@ -113,6 +170,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
113
170
|
source: "app",
|
|
114
171
|
kind: "boolean",
|
|
115
172
|
fallback: "false",
|
|
173
|
+
json: "boolean",
|
|
116
174
|
options: ON_OFF,
|
|
117
175
|
},
|
|
118
176
|
{
|
|
@@ -123,8 +181,82 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
123
181
|
source: "bytefly",
|
|
124
182
|
kind: "boolean",
|
|
125
183
|
fallback: "false",
|
|
184
|
+
json: "boolean",
|
|
185
|
+
options: ON_OFF,
|
|
186
|
+
config: { section: "notify", path: ["enabled"] },
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "notify.method",
|
|
190
|
+
group: "notifications",
|
|
191
|
+
label: "Delivery method",
|
|
192
|
+
description: "How a notification is delivered; Auto picks from the terminal, then the platform.",
|
|
193
|
+
source: "bytefly",
|
|
194
|
+
kind: "enum",
|
|
195
|
+
fallback: "auto",
|
|
196
|
+
options: METHOD_OPTIONS,
|
|
197
|
+
config: { section: "notify", path: ["method"] },
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: "notify.title",
|
|
201
|
+
group: "notifications",
|
|
202
|
+
label: "Notification title",
|
|
203
|
+
description: "Shown as the title of every OS notification; up to 40 characters.",
|
|
204
|
+
source: "bytefly",
|
|
205
|
+
kind: "text",
|
|
206
|
+
fallback: "Pi",
|
|
207
|
+
options: [],
|
|
208
|
+
text: {
|
|
209
|
+
maxLength: 40,
|
|
210
|
+
validate: (raw) => (raw.trim() === "" ? "A title is required." : undefined),
|
|
211
|
+
},
|
|
212
|
+
config: { section: "notify", path: ["title"] },
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "doctor.proactive",
|
|
216
|
+
group: "diagnostics",
|
|
217
|
+
label: "Proactive health check",
|
|
218
|
+
description: "Probe enabled extensions at session start and warn about a missing requirement.",
|
|
219
|
+
source: "bytefly",
|
|
220
|
+
kind: "boolean",
|
|
221
|
+
fallback: "true",
|
|
222
|
+
json: "boolean",
|
|
126
223
|
options: ON_OFF,
|
|
127
|
-
config: { section: "
|
|
224
|
+
config: { section: "doctor", path: ["proactive"] },
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "price.currency.code",
|
|
228
|
+
group: "pricing",
|
|
229
|
+
label: "Currency",
|
|
230
|
+
description:
|
|
231
|
+
"CNY prices DeepSeek from its yuan rates; USD from its published dollars, falling back to Pi's recorded cost.",
|
|
232
|
+
source: "bytefly",
|
|
233
|
+
kind: "enum",
|
|
234
|
+
fallback: DEFAULT_CURRENCY.code,
|
|
235
|
+
options: [
|
|
236
|
+
{ value: "CNY", label: "CNY (¥)" },
|
|
237
|
+
{ value: "USD", label: "USD ($)" },
|
|
238
|
+
],
|
|
239
|
+
config: { section: "price", path: ["currency", "code"] },
|
|
240
|
+
// Choosing a currency also sets its default precision, so the screen and the
|
|
241
|
+
// footer agree; the precision stays overridable afterwards.
|
|
242
|
+
also: (raw) => {
|
|
243
|
+
const currency = CURRENCY_DEFAULTS[raw as CurrencyCode];
|
|
244
|
+
return currency === undefined
|
|
245
|
+
? []
|
|
246
|
+
: [{ section: "price", path: ["currency", "decimals"], value: currency.decimals }];
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: "price.currency.decimals",
|
|
251
|
+
group: "pricing",
|
|
252
|
+
label: "Cost decimals",
|
|
253
|
+
description: "Digits shown after the decimal point in the footer's cost total.",
|
|
254
|
+
source: "bytefly",
|
|
255
|
+
kind: "enum",
|
|
256
|
+
fallback: "2",
|
|
257
|
+
json: "number",
|
|
258
|
+
options: DECIMALS_OPTIONS,
|
|
259
|
+
config: { section: "price", path: ["currency", "decimals"] },
|
|
128
260
|
},
|
|
129
261
|
{
|
|
130
262
|
id: "chrome.rail",
|
|
@@ -135,7 +267,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
135
267
|
kind: "enum",
|
|
136
268
|
fallback: GLYPHS.divider,
|
|
137
269
|
options: RAIL_OPTIONS,
|
|
138
|
-
config: { section: "chrome",
|
|
270
|
+
config: { section: "chrome", path: ["rail"] },
|
|
139
271
|
},
|
|
140
272
|
{
|
|
141
273
|
id: "outputPad",
|
|
@@ -145,6 +277,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
145
277
|
source: "app",
|
|
146
278
|
kind: "enum",
|
|
147
279
|
fallback: "1",
|
|
280
|
+
json: "number",
|
|
148
281
|
options: [
|
|
149
282
|
{ value: "0", label: "0" },
|
|
150
283
|
{ value: "1", label: "1" },
|
|
@@ -158,6 +291,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
158
291
|
source: "app",
|
|
159
292
|
kind: "boolean",
|
|
160
293
|
fallback: "false",
|
|
294
|
+
json: "boolean",
|
|
161
295
|
options: ON_OFF,
|
|
162
296
|
},
|
|
163
297
|
];
|
|
@@ -177,6 +311,21 @@ export function validValue(row: SettingDescriptor, raw: string): boolean {
|
|
|
177
311
|
return row.options.some((option) => option.value === raw);
|
|
178
312
|
}
|
|
179
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Why a text row cannot store `raw`, or `undefined` when it can.
|
|
316
|
+
*
|
|
317
|
+
* The limits mirror the loaders (`notify.title` is sliced to 40, the currency
|
|
318
|
+
* symbol to 4) so the editor cannot save a value the next read shortens.
|
|
319
|
+
*/
|
|
320
|
+
export function textError(row: SettingDescriptor, raw: string): string | undefined {
|
|
321
|
+
const rule = row.text;
|
|
322
|
+
if (!rule) return `${row.label} is not a text setting.`;
|
|
323
|
+
if (raw.length > rule.maxLength) {
|
|
324
|
+
return `Too long: ${raw.length} characters, the limit is ${rule.maxLength}.`;
|
|
325
|
+
}
|
|
326
|
+
return rule.validate?.(raw);
|
|
327
|
+
}
|
|
328
|
+
|
|
180
329
|
/** The display label for `raw`, or the raw value when it is a valid custom one. */
|
|
181
330
|
function labelFor(row: SettingDescriptor, raw: string): string {
|
|
182
331
|
return row.options.find((option) => option.value === raw)?.label ?? raw;
|
|
@@ -204,15 +353,35 @@ function appValue(snapshot: SettingsSnapshot, row: SettingDescriptor): { value:
|
|
|
204
353
|
}
|
|
205
354
|
}
|
|
206
355
|
|
|
207
|
-
/** Whether a
|
|
356
|
+
/** Whether a document states this row's leaf, so the origin is truthful. */
|
|
357
|
+
function statesConfig(config: SettingConfig, document: Record<string, unknown>): boolean {
|
|
358
|
+
return configValueAt(document, [config.section, ...config.path]) !== undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Where the effective value came from: the project file, the personal one, or nothing. */
|
|
208
362
|
function originFor(config: SettingConfig, snapshot: SettingsSnapshot, scope: WriteScope): SettingOrigin {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (projectRaw && config.key in projectRaw) return "project";
|
|
212
|
-
if (scope === "project") return "inherited";
|
|
213
|
-
return globalRaw && config.key in globalRaw ? "global" : "default";
|
|
363
|
+
if (scope === "project") return statesConfig(config, snapshot.project) ? "project" : "inherited";
|
|
364
|
+
return statesConfig(config, snapshot.global) ? "global" : "default";
|
|
214
365
|
}
|
|
215
366
|
|
|
367
|
+
/** Read one leaf of a `bytefly.json` section: project over global over defaults. */
|
|
368
|
+
type SectionReader = (
|
|
369
|
+
globalRaw: Record<string, unknown> | undefined,
|
|
370
|
+
projectRaw: Record<string, unknown> | undefined,
|
|
371
|
+
path: readonly string[],
|
|
372
|
+
) => unknown;
|
|
373
|
+
|
|
374
|
+
const SECTION_READERS: Record<ConfigSectionName, SectionReader> = {
|
|
375
|
+
notify: (globalRaw, projectRaw, path) =>
|
|
376
|
+
configValueAt(normalizeNotifyConfig(projectRaw, normalizeNotifyConfig(globalRaw, DEFAULT_NOTIFY_CONFIG)), path),
|
|
377
|
+
chrome: (globalRaw, projectRaw, path) =>
|
|
378
|
+
configValueAt(normalizeChromeConfig(projectRaw, normalizeChromeConfig(globalRaw, DEFAULT_CHROME_CONFIG)), path),
|
|
379
|
+
doctor: (globalRaw, projectRaw, path) =>
|
|
380
|
+
configValueAt(normalizeDoctorConfig(projectRaw, normalizeDoctorConfig(globalRaw, DEFAULT_DOCTOR_CONFIG)), path),
|
|
381
|
+
price: (globalRaw, projectRaw, path) =>
|
|
382
|
+
configValueAt(normalizeByteflyConfig(projectRaw, normalizeByteflyConfig(globalRaw, DEFAULT_BYTEFLY_CONFIG)), path),
|
|
383
|
+
};
|
|
384
|
+
|
|
216
385
|
/** Read a `bytefly.json` preference, normalized over the global section. */
|
|
217
386
|
function byteflyValue(
|
|
218
387
|
snapshot: SettingsSnapshot,
|
|
@@ -223,15 +392,11 @@ function byteflyValue(
|
|
|
223
392
|
if (!config) throw new Error(`Setting ${row.id} has no config section`);
|
|
224
393
|
const globalRaw = configSection(snapshot.global, config.section);
|
|
225
394
|
const projectRaw = scope === "project" ? configSection(snapshot.project, config.section) : undefined;
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
const global = normalizeChromeConfig(globalRaw, DEFAULT_CHROME_CONFIG);
|
|
233
|
-
const effective = normalizeChromeConfig(projectRaw, global);
|
|
234
|
-
return { value: effective.rail, origin };
|
|
395
|
+
const leaf = SECTION_READERS[config.section](globalRaw, projectRaw, config.path);
|
|
396
|
+
return {
|
|
397
|
+
value: leaf === undefined ? row.fallback : String(leaf),
|
|
398
|
+
origin: originFor(config, snapshot, scope),
|
|
399
|
+
};
|
|
235
400
|
}
|
|
236
401
|
|
|
237
402
|
/** Build the rows for `scope` from a snapshot. */
|
|
@@ -244,23 +409,21 @@ export function settingViews(snapshot: SettingsSnapshot, scope: WriteScope): Set
|
|
|
244
409
|
|
|
245
410
|
/** The next curated value, wrapping; an uncurated value starts the cycle. */
|
|
246
411
|
export function nextValue(row: SettingDescriptor, current: string): SettingOption {
|
|
412
|
+
// A text row has no cycle: the value it holds is the value it keeps.
|
|
413
|
+
if (row.options.length === 0) return { value: current, label: current };
|
|
247
414
|
const index = row.options.findIndex((option) => option.value === current);
|
|
248
415
|
return row.options[(index + 1) % row.options.length] as SettingOption;
|
|
249
416
|
}
|
|
250
417
|
|
|
251
|
-
/** The JSON value a raw option stores, per row. */
|
|
418
|
+
/** The JSON value a raw option stores, per the row's declared JSON type. */
|
|
252
419
|
export function storedValue(row: SettingDescriptor, raw: string): unknown {
|
|
253
|
-
switch (row.
|
|
254
|
-
case "
|
|
255
|
-
return raw === "1" ? 1 : 0;
|
|
256
|
-
case "byteflyUi":
|
|
257
|
-
case "notify.enabled":
|
|
258
|
-
case "showHardwareCursor":
|
|
420
|
+
switch (row.json) {
|
|
421
|
+
case "boolean":
|
|
259
422
|
return raw === "true";
|
|
260
|
-
case "
|
|
261
|
-
return raw;
|
|
423
|
+
case "number":
|
|
424
|
+
return Number(raw);
|
|
262
425
|
default:
|
|
263
|
-
|
|
426
|
+
return raw;
|
|
264
427
|
}
|
|
265
428
|
}
|
|
266
429
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The panel's entry model: a flat list where group headers and selectable rows
|
|
3
|
+
* interleave, shared by both sections.
|
|
4
|
+
*
|
|
5
|
+
* Pure: the list is rebuilt from a model or a set of views, and navigation is
|
|
6
|
+
* index arithmetic, so the component only has to keep one cursor per section.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { groupViews, type SettingView } from "../settings.ts";
|
|
10
|
+
import type { PanelModel, PanelRow } from "../types.ts";
|
|
11
|
+
|
|
12
|
+
/** A flat display entry: a group header or a selectable row. */
|
|
13
|
+
export type PanelEntry =
|
|
14
|
+
| { kind: "group"; key: string; label: string; count: number }
|
|
15
|
+
| { kind: "row"; row: PanelRow }
|
|
16
|
+
| { kind: "setting"; setting: SettingView };
|
|
17
|
+
|
|
18
|
+
/** A selectable entry's stable key, or `undefined` for a group header. */
|
|
19
|
+
export function entryKey(entry: PanelEntry | undefined): string | undefined {
|
|
20
|
+
if (entry?.kind === "row") return entry.row.key;
|
|
21
|
+
if (entry?.kind === "setting") return entry.setting.id;
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Whether an entry can take the focus. */
|
|
26
|
+
export function isSelectable(entry: PanelEntry | undefined): boolean {
|
|
27
|
+
return entryKey(entry) !== undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The next selectable entry from `from` in `direction`, or `undefined`. */
|
|
31
|
+
export function nextRow(entries: PanelEntry[], from: number, direction: -1 | 1): number | undefined {
|
|
32
|
+
for (let index = from + direction; index >= 0 && index < entries.length; index += direction) {
|
|
33
|
+
if (isSelectable(entries[index])) return index;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Interleave the extension model's groups and rows into one display list. */
|
|
39
|
+
export function entriesOf(model: PanelModel): PanelEntry[] {
|
|
40
|
+
const entries: PanelEntry[] = [];
|
|
41
|
+
for (const group of model.groups) {
|
|
42
|
+
entries.push({ kind: "group", key: group.key, label: group.label, count: group.count });
|
|
43
|
+
for (const row of model.rows) {
|
|
44
|
+
if (row.group === group.key) entries.push({ kind: "row", row });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return entries;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Interleave the settings views into the same group/row display list. */
|
|
51
|
+
export function settingEntries(views: readonly SettingView[]): PanelEntry[] {
|
|
52
|
+
const entries: PanelEntry[] = [];
|
|
53
|
+
for (const group of groupViews(views)) {
|
|
54
|
+
entries.push({ kind: "group", key: group.key, label: group.label, count: group.views.length });
|
|
55
|
+
for (const view of group.views) entries.push({ kind: "setting", setting: view });
|
|
56
|
+
}
|
|
57
|
+
return entries;
|
|
58
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Extensions section: its summary line, one row per resolved extension, and
|
|
3
|
+
* the detail pane for the focused row.
|
|
4
|
+
*
|
|
5
|
+
* Pure functions over the merged model — the component owns the cursor, the
|
|
6
|
+
* filter, and the probe state, and passes them in.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { sanitizeLine } from "../../../../lib/format.ts";
|
|
11
|
+
import { clip } from "../../../../lib/tui/columns.ts";
|
|
12
|
+
import { checkbox, listRow } from "../../../../lib/tui/rows.ts";
|
|
13
|
+
import { paint } from "../../../../lib/tui/tokens.ts";
|
|
14
|
+
import { detailLines as reportDetailLines } from "../../doctor/format.ts";
|
|
15
|
+
import { rowBadge, summarySegments, type PanelScope } from "../format.ts";
|
|
16
|
+
import type { PanelModel, PanelRow } from "../types.ts";
|
|
17
|
+
import { countsLine, savedLine } from "./frame.ts";
|
|
18
|
+
|
|
19
|
+
/** The section's hits, its scope, and how many need attention. */
|
|
20
|
+
export function extensionSummary(
|
|
21
|
+
theme: Theme,
|
|
22
|
+
width: number,
|
|
23
|
+
model: PanelModel,
|
|
24
|
+
scope: PanelScope,
|
|
25
|
+
filter: string | undefined,
|
|
26
|
+
): string {
|
|
27
|
+
return countsLine(theme, width, summarySegments(model, scope, { filter, compact: width < 72 }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One extension row: its checkbox, health glyph, name, and verdict. */
|
|
31
|
+
export function extensionRow(theme: Theme, width: number, row: PanelRow, selected: boolean): string {
|
|
32
|
+
const badge = rowBadge(row);
|
|
33
|
+
return listRow(theme, width, {
|
|
34
|
+
selected,
|
|
35
|
+
// A reported-only row has no enable pattern, but still reserves the box
|
|
36
|
+
// column so the list stays aligned.
|
|
37
|
+
leading: row.item ? checkbox(theme, row.item.enabled) : " ",
|
|
38
|
+
glyph: badge.glyph ?? " ",
|
|
39
|
+
glyphRole: badge.role,
|
|
40
|
+
label: sanitizeLine(row.label),
|
|
41
|
+
labelRole: selected ? "accent" : row.item && !row.item.enabled ? "faint" : "label",
|
|
42
|
+
trailing: badge.text,
|
|
43
|
+
// The glyph carries the status colour; the word stays quiet unless it is a
|
|
44
|
+
// problem, so a clean row does not shout.
|
|
45
|
+
trailingRole: badge.role === "bad" || badge.role === "warn" ? badge.role : "faint",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The focused extension's label, findings, and the pending-changes note. */
|
|
50
|
+
export function extensionDetail(
|
|
51
|
+
theme: Theme,
|
|
52
|
+
width: number,
|
|
53
|
+
row: PanelRow | undefined,
|
|
54
|
+
budget: number,
|
|
55
|
+
expanded: boolean,
|
|
56
|
+
dirty: boolean,
|
|
57
|
+
): string[] {
|
|
58
|
+
const lines: string[] = [];
|
|
59
|
+
const badge = row ? rowBadge(row) : undefined;
|
|
60
|
+
if (row && badge?.text !== undefined) {
|
|
61
|
+
const head = `${paint(theme, "meta", sanitizeLine(row.label))} ${paint(theme, "faint", "—")} ${paint(theme, badge.role, badge.text)}`;
|
|
62
|
+
lines.push(clip(` ${head}`, width));
|
|
63
|
+
} else if (row?.item) {
|
|
64
|
+
lines.push(clip(` ${paint(theme, "meta", sanitizeLine(row.item.path))}`, width));
|
|
65
|
+
}
|
|
66
|
+
if (row?.report && budget > 0) {
|
|
67
|
+
lines.push(...reportDetailLines(row.report, theme, width, budget, expanded));
|
|
68
|
+
}
|
|
69
|
+
lines.push(...savedLine(theme, width, dirty));
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The section's key hints, in the order they drop as the width runs out. */
|
|
74
|
+
export function extensionHints(options: { expanded: boolean; refreshing: boolean; projectScope: boolean }): string[] {
|
|
75
|
+
return [
|
|
76
|
+
"Esc close",
|
|
77
|
+
"←/→",
|
|
78
|
+
"Space toggle",
|
|
79
|
+
options.expanded ? "Enter collapse" : "Enter detail",
|
|
80
|
+
"Tab scope",
|
|
81
|
+
// Only Project scope can hold an override, so the key is not offered elsewhere.
|
|
82
|
+
...(options.projectScope ? ["x inherit"] : []),
|
|
83
|
+
options.refreshing ? "refreshing…" : "r refresh",
|
|
84
|
+
"↑/↓ select",
|
|
85
|
+
];
|
|
86
|
+
}
|