@gavin-hu/my-pi-agent 0.13.1 → 0.14.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 +14 -11
- package/README.zh-CN.md +12 -12
- package/extensions/bytefly/README.md +140 -58
- package/extensions/bytefly/doctor/config.ts +4 -1
- package/extensions/bytefly/footer/price/config.ts +3 -2
- package/extensions/bytefly/header/component.ts +30 -8
- package/extensions/bytefly/header/config.ts +22 -0
- package/extensions/bytefly/header/register.ts +2 -1
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notify/config.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +1 -1
- package/extensions/bytefly/panel/format.ts +12 -19
- package/extensions/bytefly/panel/settings-runtime.ts +62 -18
- package/extensions/bytefly/panel/settings.ts +239 -43
- package/extensions/bytefly/panel/tui/settings-view.ts +49 -3
- package/extensions/bytefly/panel/tui.ts +87 -19
- package/extensions/bytefly/separator/component.ts +7 -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/working/config.ts +4 -5
- package/extensions/bytefly/working/format.ts +24 -7
- package/extensions/bytefly/working/register.ts +34 -3
- package/extensions/memory/README.md +2 -0
- package/extensions/plan/README.md +12 -0
- package/extensions/subagent/README.md +60 -5
- package/extensions/subagent/config.ts +58 -0
- package/extensions/subagent/index.ts +27 -1
- package/extensions/subagent/orchestrate.ts +16 -3
- package/extensions/subagent/render.ts +12 -0
- package/extensions/subagent/run.ts +100 -37
- package/extensions/subagent/transcripts.ts +202 -0
- package/extensions/subagent/types.ts +12 -0
- package/extensions/watch/README.md +2 -0
- package/extensions/wechat/README.md +2 -0
- package/lib/README.md +1 -1
- package/lib/config.ts +33 -9
- package/lib/format.ts +6 -0
- package/package.json +2 -1
- package/themes/bytefly-dark.json +2 -3
- package/themes/bytefly-light.json +2 -3
|
@@ -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;
|
|
@@ -116,7 +138,7 @@ export interface SettingsSnapshot {
|
|
|
116
138
|
}
|
|
117
139
|
|
|
118
140
|
/** One rendered group: its heading and the rows under it. */
|
|
119
|
-
|
|
141
|
+
interface GroupedSettings {
|
|
120
142
|
key: string;
|
|
121
143
|
label: string;
|
|
122
144
|
views: SettingView[];
|
|
@@ -130,6 +152,10 @@ export const SETTING_GROUPS: readonly SettingGroup[] = [
|
|
|
130
152
|
{ key: "pricing", label: "Pricing" },
|
|
131
153
|
{ key: "transcript", label: "Transcript" },
|
|
132
154
|
{ key: "terminal", label: "Terminal" },
|
|
155
|
+
{ key: "worktree", label: "Worktree" },
|
|
156
|
+
{ key: "snapshots", label: "Snapshots" },
|
|
157
|
+
{ key: "jobs", label: "Jobs and watchers" },
|
|
158
|
+
{ key: "memory", label: "Memory" },
|
|
133
159
|
];
|
|
134
160
|
|
|
135
161
|
const ON_OFF: readonly SettingOption[] = [
|
|
@@ -147,8 +173,8 @@ const METHOD_OPTIONS: readonly SettingOption[] = [
|
|
|
147
173
|
{ value: "notify-send", label: "Linux" },
|
|
148
174
|
];
|
|
149
175
|
|
|
150
|
-
/** Cost precision 0
|
|
151
|
-
const DECIMALS_OPTIONS: readonly SettingOption[] =
|
|
176
|
+
/** Cost precision 0 through the loader's ceiling, the range `price` clamps `decimals` to. */
|
|
177
|
+
const DECIMALS_OPTIONS: readonly SettingOption[] = Array.from({ length: MAX_DECIMALS + 1 }, (_, value) => ({
|
|
152
178
|
value: String(value),
|
|
153
179
|
label: String(value),
|
|
154
180
|
}));
|
|
@@ -292,7 +318,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
292
318
|
fallback: "Pi",
|
|
293
319
|
options: [],
|
|
294
320
|
text: {
|
|
295
|
-
maxLength:
|
|
321
|
+
maxLength: MAX_TITLE_LENGTH,
|
|
296
322
|
validate: (raw) => (raw.trim() === "" ? "A title is required." : undefined),
|
|
297
323
|
},
|
|
298
324
|
config: { section: "notify", path: ["title"] },
|
|
@@ -313,13 +339,13 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
313
339
|
id: "doctor.timeoutMs",
|
|
314
340
|
group: "diagnostics",
|
|
315
341
|
label: "Probe timeout",
|
|
316
|
-
description:
|
|
342
|
+
description: `How long one doctor probe may take, in milliseconds; ${TIMEOUT_MS_RANGE.min} to ${TIMEOUT_MS_RANGE.max}.`,
|
|
317
343
|
source: "bytefly",
|
|
318
344
|
kind: "text",
|
|
319
345
|
fallback: String(DEFAULT_DOCTOR_CONFIG.timeoutMs),
|
|
320
346
|
json: "number",
|
|
321
347
|
options: [],
|
|
322
|
-
text: integerRule(
|
|
348
|
+
text: integerRule(TIMEOUT_MS_RANGE.min, TIMEOUT_MS_RANGE.max),
|
|
323
349
|
config: { section: "doctor", path: ["timeoutMs"] },
|
|
324
350
|
},
|
|
325
351
|
{
|
|
@@ -367,7 +393,7 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
367
393
|
fallback: "0",
|
|
368
394
|
json: "number",
|
|
369
395
|
options: [],
|
|
370
|
-
text: amountRule(0,
|
|
396
|
+
text: amountRule(0, MAX_BUDGET),
|
|
371
397
|
config: { section: "price", path: ["budget"] },
|
|
372
398
|
},
|
|
373
399
|
{
|
|
@@ -377,11 +403,25 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
377
403
|
description: "The single-column glyph that leads a tool result or notice body, e.g. ╎ │ ┃ ▏.",
|
|
378
404
|
source: "bytefly",
|
|
379
405
|
kind: "text",
|
|
380
|
-
fallback:
|
|
406
|
+
fallback: DEFAULT_CHROME_CONFIG.rail,
|
|
381
407
|
options: [],
|
|
382
408
|
text: RAIL_RULE,
|
|
383
409
|
config: { section: "chrome", path: ["rail"] },
|
|
384
410
|
},
|
|
411
|
+
{
|
|
412
|
+
id: "goal.achieved",
|
|
413
|
+
group: "transcript",
|
|
414
|
+
label: "Achieved goal",
|
|
415
|
+
description: "How a completed goal renders: the dimmed rail line, or nothing.",
|
|
416
|
+
source: "bytefly",
|
|
417
|
+
kind: "enum",
|
|
418
|
+
fallback: "hide",
|
|
419
|
+
options: [
|
|
420
|
+
{ value: "show", label: "Show" },
|
|
421
|
+
{ value: "hide", label: "Hide" },
|
|
422
|
+
],
|
|
423
|
+
config: { section: "goal", path: ["achieved"] },
|
|
424
|
+
},
|
|
385
425
|
{
|
|
386
426
|
id: "outputPad",
|
|
387
427
|
group: "transcript",
|
|
@@ -407,6 +447,147 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
407
447
|
json: "boolean",
|
|
408
448
|
options: ON_OFF,
|
|
409
449
|
},
|
|
450
|
+
{
|
|
451
|
+
id: "worktree.baseRef",
|
|
452
|
+
group: "worktree",
|
|
453
|
+
label: "Base ref",
|
|
454
|
+
description: "Where a new worktree branches from: the remote default branch, or the current local HEAD.",
|
|
455
|
+
source: "bytefly",
|
|
456
|
+
kind: "enum",
|
|
457
|
+
fallback: "fresh",
|
|
458
|
+
options: [
|
|
459
|
+
{ value: "fresh", label: "Fresh" },
|
|
460
|
+
{ value: "head", label: "Head" },
|
|
461
|
+
],
|
|
462
|
+
config: { section: "worktree", path: ["baseRef"] },
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
id: "worktree.branchPrefix",
|
|
466
|
+
group: "worktree",
|
|
467
|
+
label: "Branch prefix",
|
|
468
|
+
description: "Prepended to every branch a managed worktree creates.",
|
|
469
|
+
source: "bytefly",
|
|
470
|
+
kind: "text",
|
|
471
|
+
fallback: "worktree-",
|
|
472
|
+
options: [],
|
|
473
|
+
text: {
|
|
474
|
+
// The loader trims and rejects an empty prefix but sets no ceiling; this
|
|
475
|
+
// only stops a runaway buffer.
|
|
476
|
+
maxLength: 80,
|
|
477
|
+
validate: (raw) => (raw.trim() === "" ? "A prefix is required." : undefined),
|
|
478
|
+
},
|
|
479
|
+
config: { section: "worktree", path: ["branchPrefix"] },
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: "worktree.fetchRemote",
|
|
483
|
+
group: "worktree",
|
|
484
|
+
label: "Fetch remote",
|
|
485
|
+
description: "Fetch the remote before branching, so a new worktree starts from its latest commit.",
|
|
486
|
+
source: "bytefly",
|
|
487
|
+
kind: "boolean",
|
|
488
|
+
fallback: "true",
|
|
489
|
+
json: "boolean",
|
|
490
|
+
options: ON_OFF,
|
|
491
|
+
config: { section: "worktree", path: ["fetchRemote"] },
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
id: "worktree.pruneAfterDays",
|
|
495
|
+
group: "worktree",
|
|
496
|
+
label: "Prune after (days)",
|
|
497
|
+
description: "Age at which `/worktree prune` removes a clean worktree nobody is using.",
|
|
498
|
+
source: "bytefly",
|
|
499
|
+
kind: "text",
|
|
500
|
+
fallback: "7",
|
|
501
|
+
json: "number",
|
|
502
|
+
options: [],
|
|
503
|
+
text: integerRule(0, 3650),
|
|
504
|
+
config: { section: "worktree", path: ["pruneAfterDays"] },
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
id: "rewind.autoSnapshots",
|
|
508
|
+
group: "snapshots",
|
|
509
|
+
label: "Automatic snapshots",
|
|
510
|
+
description: "Take one rewind snapshot per prompt, before the first mutating tool call.",
|
|
511
|
+
source: "bytefly",
|
|
512
|
+
kind: "boolean",
|
|
513
|
+
fallback: "true",
|
|
514
|
+
json: "boolean",
|
|
515
|
+
options: ON_OFF,
|
|
516
|
+
config: { section: "rewind", path: ["autoSnapshots"] },
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
id: "rewind.max",
|
|
520
|
+
group: "snapshots",
|
|
521
|
+
label: "Snapshots kept",
|
|
522
|
+
description: "Snapshots kept per repository before the oldest are pruned; 0 keeps none.",
|
|
523
|
+
source: "bytefly",
|
|
524
|
+
kind: "text",
|
|
525
|
+
fallback: "20",
|
|
526
|
+
json: "number",
|
|
527
|
+
options: [],
|
|
528
|
+
text: integerRule(0, 1000),
|
|
529
|
+
config: { section: "rewind", path: ["max"] },
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
id: "rewind.safetySnapshot",
|
|
533
|
+
group: "snapshots",
|
|
534
|
+
label: "Safety snapshot",
|
|
535
|
+
description: "Snapshot the current state before a restore, so a rewind can itself be undone.",
|
|
536
|
+
source: "bytefly",
|
|
537
|
+
kind: "boolean",
|
|
538
|
+
fallback: "true",
|
|
539
|
+
json: "boolean",
|
|
540
|
+
options: ON_OFF,
|
|
541
|
+
config: { section: "rewind", path: ["safetySnapshot"] },
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
id: "job.wakeOnFinish",
|
|
545
|
+
group: "jobs",
|
|
546
|
+
label: "Wake when a job finishes",
|
|
547
|
+
description: "Start one agent turn when any background job finishes.",
|
|
548
|
+
source: "bytefly",
|
|
549
|
+
kind: "boolean",
|
|
550
|
+
fallback: "false",
|
|
551
|
+
json: "boolean",
|
|
552
|
+
options: ON_OFF,
|
|
553
|
+
config: { section: "job", path: ["wakeOnFinish"] },
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
id: "job.detachedByDefault",
|
|
557
|
+
group: "jobs",
|
|
558
|
+
label: "Keep jobs after exit",
|
|
559
|
+
description: "Leave jobs running when the session ends, unless the job opts out.",
|
|
560
|
+
source: "bytefly",
|
|
561
|
+
kind: "boolean",
|
|
562
|
+
fallback: "false",
|
|
563
|
+
json: "boolean",
|
|
564
|
+
options: ON_OFF,
|
|
565
|
+
config: { section: "job", path: ["detachedByDefault"] },
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
id: "watch.wakeOnMatch",
|
|
569
|
+
group: "jobs",
|
|
570
|
+
label: "Wake on a match",
|
|
571
|
+
description: "Start one agent turn when a watcher matches while the agent is idle.",
|
|
572
|
+
source: "bytefly",
|
|
573
|
+
kind: "boolean",
|
|
574
|
+
fallback: "false",
|
|
575
|
+
json: "boolean",
|
|
576
|
+
options: ON_OFF,
|
|
577
|
+
config: { section: "watch", path: ["wakeOnMatch"] },
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
id: "memory.inject",
|
|
581
|
+
group: "memory",
|
|
582
|
+
label: "Inject notes",
|
|
583
|
+
description: "Put stored memory notes in context before each run.",
|
|
584
|
+
source: "bytefly",
|
|
585
|
+
kind: "boolean",
|
|
586
|
+
fallback: "true",
|
|
587
|
+
json: "boolean",
|
|
588
|
+
options: ON_OFF,
|
|
589
|
+
config: { section: "memory", path: ["inject"] },
|
|
590
|
+
},
|
|
410
591
|
];
|
|
411
592
|
|
|
412
593
|
/** The rows a scope can write: app rows are personal-file only. */
|
|
@@ -414,21 +595,13 @@ export function rowsForScope(scope: WriteScope): readonly SettingDescriptor[] {
|
|
|
414
595
|
return scope === "global" ? SETTING_ROWS : SETTING_ROWS.filter((row) => row.source === "bytefly");
|
|
415
596
|
}
|
|
416
597
|
|
|
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
598
|
/**
|
|
428
599
|
* Why a text row cannot store `raw`, or `undefined` when it can.
|
|
429
600
|
*
|
|
430
|
-
* The limits
|
|
431
|
-
*
|
|
601
|
+
* The limits are the loaders' own (`notify.title` is sliced to `MAX_TITLE_LENGTH`,
|
|
602
|
+
* the rail to one column) so the editor cannot save a value the next read shortens.
|
|
603
|
+
* A row that is not text is refused here rather than in the caller, so a programmatic
|
|
604
|
+
* `setValue` gets the same reason the screen would show.
|
|
432
605
|
*/
|
|
433
606
|
export function textError(row: SettingDescriptor, raw: string): string | undefined {
|
|
434
607
|
const rule = row.text;
|
|
@@ -449,16 +622,16 @@ function appValue(snapshot: SettingsSnapshot, row: SettingDescriptor): { value:
|
|
|
449
622
|
switch (row.id) {
|
|
450
623
|
case "byteflyUi": {
|
|
451
624
|
const quiet = snapshot.app.quietStartup;
|
|
452
|
-
return { value: quiet === true ? "true" :
|
|
625
|
+
return { value: quiet === true ? "true" : row.fallback, origin: quiet === undefined ? "default" : "global" };
|
|
453
626
|
}
|
|
454
627
|
case "outputPad":
|
|
455
628
|
return {
|
|
456
|
-
value: snapshot.app.outputPad === 0 ? "0" :
|
|
629
|
+
value: snapshot.app.outputPad === 0 ? "0" : row.fallback,
|
|
457
630
|
origin: snapshot.app.outputPad === undefined ? "default" : "global",
|
|
458
631
|
};
|
|
459
632
|
case "showHardwareCursor":
|
|
460
633
|
return {
|
|
461
|
-
value: snapshot.app.showHardwareCursor === true ? "true" :
|
|
634
|
+
value: snapshot.app.showHardwareCursor === true ? "true" : row.fallback,
|
|
462
635
|
origin: snapshot.app.showHardwareCursor === undefined ? "default" : "global",
|
|
463
636
|
};
|
|
464
637
|
default:
|
|
@@ -484,7 +657,11 @@ type SectionReader = (
|
|
|
484
657
|
path: readonly string[],
|
|
485
658
|
) => unknown;
|
|
486
659
|
|
|
487
|
-
|
|
660
|
+
/**
|
|
661
|
+
* The four sections bytefly owns, each read through its own normalizer so the
|
|
662
|
+
* screen shows the value the loader keeps.
|
|
663
|
+
*/
|
|
664
|
+
const SECTION_READERS: Partial<Record<ConfigSectionName, SectionReader>> = {
|
|
488
665
|
notify: (globalRaw, projectRaw, path) =>
|
|
489
666
|
configValueAt(normalizeNotifyConfig(projectRaw, normalizeNotifyConfig(globalRaw, DEFAULT_NOTIFY_CONFIG)), path),
|
|
490
667
|
chrome: (globalRaw, projectRaw, path) =>
|
|
@@ -495,7 +672,22 @@ const SECTION_READERS: Record<ConfigSectionName, SectionReader> = {
|
|
|
495
672
|
configValueAt(normalizeByteflyConfig(projectRaw, normalizeByteflyConfig(globalRaw, DEFAULT_BYTEFLY_CONFIG)), path),
|
|
496
673
|
};
|
|
497
674
|
|
|
498
|
-
/**
|
|
675
|
+
/**
|
|
676
|
+
* Read a leaf whose owning extension owns the normalizer.
|
|
677
|
+
*
|
|
678
|
+
* A section without a reader here belongs to another extension, and importing its
|
|
679
|
+
* normalizer would be one extension importing another's module. So the row states
|
|
680
|
+
* the loader's default in `fallback`, `test/settings-coverage.test.ts` holds the
|
|
681
|
+
* two together, and the visible cost is that a hand-edited value outside the
|
|
682
|
+
* loader's clamp shows as written until it is reset.
|
|
683
|
+
*/
|
|
684
|
+
function rawLeaf(snapshot: SettingsSnapshot, config: SettingConfig, scope: WriteScope): unknown {
|
|
685
|
+
const projectLeaf =
|
|
686
|
+
scope === "project" ? configValueAt(configSection(snapshot.project, config.section), config.path) : undefined;
|
|
687
|
+
return projectLeaf ?? configValueAt(configSection(snapshot.global, config.section), config.path);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** Read a `bytefly.json` preference: normalized where bytefly owns the section. */
|
|
499
691
|
function byteflyValue(
|
|
500
692
|
snapshot: SettingsSnapshot,
|
|
501
693
|
row: SettingDescriptor,
|
|
@@ -505,8 +697,12 @@ function byteflyValue(
|
|
|
505
697
|
if (!config) throw new Error(`Setting ${row.id} has no config section`);
|
|
506
698
|
const globalRaw = configSection(snapshot.global, config.section);
|
|
507
699
|
const projectRaw = scope === "project" ? configSection(snapshot.project, config.section) : undefined;
|
|
508
|
-
const
|
|
700
|
+
const reader = SECTION_READERS[config.section];
|
|
701
|
+
const leaf = reader ? reader(globalRaw, projectRaw, config.path) : rawLeaf(snapshot, config, scope);
|
|
509
702
|
return {
|
|
703
|
+
// A normalizer is total, so its fallback only covers a future row whose
|
|
704
|
+
// normalizer has no default yet; a raw read needs it when neither document
|
|
705
|
+
// states the key.
|
|
510
706
|
value: leaf === undefined ? row.fallback : String(leaf),
|
|
511
707
|
origin: originFor(config, snapshot, scope),
|
|
512
708
|
};
|
|
@@ -57,7 +57,7 @@ export function projectNote(theme: Theme, width: number): string {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/** Where the focused row's value is written, and which scope supplied it. */
|
|
60
|
-
|
|
60
|
+
function originLine(view: SettingView, paths: { scope: WriteScope; cwd: string; agentDir: string }): string {
|
|
61
61
|
const path =
|
|
62
62
|
view.source === "app" ? join(paths.agentDir, PI_SETTINGS_FILE) : configPath(paths.scope, paths.cwd, paths.agentDir);
|
|
63
63
|
const file = sanitizeLine(path);
|
|
@@ -105,8 +105,54 @@ export function settingDetail(options: {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/** The section's key hints, in the order they drop as the width runs out. */
|
|
108
|
-
export function settingsHints(): string[] {
|
|
109
|
-
return [
|
|
108
|
+
export function settingsHints(json = false): string[] {
|
|
109
|
+
return [
|
|
110
|
+
"Esc close",
|
|
111
|
+
"←/→",
|
|
112
|
+
"Space change",
|
|
113
|
+
"Tab scope",
|
|
114
|
+
"u reset",
|
|
115
|
+
json ? "J hide" : "J JSON",
|
|
116
|
+
"[ ] group",
|
|
117
|
+
"↑/↓ select",
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The focused row's section as the active scope stores it.
|
|
123
|
+
*
|
|
124
|
+
* This is the only view of the keys the catalogue leaves to JSON, so it names the
|
|
125
|
+
* section and its file rather than pretending to be complete: the pane is clipped
|
|
126
|
+
* to the detail budget and says how many lines it left out. `section` is the row's
|
|
127
|
+
* `bytefly.json` section, which the caller has already established.
|
|
128
|
+
*/
|
|
129
|
+
export function sectionJsonPane(options: {
|
|
130
|
+
theme: Theme;
|
|
131
|
+
width: number;
|
|
132
|
+
section: string;
|
|
133
|
+
view: SettingView;
|
|
134
|
+
document: unknown;
|
|
135
|
+
budget: number;
|
|
136
|
+
paths: { scope: WriteScope; cwd: string; agentDir: string };
|
|
137
|
+
}): string[] {
|
|
138
|
+
const { theme, width, section, view, document, budget, paths } = options;
|
|
139
|
+
const source = paint(theme, "faint", originLine(view, paths));
|
|
140
|
+
const head = `${paint(theme, "meta", sanitizeLine(section))} ${paint(theme, "faint", "—")} ${source}`;
|
|
141
|
+
const lines = [clip(`${INDENT}${head}`, width)];
|
|
142
|
+
const room = Math.max(0, budget - lines.length);
|
|
143
|
+
const body = document === undefined ? [] : JSON.stringify(document, null, 2).split("\n");
|
|
144
|
+
|
|
145
|
+
if (body.length === 0) {
|
|
146
|
+
if (room > 0) lines.push(clip(`${INDENT}${paint(theme, "faint", "Not set in this scope.")}`, width));
|
|
147
|
+
return lines;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const shown = body.slice(0, room);
|
|
151
|
+
if (body.length > room && shown.length > 0) {
|
|
152
|
+
shown[shown.length - 1] = `… ${body.length - room} more lines`;
|
|
153
|
+
}
|
|
154
|
+
for (const line of shown) lines.push(clip(`${INDENT}${paint(theme, "faint", sanitizeLine(line))}`, width));
|
|
155
|
+
return lines;
|
|
110
156
|
}
|
|
111
157
|
|
|
112
158
|
/** The editor's key hints: `Esc` cancels the edit, it does not close the screen. */
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
import {
|
|
44
44
|
editHints,
|
|
45
45
|
projectNote,
|
|
46
|
+
sectionJsonPane,
|
|
46
47
|
settingDetail,
|
|
47
48
|
settingRow,
|
|
48
49
|
settingsHints,
|
|
@@ -103,6 +104,8 @@ export class PanelComponent implements Component {
|
|
|
103
104
|
private notice: string | undefined;
|
|
104
105
|
/** The text row being edited: its id, the buffer, and the caret offset. */
|
|
105
106
|
private editing: { id: string; buffer: string; caret: number } | undefined;
|
|
107
|
+
/** True while the Settings detail pane shows the focused row's section as JSON. */
|
|
108
|
+
private json = false;
|
|
106
109
|
/** The visible section. */
|
|
107
110
|
private section: PanelSection;
|
|
108
111
|
|
|
@@ -182,6 +185,8 @@ export class PanelComponent implements Component {
|
|
|
182
185
|
);
|
|
183
186
|
return;
|
|
184
187
|
}
|
|
188
|
+
// The JSON pane names a file, so a scope switch has to close it.
|
|
189
|
+
this.json = false;
|
|
185
190
|
this.options.runtime.setScope(next);
|
|
186
191
|
this.options.requestRender();
|
|
187
192
|
}
|
|
@@ -234,6 +239,29 @@ export class PanelComponent implements Component {
|
|
|
234
239
|
.then(() => this.options.requestRender());
|
|
235
240
|
}
|
|
236
241
|
|
|
242
|
+
/** Delete the focused setting's leaf, so its default applies again. */
|
|
243
|
+
private resetSetting(): void {
|
|
244
|
+
const entry = this.activeEntries()[this.cursor.selected];
|
|
245
|
+
if (entry?.kind !== "setting") return;
|
|
246
|
+
const scope = this.options.runtime.scope();
|
|
247
|
+
this.chain = this.chain
|
|
248
|
+
.then(() => this.options.settings.reset(scope, entry.setting.id))
|
|
249
|
+
.catch((error: unknown) => this.notify(error instanceof Error ? error.message : String(error)))
|
|
250
|
+
.then(() => this.options.requestRender());
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Toggle the detail pane between the row and its whole section as stored. */
|
|
254
|
+
private toggleJson(): void {
|
|
255
|
+
const entry = this.activeEntries()[this.cursor.selected];
|
|
256
|
+
if (entry?.kind !== "setting") return;
|
|
257
|
+
if (!entry.setting.config) {
|
|
258
|
+
this.notify(`${entry.setting.label} is a Pi setting; bytefly.json has no section for it.`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.json = !this.json;
|
|
262
|
+
this.options.requestRender();
|
|
263
|
+
}
|
|
264
|
+
|
|
237
265
|
/** Store the editor's buffer; a refusal keeps it open with the reason shown. */
|
|
238
266
|
private commitEdit(): void {
|
|
239
267
|
const editing = this.editing;
|
|
@@ -348,6 +376,7 @@ export class PanelComponent implements Component {
|
|
|
348
376
|
private switchSection(next: PanelSection): void {
|
|
349
377
|
if (next === this.section) return;
|
|
350
378
|
this.section = next;
|
|
379
|
+
this.json = false;
|
|
351
380
|
this.prime();
|
|
352
381
|
this.options.requestRender();
|
|
353
382
|
}
|
|
@@ -362,6 +391,14 @@ export class PanelComponent implements Component {
|
|
|
362
391
|
this.select(lastRow(this.activeEntries()));
|
|
363
392
|
return;
|
|
364
393
|
}
|
|
394
|
+
if (data === "[") {
|
|
395
|
+
this.jumpGroup(-1);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (data === "]") {
|
|
399
|
+
this.jumpGroup(1);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
365
402
|
switch (navIntent(data)) {
|
|
366
403
|
case "up":
|
|
367
404
|
this.move(-1);
|
|
@@ -384,6 +421,26 @@ export class PanelComponent implements Component {
|
|
|
384
421
|
}
|
|
385
422
|
}
|
|
386
423
|
|
|
424
|
+
/** Move to the first row of the next (or previous) group. */
|
|
425
|
+
private jumpGroup(direction: -1 | 1): void {
|
|
426
|
+
const entries = this.activeEntries();
|
|
427
|
+
const headers = entries.flatMap((entry, index) => (entry.kind === "group" ? [index] : []));
|
|
428
|
+
const selected = this.cursor.selected;
|
|
429
|
+
// Step past the focused row's own header: landing on it would not move.
|
|
430
|
+
const own = headers.filter((index) => index < selected).at(-1);
|
|
431
|
+
const target =
|
|
432
|
+
direction === 1
|
|
433
|
+
? headers.find((index) => index > selected)
|
|
434
|
+
: headers.filter((index) => index < (own ?? 0)).at(-1);
|
|
435
|
+
if (target === undefined) return;
|
|
436
|
+
for (let index = target + 1; index < entries.length; index += 1) {
|
|
437
|
+
if (isSelectable(entries[index])) {
|
|
438
|
+
this.select(index);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
387
444
|
/** Settings keys: change, scope, navigation. `←`/`→` stay on sections. */
|
|
388
445
|
private handleSettingsInput(data: string): void {
|
|
389
446
|
this.clampSelection(this.activeEntries());
|
|
@@ -395,6 +452,14 @@ export class PanelComponent implements Component {
|
|
|
395
452
|
this.switchScope();
|
|
396
453
|
return;
|
|
397
454
|
}
|
|
455
|
+
if (data === "u") {
|
|
456
|
+
this.resetSetting();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (data === "J") {
|
|
460
|
+
this.toggleJson();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
398
463
|
this.navigate(data);
|
|
399
464
|
}
|
|
400
465
|
|
|
@@ -483,7 +548,7 @@ export class PanelComponent implements Component {
|
|
|
483
548
|
if (this.editing) return hintLine(this.theme, width, editHints());
|
|
484
549
|
const hints =
|
|
485
550
|
this.section === "settings"
|
|
486
|
-
? settingsHints()
|
|
551
|
+
? settingsHints(this.json)
|
|
487
552
|
: extensionHints({
|
|
488
553
|
expanded,
|
|
489
554
|
refreshing: this.refreshing,
|
|
@@ -527,33 +592,36 @@ export class PanelComponent implements Component {
|
|
|
527
592
|
this.theme,
|
|
528
593
|
width,
|
|
529
594
|
this.section,
|
|
530
|
-
settingsSummary(this.theme, width, scope,
|
|
595
|
+
settingsSummary(this.theme, width, scope, views.length),
|
|
531
596
|
);
|
|
532
597
|
const entries = settingEntries(views);
|
|
533
598
|
const note = scope === "project" ? projectNote(this.theme, width) : undefined;
|
|
534
599
|
|
|
535
|
-
if (entries.length === 0) {
|
|
536
|
-
return dockListFrame({
|
|
537
|
-
header,
|
|
538
|
-
summary,
|
|
539
|
-
body: emptyState(this.theme, width, { label: "No settings found." }),
|
|
540
|
-
hint: this.hintRow(width),
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
|
|
544
600
|
this.retainFocus(entries);
|
|
545
601
|
this.clampSelection(entries);
|
|
546
602
|
|
|
547
603
|
const focusedEntry = entries[this.cursor.selected];
|
|
548
604
|
const focused = focusedEntry?.kind === "setting" ? focusedEntry.setting : undefined;
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
605
|
+
const jsonConfig = this.json ? focused?.config : undefined;
|
|
606
|
+
const detail =
|
|
607
|
+
jsonConfig && focused
|
|
608
|
+
? sectionJsonPane({
|
|
609
|
+
theme: this.theme,
|
|
610
|
+
width,
|
|
611
|
+
section: jsonConfig.section,
|
|
612
|
+
view: focused,
|
|
613
|
+
document: this.options.settings.sectionDocument(scope, jsonConfig.section),
|
|
614
|
+
budget: this.detailRows(true),
|
|
615
|
+
paths: { scope, cwd: this.options.cwd, agentDir: this.options.agentDir },
|
|
616
|
+
})
|
|
617
|
+
: settingDetail({
|
|
618
|
+
theme: this.theme,
|
|
619
|
+
width,
|
|
620
|
+
view: focused,
|
|
621
|
+
budget: this.detailRows(false),
|
|
622
|
+
dirty: this.dirty,
|
|
623
|
+
paths: { scope, cwd: this.options.cwd, agentDir: this.options.agentDir },
|
|
624
|
+
});
|
|
557
625
|
|
|
558
626
|
this.cursor.visible = fitRows(
|
|
559
627
|
this.options.viewportRows,
|