@gavin-hu/my-pi-agent 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -33
- package/README.zh-CN.md +44 -27
- package/extensions/bytefly/README.md +193 -140
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +3 -2
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +6 -11
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +21 -1
- package/extensions/bytefly/panel/format.ts +5 -2
- package/extensions/bytefly/panel/settings-runtime.ts +26 -6
- package/extensions/bytefly/panel/settings.ts +41 -4
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
- package/extensions/bytefly/panel/tui.ts +94 -152
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +14 -4
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +94 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +61 -6
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +4 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +91 -2
- package/extensions/subagent/render.ts +13 -0
- package/extensions/subagent/run.ts +17 -16
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/types.ts +11 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +1 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +50 -3
- package/lib/errors.ts +12 -0
- package/lib/format.ts +50 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +22 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
11
|
+
import { sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
12
12
|
import { sanitizeLine } from "../../../../lib/format.ts";
|
|
13
13
|
import { clip } from "../../../../lib/tui/columns.ts";
|
|
14
14
|
import { BODY_INDENT } from "../../../../lib/tui/indent.ts";
|
|
@@ -23,6 +23,34 @@ import { countsLine, INDENT, savedLine } from "./frame.ts";
|
|
|
23
23
|
/** Pi's personal settings file, named in the detail pane. */
|
|
24
24
|
const PI_SETTINGS_FILE = "settings.json";
|
|
25
25
|
|
|
26
|
+
/** Columns the marker and the leading cell take before the label. */
|
|
27
|
+
const ROW_PREFIX_WIDTH = 4;
|
|
28
|
+
/** Columns a value keeps when the row has to choose between the label and the value. */
|
|
29
|
+
const MIN_VALUE_WIDTH = 8;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The editing cell: the buffer around the caret, scrolled so the caret is always
|
|
33
|
+
* visible, with ellipses where text is off-screen. The cell fits `budget`
|
|
34
|
+
* columns, so a long buffer cannot push the label out of the row.
|
|
35
|
+
*/
|
|
36
|
+
function editingCell(buffer: string, caret: number, budget: number): string {
|
|
37
|
+
const width = Math.max(1, budget);
|
|
38
|
+
const before = buffer.slice(0, caret);
|
|
39
|
+
const beforeWidth = visibleWidth(before);
|
|
40
|
+
const caretRoom = width - 1;
|
|
41
|
+
if (beforeWidth > caretRoom) {
|
|
42
|
+
// Reserve one column for the leading ellipsis; the caret sits at the right edge.
|
|
43
|
+
const shownWidth = Math.max(0, caretRoom - 1);
|
|
44
|
+
const prefix = shownWidth > 0 ? "…" : "";
|
|
45
|
+
const shown = shownWidth > 0 ? sliceByColumn(before, beforeWidth - shownWidth, shownWidth, true) : "";
|
|
46
|
+
return `${prefix}${shown}▏`;
|
|
47
|
+
}
|
|
48
|
+
const used = beforeWidth + 1;
|
|
49
|
+
const afterRoom = Math.max(0, width - used);
|
|
50
|
+
const after = afterRoom > 0 ? truncateToWidth(buffer.slice(caret), afterRoom, "…") : "";
|
|
51
|
+
return `${before}▏${after}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
26
54
|
/** The rows this section shows, and the scope they were read in. */
|
|
27
55
|
export function settingsSummary(theme: Theme, width: number, scope: PanelScope, count: number): string {
|
|
28
56
|
return countsLine(theme, width, settingsSummarySegments(scope, count));
|
|
@@ -36,17 +64,19 @@ export function settingRow(
|
|
|
36
64
|
selected: boolean,
|
|
37
65
|
editing?: { buffer: string; caret: number },
|
|
38
66
|
): string {
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
67
|
+
const label = sanitizeLine(view.label);
|
|
68
|
+
// Keep the whole label when it fits, and never let a long value or an editing
|
|
69
|
+
// 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 - 1);
|
|
71
|
+
const labelReserve = Math.min(visibleWidth(label), Math.max(0, available - MIN_VALUE_WIDTH));
|
|
72
|
+
const valueBudget = Math.max(1, available - labelReserve);
|
|
73
|
+
const rawValue = editing ? editingCell(editing.buffer, editing.caret, valueBudget) : view.valueLabel;
|
|
44
74
|
return listRow(theme, width, {
|
|
45
75
|
selected,
|
|
46
76
|
leading: view.kind === "boolean" ? checkbox(theme, view.value === "true") : " ",
|
|
47
|
-
label
|
|
77
|
+
label,
|
|
48
78
|
labelRole: selected ? "accent" : "label",
|
|
49
|
-
trailing: sanitizeLine(
|
|
79
|
+
trailing: clip(sanitizeLine(rawValue), valueBudget),
|
|
50
80
|
trailingRole: editing ? "accent" : "meta",
|
|
51
81
|
});
|
|
52
82
|
}
|
|
@@ -58,8 +88,9 @@ export function projectNote(theme: Theme, width: number): string {
|
|
|
58
88
|
|
|
59
89
|
/** Where the focused row's value is written, and which scope supplied it. */
|
|
60
90
|
function originLine(view: SettingView, paths: { scope: WriteScope; cwd: string; agentDir: string }): string {
|
|
91
|
+
const scope = view.origin === "inherited" ? "global" : paths.scope;
|
|
61
92
|
const path =
|
|
62
|
-
view.source === "app" ? join(paths.agentDir, PI_SETTINGS_FILE) : configPath(
|
|
93
|
+
view.source === "app" ? join(paths.agentDir, PI_SETTINGS_FILE) : configPath(scope, paths.cwd, paths.agentDir);
|
|
63
94
|
const file = sanitizeLine(path);
|
|
64
95
|
switch (view.origin) {
|
|
65
96
|
case "inherited":
|
|
@@ -90,7 +121,7 @@ export function settingDetail(options: {
|
|
|
90
121
|
// The pending-changes marker survives the collapsed body: on a short terminal
|
|
91
122
|
// the value still changed, and the screen has to say so.
|
|
92
123
|
const saved = savedLine(theme, width, dirty);
|
|
93
|
-
if (budget === 0) return
|
|
124
|
+
if (budget === 0) return saved;
|
|
94
125
|
|
|
95
126
|
const lines = [headLine];
|
|
96
127
|
const descriptionWidth = Math.max(1, width - BODY_INDENT * 2);
|
|
@@ -130,28 +161,36 @@ export function sectionJsonPane(options: {
|
|
|
130
161
|
theme: Theme;
|
|
131
162
|
width: number;
|
|
132
163
|
section: string;
|
|
133
|
-
view: SettingView;
|
|
134
164
|
document: unknown;
|
|
135
165
|
budget: number;
|
|
166
|
+
dirty: boolean;
|
|
136
167
|
paths: { scope: WriteScope; cwd: string; agentDir: string };
|
|
137
168
|
}): string[] {
|
|
138
|
-
const { theme, width, section,
|
|
139
|
-
const
|
|
169
|
+
const { theme, width, section, document, budget, dirty, paths } = options;
|
|
170
|
+
const saved = savedLine(theme, width, dirty);
|
|
171
|
+
if (budget === 0) return saved;
|
|
172
|
+
const scope = paths.scope === "global" ? "Global" : "Project";
|
|
173
|
+
const file = sanitizeLine(configPath(paths.scope, paths.cwd, paths.agentDir));
|
|
174
|
+
const source = paint(theme, "faint", `${scope} · ${file}`);
|
|
140
175
|
const head = `${paint(theme, "meta", sanitizeLine(section))} ${paint(theme, "faint", "—")} ${source}`;
|
|
141
176
|
const lines = [clip(`${INDENT}${head}`, width)];
|
|
142
|
-
const room = Math.max(0, budget - lines.length);
|
|
177
|
+
const room = Math.max(0, budget - lines.length - saved.length);
|
|
143
178
|
const body = document === undefined ? [] : JSON.stringify(document, null, 2).split("\n");
|
|
144
179
|
|
|
145
180
|
if (body.length === 0) {
|
|
146
181
|
if (room > 0) lines.push(clip(`${INDENT}${paint(theme, "faint", "Not set in this scope.")}`, width));
|
|
182
|
+
lines.push(...saved);
|
|
147
183
|
return lines;
|
|
148
184
|
}
|
|
149
185
|
|
|
150
186
|
const shown = body.slice(0, room);
|
|
151
187
|
if (body.length > room && shown.length > 0) {
|
|
152
|
-
|
|
188
|
+
// The marker replaces one already-shown line, so the hidden count is one
|
|
189
|
+
// larger than the number of lines the slice left out.
|
|
190
|
+
shown[shown.length - 1] = `… ${body.length - room + 1} more lines`;
|
|
153
191
|
}
|
|
154
192
|
for (const line of shown) lines.push(clip(`${INDENT}${paint(theme, "faint", sanitizeLine(line))}`, width));
|
|
193
|
+
lines.push(...saved);
|
|
155
194
|
return lines;
|
|
156
195
|
}
|
|
157
196
|
|
|
@@ -17,29 +17,20 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
17
17
|
import { Key, matchesKey, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
18
18
|
import type { ViewportRowsSource } from "../../../lib/tui/chrome.ts";
|
|
19
19
|
import { dockListFrame } from "../../../lib/tui/chrome.ts";
|
|
20
|
-
import {
|
|
20
|
+
import { isCloseKey, ListCursor, navIntent, wheelDelta } from "../../../lib/tui/cursor.ts";
|
|
21
21
|
import { emptyState } from "../../../lib/tui/rows.ts";
|
|
22
22
|
import { titleRule } from "../../../lib/tui/rules.ts";
|
|
23
23
|
import type { ExtensionReport } from "../../../lib/requirements.ts";
|
|
24
24
|
import type { ExtensionsPickerRuntime } from "../picker/runtime.ts";
|
|
25
25
|
import { buildPanelModel } from "./join.ts";
|
|
26
|
+
import { SETTING_ROWS, UNTRUSTED_SETTINGS_NOTE } from "./settings.ts";
|
|
26
27
|
import type { SettingsSurface } from "./settings-runtime.ts";
|
|
27
28
|
import type { PanelModel, PanelSection } from "./types.ts";
|
|
28
29
|
import { entriesOf, entryKey, isSelectable, settingEntries, type PanelEntry } from "./tui/entries.ts";
|
|
30
|
+
import { applyEditKey, type EditingState } from "./tui/editing.ts";
|
|
29
31
|
import { clampSelection, firstRow, lastRow, moveRows, retainFocus } from "./tui/nav.ts";
|
|
30
32
|
import { extensionDetail, extensionHints, extensionRow, extensionSummary } from "./tui/extensions-view.ts";
|
|
31
|
-
import {
|
|
32
|
-
BODY_START,
|
|
33
|
-
DETAIL_ROWS,
|
|
34
|
-
EXPANDED_DETAIL_FALLBACK,
|
|
35
|
-
FRAME_ROWS,
|
|
36
|
-
groupLine,
|
|
37
|
-
hintLine,
|
|
38
|
-
MIN_BODY_ROWS,
|
|
39
|
-
noticeLine,
|
|
40
|
-
SCREEN_DEFAULT_ROWS,
|
|
41
|
-
sectionBar,
|
|
42
|
-
} from "./tui/frame.ts";
|
|
33
|
+
import { BODY_START, detailRows, groupLine, hintLine, noticeLine, panelFrame, sectionBar } from "./tui/frame.ts";
|
|
43
34
|
import {
|
|
44
35
|
editHints,
|
|
45
36
|
projectNote,
|
|
@@ -50,12 +41,6 @@ import {
|
|
|
50
41
|
settingsSummary,
|
|
51
42
|
} from "./tui/settings-view.ts";
|
|
52
43
|
|
|
53
|
-
/** Terminal rows, or `undefined` when the height is unknown. */
|
|
54
|
-
function totalRows(source: ViewportRowsSource | undefined): number | undefined {
|
|
55
|
-
const rows = typeof source === "function" ? source() : source;
|
|
56
|
-
return rows !== undefined && Number.isFinite(rows) && rows > 0 ? rows : undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
44
|
/** Host seams for the panel screen, injected by the command runner. */
|
|
60
45
|
export interface PanelComponentOptions {
|
|
61
46
|
/** The session's picker runtime: items, scope, toggles, dirty flag. */
|
|
@@ -102,10 +87,12 @@ export class PanelComponent implements Component {
|
|
|
102
87
|
private probed = false;
|
|
103
88
|
/** Transient feedback (a refusal or a failed write), shown in the hint row. */
|
|
104
89
|
private notice: string | undefined;
|
|
105
|
-
/** The text row being edited
|
|
106
|
-
private editing:
|
|
90
|
+
/** The text row being edited, if any. */
|
|
91
|
+
private editing: EditingState | undefined;
|
|
107
92
|
/** True while the Settings detail pane shows the focused row's section as JSON. */
|
|
108
93
|
private json = false;
|
|
94
|
+
/** Whether the last Settings render kept the project note; the click mapping needs it. */
|
|
95
|
+
private noteShown = false;
|
|
109
96
|
/** The visible section. */
|
|
110
97
|
private section: PanelSection;
|
|
111
98
|
|
|
@@ -180,14 +167,22 @@ export class PanelComponent implements Component {
|
|
|
180
167
|
if (next === "project" && !this.options.runtime.projectAvailable()) {
|
|
181
168
|
this.notify(
|
|
182
169
|
this.section === "settings"
|
|
183
|
-
?
|
|
170
|
+
? UNTRUSTED_SETTINGS_NOTE
|
|
184
171
|
: "Project is not trusted; manage extensions in Global scope only.",
|
|
185
172
|
);
|
|
186
173
|
return;
|
|
187
174
|
}
|
|
188
175
|
// The JSON pane names a file, so a scope switch has to close it.
|
|
189
176
|
this.json = false;
|
|
177
|
+
const focused = entryKey(this.activeEntries()[this.cursor.selected]);
|
|
190
178
|
this.options.runtime.setScope(next);
|
|
179
|
+
// Keep the setting under the cursor across the layer switch; the two scopes
|
|
180
|
+
// do not share entry indices because Project drops the app rows, so the
|
|
181
|
+
// render's focus retention and clamp land on the nearest row when it cannot.
|
|
182
|
+
if (focused !== undefined) {
|
|
183
|
+
const index = this.activeEntries().findIndex((entry) => entryKey(entry) === focused);
|
|
184
|
+
if (index >= 0) this.cursor.selected = index;
|
|
185
|
+
}
|
|
191
186
|
this.options.requestRender();
|
|
192
187
|
}
|
|
193
188
|
|
|
@@ -255,6 +250,9 @@ export class PanelComponent implements Component {
|
|
|
255
250
|
const entry = this.activeEntries()[this.cursor.selected];
|
|
256
251
|
if (entry?.kind !== "setting") return;
|
|
257
252
|
if (!entry.setting.config) {
|
|
253
|
+
// There is no section to show, so leave the mode off rather than keeping a
|
|
254
|
+
// hidden toggle that would reopen the pane on another row.
|
|
255
|
+
this.json = false;
|
|
258
256
|
this.notify(`${entry.setting.label} is a Pi setting; bytefly.json has no section for it.`);
|
|
259
257
|
return;
|
|
260
258
|
}
|
|
@@ -276,63 +274,26 @@ export class PanelComponent implements Component {
|
|
|
276
274
|
.then(() => this.options.requestRender());
|
|
277
275
|
}
|
|
278
276
|
|
|
279
|
-
/** The text row being edited, read live so a scope switch cannot point at a stale row. */
|
|
280
|
-
private editingRow(): { view: ReturnType<SettingsSurface["views"]>[number] } | undefined {
|
|
281
|
-
const editing = this.editing;
|
|
282
|
-
if (!editing) return undefined;
|
|
283
|
-
const scope = this.options.runtime.scope();
|
|
284
|
-
const view = this.options.settings.views(scope).find((entry) => entry.id === editing.id);
|
|
285
|
-
return view ? { view } : undefined;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
277
|
/**
|
|
289
|
-
* Editor keys. `Esc`
|
|
290
|
-
* everything else the screen binds is inert until the editor closes.
|
|
278
|
+
* Editor keys. `Esc`/`Ctrl+C` cancel the edit rather than closing the screen,
|
|
279
|
+
* and everything else the screen binds is inert until the editor closes.
|
|
291
280
|
*/
|
|
292
281
|
private handleEditingInput(data: string): void {
|
|
293
282
|
const editing = this.editing;
|
|
294
283
|
if (!editing) return;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
if (matchesKey(data, Key.enter)) {
|
|
284
|
+
const max = SETTING_ROWS.find((row) => row.id === editing.id)?.text?.maxLength;
|
|
285
|
+
const action = applyEditKey(data, editing, max);
|
|
286
|
+
if (action.kind === "commit") {
|
|
301
287
|
void this.commitEdit();
|
|
302
288
|
return;
|
|
303
289
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
} else if (matchesKey(data, Key.delete)) {
|
|
309
|
-
if (caret < buffer.length)
|
|
310
|
-
this.editing = { ...editing, buffer: buffer.slice(0, caret) + buffer.slice(caret + 1) };
|
|
311
|
-
} else if (matchesKey(data, Key.left)) {
|
|
312
|
-
this.editing = { ...editing, caret: Math.max(0, caret - 1) };
|
|
313
|
-
} else if (matchesKey(data, Key.right)) {
|
|
314
|
-
this.editing = { ...editing, caret: Math.min(buffer.length, caret + 1) };
|
|
315
|
-
// `Ctrl+A`/`Ctrl+E` are the emacs spellings of Home/End, which pi-tui's
|
|
316
|
-
// `Key.home`/`Key.end` do not match; both are offered because a terminal
|
|
317
|
-
// may send either.
|
|
318
|
-
} else if (matchesKey(data, Key.home) || matchesKey(data, "ctrl+a")) {
|
|
319
|
-
this.editing = { ...editing, caret: 0 };
|
|
320
|
-
} else if (matchesKey(data, Key.end) || matchesKey(data, "ctrl+e")) {
|
|
321
|
-
this.editing = { ...editing, caret: buffer.length };
|
|
322
|
-
} else if (matchesKey(data, "ctrl+u")) {
|
|
323
|
-
this.editing = { ...editing, buffer: "", caret: 0 };
|
|
324
|
-
} else {
|
|
325
|
-
// Anything else must be printable; control sequences are not text.
|
|
326
|
-
const typed = [...data].filter((char) => char >= " " && char !== "\u007f").join("");
|
|
327
|
-
const max = this.editingRow()?.view.text?.maxLength;
|
|
328
|
-
if (typed && max !== undefined && buffer.length + typed.length <= max) {
|
|
329
|
-
this.editing = {
|
|
330
|
-
...editing,
|
|
331
|
-
buffer: buffer.slice(0, caret) + typed + buffer.slice(caret),
|
|
332
|
-
caret: caret + typed.length,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
290
|
+
if (action.kind === "cancel") {
|
|
291
|
+
this.editing = undefined;
|
|
292
|
+
this.options.requestRender();
|
|
293
|
+
return;
|
|
335
294
|
}
|
|
295
|
+
if (!action.changed) return;
|
|
296
|
+
this.editing = action.state;
|
|
336
297
|
this.options.requestRender();
|
|
337
298
|
}
|
|
338
299
|
|
|
@@ -534,23 +495,24 @@ export class PanelComponent implements Component {
|
|
|
534
495
|
if (entries.length === 0) return undefined;
|
|
535
496
|
// The Settings section prints the "Global only" note first in Project scope,
|
|
536
497
|
// so the rows start one line lower there.
|
|
537
|
-
const note = this.section === "settings" && this.
|
|
498
|
+
const note = this.section === "settings" && this.noteShown ? 1 : 0;
|
|
538
499
|
const line = event.y - (BODY_START + note);
|
|
539
|
-
|
|
540
|
-
|
|
500
|
+
const window = this.cursor.window(entries.length);
|
|
501
|
+
if (line < 0 || line >= window.end - window.start) return undefined;
|
|
502
|
+
const index = window.start + line;
|
|
541
503
|
if (!isSelectable(entries[index])) return undefined;
|
|
542
504
|
this.select(index);
|
|
543
505
|
return { handled: true };
|
|
544
506
|
}
|
|
545
507
|
|
|
546
508
|
/** Key hints for the active section; whole hints drop as the width runs out. */
|
|
547
|
-
private hint(width: number, expanded =
|
|
509
|
+
private hint(width: number, options: { expanded?: boolean; jsonOpen?: boolean } = {}): string {
|
|
548
510
|
if (this.editing) return hintLine(this.theme, width, editHints());
|
|
549
511
|
const hints =
|
|
550
512
|
this.section === "settings"
|
|
551
|
-
? settingsHints(
|
|
513
|
+
? settingsHints(options.jsonOpen === true)
|
|
552
514
|
: extensionHints({
|
|
553
|
-
expanded,
|
|
515
|
+
expanded: options.expanded === true,
|
|
554
516
|
refreshing: this.refreshing,
|
|
555
517
|
projectScope: this.options.runtime.scope() === "project",
|
|
556
518
|
});
|
|
@@ -558,21 +520,8 @@ export class PanelComponent implements Component {
|
|
|
558
520
|
}
|
|
559
521
|
|
|
560
522
|
/** The key-hint row, or the notice that temporarily replaces it. */
|
|
561
|
-
private hintRow(width: number, expanded =
|
|
562
|
-
return this.notice !== undefined ? noticeLine(this.theme, width, this.notice) : this.hint(width,
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Detail rows the focused row gets: the fixed pane when collapsed, the free
|
|
567
|
-
* height when expanded. Zero when the terminal cannot hold one body row next to
|
|
568
|
-
* the pane, so the hint row is never pushed off the screen.
|
|
569
|
-
*/
|
|
570
|
-
private detailRows(expanded: boolean): number {
|
|
571
|
-
const total = totalRows(this.options.viewportRows);
|
|
572
|
-
if (total === undefined) return expanded ? EXPANDED_DETAIL_FALLBACK : DETAIL_ROWS;
|
|
573
|
-
const available = total - FRAME_ROWS - MIN_BODY_ROWS;
|
|
574
|
-
if (available < DETAIL_ROWS + 1) return 0;
|
|
575
|
-
return expanded ? Math.max(DETAIL_ROWS, available) : DETAIL_ROWS;
|
|
523
|
+
private hintRow(width: number, options: { expanded?: boolean; jsonOpen?: boolean } = {}): string {
|
|
524
|
+
return this.notice !== undefined ? noticeLine(this.theme, width, this.notice) : this.hint(width, options);
|
|
576
525
|
}
|
|
577
526
|
|
|
578
527
|
/** Expand or collapse the focused row's findings. */
|
|
@@ -584,6 +533,13 @@ export class PanelComponent implements Component {
|
|
|
584
533
|
this.options.requestRender();
|
|
585
534
|
}
|
|
586
535
|
|
|
536
|
+
/** Retain the focus across a rebuild and return the entry it landed on. */
|
|
537
|
+
private retain(entries: PanelEntry[]): PanelEntry | undefined {
|
|
538
|
+
this.retainFocus(entries);
|
|
539
|
+
this.clampSelection(entries);
|
|
540
|
+
return entries[this.cursor.selected];
|
|
541
|
+
}
|
|
542
|
+
|
|
587
543
|
/** The Settings body: scope counts, the project note, rows, and detail. */
|
|
588
544
|
private renderSettings(width: number, header: string): string[] {
|
|
589
545
|
const scope = this.options.runtime.scope();
|
|
@@ -597,11 +553,11 @@ export class PanelComponent implements Component {
|
|
|
597
553
|
const entries = settingEntries(views);
|
|
598
554
|
const note = scope === "project" ? projectNote(this.theme, width) : undefined;
|
|
599
555
|
|
|
600
|
-
this.
|
|
601
|
-
this.clampSelection(entries);
|
|
602
|
-
|
|
603
|
-
const focusedEntry = entries[this.cursor.selected];
|
|
556
|
+
const focusedEntry = this.retain(entries);
|
|
604
557
|
const focused = focusedEntry?.kind === "setting" ? focusedEntry.setting : undefined;
|
|
558
|
+
// A row with no section leaves the mode off, so moving away and back cannot
|
|
559
|
+
// reopen a pane the user closed.
|
|
560
|
+
if (focused && !focused.config) this.json = false;
|
|
605
561
|
const jsonConfig = this.json ? focused?.config : undefined;
|
|
606
562
|
const detail =
|
|
607
563
|
jsonConfig && focused
|
|
@@ -609,50 +565,40 @@ export class PanelComponent implements Component {
|
|
|
609
565
|
theme: this.theme,
|
|
610
566
|
width,
|
|
611
567
|
section: jsonConfig.section,
|
|
612
|
-
view: focused,
|
|
613
568
|
document: this.options.settings.sectionDocument(scope, jsonConfig.section),
|
|
614
|
-
budget:
|
|
569
|
+
budget: detailRows(true, this.options.viewportRows),
|
|
570
|
+
dirty: this.dirty,
|
|
615
571
|
paths: { scope, cwd: this.options.cwd, agentDir: this.options.agentDir },
|
|
616
572
|
})
|
|
617
573
|
: settingDetail({
|
|
618
574
|
theme: this.theme,
|
|
619
575
|
width,
|
|
620
576
|
view: focused,
|
|
621
|
-
budget:
|
|
577
|
+
budget: detailRows(false, this.options.viewportRows),
|
|
622
578
|
dirty: this.dirty,
|
|
623
579
|
paths: { scope, cwd: this.options.cwd, agentDir: this.options.agentDir },
|
|
624
580
|
});
|
|
625
581
|
|
|
626
|
-
|
|
627
|
-
this.
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
);
|
|
632
|
-
this.cursor.sync(entries.length);
|
|
633
|
-
const { start, end } = this.cursor.window(entries.length);
|
|
634
|
-
|
|
635
|
-
const body: string[] = [];
|
|
636
|
-
if (note) body.push(note);
|
|
637
|
-
for (let index = start; index < end; index += 1) {
|
|
638
|
-
const entry = entries[index];
|
|
639
|
-
if (entry.kind === "group") body.push(groupLine(this.theme, width, entry));
|
|
640
|
-
else if (entry.kind === "setting") {
|
|
641
|
-
const editing = this.editing?.id === entry.setting.id ? this.editing : undefined;
|
|
642
|
-
body.push(settingRow(this.theme, width, entry.setting, index === this.cursor.selected, editing));
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
return dockListFrame({
|
|
582
|
+
const { lines, noteShown } = panelFrame({
|
|
583
|
+
theme: this.theme,
|
|
584
|
+
viewportRows: this.options.viewportRows,
|
|
585
|
+
cursor: this.cursor,
|
|
586
|
+
width,
|
|
647
587
|
header,
|
|
648
588
|
summary,
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
589
|
+
entries,
|
|
590
|
+
note,
|
|
591
|
+
detail,
|
|
592
|
+
hint: this.hintRow(width, { jsonOpen: jsonConfig !== undefined }),
|
|
593
|
+
draw: (entry, index) => {
|
|
594
|
+
if (entry.kind === "group") return groupLine(this.theme, width, entry);
|
|
595
|
+
if (entry.kind !== "setting") return undefined;
|
|
596
|
+
const editing = this.editing?.id === entry.setting.id ? this.editing : undefined;
|
|
597
|
+
return settingRow(this.theme, width, entry.setting, index === this.cursor.selected, editing);
|
|
598
|
+
},
|
|
655
599
|
});
|
|
600
|
+
this.noteShown = noteShown;
|
|
601
|
+
return lines;
|
|
656
602
|
}
|
|
657
603
|
|
|
658
604
|
render(width: number): string[] {
|
|
@@ -678,39 +624,35 @@ export class PanelComponent implements Component {
|
|
|
678
624
|
});
|
|
679
625
|
}
|
|
680
626
|
|
|
681
|
-
this.
|
|
682
|
-
this.clampSelection(entries);
|
|
683
|
-
|
|
684
|
-
const focusedEntry = entries[this.cursor.selected];
|
|
627
|
+
const focusedEntry = this.retain(entries);
|
|
685
628
|
const focused = focusedEntry?.kind === "row" ? focusedEntry.row : undefined;
|
|
686
629
|
const expanded = focused !== undefined && this.expanded.has(focused.key);
|
|
687
|
-
const detail = extensionDetail(
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
630
|
+
const detail = extensionDetail(
|
|
631
|
+
this.theme,
|
|
632
|
+
w,
|
|
633
|
+
focused,
|
|
634
|
+
detailRows(expanded, this.options.viewportRows),
|
|
635
|
+
expanded,
|
|
636
|
+
this.dirty,
|
|
694
637
|
);
|
|
695
|
-
this.cursor.sync(entries.length);
|
|
696
|
-
const { start, end } = this.cursor.window(entries.length);
|
|
697
|
-
|
|
698
|
-
const body: string[] = [];
|
|
699
|
-
for (let index = start; index < end; index += 1) {
|
|
700
|
-
const entry = entries[index];
|
|
701
|
-
if (entry.kind === "group") body.push(groupLine(this.theme, w, entry));
|
|
702
|
-
else if (entry.kind === "row") body.push(extensionRow(this.theme, w, entry.row, index === this.cursor.selected));
|
|
703
|
-
}
|
|
704
638
|
|
|
705
|
-
|
|
639
|
+
const { lines, noteShown } = panelFrame({
|
|
640
|
+
theme: this.theme,
|
|
641
|
+
viewportRows: this.options.viewportRows,
|
|
642
|
+
cursor: this.cursor,
|
|
643
|
+
width: w,
|
|
706
644
|
header,
|
|
707
645
|
summary,
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
646
|
+
entries,
|
|
647
|
+
detail,
|
|
648
|
+
hint: this.hintRow(w, { expanded }),
|
|
649
|
+
draw: (entry, index) => {
|
|
650
|
+
if (entry.kind === "group") return groupLine(this.theme, w, entry);
|
|
651
|
+
if (entry.kind !== "row") return undefined;
|
|
652
|
+
return extensionRow(this.theme, w, entry.row, index === this.cursor.selected);
|
|
653
|
+
},
|
|
714
654
|
});
|
|
655
|
+
this.noteShown = noteShown;
|
|
656
|
+
return lines;
|
|
715
657
|
}
|
|
716
658
|
}
|
|
@@ -50,6 +50,7 @@ export function createSettingsStore(manager: SettingsManager): SettingsStore {
|
|
|
50
50
|
setQuietStartup: (quiet) => manager.setQuietStartup(quiet),
|
|
51
51
|
setOutputPad: (padding) => manager.setOutputPad(padding),
|
|
52
52
|
setShowHardwareCursor: (enabled) => manager.setShowHardwareCursor(enabled),
|
|
53
|
+
setTheme: (theme) => manager.setTheme(theme),
|
|
53
54
|
isProjectTrusted: () => manager.isProjectTrusted(),
|
|
54
55
|
flush: () => manager.flush(),
|
|
55
56
|
drainErrors: () => manager.drainErrors(),
|
|
@@ -37,6 +37,8 @@ export interface SettingsStore {
|
|
|
37
37
|
setQuietStartup(quiet: QuietStartup): void;
|
|
38
38
|
setOutputPad(padding: 0 | 1): void;
|
|
39
39
|
setShowHardwareCursor(enabled: boolean): void;
|
|
40
|
+
/** Pi's theme setting is a personal-file preference. */
|
|
41
|
+
setTheme(theme: string): void;
|
|
40
42
|
isProjectTrusted(): boolean;
|
|
41
43
|
flush(): Promise<void>;
|
|
42
44
|
drainErrors(): Array<{ error: Error }>;
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The transcript component that draws one separator entry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* `render` runs on every frame Pi draws, and a session holds one separator per
|
|
5
|
+
* turn, so the line is a pure function of the stored entry data, the width, and
|
|
6
|
+
* the theme it was built with. `centeredRule` paints and measures a full-width
|
|
7
|
+
* rule, so the placed line is memoized on the width (`RenderCache`); without it
|
|
8
|
+
* a long session rebuilds every turn's rule on every frame. Pi rebuilds the
|
|
9
|
+
* component from the renderer on a theme change (its
|
|
10
|
+
* `CustomEntryComponent.invalidate` re-runs it), so the captured theme cannot go
|
|
11
|
+
* stale; `invalidate` drops the memo anyway, in case that changes.
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
9
15
|
import type { Component } from "@earendil-works/pi-tui";
|
|
16
|
+
import { RenderCache } from "../../../lib/tui/render-cache.ts";
|
|
10
17
|
import { separatorLine } from "./format.ts";
|
|
11
18
|
import type { TurnSeparatorData } from "./types.ts";
|
|
12
19
|
|
|
13
20
|
/** Renders one separator entry; the data comes from the stored entry. */
|
|
14
21
|
export class SeparatorComponent implements Component {
|
|
22
|
+
private readonly cache = new RenderCache();
|
|
23
|
+
|
|
15
24
|
constructor(
|
|
16
25
|
private readonly data: TurnSeparatorData,
|
|
17
26
|
private readonly theme: Theme,
|
|
@@ -19,10 +28,11 @@ export class SeparatorComponent implements Component {
|
|
|
19
28
|
) {}
|
|
20
29
|
|
|
21
30
|
invalidate(): void {
|
|
22
|
-
|
|
31
|
+
this.cache.invalidate();
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
render(width: number): string[] {
|
|
26
|
-
|
|
35
|
+
const w = Math.max(1, width);
|
|
36
|
+
return this.cache.lines(w, undefined, () => separatorLine(this.data, w, this.theme, this.pad));
|
|
27
37
|
}
|
|
28
38
|
}
|