@gavin-hu/my-pi-agent 0.15.1 → 0.17.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 +22 -13
- package/README.zh-CN.md +19 -11
- package/extensions/bytefly/README.md +255 -200
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notice/component.ts +6 -4
- package/extensions/bytefly/notice/config.ts +16 -2
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +52 -22
- package/extensions/bytefly/panel/join.ts +36 -15
- package/extensions/bytefly/panel/settings.ts +72 -392
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/entries.ts +16 -14
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -58
- package/extensions/bytefly/panel/tui/frame.ts +11 -3
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +8 -12
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -333
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +22 -10
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/bytefly/working/register.ts +2 -8
- package/extensions/check/tools.ts +2 -11
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +172 -0
- package/extensions/context/collect.ts +53 -0
- package/extensions/context/commands.ts +100 -0
- package/extensions/context/format.ts +84 -0
- package/extensions/context/index.ts +16 -0
- package/extensions/context/report.ts +332 -0
- package/extensions/context/tui.ts +284 -0
- package/extensions/context/types.ts +124 -0
- package/extensions/file-browse/README.md +67 -19
- package/extensions/file-browse/client.ts +16 -463
- package/extensions/file-browse/config.ts +8 -0
- package/extensions/file-browse/html.ts +9 -7
- package/extensions/file-browse/index.ts +17 -2
- package/extensions/file-browse/live-client.ts +13 -13
- package/extensions/file-browse/live.ts +44 -10
- package/extensions/file-browse/markdown.ts +3 -10
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/raw-html.ts +3 -10
- package/extensions/file-browse/router.ts +2 -2
- package/extensions/file-browse/server.ts +13 -2
- package/extensions/file-browse/styles.ts +8 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-open/README.md +1 -2
- package/extensions/{document → file-read}/README.md +76 -57
- package/extensions/{document → file-read}/config.ts +12 -9
- package/extensions/{document → file-read}/extract/doc.ts +2 -2
- package/extensions/{document → file-read}/extract/docx.ts +3 -3
- package/extensions/{document → file-read}/extract/file.ts +9 -9
- package/extensions/{document → file-read}/extract/odt.ts +10 -8
- package/extensions/{document → file-read}/extract/pdf.ts +3 -3
- package/extensions/{document → file-read}/extract/rtf.ts +3 -3
- package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
- package/extensions/{document → file-read}/formats.ts +11 -11
- package/extensions/file-read/index.ts +17 -0
- package/extensions/{document → file-read}/paging.ts +15 -13
- package/extensions/{document → file-read}/render.ts +17 -13
- package/extensions/{document → file-read}/schema.ts +7 -7
- package/extensions/{document → file-read}/tool.ts +36 -28
- package/extensions/goal/tools.ts +16 -25
- package/extensions/job/commands.ts +32 -46
- package/extensions/job/index.ts +20 -45
- package/extensions/job/runtime.ts +6 -8
- package/extensions/job/tools.ts +58 -103
- package/extensions/memory/commands.ts +30 -44
- package/extensions/memory/tools.ts +10 -20
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +88 -33
- package/extensions/plan/commands.ts +19 -4
- package/extensions/plan/config.ts +27 -0
- package/extensions/plan/plan-index.ts +3 -1
- package/extensions/plan/plans.ts +31 -13
- package/extensions/plan/review.ts +34 -14
- package/extensions/plan/runtime.ts +3 -1
- package/extensions/plan/steps.ts +43 -14
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +16 -17
- package/extensions/plan/tools/exit.ts +60 -50
- package/extensions/plan/tools/steps.ts +13 -26
- package/extensions/plan/tools/write.ts +30 -38
- package/extensions/plan/tools.ts +5 -2
- package/extensions/plan/tui.ts +9 -7
- package/extensions/plan/types.ts +8 -0
- package/extensions/plan/web-chrome.ts +101 -0
- package/extensions/plan/web-client.ts +98 -311
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +28 -2
- package/extensions/plan/web-router.ts +2 -2
- package/extensions/plan/web.ts +21 -12
- package/extensions/question/tools.ts +5 -4
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/subagent/README.md +25 -12
- package/extensions/subagent/background.ts +1 -7
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +15 -20
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +5 -5
- package/extensions/watch/commands.ts +33 -47
- package/extensions/watch/index.ts +10 -35
- package/extensions/watch/runtime.ts +5 -7
- package/extensions/watch/tools.ts +38 -77
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/search/search.ts +9 -18
- package/extensions/wechat/README.md +40 -18
- package/extensions/wechat/bridge.ts +38 -40
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +1 -1
- package/extensions/wechat/interaction.ts +25 -4
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +7 -5
- package/extensions/wechat/prompt.ts +19 -4
- package/extensions/wechat/qrcode.ts +9 -9
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/timer.ts +9 -30
- package/extensions/wechat/tools.ts +7 -18
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -38
- package/lib/README.md +15 -11
- package/lib/config.ts +12 -0
- package/lib/escape.ts +15 -2
- package/lib/format.ts +6 -2
- package/lib/interaction.ts +18 -1
- package/lib/pdf.ts +2 -2
- package/lib/report.ts +75 -2
- package/lib/requirements.ts +2 -2
- package/lib/screen-session.ts +85 -0
- package/lib/timer.ts +61 -0
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +19 -11
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/columns.ts +33 -8
- package/lib/tui/dock-screen.ts +6 -2
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/rows.ts +15 -6
- package/lib/tui/rules.ts +4 -8
- package/lib/tui/tool.ts +55 -2
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +10 -4
- package/skills/changelog/SKILL.md +148 -0
- package/skills/next/SKILL.md +184 -0
- package/extensions/document/index.ts +0 -17
- /package/extensions/{document → file-read}/deadline.ts +0 -0
- /package/extensions/{document → file-read}/extract/vendor.d.ts +0 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The panel's entry model:
|
|
3
|
-
*
|
|
2
|
+
* The panel's entry model: one flat list of selectable rows per section.
|
|
3
|
+
*
|
|
4
|
+
* Extensions interleaves group headings with rows — a source group says where a
|
|
5
|
+
* resource came from, which the row cannot — while Settings is flat, in catalogue
|
|
6
|
+
* order. Both are built from a model or a set of views, so the component only has
|
|
7
|
+
* to keep one cursor per section.
|
|
4
8
|
*
|
|
5
9
|
* Pure: the list is rebuilt from a model or a set of views, and navigation is
|
|
6
|
-
* index arithmetic
|
|
10
|
+
* index arithmetic.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
|
-
import {
|
|
13
|
+
import type { SettingView } from "../settings.ts";
|
|
10
14
|
import type { PanelModel, PanelRow } from "../types.ts";
|
|
11
15
|
|
|
12
16
|
/** A flat display entry: a group header or a selectable row. */
|
|
@@ -30,9 +34,9 @@ export function isSelectable(entry: PanelEntry | undefined): boolean {
|
|
|
30
34
|
/**
|
|
31
35
|
* One display window as ordinals over the selectable rows it contains.
|
|
32
36
|
*
|
|
33
|
-
* The range row counts rows, not entries:
|
|
34
|
-
*
|
|
35
|
-
*
|
|
37
|
+
* The range row counts rows, not entries: an extension list's group headings are
|
|
38
|
+
* not focusable, so `showing 3–5 of 12` is what the user can land on.
|
|
39
|
+
* `formatRange` prints `start + 1` through `end`, so the ordinals here are
|
|
36
40
|
* zero-based and `end` is exclusive.
|
|
37
41
|
*/
|
|
38
42
|
export function selectableRange(
|
|
@@ -71,12 +75,10 @@ export function entriesOf(model: PanelModel): PanelEntry[] {
|
|
|
71
75
|
return entries;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* The Settings entries: one row per view, in catalogue order, with no group
|
|
80
|
+
* headings — the list is short enough to read whole.
|
|
81
|
+
*/
|
|
75
82
|
export function settingEntries(views: readonly SettingView[]): PanelEntry[] {
|
|
76
|
-
|
|
77
|
-
for (const group of groupViews(views)) {
|
|
78
|
-
entries.push({ kind: "group", key: group.key, label: group.label, count: group.views.length });
|
|
79
|
-
for (const view of group.views) entries.push({ kind: "setting", setting: view });
|
|
80
|
-
}
|
|
81
|
-
return entries;
|
|
83
|
+
return views.map((setting) => ({ kind: "setting", setting }));
|
|
82
84
|
}
|
|
@@ -1,51 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Extensions section
|
|
3
|
-
* the detail pane for the focused row.
|
|
2
|
+
* The Extensions section's focused-row pane.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The row, the summary, and the key hints are the shared list grammar; what is
|
|
5
|
+
* specific here is the doctor's findings, including the resource path when a row
|
|
6
|
+
* has no verdict.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import type { ScreenHint } from "../../../../lib/tui/chrome.ts";
|
|
11
10
|
import { sanitizeLine } from "../../../../lib/format.ts";
|
|
12
11
|
import { clip } from "../../../../lib/tui/columns.ts";
|
|
13
|
-
import { checkbox, listRow } from "../../../../lib/tui/rows.ts";
|
|
14
12
|
import { paint } from "../../../../lib/tui/tokens.ts";
|
|
15
13
|
import { detailLines as reportDetailLines } from "../../doctor/format.ts";
|
|
16
|
-
import { rowBadge
|
|
17
|
-
import type {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
/** The section's hits, its scope, and how many need attention. */
|
|
21
|
-
export function extensionSummary(
|
|
22
|
-
theme: Theme,
|
|
23
|
-
width: number,
|
|
24
|
-
model: PanelModel,
|
|
25
|
-
scope: PanelScope,
|
|
26
|
-
filter: string | undefined,
|
|
27
|
-
): string {
|
|
28
|
-
return countsLine(theme, width, summarySegments(model, scope, { filter, compact: width < 72 }));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** One extension row: its checkbox, health glyph, name, and verdict. */
|
|
32
|
-
export function extensionRow(theme: Theme, width: number, row: PanelRow, selected: boolean): string {
|
|
33
|
-
const badge = rowBadge(row);
|
|
34
|
-
return listRow(theme, width, {
|
|
35
|
-
selected,
|
|
36
|
-
// A reported-only row has no enable pattern, but still reserves the box
|
|
37
|
-
// column so the list stays aligned.
|
|
38
|
-
leading: row.item ? checkbox(theme, row.item.enabled) : " ",
|
|
39
|
-
glyph: badge.glyph ?? " ",
|
|
40
|
-
glyphRole: badge.role,
|
|
41
|
-
label: sanitizeLine(row.label),
|
|
42
|
-
labelRole: selected ? "accent" : row.item && !row.item.enabled ? "faint" : "label",
|
|
43
|
-
trailing: badge.text,
|
|
44
|
-
// The glyph carries the status colour; the word stays quiet unless it is a
|
|
45
|
-
// problem, so a clean row does not shout.
|
|
46
|
-
trailingRole: badge.role === "bad" || badge.role === "warn" ? badge.role : "faint",
|
|
47
|
-
});
|
|
48
|
-
}
|
|
14
|
+
import { rowBadge } from "../format.ts";
|
|
15
|
+
import type { PanelRow } from "../types.ts";
|
|
16
|
+
import { savedLine } from "./frame.ts";
|
|
49
17
|
|
|
50
18
|
/** The focused extension's findings and the pending-changes note. */
|
|
51
19
|
export function extensionDetail(
|
|
@@ -70,22 +38,3 @@ export function extensionDetail(
|
|
|
70
38
|
lines.push(...savedLine(theme, width, dirty));
|
|
71
39
|
return lines;
|
|
72
40
|
}
|
|
73
|
-
|
|
74
|
-
/** The section's key hints, in the order they drop as the width runs out. */
|
|
75
|
-
export function extensionHints(options: {
|
|
76
|
-
expanded: boolean;
|
|
77
|
-
refreshing: boolean;
|
|
78
|
-
projectScope: boolean;
|
|
79
|
-
}): ScreenHint[] {
|
|
80
|
-
return [
|
|
81
|
-
"Esc close",
|
|
82
|
-
"←/→",
|
|
83
|
-
{ text: "Space toggle", color: "accent" },
|
|
84
|
-
{ text: options.expanded ? "Enter collapse" : "Enter detail", color: "accent" },
|
|
85
|
-
"Tab scope",
|
|
86
|
-
// Only Project scope can hold an override, so the key is not offered elsewhere.
|
|
87
|
-
...(options.projectScope ? ["x inherit"] : []),
|
|
88
|
-
options.refreshing ? "refreshing…" : "r refresh",
|
|
89
|
-
"↑/↓ select",
|
|
90
|
-
];
|
|
91
|
-
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { clip, fitCells, splitRow } from "../../../../lib/tui/columns.ts";
|
|
20
20
|
import { fitRows, formatRange, type ListCursor } from "../../../../lib/tui/cursor.ts";
|
|
21
21
|
import { BODY_INDENT } from "../../../../lib/tui/indent.ts";
|
|
22
|
+
import { emptyState } from "../../../../lib/tui/rows.ts";
|
|
22
23
|
import { paint } from "../../../../lib/tui/tokens.ts";
|
|
23
24
|
import { sectionSegments, type SummarySegment } from "../format.ts";
|
|
24
25
|
import type { PanelSection } from "../types.ts";
|
|
@@ -26,10 +27,10 @@ import { selectableRange, type PanelEntry } from "./entries.ts";
|
|
|
26
27
|
|
|
27
28
|
/** Body rows when the terminal height is unknown. */
|
|
28
29
|
export const SCREEN_DEFAULT_ROWS = 16;
|
|
29
|
-
/** Fixed rows around the body: header, summary, blank, blank, hint,
|
|
30
|
+
/** Fixed rows around the body: header, summary, blank, blank, hint, closing rule. */
|
|
30
31
|
export const FRAME_ROWS = 6;
|
|
31
|
-
/** Detail rows reserved for the focused row. */
|
|
32
|
-
export const DETAIL_ROWS =
|
|
32
|
+
/** Detail rows reserved for the focused row: its head, one description line, and its origin. */
|
|
33
|
+
export const DETAIL_ROWS = 3;
|
|
33
34
|
/** Body rows that must survive before the detail pane collapses to its title. */
|
|
34
35
|
export const MIN_BODY_ROWS = 3;
|
|
35
36
|
/** Detail rows when the terminal height is unknown and the focused row is expanded. */
|
|
@@ -142,6 +143,8 @@ export interface PanelFrameInput {
|
|
|
142
143
|
note?: string;
|
|
143
144
|
detail: string[];
|
|
144
145
|
hint: string;
|
|
146
|
+
/** The line a section shows in place of its body when it has no entries. */
|
|
147
|
+
emptyLabel?: string;
|
|
145
148
|
draw: (entry: PanelEntry, index: number) => string | undefined;
|
|
146
149
|
}
|
|
147
150
|
|
|
@@ -164,6 +167,9 @@ export function panelFrame(input: PanelFrameInput): { lines: string[]; noteShown
|
|
|
164
167
|
|
|
165
168
|
const body: string[] = [];
|
|
166
169
|
if (note) body.push(note);
|
|
170
|
+
if (entries.length === 0 && input.emptyLabel !== undefined) {
|
|
171
|
+
body.push(...emptyState(theme, width, { label: input.emptyLabel }));
|
|
172
|
+
}
|
|
167
173
|
for (let index = start; index < end; index += 1) {
|
|
168
174
|
const line = draw(entries[index], index);
|
|
169
175
|
if (line !== undefined) body.push(line);
|
|
@@ -175,6 +181,8 @@ export function panelFrame(input: PanelFrameInput): { lines: string[]; noteShown
|
|
|
175
181
|
const fitted = fitOptionalLines({ viewportRows, body, note, range, detail });
|
|
176
182
|
return {
|
|
177
183
|
lines: dockListFrame({
|
|
184
|
+
theme,
|
|
185
|
+
width,
|
|
178
186
|
header: input.header,
|
|
179
187
|
summary: input.summary,
|
|
180
188
|
body: fitted.body,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grouped list sections' shared pieces: the headline counts, one row, and
|
|
3
|
+
* the key hints.
|
|
4
|
+
*
|
|
5
|
+
* Extensions and Skills draw the same row grammar — a checkbox, a verdict, and
|
|
6
|
+
* the resource's name — and offer the same keys, so the row, the summary, and
|
|
7
|
+
* the hint row live here and each section supplies only what it can say about
|
|
8
|
+
* the focused row and whether it has anything to refresh.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { sanitizeLine } from "../../../../lib/format.ts";
|
|
13
|
+
import type { ScreenHint } from "../../../../lib/tui/chrome.ts";
|
|
14
|
+
import { checkbox, listRow } from "../../../../lib/tui/rows.ts";
|
|
15
|
+
import { rowBadge, type PanelScope, summarySegments, type SummaryOptions } from "../format.ts";
|
|
16
|
+
import type { PanelModel, PanelRow } from "../types.ts";
|
|
17
|
+
import { countsLine } from "./frame.ts";
|
|
18
|
+
|
|
19
|
+
/** The section's headline: the scope, the row count, and what is off. */
|
|
20
|
+
export function resourceSummary(
|
|
21
|
+
theme: Theme,
|
|
22
|
+
width: number,
|
|
23
|
+
model: PanelModel,
|
|
24
|
+
scope: PanelScope,
|
|
25
|
+
options: SummaryOptions = {},
|
|
26
|
+
): string {
|
|
27
|
+
return countsLine(theme, width, summarySegments(model, scope, { ...options, compact: width < 72 }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One resource row: its checkbox, verdict, and name. */
|
|
31
|
+
export function resourceRow(theme: Theme, width: number, row: PanelRow, selected: boolean): string {
|
|
32
|
+
const badge = rowBadge(row);
|
|
33
|
+
return listRow(theme, width, {
|
|
34
|
+
selected,
|
|
35
|
+
// A reported-only row has no enable pattern, but still reserves the box
|
|
36
|
+
// column so the list stays aligned.
|
|
37
|
+
leading: row.item ? checkbox(theme, row.item.enabled) : " ",
|
|
38
|
+
glyph: badge.glyph ?? " ",
|
|
39
|
+
glyphRole: badge.role,
|
|
40
|
+
label: sanitizeLine(row.label),
|
|
41
|
+
labelRole: selected ? "accent" : row.item && !row.item.enabled ? "faint" : "label",
|
|
42
|
+
trailing: badge.text,
|
|
43
|
+
// The glyph carries the status colour; the word stays quiet unless it is a
|
|
44
|
+
// problem, so a clean row does not shout.
|
|
45
|
+
trailingRole: badge.role === "bad" || badge.role === "warn" ? badge.role : "faint",
|
|
46
|
+
// The verdict sits beside the name instead of at the far edge of a wide
|
|
47
|
+
// terminal, where a two-word verdict would be unreadable.
|
|
48
|
+
align: "inline",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SectionHintOptions {
|
|
53
|
+
expanded: boolean;
|
|
54
|
+
projectScope: boolean;
|
|
55
|
+
/** The refresh key's state; omitted by a section with nothing to refresh. */
|
|
56
|
+
refreshing?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The section's key hints, in the order they drop as the width runs out. */
|
|
60
|
+
export function sectionHints({ expanded, projectScope, refreshing }: SectionHintOptions): ScreenHint[] {
|
|
61
|
+
return [
|
|
62
|
+
"Esc close",
|
|
63
|
+
"←/→",
|
|
64
|
+
{ text: "Space toggle", color: "accent" },
|
|
65
|
+
{ text: expanded ? "Enter collapse" : "Enter detail", color: "accent" },
|
|
66
|
+
"Tab scope",
|
|
67
|
+
// Only Project scope can hold an override, so the key is not offered elsewhere.
|
|
68
|
+
...(projectScope ? ["x inherit"] : []),
|
|
69
|
+
...(refreshing === undefined ? [] : [refreshing ? "refreshing…" : "r refresh"]),
|
|
70
|
+
"↑/↓ select",
|
|
71
|
+
];
|
|
72
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cursor arithmetic for the panel's
|
|
3
|
-
* rebuild, land on the nearest row when the shape changes,
|
|
4
|
-
*
|
|
2
|
+
* Cursor arithmetic for the panel's lists: keep the focus on a row across a
|
|
3
|
+
* rebuild, land on the nearest row when the shape changes, move by rows while
|
|
4
|
+
* skipping group headers, and read one navigation key.
|
|
5
5
|
*
|
|
6
6
|
* Pure: each function takes the entries and the cursor and mutates only the
|
|
7
7
|
* cursor it was handed, so the component keeps one cursor per section and no
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ListCursor } from "../../../../lib/tui/cursor.ts";
|
|
12
|
+
import { navIntent } from "../../../../lib/tui/cursor.ts";
|
|
12
13
|
import { entryKey, isSelectable, nextRow, type PanelEntry } from "./entries.ts";
|
|
13
14
|
|
|
14
15
|
/** The first selectable entry, or `undefined` when the list has none. */
|
|
@@ -73,3 +74,70 @@ export function moveRows(entries: PanelEntry[], cursor: ListCursor, delta: numbe
|
|
|
73
74
|
}
|
|
74
75
|
cursor.set(index, entries.length);
|
|
75
76
|
}
|
|
77
|
+
|
|
78
|
+
/** Move to the first row of the next (or previous) group. */
|
|
79
|
+
export function jumpGroup(entries: PanelEntry[], cursor: ListCursor, direction: -1 | 1): void {
|
|
80
|
+
const headers = entries.flatMap((entry, index) => (entry.kind === "group" ? [index] : []));
|
|
81
|
+
const selected = cursor.selected;
|
|
82
|
+
// Step past the focused row's own header: landing on it would not move.
|
|
83
|
+
const own = headers.filter((index) => index < selected).at(-1);
|
|
84
|
+
const target =
|
|
85
|
+
direction === 1 ? headers.find((index) => index > selected) : headers.filter((index) => index < (own ?? 0)).at(-1);
|
|
86
|
+
if (target === undefined) return;
|
|
87
|
+
for (let index = target + 1; index < entries.length; index += 1) {
|
|
88
|
+
if (isSelectable(entries[index])) {
|
|
89
|
+
cursor.set(index, entries.length);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Move the cursor to `index`, clamped to the list. */
|
|
96
|
+
export function selectRow(entries: PanelEntry[], cursor: ListCursor, index: number | undefined): void {
|
|
97
|
+
if (index === undefined) return;
|
|
98
|
+
cursor.set(index, entries.length);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Read one navigation key against a list. `grouped` is true for the resource
|
|
103
|
+
* lists, whose headings the bracket keys jump between; the Settings list is flat.
|
|
104
|
+
*/
|
|
105
|
+
export function navigateEntries(
|
|
106
|
+
entries: PanelEntry[],
|
|
107
|
+
cursor: ListCursor,
|
|
108
|
+
data: string,
|
|
109
|
+
options: { grouped: boolean },
|
|
110
|
+
): void {
|
|
111
|
+
if (data === "g") {
|
|
112
|
+
selectRow(entries, cursor, firstRow(entries));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (data === "G") {
|
|
116
|
+
selectRow(entries, cursor, lastRow(entries));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (options.grouped && (data === "[" || data === "]")) {
|
|
120
|
+
jumpGroup(entries, cursor, data === "[" ? -1 : 1);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
switch (navIntent(data)) {
|
|
124
|
+
case "up":
|
|
125
|
+
moveRows(entries, cursor, -1);
|
|
126
|
+
return;
|
|
127
|
+
case "down":
|
|
128
|
+
moveRows(entries, cursor, 1);
|
|
129
|
+
return;
|
|
130
|
+
case "pageUp":
|
|
131
|
+
moveRows(entries, cursor, -Math.max(1, cursor.visible));
|
|
132
|
+
return;
|
|
133
|
+
case "pageDown":
|
|
134
|
+
moveRows(entries, cursor, Math.max(1, cursor.visible));
|
|
135
|
+
return;
|
|
136
|
+
case "home":
|
|
137
|
+
selectRow(entries, cursor, firstRow(entries));
|
|
138
|
+
return;
|
|
139
|
+
case "end":
|
|
140
|
+
selectRow(entries, cursor, lastRow(entries));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Settings section: its own state (the editor and the JSON pane), its key
|
|
3
|
+
* handling, and the screen it draws.
|
|
4
|
+
*
|
|
5
|
+
* The component owns the section switch, the write scope, and the cursor. This
|
|
6
|
+
* class owns what only Settings has, so `tui.ts` stays the switch and the shared
|
|
7
|
+
* list keys, and the section reads and writes through the same surface.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
12
|
+
import type { ViewportRowsSource } from "../../../../lib/tui/chrome.ts";
|
|
13
|
+
import type { ListCursor } from "../../../../lib/tui/cursor.ts";
|
|
14
|
+
import type { WriteScope } from "../../picker/types.ts";
|
|
15
|
+
import type { SettingsSurface } from "../settings-runtime.ts";
|
|
16
|
+
import { SETTING_ROWS } from "../settings.ts";
|
|
17
|
+
import { applyEditKey, type EditingState } from "./editing.ts";
|
|
18
|
+
import { settingEntries, type PanelEntry } from "./entries.ts";
|
|
19
|
+
import { detailRows, groupLine, panelFrame, sectionBar } from "./frame.ts";
|
|
20
|
+
import { clampSelection } from "./nav.ts";
|
|
21
|
+
import { projectNote, sectionJsonPane, settingDetail, settingRow, settingsSummary } from "./settings-view.ts";
|
|
22
|
+
|
|
23
|
+
/** What the Settings section needs from the component that hosts it. */
|
|
24
|
+
export interface SettingsSectionDeps {
|
|
25
|
+
/** The rows, values, and writes the section reads and calls. */
|
|
26
|
+
surface: SettingsSurface;
|
|
27
|
+
theme: Theme;
|
|
28
|
+
scope(): WriteScope;
|
|
29
|
+
cursor: ListCursor;
|
|
30
|
+
viewportRows: ViewportRowsSource | undefined;
|
|
31
|
+
cwd: string;
|
|
32
|
+
agentDir: string;
|
|
33
|
+
/** Keep the focused row across a rebuild and return it. */
|
|
34
|
+
retain(entries: PanelEntry[]): PanelEntry | undefined;
|
|
35
|
+
/** Panel navigation, for the keys this section does not own. */
|
|
36
|
+
navigate(data: string): void;
|
|
37
|
+
/** Flip the Global/Project scope. */
|
|
38
|
+
switchScope(): void;
|
|
39
|
+
notify(message: string): void;
|
|
40
|
+
requestRender(): void;
|
|
41
|
+
/** Keep the frame's project-note marker in step for the mouse mapping. */
|
|
42
|
+
setNoteShown(shown: boolean): void;
|
|
43
|
+
/** The hint row, already assembled (or the notice that replaces it). */
|
|
44
|
+
hint(width: number): string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The Settings section's state and screen. */
|
|
48
|
+
export class SettingsSection {
|
|
49
|
+
private chain: Promise<void> = Promise.resolve();
|
|
50
|
+
private editing: EditingState | undefined;
|
|
51
|
+
private json = false;
|
|
52
|
+
|
|
53
|
+
constructor(private readonly deps: SettingsSectionDeps) {}
|
|
54
|
+
|
|
55
|
+
/** Whether a text row is open in the editor, so the hint row and mouse defer. */
|
|
56
|
+
get isEditing(): boolean {
|
|
57
|
+
return this.editing !== undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether the detail pane shows the focused row's section as JSON. */
|
|
61
|
+
get jsonOpen(): boolean {
|
|
62
|
+
return this.json;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Run `task` after the previous write, then repaint once. */
|
|
66
|
+
private write(task: () => Promise<unknown>, onSettled?: (result: unknown) => void): void {
|
|
67
|
+
this.chain = this.chain
|
|
68
|
+
.then(task)
|
|
69
|
+
.then(onSettled)
|
|
70
|
+
.catch((error: unknown) => this.deps.notify(error instanceof Error ? error.message : String(error)))
|
|
71
|
+
.then(() => this.deps.requestRender());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The rows for the active scope, as display entries. */
|
|
75
|
+
private entries(): PanelEntry[] {
|
|
76
|
+
return settingEntries(this.deps.surface.views(this.deps.scope()));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The focused entry, after clamping the cursor onto a row. */
|
|
80
|
+
private focused(): PanelEntry | undefined {
|
|
81
|
+
const entries = this.entries();
|
|
82
|
+
clampSelection(entries, this.deps.cursor);
|
|
83
|
+
return entries[this.deps.cursor.selected];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Drop the editor and the JSON pane when another section takes over. */
|
|
87
|
+
reset(): void {
|
|
88
|
+
this.editing = undefined;
|
|
89
|
+
this.json = false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A click leaves the editor the way `Esc` does: nothing is written. */
|
|
93
|
+
cancelEdit(): void {
|
|
94
|
+
this.reset();
|
|
95
|
+
this.deps.requestRender();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Change the focused setting: cycle it, or open the editor for a text row. */
|
|
99
|
+
private changeSetting(): void {
|
|
100
|
+
const entry = this.focused();
|
|
101
|
+
if (entry?.kind !== "setting") return;
|
|
102
|
+
const view = entry.setting;
|
|
103
|
+
if (view.kind === "text") {
|
|
104
|
+
this.editing = { id: view.id, buffer: view.value, caret: view.value.length };
|
|
105
|
+
this.deps.requestRender();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const scope = this.deps.scope();
|
|
109
|
+
this.write(() => this.deps.surface.change(scope, view.id));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Delete the focused setting's leaf, so its default applies again. */
|
|
113
|
+
private resetSetting(): void {
|
|
114
|
+
const entry = this.focused();
|
|
115
|
+
if (entry?.kind !== "setting") return;
|
|
116
|
+
const scope = this.deps.scope();
|
|
117
|
+
this.write(() => this.deps.surface.reset(scope, entry.setting.id));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Toggle the detail pane between the row and its whole section as stored. */
|
|
121
|
+
private toggleJson(): void {
|
|
122
|
+
const entry = this.focused();
|
|
123
|
+
if (entry?.kind !== "setting") return;
|
|
124
|
+
if (!entry.setting.config) {
|
|
125
|
+
// There is no section to show, so leave the mode off rather than keeping a
|
|
126
|
+
// hidden toggle that would reopen the pane on another row.
|
|
127
|
+
this.json = false;
|
|
128
|
+
this.deps.notify(`${entry.setting.label} is a Pi setting; bytefly.json has no section for it.`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.json = !this.json;
|
|
132
|
+
this.deps.requestRender();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Store the editor's buffer; a refusal keeps it open with the reason shown. */
|
|
136
|
+
private commitEdit(): void {
|
|
137
|
+
const editing = this.editing;
|
|
138
|
+
if (!editing) return;
|
|
139
|
+
const scope = this.deps.scope();
|
|
140
|
+
this.write(
|
|
141
|
+
() => this.deps.surface.setValue(scope, editing.id, editing.buffer),
|
|
142
|
+
(saved) => {
|
|
143
|
+
if (saved === true) this.editing = undefined;
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Editor keys. `Esc`/`Ctrl+C` cancel the edit rather than closing the screen,
|
|
150
|
+
* and everything else the section binds is inert until the editor closes.
|
|
151
|
+
*/
|
|
152
|
+
private handleEditingInput(data: string): void {
|
|
153
|
+
const editing = this.editing;
|
|
154
|
+
if (!editing) return;
|
|
155
|
+
const max = SETTING_ROWS.find((row) => row.id === editing.id)?.text?.maxLength;
|
|
156
|
+
const action = applyEditKey(data, editing, max);
|
|
157
|
+
if (action.kind === "commit") {
|
|
158
|
+
void this.commitEdit();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (action.kind === "cancel") {
|
|
162
|
+
this.editing = undefined;
|
|
163
|
+
this.deps.requestRender();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!action.changed) return;
|
|
167
|
+
this.editing = action.state;
|
|
168
|
+
this.deps.requestRender();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** One key: editor keys first, then change, scope, reset, JSON, navigation. */
|
|
172
|
+
handleInput(data: string): void {
|
|
173
|
+
if (this.editing) {
|
|
174
|
+
this.handleEditingInput(data);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.focused();
|
|
178
|
+
if (matchesKey(data, Key.space) || matchesKey(data, Key.enter)) {
|
|
179
|
+
this.changeSetting();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (matchesKey(data, Key.tab)) {
|
|
183
|
+
this.deps.switchScope();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (data === "u") {
|
|
187
|
+
this.resetSetting();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (data === "J") {
|
|
191
|
+
this.toggleJson();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.deps.navigate(data);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The Settings body: scope counts, the project note, rows, and detail. */
|
|
198
|
+
render(width: number, header: string): string[] {
|
|
199
|
+
const { theme, surface, viewportRows, cwd, agentDir } = this.deps;
|
|
200
|
+
const scope = this.deps.scope();
|
|
201
|
+
const views = surface.views(scope);
|
|
202
|
+
const entries = settingEntries(views);
|
|
203
|
+
const focusedEntry = this.deps.retain(entries);
|
|
204
|
+
const focused = focusedEntry?.kind === "setting" ? focusedEntry.setting : undefined;
|
|
205
|
+
// A row with no section leaves the mode off, so moving away and back cannot
|
|
206
|
+
// reopen a pane the user closed.
|
|
207
|
+
if (focused && !focused.config) this.json = false;
|
|
208
|
+
const jsonSection = this.json ? focused?.config : undefined;
|
|
209
|
+
const paths = { scope, cwd, agentDir };
|
|
210
|
+
const detail = jsonSection
|
|
211
|
+
? sectionJsonPane({
|
|
212
|
+
theme,
|
|
213
|
+
width,
|
|
214
|
+
section: jsonSection.section,
|
|
215
|
+
document: surface.sectionDocument(scope, jsonSection.section),
|
|
216
|
+
budget: detailRows(true, viewportRows),
|
|
217
|
+
dirty: surface.isDirty(),
|
|
218
|
+
paths,
|
|
219
|
+
})
|
|
220
|
+
: settingDetail({
|
|
221
|
+
theme,
|
|
222
|
+
width,
|
|
223
|
+
view: focused,
|
|
224
|
+
budget: detailRows(false, viewportRows),
|
|
225
|
+
dirty: surface.isDirty(),
|
|
226
|
+
paths,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const { lines, noteShown } = panelFrame({
|
|
230
|
+
theme,
|
|
231
|
+
viewportRows,
|
|
232
|
+
cursor: this.deps.cursor,
|
|
233
|
+
width,
|
|
234
|
+
header,
|
|
235
|
+
summary: sectionBar(theme, width, "settings", settingsSummary(theme, width, scope, views.length)),
|
|
236
|
+
entries,
|
|
237
|
+
note: scope === "project" ? projectNote(theme, width) : undefined,
|
|
238
|
+
detail,
|
|
239
|
+
hint: this.deps.hint(width),
|
|
240
|
+
draw: (entry, index) => {
|
|
241
|
+
if (entry.kind === "group") return groupLine(theme, width, entry);
|
|
242
|
+
if (entry.kind !== "setting") return undefined;
|
|
243
|
+
const editing = this.editing?.id === entry.setting.id ? this.editing : undefined;
|
|
244
|
+
return settingRow(theme, width, entry.setting, index === this.deps.cursor.selected, editing);
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
this.deps.setNoteShown(noteShown);
|
|
248
|
+
return lines;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -27,6 +27,8 @@ const PI_SETTINGS_FILE = "settings.json";
|
|
|
27
27
|
const ROW_PREFIX_WIDTH = 4;
|
|
28
28
|
/** Columns a value keeps when the row has to choose between the label and the value. */
|
|
29
29
|
const MIN_VALUE_WIDTH = 8;
|
|
30
|
+
/** Columns between the label and its value. */
|
|
31
|
+
const VALUE_GAP = 2;
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* The editing cell: the buffer around the caret, scrolled so the caret is always
|
|
@@ -56,7 +58,7 @@ export function settingsSummary(theme: Theme, width: number, scope: PanelScope,
|
|
|
56
58
|
return countsLine(theme, width, settingsSummarySegments(scope, count));
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
/** One setting row: a checkbox for booleans, the value
|
|
61
|
+
/** One setting row: a checkbox for booleans, and the value beside the label. */
|
|
60
62
|
export function settingRow(
|
|
61
63
|
theme: Theme,
|
|
62
64
|
width: number,
|
|
@@ -67,7 +69,7 @@ export function settingRow(
|
|
|
67
69
|
const label = sanitizeLine(view.label);
|
|
68
70
|
// Keep the whole label when it fits, and never let a long value or an editing
|
|
69
71
|
// buffer erase it: the value gets what is left, with a floor of eight columns.
|
|
70
|
-
const available = Math.max(1, width - ROW_PREFIX_WIDTH -
|
|
72
|
+
const available = Math.max(1, width - ROW_PREFIX_WIDTH - VALUE_GAP);
|
|
71
73
|
const labelReserve = Math.min(visibleWidth(label), Math.max(0, available - MIN_VALUE_WIDTH));
|
|
72
74
|
const valueBudget = Math.max(1, available - labelReserve);
|
|
73
75
|
const rawValue = editing ? editingCell(editing.buffer, editing.caret, valueBudget) : view.valueLabel;
|
|
@@ -78,6 +80,9 @@ export function settingRow(
|
|
|
78
80
|
labelRole: selected ? "accent" : "label",
|
|
79
81
|
trailing: clip(sanitizeLine(rawValue), valueBudget),
|
|
80
82
|
trailingRole: editing ? "accent" : "meta",
|
|
83
|
+
// The value sits beside the label, so a wide terminal never strands it at
|
|
84
|
+
// the far edge where the eye has to travel to read it.
|
|
85
|
+
align: "inline",
|
|
81
86
|
});
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -137,16 +142,7 @@ export function settingDetail(options: {
|
|
|
137
142
|
|
|
138
143
|
/** The section's key hints, in the order they drop as the width runs out. */
|
|
139
144
|
export function settingsHints(json = false): string[] {
|
|
140
|
-
return [
|
|
141
|
-
"Esc close",
|
|
142
|
-
"←/→",
|
|
143
|
-
"Space change",
|
|
144
|
-
"Tab scope",
|
|
145
|
-
"u reset",
|
|
146
|
-
json ? "J hide" : "J JSON",
|
|
147
|
-
"[ ] group",
|
|
148
|
-
"↑/↓ select",
|
|
149
|
-
];
|
|
145
|
+
return ["Esc close", "←/→", "Space change", "Tab scope", "u reset", json ? "J hide" : "J JSON", "↑/↓ select"];
|
|
150
146
|
}
|
|
151
147
|
|
|
152
148
|
/**
|