@gavin-hu/my-pi-agent 0.11.4 → 0.13.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 +38 -26
- package/README.zh-CN.md +33 -23
- package/extensions/bytefly/README.md +381 -225
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +52 -6
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- 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/register.ts +13 -2
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +30 -2
- package/extensions/bytefly/footer/types.ts +12 -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 +15 -4
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +293 -48
- 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/cache.ts +73 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +87 -0
- package/extensions/bytefly/update/register.ts +158 -0
- package/extensions/bytefly/update/state.ts +40 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +24 -14
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +14 -5
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- 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 +45 -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/schema.ts +6 -2
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +56 -14
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +83 -36
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +84 -19
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +91 -2
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +246 -0
- package/extensions/plan/runtime.ts +73 -24
- package/extensions/plan/steps.ts +60 -39
- package/extensions/plan/todos.ts +29 -60
- package/extensions/plan/tools.ts +188 -244
- package/extensions/plan/tui.ts +18 -31
- package/extensions/plan/types.ts +12 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +7 -2
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +106 -41
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -3
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- 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/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +46 -16
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +20 -21
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +14 -3
- package/extensions/web-access/text.ts +50 -11
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +12 -9
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +340 -46
- package/lib/todo-state.ts +62 -0
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/bytefly.schema.json +0 -785
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question-cancelled.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory-long-subject.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc-running.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch-rejected.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
|
@@ -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,29 @@
|
|
|
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The catalogue covers every scalar preference: booleans, enums, short strings,
|
|
10
|
+
* and — with `doctor.timeoutMs` — a number. Each row states its own rule, so a
|
|
11
|
+
* value the screen accepts is a value the loader keeps. Arrays (`doctor.skip`),
|
|
12
|
+
* the `rates` table, and `price.peak` stay JSON edits: a list, a table, and a
|
|
13
|
+
* nested object are not one row.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
import { configSection } from "../../../lib/config.ts";
|
|
16
|
+
import { configSection, configValueAt } from "../../../lib/config.ts";
|
|
15
17
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
16
|
-
import { DEFAULT_CHROME_CONFIG, normalizeChromeConfig } from "../chrome.ts";
|
|
18
|
+
import { DEFAULT_CHROME_CONFIG, isRailGlyph, normalizeChromeConfig } from "../chrome.ts";
|
|
19
|
+
import { DEFAULT_CONFIG as DEFAULT_DOCTOR_CONFIG, normalizeConfig as normalizeDoctorConfig } from "../doctor/config.ts";
|
|
20
|
+
import {
|
|
21
|
+
CURRENCY_DEFAULTS,
|
|
22
|
+
DEFAULT_BYTEFLY_CONFIG,
|
|
23
|
+
DEFAULT_CURRENCY,
|
|
24
|
+
normalizeByteflyConfig,
|
|
25
|
+
} from "../footer/price/config.ts";
|
|
26
|
+
import type { CurrencyCode } from "../footer/price/types.ts";
|
|
17
27
|
import { DEFAULT_NOTIFY_CONFIG, normalizeNotifyConfig } from "../notify/config.ts";
|
|
18
28
|
import type { Settings, WriteScope } from "../picker/types.ts";
|
|
19
29
|
|
|
@@ -21,7 +31,7 @@ import type { Settings, WriteScope } from "../picker/types.ts";
|
|
|
21
31
|
export type SettingSource = "app" | "bytefly";
|
|
22
32
|
|
|
23
33
|
/** The row control a preference uses. */
|
|
24
|
-
export type SettingKind = "boolean" | "enum";
|
|
34
|
+
export type SettingKind = "boolean" | "enum" | "text";
|
|
25
35
|
|
|
26
36
|
/** Where the effective value came from. */
|
|
27
37
|
export type SettingOrigin = "default" | "global" | "project" | "inherited";
|
|
@@ -32,14 +42,35 @@ export interface SettingOption {
|
|
|
32
42
|
label: string;
|
|
33
43
|
}
|
|
34
44
|
|
|
45
|
+
/** What a text row accepts, checked before a write so the file cannot hold less. */
|
|
46
|
+
export interface TextRule {
|
|
47
|
+
/** Longest accepted value, in the units the loader slices with. */
|
|
48
|
+
maxLength: number;
|
|
49
|
+
/** Why `raw` cannot be stored, or `undefined` when it can. */
|
|
50
|
+
validate?: (raw: string) => string | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
/** One heading in the Settings list. */
|
|
36
54
|
export interface SettingGroup {
|
|
37
55
|
key: string;
|
|
38
56
|
label: string;
|
|
39
57
|
}
|
|
40
58
|
|
|
41
|
-
/**
|
|
42
|
-
type
|
|
59
|
+
/** The `bytefly.json` sections the Settings section can read and write. */
|
|
60
|
+
export type ConfigSectionName = "notify" | "chrome" | "doctor" | "price";
|
|
61
|
+
|
|
62
|
+
/** Where a `bytefly.json` row lives: a section, and the path to its leaf. */
|
|
63
|
+
export interface SettingConfig {
|
|
64
|
+
section: ConfigSectionName;
|
|
65
|
+
path: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One `bytefly.json` leaf a row writes alongside its own. */
|
|
69
|
+
export interface SettingLeaf {
|
|
70
|
+
section: ConfigSectionName;
|
|
71
|
+
path: readonly string[];
|
|
72
|
+
value: unknown;
|
|
73
|
+
}
|
|
43
74
|
|
|
44
75
|
/** The static definition of one preference. */
|
|
45
76
|
export interface SettingDescriptor {
|
|
@@ -51,12 +82,21 @@ export interface SettingDescriptor {
|
|
|
51
82
|
description: string;
|
|
52
83
|
source: SettingSource;
|
|
53
84
|
kind: SettingKind;
|
|
54
|
-
/** Curated values, in cycle order. */
|
|
85
|
+
/** Curated values, in cycle order; empty for a text row. */
|
|
55
86
|
options: readonly SettingOption[];
|
|
87
|
+
/** For `kind: "text"`: the length limit and any rule the value must satisfy. */
|
|
88
|
+
text?: TextRule;
|
|
56
89
|
/** The value shown when neither file sets the key. */
|
|
57
90
|
fallback: string;
|
|
58
|
-
/**
|
|
91
|
+
/** The JSON type the row stores; string when omitted. */
|
|
92
|
+
json?: "boolean" | "number";
|
|
93
|
+
/** For `bytefly` rows: the section and leaf path this row edits. */
|
|
59
94
|
config?: SettingConfig;
|
|
95
|
+
/**
|
|
96
|
+
* Extra leaves written with this row's own, for a value that implies another.
|
|
97
|
+
* Kept on the row so the writer stays generic and the rule is stated once.
|
|
98
|
+
*/
|
|
99
|
+
also?: (raw: string) => readonly SettingLeaf[];
|
|
60
100
|
}
|
|
61
101
|
|
|
62
102
|
/** A row with the value read from one scope. */
|
|
@@ -86,6 +126,8 @@ export interface GroupedSettings {
|
|
|
86
126
|
export const SETTING_GROUPS: readonly SettingGroup[] = [
|
|
87
127
|
{ key: "startup", label: "Startup" },
|
|
88
128
|
{ key: "notifications", label: "Notifications" },
|
|
129
|
+
{ key: "diagnostics", label: "Diagnostics" },
|
|
130
|
+
{ key: "pricing", label: "Pricing" },
|
|
89
131
|
{ key: "transcript", label: "Transcript" },
|
|
90
132
|
{ key: "terminal", label: "Terminal" },
|
|
91
133
|
];
|
|
@@ -95,14 +137,49 @@ const ON_OFF: readonly SettingOption[] = [
|
|
|
95
137
|
{ value: "true", label: "On" },
|
|
96
138
|
];
|
|
97
139
|
|
|
98
|
-
/**
|
|
99
|
-
const
|
|
100
|
-
{ value:
|
|
101
|
-
{ value: "
|
|
102
|
-
{ value: "
|
|
103
|
-
{ value: "
|
|
140
|
+
/** The notify section's delivery methods, labeled by the surface each reaches. */
|
|
141
|
+
const METHOD_OPTIONS: readonly SettingOption[] = [
|
|
142
|
+
{ value: "auto", label: "Auto" },
|
|
143
|
+
{ value: "osc777", label: "OSC 777" },
|
|
144
|
+
{ value: "osc99", label: "OSC 99" },
|
|
145
|
+
{ value: "toast", label: "Windows toast" },
|
|
146
|
+
{ value: "osascript", label: "macOS" },
|
|
147
|
+
{ value: "notify-send", label: "Linux" },
|
|
104
148
|
];
|
|
105
149
|
|
|
150
|
+
/** Cost precision 0-4, the range the `price` section clamps `decimals` to. */
|
|
151
|
+
const DECIMALS_OPTIONS: readonly SettingOption[] = [0, 1, 2, 3, 4].map((value) => ({
|
|
152
|
+
value: String(value),
|
|
153
|
+
label: String(value),
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A text rule for a whole number inside an inclusive range.
|
|
158
|
+
*
|
|
159
|
+
* The bounds are the loader's own (`doctor.timeoutMs` is clamped to 100-30000),
|
|
160
|
+
* so an accepted value is never silently clamped on the next read. `maxLength`
|
|
161
|
+
* only stops a runaway buffer; `validate` is the rule.
|
|
162
|
+
*/
|
|
163
|
+
function integerRule(min: number, max: number): TextRule {
|
|
164
|
+
return {
|
|
165
|
+
maxLength: String(max).length + 2,
|
|
166
|
+
validate(raw) {
|
|
167
|
+
const text = raw.trim();
|
|
168
|
+
if (!/^\d+$/.test(text)) return `Use a whole number, ${min} to ${max}.`;
|
|
169
|
+
const value = Number(text);
|
|
170
|
+
if (value < min || value > max) return `Out of range: ${min} to ${max}.`;
|
|
171
|
+
return undefined;
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The rail's rule: the loader drops anything that is not one column. */
|
|
177
|
+
const RAIL_RULE: TextRule = {
|
|
178
|
+
maxLength: 4,
|
|
179
|
+
validate: (raw) =>
|
|
180
|
+
isRailGlyph(raw) ? undefined : "Use exactly one column: a single glyph, with no spaces or control characters.",
|
|
181
|
+
};
|
|
182
|
+
|
|
106
183
|
/** The v1 catalogue, in screen order. */
|
|
107
184
|
export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
108
185
|
{
|
|
@@ -113,6 +190,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
113
190
|
source: "app",
|
|
114
191
|
kind: "boolean",
|
|
115
192
|
fallback: "false",
|
|
193
|
+
json: "boolean",
|
|
116
194
|
options: ON_OFF,
|
|
117
195
|
},
|
|
118
196
|
{
|
|
@@ -123,19 +201,155 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
123
201
|
source: "bytefly",
|
|
124
202
|
kind: "boolean",
|
|
125
203
|
fallback: "false",
|
|
204
|
+
json: "boolean",
|
|
205
|
+
options: ON_OFF,
|
|
206
|
+
config: { section: "notify", path: ["enabled"] },
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "notify.turnFinished",
|
|
210
|
+
group: "notifications",
|
|
211
|
+
label: "Turn finished",
|
|
212
|
+
description: "Announce it when a full agent run settles.",
|
|
213
|
+
source: "bytefly",
|
|
214
|
+
kind: "boolean",
|
|
215
|
+
fallback: "true",
|
|
216
|
+
json: "boolean",
|
|
217
|
+
options: ON_OFF,
|
|
218
|
+
config: { section: "notify", path: ["turnFinished"] },
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "notify.needsInput",
|
|
222
|
+
group: "notifications",
|
|
223
|
+
label: "Needs input",
|
|
224
|
+
description: "Announce it when the agent opens a prompt and waits for an answer.",
|
|
225
|
+
source: "bytefly",
|
|
226
|
+
kind: "boolean",
|
|
227
|
+
fallback: "true",
|
|
228
|
+
json: "boolean",
|
|
126
229
|
options: ON_OFF,
|
|
127
|
-
config: { section: "notify",
|
|
230
|
+
config: { section: "notify", path: ["needsInput"] },
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "notify.jobFinished",
|
|
234
|
+
group: "notifications",
|
|
235
|
+
label: "Job finished",
|
|
236
|
+
description: "Announce it when a background job or watcher settles.",
|
|
237
|
+
source: "bytefly",
|
|
238
|
+
kind: "boolean",
|
|
239
|
+
fallback: "true",
|
|
240
|
+
json: "boolean",
|
|
241
|
+
options: ON_OFF,
|
|
242
|
+
config: { section: "notify", path: ["jobFinished"] },
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: "notify.matchFound",
|
|
246
|
+
group: "notifications",
|
|
247
|
+
label: "Watcher matched",
|
|
248
|
+
description: "Announce it when a `watch` whose `wake` is on matches while the agent is idle.",
|
|
249
|
+
source: "bytefly",
|
|
250
|
+
kind: "boolean",
|
|
251
|
+
fallback: "true",
|
|
252
|
+
json: "boolean",
|
|
253
|
+
options: ON_OFF,
|
|
254
|
+
config: { section: "notify", path: ["matchFound"] },
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: "notify.method",
|
|
258
|
+
group: "notifications",
|
|
259
|
+
label: "Delivery method",
|
|
260
|
+
description: "How a notification is delivered; Auto picks from the terminal, then the platform.",
|
|
261
|
+
source: "bytefly",
|
|
262
|
+
kind: "enum",
|
|
263
|
+
fallback: "auto",
|
|
264
|
+
options: METHOD_OPTIONS,
|
|
265
|
+
config: { section: "notify", path: ["method"] },
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: "notify.title",
|
|
269
|
+
group: "notifications",
|
|
270
|
+
label: "Notification title",
|
|
271
|
+
description: "Shown as the title of every OS notification; up to 40 characters.",
|
|
272
|
+
source: "bytefly",
|
|
273
|
+
kind: "text",
|
|
274
|
+
fallback: "Pi",
|
|
275
|
+
options: [],
|
|
276
|
+
text: {
|
|
277
|
+
maxLength: 40,
|
|
278
|
+
validate: (raw) => (raw.trim() === "" ? "A title is required." : undefined),
|
|
279
|
+
},
|
|
280
|
+
config: { section: "notify", path: ["title"] },
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
id: "doctor.proactive",
|
|
284
|
+
group: "diagnostics",
|
|
285
|
+
label: "Proactive health check",
|
|
286
|
+
description: "Probe enabled extensions at session start and warn about a missing requirement.",
|
|
287
|
+
source: "bytefly",
|
|
288
|
+
kind: "boolean",
|
|
289
|
+
fallback: "true",
|
|
290
|
+
json: "boolean",
|
|
291
|
+
options: ON_OFF,
|
|
292
|
+
config: { section: "doctor", path: ["proactive"] },
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: "doctor.timeoutMs",
|
|
296
|
+
group: "diagnostics",
|
|
297
|
+
label: "Probe timeout",
|
|
298
|
+
description: "How long one doctor probe may take, in milliseconds; 100 to 30000.",
|
|
299
|
+
source: "bytefly",
|
|
300
|
+
kind: "text",
|
|
301
|
+
fallback: String(DEFAULT_DOCTOR_CONFIG.timeoutMs),
|
|
302
|
+
json: "number",
|
|
303
|
+
options: [],
|
|
304
|
+
text: integerRule(100, 30_000),
|
|
305
|
+
config: { section: "doctor", path: ["timeoutMs"] },
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: "price.currency.code",
|
|
309
|
+
group: "pricing",
|
|
310
|
+
label: "Currency",
|
|
311
|
+
description:
|
|
312
|
+
"CNY prices DeepSeek from its yuan rates; USD from its published dollars, falling back to Pi's recorded cost.",
|
|
313
|
+
source: "bytefly",
|
|
314
|
+
kind: "enum",
|
|
315
|
+
fallback: DEFAULT_CURRENCY.code,
|
|
316
|
+
options: [
|
|
317
|
+
{ value: "CNY", label: "CNY (¥)" },
|
|
318
|
+
{ value: "USD", label: "USD ($)" },
|
|
319
|
+
],
|
|
320
|
+
config: { section: "price", path: ["currency", "code"] },
|
|
321
|
+
// Choosing a currency also sets its default precision, so the screen and the
|
|
322
|
+
// footer agree; the precision stays overridable afterwards.
|
|
323
|
+
also: (raw) => {
|
|
324
|
+
const currency = CURRENCY_DEFAULTS[raw as CurrencyCode];
|
|
325
|
+
return currency === undefined
|
|
326
|
+
? []
|
|
327
|
+
: [{ section: "price", path: ["currency", "decimals"], value: currency.decimals }];
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
id: "price.currency.decimals",
|
|
332
|
+
group: "pricing",
|
|
333
|
+
label: "Cost decimals",
|
|
334
|
+
description: "Digits shown after the decimal point in the footer's cost total.",
|
|
335
|
+
source: "bytefly",
|
|
336
|
+
kind: "enum",
|
|
337
|
+
fallback: "2",
|
|
338
|
+
json: "number",
|
|
339
|
+
options: DECIMALS_OPTIONS,
|
|
340
|
+
config: { section: "price", path: ["currency", "decimals"] },
|
|
128
341
|
},
|
|
129
342
|
{
|
|
130
343
|
id: "chrome.rail",
|
|
131
344
|
group: "transcript",
|
|
132
345
|
label: "Rail glyph",
|
|
133
|
-
description: "The single-column glyph that leads a tool result or notice body.",
|
|
346
|
+
description: "The single-column glyph that leads a tool result or notice body, e.g. ╎ │ ┃ ▏.",
|
|
134
347
|
source: "bytefly",
|
|
135
|
-
kind: "
|
|
348
|
+
kind: "text",
|
|
136
349
|
fallback: GLYPHS.divider,
|
|
137
|
-
options:
|
|
138
|
-
|
|
350
|
+
options: [],
|
|
351
|
+
text: RAIL_RULE,
|
|
352
|
+
config: { section: "chrome", path: ["rail"] },
|
|
139
353
|
},
|
|
140
354
|
{
|
|
141
355
|
id: "outputPad",
|
|
@@ -145,6 +359,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
145
359
|
source: "app",
|
|
146
360
|
kind: "enum",
|
|
147
361
|
fallback: "1",
|
|
362
|
+
json: "number",
|
|
148
363
|
options: [
|
|
149
364
|
{ value: "0", label: "0" },
|
|
150
365
|
{ value: "1", label: "1" },
|
|
@@ -158,6 +373,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
158
373
|
source: "app",
|
|
159
374
|
kind: "boolean",
|
|
160
375
|
fallback: "false",
|
|
376
|
+
json: "boolean",
|
|
161
377
|
options: ON_OFF,
|
|
162
378
|
},
|
|
163
379
|
];
|
|
@@ -177,6 +393,21 @@ export function validValue(row: SettingDescriptor, raw: string): boolean {
|
|
|
177
393
|
return row.options.some((option) => option.value === raw);
|
|
178
394
|
}
|
|
179
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Why a text row cannot store `raw`, or `undefined` when it can.
|
|
398
|
+
*
|
|
399
|
+
* The limits mirror the loaders (`notify.title` is sliced to 40, the currency
|
|
400
|
+
* symbol to 4) so the editor cannot save a value the next read shortens.
|
|
401
|
+
*/
|
|
402
|
+
export function textError(row: SettingDescriptor, raw: string): string | undefined {
|
|
403
|
+
const rule = row.text;
|
|
404
|
+
if (!rule) return `${row.label} is not a text setting.`;
|
|
405
|
+
if (raw.length > rule.maxLength) {
|
|
406
|
+
return `Too long: ${raw.length} characters, the limit is ${rule.maxLength}.`;
|
|
407
|
+
}
|
|
408
|
+
return rule.validate?.(raw);
|
|
409
|
+
}
|
|
410
|
+
|
|
180
411
|
/** The display label for `raw`, or the raw value when it is a valid custom one. */
|
|
181
412
|
function labelFor(row: SettingDescriptor, raw: string): string {
|
|
182
413
|
return row.options.find((option) => option.value === raw)?.label ?? raw;
|
|
@@ -204,15 +435,35 @@ function appValue(snapshot: SettingsSnapshot, row: SettingDescriptor): { value:
|
|
|
204
435
|
}
|
|
205
436
|
}
|
|
206
437
|
|
|
207
|
-
/** Whether a
|
|
438
|
+
/** Whether a document states this row's leaf, so the origin is truthful. */
|
|
439
|
+
function statesConfig(config: SettingConfig, document: Record<string, unknown>): boolean {
|
|
440
|
+
return configValueAt(document, [config.section, ...config.path]) !== undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Where the effective value came from: the project file, the personal one, or nothing. */
|
|
208
444
|
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";
|
|
445
|
+
if (scope === "project") return statesConfig(config, snapshot.project) ? "project" : "inherited";
|
|
446
|
+
return statesConfig(config, snapshot.global) ? "global" : "default";
|
|
214
447
|
}
|
|
215
448
|
|
|
449
|
+
/** Read one leaf of a `bytefly.json` section: project over global over defaults. */
|
|
450
|
+
type SectionReader = (
|
|
451
|
+
globalRaw: Record<string, unknown> | undefined,
|
|
452
|
+
projectRaw: Record<string, unknown> | undefined,
|
|
453
|
+
path: readonly string[],
|
|
454
|
+
) => unknown;
|
|
455
|
+
|
|
456
|
+
const SECTION_READERS: Record<ConfigSectionName, SectionReader> = {
|
|
457
|
+
notify: (globalRaw, projectRaw, path) =>
|
|
458
|
+
configValueAt(normalizeNotifyConfig(projectRaw, normalizeNotifyConfig(globalRaw, DEFAULT_NOTIFY_CONFIG)), path),
|
|
459
|
+
chrome: (globalRaw, projectRaw, path) =>
|
|
460
|
+
configValueAt(normalizeChromeConfig(projectRaw, normalizeChromeConfig(globalRaw, DEFAULT_CHROME_CONFIG)), path),
|
|
461
|
+
doctor: (globalRaw, projectRaw, path) =>
|
|
462
|
+
configValueAt(normalizeDoctorConfig(projectRaw, normalizeDoctorConfig(globalRaw, DEFAULT_DOCTOR_CONFIG)), path),
|
|
463
|
+
price: (globalRaw, projectRaw, path) =>
|
|
464
|
+
configValueAt(normalizeByteflyConfig(projectRaw, normalizeByteflyConfig(globalRaw, DEFAULT_BYTEFLY_CONFIG)), path),
|
|
465
|
+
};
|
|
466
|
+
|
|
216
467
|
/** Read a `bytefly.json` preference, normalized over the global section. */
|
|
217
468
|
function byteflyValue(
|
|
218
469
|
snapshot: SettingsSnapshot,
|
|
@@ -223,15 +474,11 @@ function byteflyValue(
|
|
|
223
474
|
if (!config) throw new Error(`Setting ${row.id} has no config section`);
|
|
224
475
|
const globalRaw = configSection(snapshot.global, config.section);
|
|
225
476
|
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 };
|
|
477
|
+
const leaf = SECTION_READERS[config.section](globalRaw, projectRaw, config.path);
|
|
478
|
+
return {
|
|
479
|
+
value: leaf === undefined ? row.fallback : String(leaf),
|
|
480
|
+
origin: originFor(config, snapshot, scope),
|
|
481
|
+
};
|
|
235
482
|
}
|
|
236
483
|
|
|
237
484
|
/** Build the rows for `scope` from a snapshot. */
|
|
@@ -244,23 +491,21 @@ export function settingViews(snapshot: SettingsSnapshot, scope: WriteScope): Set
|
|
|
244
491
|
|
|
245
492
|
/** The next curated value, wrapping; an uncurated value starts the cycle. */
|
|
246
493
|
export function nextValue(row: SettingDescriptor, current: string): SettingOption {
|
|
494
|
+
// A text row has no cycle: the value it holds is the value it keeps.
|
|
495
|
+
if (row.options.length === 0) return { value: current, label: current };
|
|
247
496
|
const index = row.options.findIndex((option) => option.value === current);
|
|
248
497
|
return row.options[(index + 1) % row.options.length] as SettingOption;
|
|
249
498
|
}
|
|
250
499
|
|
|
251
|
-
/** The JSON value a raw option stores, per row. */
|
|
500
|
+
/** The JSON value a raw option stores, per the row's declared JSON type. */
|
|
252
501
|
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":
|
|
502
|
+
switch (row.json) {
|
|
503
|
+
case "boolean":
|
|
259
504
|
return raw === "true";
|
|
260
|
-
case "
|
|
261
|
-
return raw;
|
|
505
|
+
case "number":
|
|
506
|
+
return Number(raw);
|
|
262
507
|
default:
|
|
263
|
-
|
|
508
|
+
return raw;
|
|
264
509
|
}
|
|
265
510
|
}
|
|
266
511
|
|
|
@@ -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
|
+
}
|