@gavin-hu/my-pi-agent 0.19.0 → 0.20.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 +17 -11
- package/README.zh-CN.md +13 -8
- package/docs/extensions/README.md +1 -1
- package/docs/extensions/board.md +286 -123
- package/docs/extensions/bytefly.md +381 -223
- package/docs/extensions/file-browse.md +14 -7
- package/docs/extensions/job.md +1 -2
- package/docs/extensions/plan.md +56 -11
- package/docs/extensions/session-bus.md +23 -10
- package/docs/extensions/team.md +45 -17
- package/docs/extensions/wechat.md +2 -1
- package/docs/extensions/worktree.md +30 -15
- package/extensions/board/commands.ts +23 -5
- package/extensions/board/format.ts +107 -9
- package/extensions/board/join.ts +1 -1
- package/extensions/board/page.ts +170 -94
- package/extensions/board/result.ts +9 -0
- package/extensions/board/schema.ts +22 -7
- package/extensions/board/seat.ts +9 -10
- package/extensions/board/snapshot.ts +1 -0
- package/extensions/board/tools.ts +186 -21
- package/extensions/board/transport/router.ts +27 -108
- package/extensions/board/transport/server.ts +18 -29
- package/extensions/board/types.ts +67 -0
- package/extensions/board/view/client.ts +23 -13
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +28 -24
- package/extensions/board/view/live.ts +5 -171
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +48 -88
- package/extensions/board/view/render.ts +213 -190
- package/extensions/board/view/styles.ts +65 -48
- package/extensions/board/view/team.ts +63 -41
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +28 -1
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings.ts +72 -23
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/check/commands.ts +2 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +8 -547
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/transport/router.ts +0 -2
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/view/html.ts +6 -7
- package/extensions/file-browse/view/markdown.ts +11 -13
- package/extensions/file-browse/view/raw-html.ts +18 -13
- package/extensions/job/runtime.ts +4 -4
- package/extensions/plan/feedback.ts +40 -16
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +17 -1
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plans.ts +19 -23
- package/extensions/plan/policy.ts +10 -5
- package/extensions/plan/runtime.ts +31 -2
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +10 -1
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/types.ts +15 -0
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -1
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +0 -1
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/rewind/commands/review.ts +2 -1
- package/extensions/session-bus/rows.ts +7 -3
- package/extensions/session-bus/runtime.ts +100 -38
- package/extensions/session-bus/store.ts +14 -2
- package/extensions/session-bus/tui.ts +1 -1
- package/extensions/session-bus/types.ts +10 -1
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/team/commands.ts +41 -8
- package/extensions/team/format.ts +14 -0
- package/extensions/team/index.ts +2 -1
- package/extensions/team/tools.ts +30 -9
- package/extensions/team/types.ts +17 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/wechat/bridge.ts +43 -172
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +15 -285
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +6 -5
- package/lib/board/card.ts +192 -3
- package/lib/board/links.ts +26 -2
- package/lib/board/page-pointer.ts +86 -32
- package/lib/board/round-text.ts +11 -2
- package/lib/board/round.ts +25 -20
- package/lib/board/rules.ts +144 -11
- package/lib/board/store.ts +239 -52
- package/lib/board/view.ts +94 -10
- package/lib/format.ts +22 -0
- package/lib/http/serve.ts +3 -0
- package/lib/http/sink.ts +34 -9
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +14 -1
- package/lib/session-roster.ts +74 -3
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +13 -0
- package/lib/tui/glyphs.ts +2 -3
- package/lib/web/README.md +52 -2
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/package.json +1 -1
- package/extensions/board/view/actions.ts +0 -306
- package/extensions/board/writes.ts +0 -99
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/job/process.ts +0 -35
|
@@ -1,35 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
* the key hints.
|
|
2
|
+
* The resource row: the one row grammar Extensions and Skills share.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* A row is a checkbox, a status glyph, the resource's name, and one verdict word.
|
|
5
|
+
* A clean row carries no glyph and no word, so the column reads as one shape
|
|
6
|
+
* instead of a checkmark that comes and goes, and the eye finds the rows that
|
|
7
|
+
* need it.
|
|
8
|
+
*
|
|
9
|
+
* Pure: the theme, the width and the model row come in, one clipped line comes out.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
13
|
import { sanitizeLine } from "../../../../lib/format.ts";
|
|
13
|
-
import
|
|
14
|
+
import { clip, padTo } from "../../../../lib/tui/columns.ts";
|
|
14
15
|
import { checkbox, listRow } from "../../../../lib/tui/rows.ts";
|
|
15
|
-
import { rowBadge
|
|
16
|
-
import type {
|
|
17
|
-
|
|
16
|
+
import { rowBadge } from "../format.ts";
|
|
17
|
+
import type { PanelRow } from "../types.ts";
|
|
18
|
+
|
|
19
|
+
/** Columns the verdict keeps before the name is clipped instead. */
|
|
20
|
+
const MIN_VERDICT_WIDTH = 9;
|
|
18
21
|
|
|
19
|
-
/** The
|
|
20
|
-
export function
|
|
21
|
-
|
|
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 }));
|
|
22
|
+
/** The widest verdict the visible rows carry, so the column lines up. */
|
|
23
|
+
export function verdictColumn(rows: readonly PanelRow[]): number {
|
|
24
|
+
return rows.reduce((columns, row) => Math.max(columns, (rowBadge(row).text ?? "").length), 0);
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
/** One resource row: its checkbox, verdict, and
|
|
31
|
-
export function resourceRow(theme: Theme, width: number, row: PanelRow, selected: boolean): string {
|
|
27
|
+
/** One resource row: its checkbox, verdict glyph, name, and verdict word. */
|
|
28
|
+
export function resourceRow(theme: Theme, width: number, row: PanelRow, selected: boolean, verdicts = 0): string {
|
|
32
29
|
const badge = rowBadge(row);
|
|
30
|
+
const name = sanitizeLine(row.label);
|
|
31
|
+
// The verdict sits in its own column instead of against the name, so a column
|
|
32
|
+
// of rows can be read down rather than across; a long name loses its tail
|
|
33
|
+
// before the verdict loses its word.
|
|
34
|
+
const room = Math.max(MIN_VERDICT_WIDTH, Math.min(verdicts, Math.max(0, width - 22)));
|
|
35
|
+
const nameCell = verdicts > 0 ? padTo(clip(name, Math.max(1, width - 22)), room) : name;
|
|
33
36
|
return listRow(theme, width, {
|
|
34
37
|
selected,
|
|
35
38
|
// A reported-only row has no enable pattern, but still reserves the box
|
|
@@ -37,36 +40,12 @@ export function resourceRow(theme: Theme, width: number, row: PanelRow, selected
|
|
|
37
40
|
leading: row.item ? checkbox(theme, row.item.enabled) : " ",
|
|
38
41
|
glyph: badge.glyph ?? " ",
|
|
39
42
|
glyphRole: badge.role,
|
|
40
|
-
label:
|
|
43
|
+
label: nameCell,
|
|
41
44
|
labelRole: selected ? "accent" : row.item && !row.item.enabled ? "faint" : "label",
|
|
42
45
|
trailing: badge.text,
|
|
43
46
|
// The glyph carries the status colour; the word stays quiet unless it is a
|
|
44
47
|
// problem, so a clean row does not shout.
|
|
45
48
|
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
49
|
align: "inline",
|
|
49
50
|
});
|
|
50
51
|
}
|
|
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
|
-
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two-pane split: where the side pane starts, how wide each side is, and how
|
|
3
|
+
* one row of each is laid on one terminal line.
|
|
4
|
+
*
|
|
5
|
+
* Above the breakpoint the focused row's facts sit beside the list; below it the
|
|
6
|
+
* frame keeps the single column and the facts move under the list. The side lines
|
|
7
|
+
* are *composed onto* the list's lines rather than appended, so the two panes cost
|
|
8
|
+
* exactly the rows the list already spends — the frame's row budget is the same at
|
|
9
|
+
* every width.
|
|
10
|
+
*
|
|
11
|
+
* Value-only: pure functions, the theme passed in.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { clip, padTo } from "../../../../lib/tui/columns.ts";
|
|
16
|
+
import { GLYPHS } from "../../../../lib/tui/glyphs.ts";
|
|
17
|
+
import { paint } from "../../../../lib/tui/tokens.ts";
|
|
18
|
+
|
|
19
|
+
/** The narrowest terminal that gets a side pane. */
|
|
20
|
+
export const WIDE_MIN = 96;
|
|
21
|
+
|
|
22
|
+
/** The side pane's width bounds, in columns. */
|
|
23
|
+
export const SIDE_MIN = 36;
|
|
24
|
+
export const SIDE_MAX = 56;
|
|
25
|
+
|
|
26
|
+
/** Columns between the list and the side pane, on each side of the divider. */
|
|
27
|
+
const GAP = 1;
|
|
28
|
+
|
|
29
|
+
/** Whether this width gets a side pane. */
|
|
30
|
+
export function isWide(width: number): boolean {
|
|
31
|
+
return width >= WIDE_MIN;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The side pane's width: two fifths of the terminal, clamped. Reading the list
|
|
36
|
+
* and its facts at the same height is the point, so the side never grows past
|
|
37
|
+
* what keeps a row and its verdict readable on the left.
|
|
38
|
+
*/
|
|
39
|
+
export function sideWidth(width: number): number {
|
|
40
|
+
return Math.max(SIDE_MIN, Math.min(SIDE_MAX, Math.floor(width * 0.42)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The columns the list keeps, divider and gaps included in the accounting. */
|
|
44
|
+
export function listWidth(width: number): number {
|
|
45
|
+
return Math.max(1, width - sideWidth(width) - GAP * 2 - 1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compose one line pair: the list row padded to its column, the divider, and the
|
|
50
|
+
* side row. A pair with no side content still draws the divider, so the pane keeps
|
|
51
|
+
* one edge down the body instead of appearing and disappearing row by row.
|
|
52
|
+
*/
|
|
53
|
+
export function composeRow(theme: Theme, width: number, list: string, side: string): string {
|
|
54
|
+
const left = padTo(clip(list, listWidth(width)), listWidth(width));
|
|
55
|
+
const divider = paint(theme, "rule", GLYPHS.divider);
|
|
56
|
+
const room = Math.max(0, width - listWidth(width) - GAP * 2 - 1);
|
|
57
|
+
return clip(`${left}${" ".repeat(GAP)}${divider}${" ".repeat(GAP)}${clip(side, room)}`, width);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Compose a body of list rows with a side pane, one line pair per row. */
|
|
61
|
+
export function composeRows(theme: Theme, width: number, list: readonly string[], side: readonly string[]): string[] {
|
|
62
|
+
return list.map((line, index) => composeRow(theme, width, line, side[index] ?? ""));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Cut the side block to the rows it has, keeping the reader informed: the last row
|
|
67
|
+
* says how many lines are left out, which is the same convention the JSON pane uses.
|
|
68
|
+
*/
|
|
69
|
+
export function clipRows(rows: readonly string[], budget: number, width: number): string[] {
|
|
70
|
+
const room = Math.max(0, Math.floor(budget));
|
|
71
|
+
if (rows.length <= room) return [...rows];
|
|
72
|
+
if (room === 0) return [];
|
|
73
|
+
const kept = rows.slice(0, room - 1);
|
|
74
|
+
const hidden = rows.length - kept.length;
|
|
75
|
+
return [...kept, clip(`… ${hidden} more line${hidden === 1 ? "" : "s"}`, width)];
|
|
76
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the sections show, derived from the session's sources: the counts on the
|
|
3
|
+
* tabs, the one body height they share, the attention in the header, the filter
|
|
4
|
+
* chips, and the merged model the rows are drawn from.
|
|
5
|
+
*
|
|
6
|
+
* One module because these are one question asked five ways — "what is on this
|
|
7
|
+
* screen, and what does it add up to" — and because the component should be left
|
|
8
|
+
* with the state it mutates and the keys it reads, not the arithmetic over it.
|
|
9
|
+
*
|
|
10
|
+
* Pure: the source is handed in, no host is touched, and nothing is cached.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ExtensionReport } from "../../../../lib/requirements.ts";
|
|
14
|
+
import { filterChips, isFiltering, rowMatches, type PanelFilter } from "../filter.ts";
|
|
15
|
+
import { attentionSegments, panelCounts, type SummarySegment } from "../format.ts";
|
|
16
|
+
import { buildPanelModel, buildSkillModel, groupedEntryCount } from "../join.ts";
|
|
17
|
+
import type { ResourceItem, ResourceKind, WriteScope } from "../../picker/types.ts";
|
|
18
|
+
import type { SettingsSurface } from "../settings-runtime.ts";
|
|
19
|
+
import type { PanelModel, PanelRow, PanelSection, SkillInfo } from "../types.ts";
|
|
20
|
+
import { settingEntries } from "./entries.ts";
|
|
21
|
+
|
|
22
|
+
/** Everything the screen derives its content from. */
|
|
23
|
+
export interface ViewSource {
|
|
24
|
+
/** The write scope the settings rows are read in. */
|
|
25
|
+
scope: WriteScope;
|
|
26
|
+
/** The resolved resources of one kind. */
|
|
27
|
+
items(kind: ResourceKind): readonly ResourceItem[];
|
|
28
|
+
/** The current doctor reports. */
|
|
29
|
+
reports(): readonly ExtensionReport[];
|
|
30
|
+
/** The settings surface, for its rows. */
|
|
31
|
+
settings: SettingsSurface;
|
|
32
|
+
/** The agent directory the resource groups are labelled from. */
|
|
33
|
+
agentDir: string;
|
|
34
|
+
/** What the reader narrowed the rows to. */
|
|
35
|
+
filter: PanelFilter;
|
|
36
|
+
/** The extension the command narrowed to, if it named one. */
|
|
37
|
+
commandFilter?: string;
|
|
38
|
+
/** Something was changed this session. */
|
|
39
|
+
dirty: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The merged model for a list section. `filtered` off skips the reader's filter,
|
|
44
|
+
* which is what the header's attention counts need: narrowing the list must not
|
|
45
|
+
* hide a problem.
|
|
46
|
+
*/
|
|
47
|
+
export function modelFor(
|
|
48
|
+
source: ViewSource,
|
|
49
|
+
kind: ResourceKind,
|
|
50
|
+
skills: () => ReadonlyMap<string, SkillInfo>,
|
|
51
|
+
filtered = true,
|
|
52
|
+
): PanelModel {
|
|
53
|
+
const where = filtered && isFiltering(source.filter) ? (row: PanelRow) => rowMatches(row, source.filter) : undefined;
|
|
54
|
+
if (kind === "skills") {
|
|
55
|
+
return buildSkillModel({ items: source.items("skills"), index: skills(), agentDir: source.agentDir, where });
|
|
56
|
+
}
|
|
57
|
+
return buildPanelModel({
|
|
58
|
+
items: source.items("extensions"),
|
|
59
|
+
reports: source.reports(),
|
|
60
|
+
agentDir: source.agentDir,
|
|
61
|
+
filter: source.commandFilter,
|
|
62
|
+
where,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The section sizes the tabs print: the resources each section holds. They count
|
|
68
|
+
* what the section has, not what a filter left, so a tab does not shrink while
|
|
69
|
+
* another section is being narrowed.
|
|
70
|
+
*/
|
|
71
|
+
export function sectionCounts(source: ViewSource): Record<PanelSection, number> {
|
|
72
|
+
return {
|
|
73
|
+
extensions: source.items("extensions").length,
|
|
74
|
+
skills: source.items("skills").length,
|
|
75
|
+
settings: source.settings.views(source.scope).length,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The tallest section's entry count, group headings included. All three sections
|
|
81
|
+
* pad their body to it, which is what keeps the dock from resizing when the tabs
|
|
82
|
+
* change. Counted through `groupedEntryCount`, so measuring Skills never reads a
|
|
83
|
+
* `SKILL.md`.
|
|
84
|
+
*/
|
|
85
|
+
export function tallestBody(source: ViewSource): number {
|
|
86
|
+
const lists = (["extensions", "skills"] as const).map((kind) =>
|
|
87
|
+
groupedEntryCount(source.items(kind), source.agentDir, kind),
|
|
88
|
+
);
|
|
89
|
+
const settings = settingEntries(source.settings.views(source.scope)).length;
|
|
90
|
+
return Math.max(1, ...lists, settings);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The header's attention, over every extension rather than the filter. */
|
|
94
|
+
export function attentionFor(source: ViewSource): SummarySegment[] {
|
|
95
|
+
return attentionSegments(panelCounts(modelFor(source, "extensions", () => new Map(), false)), source.dirty);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The chips the tab row shows: the filter editor while open, else what applies. */
|
|
99
|
+
export function chipsFor(
|
|
100
|
+
source: ViewSource,
|
|
101
|
+
section: PanelSection,
|
|
102
|
+
editing: { buffer: string } | undefined,
|
|
103
|
+
): SummarySegment[] {
|
|
104
|
+
if (editing !== undefined) return [{ text: `/${editing.buffer}▏`, color: "accent" }];
|
|
105
|
+
const chips = filterChips(source.filter, section === "settings" ? "settings" : "rows");
|
|
106
|
+
// The command's own narrowing (`/bytefly plan`) is a filter the reader did not
|
|
107
|
+
// ask for this time, so the tab row says why the list is short.
|
|
108
|
+
if (source.commandFilter !== undefined && section === "extensions") {
|
|
109
|
+
chips.unshift({ text: `${source.commandFilter} only`, color: "meta" });
|
|
110
|
+
}
|
|
111
|
+
return chips;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The line a section shows in place of its body. */
|
|
115
|
+
export function emptyLabelFor(filter: PanelFilter, section: PanelSection): string {
|
|
116
|
+
if (isFiltering(filter)) return "Nothing matches the filter.";
|
|
117
|
+
if (section === "skills") return "No skills found.";
|
|
118
|
+
if (section === "settings") return "No settings found.";
|
|
119
|
+
return "No extensions found.";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The group heading a row sits under, for the block's `Source` fact. */
|
|
123
|
+
export function groupLabelFor(model: PanelModel | undefined, row: PanelRow | undefined): string | undefined {
|
|
124
|
+
if (model === undefined || row === undefined) return undefined;
|
|
125
|
+
return model.groups.find((group) => group.key === row.group)?.label;
|
|
126
|
+
}
|
|
@@ -1,35 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Settings section
|
|
3
|
-
*
|
|
2
|
+
* The Settings section's own state: the text editor, the JSON pane, and the
|
|
3
|
+
* writes. Everything it draws comes from `tui.ts` and `side-pane.ts`, because all
|
|
4
|
+
* three sections draw one frame.
|
|
4
5
|
*
|
|
5
|
-
* The component owns the section switch, the write scope,
|
|
6
|
-
*
|
|
7
|
-
* list keys, and the section reads and writes through the same surface.
|
|
6
|
+
* The component owns the section switch, the write scope, the cursor, the filter
|
|
7
|
+
* and the keys the three sections share; this class owns what only Settings has.
|
|
8
8
|
*/
|
|
9
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
10
|
import type { ListCursor } from "../../../../lib/tui/cursor.ts";
|
|
14
11
|
import type { WriteScope } from "../../picker/types.ts";
|
|
12
|
+
import { settingMatches, type PanelFilter } from "../filter.ts";
|
|
15
13
|
import type { SettingsSurface } from "../settings-runtime.ts";
|
|
16
|
-
import { SETTING_ROWS } from "../settings.ts";
|
|
14
|
+
import { SETTING_ROWS, type SettingView } from "../settings.ts";
|
|
17
15
|
import { applyEditKey, type EditingState } from "./editing.ts";
|
|
18
16
|
import { settingEntries, type PanelEntry } from "./entries.ts";
|
|
19
|
-
import { detailRows, groupLine, panelFrame, sectionBar } from "./frame.ts";
|
|
20
17
|
import { clampSelection } from "./nav.ts";
|
|
21
|
-
import { projectNote, sectionJsonPane, settingDetail, settingRow, settingsSummary } from "./settings-view.ts";
|
|
22
18
|
|
|
23
19
|
/** What the Settings section needs from the component that hosts it. */
|
|
24
20
|
export interface SettingsSectionDeps {
|
|
25
21
|
/** The rows, values, and writes the section reads and calls. */
|
|
26
22
|
surface: SettingsSurface;
|
|
27
|
-
|
|
23
|
+
/** The write scope the component owns. */
|
|
28
24
|
scope(): WriteScope;
|
|
25
|
+
/** The filter the component owns. */
|
|
26
|
+
filter(): PanelFilter;
|
|
29
27
|
cursor: ListCursor;
|
|
30
|
-
viewportRows: ViewportRowsSource | undefined;
|
|
31
|
-
cwd: string;
|
|
32
|
-
agentDir: string;
|
|
33
28
|
/** Keep the focused row across a rebuild and return it. */
|
|
34
29
|
retain(entries: PanelEntry[]): PanelEntry | undefined;
|
|
35
30
|
/** Panel navigation, for the keys this section does not own. */
|
|
@@ -38,13 +33,9 @@ export interface SettingsSectionDeps {
|
|
|
38
33
|
switchScope(): void;
|
|
39
34
|
notify(message: string): void;
|
|
40
35
|
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
36
|
}
|
|
46
37
|
|
|
47
|
-
/** The Settings section's state
|
|
38
|
+
/** The Settings section's state. */
|
|
48
39
|
export class SettingsSection {
|
|
49
40
|
private chain: Promise<void> = Promise.resolve();
|
|
50
41
|
private editing: EditingState | undefined;
|
|
@@ -52,16 +43,21 @@ export class SettingsSection {
|
|
|
52
43
|
|
|
53
44
|
constructor(private readonly deps: SettingsSectionDeps) {}
|
|
54
45
|
|
|
55
|
-
/** Whether a text row is open in the editor, so the hint row and
|
|
46
|
+
/** Whether a text row is open in the editor, so the hint row and keys defer. */
|
|
56
47
|
get isEditing(): boolean {
|
|
57
48
|
return this.editing !== undefined;
|
|
58
49
|
}
|
|
59
50
|
|
|
60
|
-
/** Whether the
|
|
51
|
+
/** Whether the focused row's section is shown as JSON. */
|
|
61
52
|
get jsonOpen(): boolean {
|
|
62
53
|
return this.json;
|
|
63
54
|
}
|
|
64
55
|
|
|
56
|
+
/** The editor's buffer and caret, for the row that is being typed into. */
|
|
57
|
+
get editingState(): EditingState | undefined {
|
|
58
|
+
return this.editing;
|
|
59
|
+
}
|
|
60
|
+
|
|
65
61
|
/** Run `task` after the previous write, then repaint once. */
|
|
66
62
|
private write(task: () => Promise<unknown>, onSettled?: (result: unknown) => void): void {
|
|
67
63
|
this.chain = this.chain
|
|
@@ -71,16 +67,30 @@ export class SettingsSection {
|
|
|
71
67
|
.then(() => this.deps.requestRender());
|
|
72
68
|
}
|
|
73
69
|
|
|
74
|
-
/** The rows
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
/** The rows the active scope shows, after the filter. */
|
|
71
|
+
views(): SettingView[] {
|
|
72
|
+
const filter = this.deps.filter();
|
|
73
|
+
return this.deps.surface.views(this.deps.scope()).filter((view) => settingMatches(view, filter));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Those rows as display entries, grouped under their headings. */
|
|
77
|
+
entries(): PanelEntry[] {
|
|
78
|
+
return settingEntries(this.views());
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
/** The focused
|
|
80
|
-
|
|
81
|
+
/** The focused setting, after clamping the cursor onto a row. */
|
|
82
|
+
focused(): SettingView | undefined {
|
|
81
83
|
const entries = this.entries();
|
|
82
84
|
clampSelection(entries, this.deps.cursor);
|
|
83
|
-
|
|
85
|
+
const entry = entries[this.deps.cursor.selected];
|
|
86
|
+
return entry?.kind === "setting" ? entry.setting : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The stored document behind the focused row, when the JSON pane is open. */
|
|
90
|
+
jsonFor(view: SettingView | undefined): { section: string; document: unknown } | undefined {
|
|
91
|
+
if (!this.json || view?.config === undefined) return undefined;
|
|
92
|
+
const { section } = view.config;
|
|
93
|
+
return { section, document: this.deps.surface.sectionDocument(this.deps.scope(), section) };
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
/** Drop the editor and the JSON pane when another section takes over. */
|
|
@@ -96,10 +106,9 @@ export class SettingsSection {
|
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
/** Change the focused setting: cycle it, or open the editor for a text row. */
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
const view = entry.setting;
|
|
109
|
+
changeSetting(): void {
|
|
110
|
+
const view = this.focused();
|
|
111
|
+
if (view === undefined) return;
|
|
103
112
|
if (view.kind === "text") {
|
|
104
113
|
this.editing = { id: view.id, buffer: view.value, caret: view.value.length };
|
|
105
114
|
this.deps.requestRender();
|
|
@@ -110,22 +119,22 @@ export class SettingsSection {
|
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
/** Delete the focused setting's leaf, so its default applies again. */
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
122
|
+
resetSetting(): void {
|
|
123
|
+
const view = this.focused();
|
|
124
|
+
if (view === undefined) return;
|
|
116
125
|
const scope = this.deps.scope();
|
|
117
|
-
this.write(() => this.deps.surface.reset(scope,
|
|
126
|
+
this.write(() => this.deps.surface.reset(scope, view.id));
|
|
118
127
|
}
|
|
119
128
|
|
|
120
|
-
/** Toggle the
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
124
|
-
if (!
|
|
129
|
+
/** Toggle the focused row between its value and the whole section as stored. */
|
|
130
|
+
toggleJson(): void {
|
|
131
|
+
const view = this.focused();
|
|
132
|
+
if (view === undefined) return;
|
|
133
|
+
if (!view.config) {
|
|
125
134
|
// There is no section to show, so leave the mode off rather than keeping a
|
|
126
135
|
// hidden toggle that would reopen the pane on another row.
|
|
127
136
|
this.json = false;
|
|
128
|
-
this.deps.notify(`${
|
|
137
|
+
this.deps.notify(`${view.label} is a Pi setting; bytefly.json has no section for it.`);
|
|
129
138
|
return;
|
|
130
139
|
}
|
|
131
140
|
this.json = !this.json;
|
|
@@ -133,7 +142,7 @@ export class SettingsSection {
|
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
/** Store the editor's buffer; a refusal keeps it open with the reason shown. */
|
|
136
|
-
|
|
145
|
+
commitEdit(): void {
|
|
137
146
|
const editing = this.editing;
|
|
138
147
|
if (!editing) return;
|
|
139
148
|
const scope = this.deps.scope();
|
|
@@ -149,13 +158,13 @@ export class SettingsSection {
|
|
|
149
158
|
* Editor keys. `Esc`/`Ctrl+C` cancel the edit rather than closing the screen,
|
|
150
159
|
* and everything else the section binds is inert until the editor closes.
|
|
151
160
|
*/
|
|
152
|
-
|
|
161
|
+
handleEditingInput(data: string): void {
|
|
153
162
|
const editing = this.editing;
|
|
154
163
|
if (!editing) return;
|
|
155
164
|
const max = SETTING_ROWS.find((row) => row.id === editing.id)?.text?.maxLength;
|
|
156
165
|
const action = applyEditKey(data, editing, max);
|
|
157
166
|
if (action.kind === "commit") {
|
|
158
|
-
|
|
167
|
+
this.commitEdit();
|
|
159
168
|
return;
|
|
160
169
|
}
|
|
161
170
|
if (action.kind === "cancel") {
|
|
@@ -167,84 +176,4 @@ export class SettingsSection {
|
|
|
167
176
|
this.editing = action.state;
|
|
168
177
|
this.deps.requestRender();
|
|
169
178
|
}
|
|
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
179
|
}
|