@gavin-hu/my-pi-agent 0.13.1 → 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 +66 -41
- package/README.zh-CN.md +53 -36
- package/extensions/bytefly/README.md +321 -186
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/config.ts +4 -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/config.ts +3 -2
- 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 +33 -10
- package/extensions/bytefly/header/config.ts +22 -0
- package/extensions/bytefly/header/register.ts +2 -1
- package/extensions/bytefly/index.ts +6 -6
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +7 -12
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +22 -2
- package/extensions/bytefly/panel/format.ts +17 -21
- package/extensions/bytefly/panel/settings-runtime.ts +86 -22
- package/extensions/bytefly/panel/settings.ts +279 -46
- 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 +98 -13
- package/extensions/bytefly/panel/tui.ts +176 -166
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +17 -6
- package/extensions/bytefly/separator/config.ts +15 -0
- package/extensions/bytefly/separator/format.ts +60 -6
- package/extensions/bytefly/separator/register.ts +68 -9
- package/extensions/bytefly/separator/types.ts +8 -0
- 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/bytefly/working/config.ts +4 -5
- package/extensions/bytefly/working/format.ts +24 -7
- package/extensions/bytefly/working/register.ts +34 -3
- 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 +3 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +106 -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 +118 -8
- 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 +62 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +116 -1
- package/extensions/subagent/orchestrate.ts +16 -3
- package/extensions/subagent/render.ts +25 -0
- package/extensions/subagent/run.ts +113 -49
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/transcripts.ts +202 -0
- package/extensions/subagent/types.ts +23 -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 +16 -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 +3 -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 +83 -12
- package/lib/errors.ts +12 -0
- package/lib/format.ts +56 -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 +23 -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/themes/bytefly-dark.json +2 -3
- package/themes/bytefly-light.json +2 -3
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
|
@@ -6,44 +6,56 @@
|
|
|
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
|
-
* value the screen accepts is a value the loader keeps.
|
|
12
|
-
* the
|
|
13
|
-
*
|
|
9
|
+
* The catalogue is curated rather than exhaustive: it holds the preferences a
|
|
10
|
+
* person changes, across every extension of the package, and each row states its
|
|
11
|
+
* own rule so a value the screen accepts is a value the loader keeps. The rest of
|
|
12
|
+
* the document stays JSON, reachable from the screen's JSON pane: a list
|
|
13
|
+
* (`doctor.skip`), a table (`price.rates`), and a schedule (`price.peak`) are not
|
|
14
|
+
* one row.
|
|
15
|
+
*
|
|
16
|
+
* Four sections have a normalizer here, so the screen shows what the footer, the
|
|
17
|
+
* notifier, and the doctor keep. The six owned by other extensions are read as the
|
|
18
|
+
* file states them, because importing their normalizer would be one extension
|
|
19
|
+
* importing another's module; each such row declares the loader's default, and
|
|
20
|
+
* `test/settings-coverage.test.ts` holds the pair together.
|
|
14
21
|
*/
|
|
15
22
|
|
|
16
23
|
import { configSection, configValueAt } from "../../../lib/config.ts";
|
|
17
|
-
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
18
24
|
import { DEFAULT_CHROME_CONFIG, isRailGlyph, normalizeChromeConfig } from "../chrome.ts";
|
|
19
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_CONFIG as DEFAULT_DOCTOR_CONFIG,
|
|
27
|
+
normalizeConfig as normalizeDoctorConfig,
|
|
28
|
+
TIMEOUT_MS_RANGE,
|
|
29
|
+
} from "../doctor/config.ts";
|
|
20
30
|
import {
|
|
21
31
|
CURRENCY_DEFAULTS,
|
|
22
32
|
DEFAULT_BYTEFLY_CONFIG,
|
|
23
33
|
DEFAULT_CURRENCY,
|
|
34
|
+
MAX_BUDGET,
|
|
35
|
+
MAX_DECIMALS,
|
|
24
36
|
normalizeByteflyConfig,
|
|
25
37
|
} from "../footer/price/config.ts";
|
|
26
38
|
import type { CurrencyCode } from "../footer/price/types.ts";
|
|
27
|
-
import { DEFAULT_NOTIFY_CONFIG, normalizeNotifyConfig } from "../notify/config.ts";
|
|
39
|
+
import { DEFAULT_NOTIFY_CONFIG, MAX_TITLE_LENGTH, normalizeNotifyConfig } from "../notify/config.ts";
|
|
28
40
|
import type { Settings, WriteScope } from "../picker/types.ts";
|
|
29
41
|
|
|
30
42
|
/** Where a preference is stored. */
|
|
31
|
-
|
|
43
|
+
type SettingSource = "app" | "bytefly";
|
|
32
44
|
|
|
33
45
|
/** The row control a preference uses. */
|
|
34
|
-
|
|
46
|
+
type SettingKind = "boolean" | "enum" | "text";
|
|
35
47
|
|
|
36
48
|
/** Where the effective value came from. */
|
|
37
|
-
|
|
49
|
+
type SettingOrigin = "default" | "global" | "project" | "inherited";
|
|
38
50
|
|
|
39
51
|
/** One curated value a row can take. */
|
|
40
|
-
|
|
52
|
+
interface SettingOption {
|
|
41
53
|
value: string;
|
|
42
54
|
label: string;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
/** What a text row accepts, checked before a write so the file cannot hold less. */
|
|
46
|
-
|
|
58
|
+
interface TextRule {
|
|
47
59
|
/** Longest accepted value, in the units the loader slices with. */
|
|
48
60
|
maxLength: number;
|
|
49
61
|
/** Why `raw` cannot be stored, or `undefined` when it can. */
|
|
@@ -51,22 +63,32 @@ export interface TextRule {
|
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
/** One heading in the Settings list. */
|
|
54
|
-
|
|
66
|
+
interface SettingGroup {
|
|
55
67
|
key: string;
|
|
56
68
|
label: string;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
/** The `bytefly.json` sections the Settings section can read and write. */
|
|
60
|
-
export type ConfigSectionName =
|
|
72
|
+
export type ConfigSectionName =
|
|
73
|
+
| "notify"
|
|
74
|
+
| "chrome"
|
|
75
|
+
| "doctor"
|
|
76
|
+
| "price"
|
|
77
|
+
| "worktree"
|
|
78
|
+
| "rewind"
|
|
79
|
+
| "job"
|
|
80
|
+
| "watch"
|
|
81
|
+
| "memory"
|
|
82
|
+
| "goal";
|
|
61
83
|
|
|
62
84
|
/** Where a `bytefly.json` row lives: a section, and the path to its leaf. */
|
|
63
|
-
|
|
85
|
+
interface SettingConfig {
|
|
64
86
|
section: ConfigSectionName;
|
|
65
87
|
path: readonly string[];
|
|
66
88
|
}
|
|
67
89
|
|
|
68
90
|
/** One `bytefly.json` leaf a row writes alongside its own. */
|
|
69
|
-
|
|
91
|
+
interface SettingLeaf {
|
|
70
92
|
section: ConfigSectionName;
|
|
71
93
|
path: readonly string[];
|
|
72
94
|
value: unknown;
|
|
@@ -113,10 +135,12 @@ export interface SettingsSnapshot {
|
|
|
113
135
|
app: Settings;
|
|
114
136
|
global: Record<string, unknown>;
|
|
115
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[];
|
|
116
140
|
}
|
|
117
141
|
|
|
118
142
|
/** One rendered group: its heading and the rows under it. */
|
|
119
|
-
|
|
143
|
+
interface GroupedSettings {
|
|
120
144
|
key: string;
|
|
121
145
|
label: string;
|
|
122
146
|
views: SettingView[];
|
|
@@ -130,6 +154,10 @@ export const SETTING_GROUPS: readonly SettingGroup[] = [
|
|
|
130
154
|
{ key: "pricing", label: "Pricing" },
|
|
131
155
|
{ key: "transcript", label: "Transcript" },
|
|
132
156
|
{ key: "terminal", label: "Terminal" },
|
|
157
|
+
{ key: "worktree", label: "Worktree" },
|
|
158
|
+
{ key: "snapshots", label: "Snapshots" },
|
|
159
|
+
{ key: "jobs", label: "Jobs and watchers" },
|
|
160
|
+
{ key: "memory", label: "Memory" },
|
|
133
161
|
];
|
|
134
162
|
|
|
135
163
|
const ON_OFF: readonly SettingOption[] = [
|
|
@@ -147,8 +175,8 @@ const METHOD_OPTIONS: readonly SettingOption[] = [
|
|
|
147
175
|
{ value: "notify-send", label: "Linux" },
|
|
148
176
|
];
|
|
149
177
|
|
|
150
|
-
/** Cost precision 0
|
|
151
|
-
const DECIMALS_OPTIONS: readonly SettingOption[] =
|
|
178
|
+
/** Cost precision 0 through the loader's ceiling, the range `price` clamps `decimals` to. */
|
|
179
|
+
const DECIMALS_OPTIONS: readonly SettingOption[] = Array.from({ length: MAX_DECIMALS + 1 }, (_, value) => ({
|
|
152
180
|
value: String(value),
|
|
153
181
|
label: String(value),
|
|
154
182
|
}));
|
|
@@ -292,7 +320,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
292
320
|
fallback: "Pi",
|
|
293
321
|
options: [],
|
|
294
322
|
text: {
|
|
295
|
-
maxLength:
|
|
323
|
+
maxLength: MAX_TITLE_LENGTH,
|
|
296
324
|
validate: (raw) => (raw.trim() === "" ? "A title is required." : undefined),
|
|
297
325
|
},
|
|
298
326
|
config: { section: "notify", path: ["title"] },
|
|
@@ -313,13 +341,13 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
313
341
|
id: "doctor.timeoutMs",
|
|
314
342
|
group: "diagnostics",
|
|
315
343
|
label: "Probe timeout",
|
|
316
|
-
description:
|
|
344
|
+
description: `How long one doctor probe may take, in milliseconds; ${TIMEOUT_MS_RANGE.min} to ${TIMEOUT_MS_RANGE.max}.`,
|
|
317
345
|
source: "bytefly",
|
|
318
346
|
kind: "text",
|
|
319
347
|
fallback: String(DEFAULT_DOCTOR_CONFIG.timeoutMs),
|
|
320
348
|
json: "number",
|
|
321
349
|
options: [],
|
|
322
|
-
text: integerRule(
|
|
350
|
+
text: integerRule(TIMEOUT_MS_RANGE.min, TIMEOUT_MS_RANGE.max),
|
|
323
351
|
config: { section: "doctor", path: ["timeoutMs"] },
|
|
324
352
|
},
|
|
325
353
|
{
|
|
@@ -367,7 +395,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
367
395
|
fallback: "0",
|
|
368
396
|
json: "number",
|
|
369
397
|
options: [],
|
|
370
|
-
text: amountRule(0,
|
|
398
|
+
text: amountRule(0, MAX_BUDGET),
|
|
371
399
|
config: { section: "price", path: ["budget"] },
|
|
372
400
|
},
|
|
373
401
|
{
|
|
@@ -377,11 +405,25 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
377
405
|
description: "The single-column glyph that leads a tool result or notice body, e.g. ╎ │ ┃ ▏.",
|
|
378
406
|
source: "bytefly",
|
|
379
407
|
kind: "text",
|
|
380
|
-
fallback:
|
|
408
|
+
fallback: DEFAULT_CHROME_CONFIG.rail,
|
|
381
409
|
options: [],
|
|
382
410
|
text: RAIL_RULE,
|
|
383
411
|
config: { section: "chrome", path: ["rail"] },
|
|
384
412
|
},
|
|
413
|
+
{
|
|
414
|
+
id: "goal.achieved",
|
|
415
|
+
group: "transcript",
|
|
416
|
+
label: "Achieved goal",
|
|
417
|
+
description: "How a completed goal renders: the dimmed rail line, or nothing.",
|
|
418
|
+
source: "bytefly",
|
|
419
|
+
kind: "enum",
|
|
420
|
+
fallback: "hide",
|
|
421
|
+
options: [
|
|
422
|
+
{ value: "show", label: "Show" },
|
|
423
|
+
{ value: "hide", label: "Hide" },
|
|
424
|
+
],
|
|
425
|
+
config: { section: "goal", path: ["achieved"] },
|
|
426
|
+
},
|
|
385
427
|
{
|
|
386
428
|
id: "outputPad",
|
|
387
429
|
group: "transcript",
|
|
@@ -396,6 +438,18 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
396
438
|
{ value: "1", label: "1" },
|
|
397
439
|
],
|
|
398
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
|
+
},
|
|
399
453
|
{
|
|
400
454
|
id: "showHardwareCursor",
|
|
401
455
|
group: "terminal",
|
|
@@ -407,28 +461,164 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
407
461
|
json: "boolean",
|
|
408
462
|
options: ON_OFF,
|
|
409
463
|
},
|
|
464
|
+
{
|
|
465
|
+
id: "worktree.baseRef",
|
|
466
|
+
group: "worktree",
|
|
467
|
+
label: "Base ref",
|
|
468
|
+
description: "Where a new worktree branches from: the remote default branch, or the current local HEAD.",
|
|
469
|
+
source: "bytefly",
|
|
470
|
+
kind: "enum",
|
|
471
|
+
fallback: "fresh",
|
|
472
|
+
options: [
|
|
473
|
+
{ value: "fresh", label: "Fresh" },
|
|
474
|
+
{ value: "head", label: "Head" },
|
|
475
|
+
],
|
|
476
|
+
config: { section: "worktree", path: ["baseRef"] },
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
id: "worktree.branchPrefix",
|
|
480
|
+
group: "worktree",
|
|
481
|
+
label: "Branch prefix",
|
|
482
|
+
description: "Prepended to every branch a managed worktree creates.",
|
|
483
|
+
source: "bytefly",
|
|
484
|
+
kind: "text",
|
|
485
|
+
fallback: "worktree-",
|
|
486
|
+
options: [],
|
|
487
|
+
text: {
|
|
488
|
+
// The loader trims and rejects an empty prefix but sets no ceiling; this
|
|
489
|
+
// only stops a runaway buffer.
|
|
490
|
+
maxLength: 80,
|
|
491
|
+
validate: (raw) => (raw.trim() === "" ? "A prefix is required." : undefined),
|
|
492
|
+
},
|
|
493
|
+
config: { section: "worktree", path: ["branchPrefix"] },
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: "worktree.fetchRemote",
|
|
497
|
+
group: "worktree",
|
|
498
|
+
label: "Fetch remote",
|
|
499
|
+
description: "Fetch the remote before branching, so a new worktree starts from its latest commit.",
|
|
500
|
+
source: "bytefly",
|
|
501
|
+
kind: "boolean",
|
|
502
|
+
fallback: "true",
|
|
503
|
+
json: "boolean",
|
|
504
|
+
options: ON_OFF,
|
|
505
|
+
config: { section: "worktree", path: ["fetchRemote"] },
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: "worktree.pruneAfterDays",
|
|
509
|
+
group: "worktree",
|
|
510
|
+
label: "Prune after (days)",
|
|
511
|
+
description: "Age at which `/worktree prune` removes a clean worktree nobody is using.",
|
|
512
|
+
source: "bytefly",
|
|
513
|
+
kind: "text",
|
|
514
|
+
fallback: "7",
|
|
515
|
+
json: "number",
|
|
516
|
+
options: [],
|
|
517
|
+
text: integerRule(0, 3650),
|
|
518
|
+
config: { section: "worktree", path: ["pruneAfterDays"] },
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
id: "rewind.autoSnapshots",
|
|
522
|
+
group: "snapshots",
|
|
523
|
+
label: "Automatic snapshots",
|
|
524
|
+
description: "Take one rewind snapshot per prompt, before the first mutating tool call.",
|
|
525
|
+
source: "bytefly",
|
|
526
|
+
kind: "boolean",
|
|
527
|
+
fallback: "true",
|
|
528
|
+
json: "boolean",
|
|
529
|
+
options: ON_OFF,
|
|
530
|
+
config: { section: "rewind", path: ["autoSnapshots"] },
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
id: "rewind.max",
|
|
534
|
+
group: "snapshots",
|
|
535
|
+
label: "Snapshots kept",
|
|
536
|
+
description: "Snapshot refs kept per repository, including pre-restore safety snapshots; 0 keeps none.",
|
|
537
|
+
source: "bytefly",
|
|
538
|
+
kind: "text",
|
|
539
|
+
fallback: "20",
|
|
540
|
+
json: "number",
|
|
541
|
+
options: [],
|
|
542
|
+
text: integerRule(0, 1000),
|
|
543
|
+
config: { section: "rewind", path: ["max"] },
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
id: "rewind.safetySnapshot",
|
|
547
|
+
group: "snapshots",
|
|
548
|
+
label: "Safety snapshot",
|
|
549
|
+
description: "Snapshot the current state before a restore, so a rewind can itself be undone.",
|
|
550
|
+
source: "bytefly",
|
|
551
|
+
kind: "boolean",
|
|
552
|
+
fallback: "true",
|
|
553
|
+
json: "boolean",
|
|
554
|
+
options: ON_OFF,
|
|
555
|
+
config: { section: "rewind", path: ["safetySnapshot"] },
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: "job.wakeOnFinish",
|
|
559
|
+
group: "jobs",
|
|
560
|
+
label: "Wake when a job finishes",
|
|
561
|
+
description: "Start one agent turn when any background job finishes.",
|
|
562
|
+
source: "bytefly",
|
|
563
|
+
kind: "boolean",
|
|
564
|
+
fallback: "false",
|
|
565
|
+
json: "boolean",
|
|
566
|
+
options: ON_OFF,
|
|
567
|
+
config: { section: "job", path: ["wakeOnFinish"] },
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
id: "job.detachedByDefault",
|
|
571
|
+
group: "jobs",
|
|
572
|
+
label: "Keep jobs after exit",
|
|
573
|
+
description: "Leave jobs running when the session ends, unless the job opts out.",
|
|
574
|
+
source: "bytefly",
|
|
575
|
+
kind: "boolean",
|
|
576
|
+
fallback: "false",
|
|
577
|
+
json: "boolean",
|
|
578
|
+
options: ON_OFF,
|
|
579
|
+
config: { section: "job", path: ["detachedByDefault"] },
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
id: "watch.wakeOnMatch",
|
|
583
|
+
group: "jobs",
|
|
584
|
+
label: "Wake on a match",
|
|
585
|
+
description: "Start one agent turn when a watcher matches while the agent is idle.",
|
|
586
|
+
source: "bytefly",
|
|
587
|
+
kind: "boolean",
|
|
588
|
+
fallback: "false",
|
|
589
|
+
json: "boolean",
|
|
590
|
+
options: ON_OFF,
|
|
591
|
+
config: { section: "watch", path: ["wakeOnMatch"] },
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
id: "memory.inject",
|
|
595
|
+
group: "memory",
|
|
596
|
+
label: "Inject notes",
|
|
597
|
+
description: "Put stored memory notes in context before each run.",
|
|
598
|
+
source: "bytefly",
|
|
599
|
+
kind: "boolean",
|
|
600
|
+
fallback: "true",
|
|
601
|
+
json: "boolean",
|
|
602
|
+
options: ON_OFF,
|
|
603
|
+
config: { section: "memory", path: ["inject"] },
|
|
604
|
+
},
|
|
410
605
|
];
|
|
411
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
|
+
|
|
412
610
|
/** The rows a scope can write: app rows are personal-file only. */
|
|
413
611
|
export function rowsForScope(scope: WriteScope): readonly SettingDescriptor[] {
|
|
414
612
|
return scope === "global" ? SETTING_ROWS : SETTING_ROWS.filter((row) => row.source === "bytefly");
|
|
415
613
|
}
|
|
416
614
|
|
|
417
|
-
/** The number of rows the section shows for `scope`. */
|
|
418
|
-
export function countSettings(scope: WriteScope): number {
|
|
419
|
-
return rowsForScope(scope).length;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/** Whether `raw` is one of a row's curated options. */
|
|
423
|
-
export function validValue(row: SettingDescriptor, raw: string): boolean {
|
|
424
|
-
return row.options.some((option) => option.value === raw);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
615
|
/**
|
|
428
616
|
* Why a text row cannot store `raw`, or `undefined` when it can.
|
|
429
617
|
*
|
|
430
|
-
* The limits
|
|
431
|
-
*
|
|
618
|
+
* The limits are the loaders' own (`notify.title` is sliced to `MAX_TITLE_LENGTH`,
|
|
619
|
+
* the rail to one column) so the editor cannot save a value the next read shortens.
|
|
620
|
+
* A row that is not text is refused here rather than in the caller, so a programmatic
|
|
621
|
+
* `setValue` gets the same reason the screen would show.
|
|
432
622
|
*/
|
|
433
623
|
export function textError(row: SettingDescriptor, raw: string): string | undefined {
|
|
434
624
|
const rule = row.text;
|
|
@@ -439,9 +629,18 @@ export function textError(row: SettingDescriptor, raw: string): string | undefin
|
|
|
439
629
|
return rule.validate?.(raw);
|
|
440
630
|
}
|
|
441
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
|
+
|
|
442
638
|
/** The display label for `raw`, or the raw value when it is a valid custom one. */
|
|
443
639
|
function labelFor(row: SettingDescriptor, raw: string): string {
|
|
444
|
-
|
|
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;
|
|
445
644
|
}
|
|
446
645
|
|
|
447
646
|
/** Read an app preference and where it came from. */
|
|
@@ -449,18 +648,23 @@ function appValue(snapshot: SettingsSnapshot, row: SettingDescriptor): { value:
|
|
|
449
648
|
switch (row.id) {
|
|
450
649
|
case "byteflyUi": {
|
|
451
650
|
const quiet = snapshot.app.quietStartup;
|
|
452
|
-
return { value: quiet === true ? "true" :
|
|
651
|
+
return { value: quiet === true ? "true" : row.fallback, origin: quiet === undefined ? "default" : "global" };
|
|
453
652
|
}
|
|
454
653
|
case "outputPad":
|
|
455
654
|
return {
|
|
456
|
-
value: snapshot.app.outputPad === 0 ? "0" :
|
|
655
|
+
value: snapshot.app.outputPad === 0 ? "0" : row.fallback,
|
|
457
656
|
origin: snapshot.app.outputPad === undefined ? "default" : "global",
|
|
458
657
|
};
|
|
459
658
|
case "showHardwareCursor":
|
|
460
659
|
return {
|
|
461
|
-
value: snapshot.app.showHardwareCursor === true ? "true" :
|
|
660
|
+
value: snapshot.app.showHardwareCursor === true ? "true" : row.fallback,
|
|
462
661
|
origin: snapshot.app.showHardwareCursor === undefined ? "default" : "global",
|
|
463
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
|
+
}
|
|
464
668
|
default:
|
|
465
669
|
throw new Error(`Unknown app setting ${row.id}`);
|
|
466
670
|
}
|
|
@@ -473,7 +677,11 @@ function statesConfig(config: SettingConfig, document: Record<string, unknown>):
|
|
|
473
677
|
|
|
474
678
|
/** Where the effective value came from: the project file, the personal one, or nothing. */
|
|
475
679
|
function originFor(config: SettingConfig, snapshot: SettingsSnapshot, scope: WriteScope): SettingOrigin {
|
|
476
|
-
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
|
+
}
|
|
477
685
|
return statesConfig(config, snapshot.global) ? "global" : "default";
|
|
478
686
|
}
|
|
479
687
|
|
|
@@ -484,7 +692,11 @@ type SectionReader = (
|
|
|
484
692
|
path: readonly string[],
|
|
485
693
|
) => unknown;
|
|
486
694
|
|
|
487
|
-
|
|
695
|
+
/**
|
|
696
|
+
* The four sections bytefly owns, each read through its own normalizer so the
|
|
697
|
+
* screen shows the value the loader keeps.
|
|
698
|
+
*/
|
|
699
|
+
const SECTION_READERS: Partial<Record<ConfigSectionName, SectionReader>> = {
|
|
488
700
|
notify: (globalRaw, projectRaw, path) =>
|
|
489
701
|
configValueAt(normalizeNotifyConfig(projectRaw, normalizeNotifyConfig(globalRaw, DEFAULT_NOTIFY_CONFIG)), path),
|
|
490
702
|
chrome: (globalRaw, projectRaw, path) =>
|
|
@@ -495,7 +707,22 @@ const SECTION_READERS: Record<ConfigSectionName, SectionReader> = {
|
|
|
495
707
|
configValueAt(normalizeByteflyConfig(projectRaw, normalizeByteflyConfig(globalRaw, DEFAULT_BYTEFLY_CONFIG)), path),
|
|
496
708
|
};
|
|
497
709
|
|
|
498
|
-
/**
|
|
710
|
+
/**
|
|
711
|
+
* Read a leaf whose owning extension owns the normalizer.
|
|
712
|
+
*
|
|
713
|
+
* A section without a reader here belongs to another extension, and importing its
|
|
714
|
+
* normalizer would be one extension importing another's module. So the row states
|
|
715
|
+
* the loader's default in `fallback`, `test/settings-coverage.test.ts` holds the
|
|
716
|
+
* two together, and the visible cost is that a hand-edited value outside the
|
|
717
|
+
* loader's clamp shows as written until it is reset.
|
|
718
|
+
*/
|
|
719
|
+
function rawLeaf(snapshot: SettingsSnapshot, config: SettingConfig, scope: WriteScope): unknown {
|
|
720
|
+
const projectLeaf =
|
|
721
|
+
scope === "project" ? configValueAt(configSection(snapshot.project, config.section), config.path) : undefined;
|
|
722
|
+
return projectLeaf ?? configValueAt(configSection(snapshot.global, config.section), config.path);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** Read a `bytefly.json` preference: normalized where bytefly owns the section. */
|
|
499
726
|
function byteflyValue(
|
|
500
727
|
snapshot: SettingsSnapshot,
|
|
501
728
|
row: SettingDescriptor,
|
|
@@ -505,8 +732,12 @@ function byteflyValue(
|
|
|
505
732
|
if (!config) throw new Error(`Setting ${row.id} has no config section`);
|
|
506
733
|
const globalRaw = configSection(snapshot.global, config.section);
|
|
507
734
|
const projectRaw = scope === "project" ? configSection(snapshot.project, config.section) : undefined;
|
|
508
|
-
const
|
|
735
|
+
const reader = SECTION_READERS[config.section];
|
|
736
|
+
const leaf = reader ? reader(globalRaw, projectRaw, config.path) : rawLeaf(snapshot, config, scope);
|
|
509
737
|
return {
|
|
738
|
+
// A normalizer is total, so its fallback only covers a future row whose
|
|
739
|
+
// normalizer has no default yet; a raw read needs it when neither document
|
|
740
|
+
// states the key.
|
|
510
741
|
value: leaf === undefined ? row.fallback : String(leaf),
|
|
511
742
|
origin: originFor(config, snapshot, scope),
|
|
512
743
|
};
|
|
@@ -515,8 +746,10 @@ function byteflyValue(
|
|
|
515
746
|
/** Build the rows for `scope` from a snapshot. */
|
|
516
747
|
export function settingViews(snapshot: SettingsSnapshot, scope: WriteScope): SettingView[] {
|
|
517
748
|
return rowsForScope(scope).map((row) => {
|
|
749
|
+
const options = row.id === "theme" ? themeOptions(snapshot.themes) : row.options;
|
|
518
750
|
const { value, origin } = row.source === "app" ? appValue(snapshot, row) : byteflyValue(snapshot, row, scope);
|
|
519
|
-
|
|
751
|
+
const view = { ...row, options, value };
|
|
752
|
+
return { ...view, valueLabel: labelFor(view, value), origin };
|
|
520
753
|
});
|
|
521
754
|
}
|
|
522
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) {
|