@gavin-hu/my-pi-agent 0.11.4 → 0.12.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 +13 -8
- package/README.zh-CN.md +5 -3
- package/assets/chrome.png +0 -0
- package/assets/plan-review.png +0 -0
- package/extensions/bytefly/README.md +291 -207
- package/extensions/bytefly/doctor/runtime.ts +16 -0
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/segments.ts +3 -2
- package/extensions/bytefly/footer/types.ts +1 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +13 -3
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +201 -38
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +32 -0
- package/extensions/bytefly/update/register.ts +72 -0
- package/extensions/bytefly/update/state.ts +28 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +2 -2
- package/extensions/document/tool.ts +1 -3
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +39 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/memory/README.md +1 -1
- package/extensions/plan/README.md +24 -8
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +18 -12
- package/extensions/plan/plans.ts +1 -1
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +243 -0
- package/extensions/plan/runtime.ts +4 -0
- package/extensions/plan/steps.ts +34 -23
- package/extensions/plan/todos.ts +5 -58
- package/extensions/plan/tools.ts +66 -222
- package/extensions/plan/tui.ts +17 -9
- package/extensions/plan/types.ts +3 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +5 -0
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +100 -36
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/subagent/index.ts +0 -2
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/fetch/tool.ts +1 -3
- package/extensions/web-access/search/tool.ts +1 -3
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/lib/README.md +2 -1
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/requirements.ts +76 -2
- package/lib/todo-state.ts +62 -0
- package/lib/tui/glyphs.ts +1 -0
- package/package.json +1 -1
- package/assets/bytefly.schema.json +0 -785
|
@@ -3,107 +3,51 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extensions lists every resolved extension with its enable checkbox and its
|
|
5
5
|
* health; Settings lists the curated preferences with their values. The
|
|
6
|
-
* component
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* component owns the state — the cursors, the focused row key per section, the
|
|
7
|
+
* expanded set, the pending write chain, and the active section — and the two
|
|
8
|
+
* section modules own how their rows and detail panes are drawn. It is derived:
|
|
9
|
+
* it rebuilds the model from the picker runtime, the reports, or the settings
|
|
10
|
+
* surface on every render, keeps the focused row's key across rebuilds, and
|
|
11
|
+
* requests a repaint only after an input changes state. The screen is drawn from
|
|
12
|
+
* the design system — a label-first `titleRule`, one `listRow` grammar, one
|
|
13
|
+
* `checkbox` — and its scroll math from `lib/tui/cursor.ts`.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
|
-
import { join } from "node:path";
|
|
16
16
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
type Component,
|
|
21
|
-
type TuiMouseEvent,
|
|
22
|
-
type TuiMouseEventResult,
|
|
23
|
-
wrapTextWithAnsi,
|
|
24
|
-
} from "@earendil-works/pi-tui";
|
|
25
|
-
import { sanitizeLine } from "../../../lib/format.ts";
|
|
26
|
-
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../../lib/tui/chrome.ts";
|
|
27
|
-
import { clip, fitCells, splitRow } from "../../../lib/tui/columns.ts";
|
|
17
|
+
import { Key, matchesKey, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
18
|
+
import type { ViewportRowsSource } from "../../../lib/tui/chrome.ts";
|
|
19
|
+
import { dockListFrame } from "../../../lib/tui/chrome.ts";
|
|
28
20
|
import { fitRows, formatRange, isCloseKey, ListCursor, navIntent, wheelDelta } from "../../../lib/tui/cursor.ts";
|
|
29
|
-
import {
|
|
30
|
-
import { checkbox, emptyState, listRow } from "../../../lib/tui/rows.ts";
|
|
21
|
+
import { emptyState } from "../../../lib/tui/rows.ts";
|
|
31
22
|
import { titleRule } from "../../../lib/tui/rules.ts";
|
|
32
|
-
import { paint } from "../../../lib/tui/tokens.ts";
|
|
33
23
|
import type { ExtensionReport } from "../../../lib/requirements.ts";
|
|
34
|
-
import { detailLines as reportDetailLines } from "../doctor/format.ts";
|
|
35
24
|
import type { ExtensionsPickerRuntime } from "../picker/runtime.ts";
|
|
36
|
-
import { rowBadge, sectionSegments, settingsSummarySegments, summarySegments, type SummarySegment } from "./format.ts";
|
|
37
25
|
import { buildPanelModel } from "./join.ts";
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
| { kind: "row"; row: PanelRow }
|
|
64
|
-
| { kind: "setting"; setting: SettingView };
|
|
65
|
-
|
|
66
|
-
/** A selectable entry's stable key, or `undefined` for a group header. */
|
|
67
|
-
function entryKey(entry: PanelEntry | undefined): string | undefined {
|
|
68
|
-
if (entry?.kind === "row") return entry.row.key;
|
|
69
|
-
if (entry?.kind === "setting") return entry.setting.id;
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Whether an entry can take the focus. */
|
|
74
|
-
function isSelectable(entry: PanelEntry | undefined): boolean {
|
|
75
|
-
return entryKey(entry) !== undefined;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** The next selectable entry from `from` in `direction`, or `undefined`. */
|
|
79
|
-
function nextRow(entries: PanelEntry[], from: number, direction: -1 | 1): number | undefined {
|
|
80
|
-
for (let index = from + direction; index >= 0 && index < entries.length; index += direction) {
|
|
81
|
-
if (isSelectable(entries[index])) return index;
|
|
82
|
-
}
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Interleave the extension model's groups and rows into one display list. */
|
|
87
|
-
function entriesOf(model: PanelModel): PanelEntry[] {
|
|
88
|
-
const entries: PanelEntry[] = [];
|
|
89
|
-
for (const group of model.groups) {
|
|
90
|
-
entries.push({ kind: "group", key: group.key, label: group.label, count: group.count });
|
|
91
|
-
for (const row of model.rows) {
|
|
92
|
-
if (row.group === group.key) entries.push({ kind: "row", row });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return entries;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Interleave the settings views into the same group/row display list. */
|
|
99
|
-
function settingEntries(views: readonly SettingView[]): PanelEntry[] {
|
|
100
|
-
const entries: PanelEntry[] = [];
|
|
101
|
-
for (const group of groupViews(views)) {
|
|
102
|
-
entries.push({ kind: "group", key: group.key, label: group.label, count: group.views.length });
|
|
103
|
-
for (const view of group.views) entries.push({ kind: "setting", setting: view });
|
|
104
|
-
}
|
|
105
|
-
return entries;
|
|
106
|
-
}
|
|
26
|
+
import type { SettingsSurface } from "./settings-runtime.ts";
|
|
27
|
+
import type { PanelModel, PanelSection } from "./types.ts";
|
|
28
|
+
import { entriesOf, entryKey, isSelectable, settingEntries, type PanelEntry } from "./tui/entries.ts";
|
|
29
|
+
import { clampSelection, firstRow, lastRow, moveRows, retainFocus } from "./tui/nav.ts";
|
|
30
|
+
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";
|
|
43
|
+
import {
|
|
44
|
+
editHints,
|
|
45
|
+
projectNote,
|
|
46
|
+
settingDetail,
|
|
47
|
+
settingRow,
|
|
48
|
+
settingsHints,
|
|
49
|
+
settingsSummary,
|
|
50
|
+
} from "./tui/settings-view.ts";
|
|
107
51
|
|
|
108
52
|
/** Terminal rows, or `undefined` when the height is unknown. */
|
|
109
53
|
function totalRows(source: ViewportRowsSource | undefined): number | undefined {
|
|
@@ -157,6 +101,8 @@ export class PanelComponent implements Component {
|
|
|
157
101
|
private probed = false;
|
|
158
102
|
/** Transient feedback (a refusal or a failed write), shown in the hint row. */
|
|
159
103
|
private notice: string | undefined;
|
|
104
|
+
/** The text row being edited: its id, the buffer, and the caret offset. */
|
|
105
|
+
private editing: { id: string; buffer: string; caret: number } | undefined;
|
|
160
106
|
/** The visible section. */
|
|
161
107
|
private section: PanelSection;
|
|
162
108
|
|
|
@@ -176,6 +122,11 @@ export class PanelComponent implements Component {
|
|
|
176
122
|
return this.options.theme;
|
|
177
123
|
}
|
|
178
124
|
|
|
125
|
+
/** Whether anything has been written this session, either section. */
|
|
126
|
+
private get dirty(): boolean {
|
|
127
|
+
return this.options.runtime.isDirty() || this.options.settings.isDirty();
|
|
128
|
+
}
|
|
129
|
+
|
|
179
130
|
invalidate(): void {
|
|
180
131
|
// Everything is rebuilt from live state on each render.
|
|
181
132
|
}
|
|
@@ -199,52 +150,16 @@ export class PanelComponent implements Component {
|
|
|
199
150
|
|
|
200
151
|
/** Keep the focus on the same row key when the list shape changes. */
|
|
201
152
|
private retainFocus(entries: PanelEntry[]): void {
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const focusedKey = this.focusedKeys[this.section];
|
|
208
|
-
if (focusedKey === undefined) return;
|
|
209
|
-
const index = entries.findIndex((candidate) => entryKey(candidate) === focusedKey);
|
|
210
|
-
if (index >= 0) this.cursor.selected = index;
|
|
153
|
+
const before = entryKey(entries[this.cursor.selected]);
|
|
154
|
+
retainFocus(entries, this.cursor, this.focusedKeys[this.section]);
|
|
155
|
+
const after = entryKey(entries[this.cursor.selected]);
|
|
156
|
+
if (after !== undefined) this.focusedKeys[this.section] = after;
|
|
157
|
+
else if (before !== undefined) this.focusedKeys[this.section] = before;
|
|
211
158
|
}
|
|
212
159
|
|
|
213
160
|
/** Move onto the nearest selectable row when the focus is not already on one. */
|
|
214
161
|
private clampSelection(entries: PanelEntry[]): void {
|
|
215
|
-
|
|
216
|
-
const index = Math.min(Math.max(0, this.cursor.selected), entries.length - 1);
|
|
217
|
-
if (isSelectable(entries[index])) {
|
|
218
|
-
this.cursor.selected = index;
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
// Prefer the closest row above, then the closest below, so a scope switch
|
|
222
|
-
// that drops the focused row leaves the cursor nearby instead of at the top.
|
|
223
|
-
for (let back = index - 1; back >= 0; back -= 1) {
|
|
224
|
-
if (isSelectable(entries[back])) {
|
|
225
|
-
this.cursor.selected = back;
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
for (let forward = index + 1; forward < entries.length; forward += 1) {
|
|
230
|
-
if (isSelectable(entries[forward])) {
|
|
231
|
-
this.cursor.selected = forward;
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private firstRow(): number | undefined {
|
|
238
|
-
const index = this.activeEntries().findIndex(isSelectable);
|
|
239
|
-
return index >= 0 ? index : undefined;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
private lastRow(): number | undefined {
|
|
243
|
-
const entries = this.activeEntries();
|
|
244
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
245
|
-
if (isSelectable(entries[index])) return index;
|
|
246
|
-
}
|
|
247
|
-
return undefined;
|
|
162
|
+
clampSelection(entries, this.cursor);
|
|
248
163
|
}
|
|
249
164
|
|
|
250
165
|
private select(index: number | undefined): void {
|
|
@@ -254,19 +169,7 @@ export class PanelComponent implements Component {
|
|
|
254
169
|
|
|
255
170
|
/** Move `delta` rows, skipping group headers. */
|
|
256
171
|
private move(delta: number): void {
|
|
257
|
-
|
|
258
|
-
this.clampSelection(this.activeEntries());
|
|
259
|
-
const entries = this.activeEntries();
|
|
260
|
-
const direction = delta < 0 ? -1 : 1;
|
|
261
|
-
let index = this.cursor.selected;
|
|
262
|
-
let remaining = Math.abs(delta);
|
|
263
|
-
while (remaining > 0) {
|
|
264
|
-
const next = nextRow(entries, index, direction);
|
|
265
|
-
if (next === undefined) break;
|
|
266
|
-
index = next;
|
|
267
|
-
remaining -= 1;
|
|
268
|
-
}
|
|
269
|
-
this.select(index);
|
|
172
|
+
moveRows(this.activeEntries(), this.cursor, delta);
|
|
270
173
|
}
|
|
271
174
|
|
|
272
175
|
private switchScope(): void {
|
|
@@ -299,18 +202,112 @@ export class PanelComponent implements Component {
|
|
|
299
202
|
.then(() => this.options.requestRender());
|
|
300
203
|
}
|
|
301
204
|
|
|
302
|
-
/**
|
|
205
|
+
/** Drop the focused extension's project override, restoring the inherited row. */
|
|
206
|
+
private clearOverride(): void {
|
|
207
|
+
const entry = this.activeEntries()[this.cursor.selected];
|
|
208
|
+
if (entry?.kind !== "row") return;
|
|
209
|
+
const item = entry.row.item;
|
|
210
|
+
if (!item) {
|
|
211
|
+
this.notify(`${entry.row.label} is not resolved; there is no override to clear.`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.chain = this.chain
|
|
215
|
+
.then(() => this.options.runtime.clearOverride(item))
|
|
216
|
+
.catch(() => undefined)
|
|
217
|
+
.then(() => this.options.requestRender());
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Change the focused setting: cycle it, or open the editor for a text row. */
|
|
303
221
|
private changeSetting(): void {
|
|
304
222
|
const entry = this.activeEntries()[this.cursor.selected];
|
|
305
223
|
if (entry?.kind !== "setting") return;
|
|
224
|
+
const view = entry.setting;
|
|
225
|
+
if (view.kind === "text") {
|
|
226
|
+
this.editing = { id: view.id, buffer: view.value, caret: view.value.length };
|
|
227
|
+
this.options.requestRender();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const scope = this.options.runtime.scope();
|
|
231
|
+
this.chain = this.chain
|
|
232
|
+
.then(() => this.options.settings.change(scope, view.id))
|
|
233
|
+
.catch((error: unknown) => this.notify(error instanceof Error ? error.message : String(error)))
|
|
234
|
+
.then(() => this.options.requestRender());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Store the editor's buffer; a refusal keeps it open with the reason shown. */
|
|
238
|
+
private commitEdit(): void {
|
|
239
|
+
const editing = this.editing;
|
|
240
|
+
if (!editing) return;
|
|
306
241
|
const scope = this.options.runtime.scope();
|
|
307
|
-
const id = entry.setting.id;
|
|
308
242
|
this.chain = this.chain
|
|
309
|
-
.then(() => this.options.settings.
|
|
243
|
+
.then(() => this.options.settings.setValue(scope, editing.id, editing.buffer))
|
|
244
|
+
.then((saved) => {
|
|
245
|
+
if (saved) this.editing = undefined;
|
|
246
|
+
})
|
|
310
247
|
.catch((error: unknown) => this.notify(error instanceof Error ? error.message : String(error)))
|
|
311
248
|
.then(() => this.options.requestRender());
|
|
312
249
|
}
|
|
313
250
|
|
|
251
|
+
/** The text row being edited, read live so a scope switch cannot point at a stale row. */
|
|
252
|
+
private editingRow(): { view: ReturnType<SettingsSurface["views"]>[number] } | undefined {
|
|
253
|
+
const editing = this.editing;
|
|
254
|
+
if (!editing) return undefined;
|
|
255
|
+
const scope = this.options.runtime.scope();
|
|
256
|
+
const view = this.options.settings.views(scope).find((entry) => entry.id === editing.id);
|
|
257
|
+
return view ? { view } : undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Editor keys. `Esc` cancels the edit rather than closing the screen, and
|
|
262
|
+
* everything else the screen binds is inert until the editor closes.
|
|
263
|
+
*/
|
|
264
|
+
private handleEditingInput(data: string): void {
|
|
265
|
+
const editing = this.editing;
|
|
266
|
+
if (!editing) return;
|
|
267
|
+
if (isCloseKey(data)) {
|
|
268
|
+
this.editing = undefined;
|
|
269
|
+
this.options.requestRender();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (matchesKey(data, Key.enter)) {
|
|
273
|
+
void this.commitEdit();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const { buffer, caret } = editing;
|
|
277
|
+
if (matchesKey(data, Key.backspace)) {
|
|
278
|
+
if (caret > 0)
|
|
279
|
+
this.editing = { ...editing, buffer: buffer.slice(0, caret - 1) + buffer.slice(caret), caret: caret - 1 };
|
|
280
|
+
} else if (matchesKey(data, Key.delete)) {
|
|
281
|
+
if (caret < buffer.length)
|
|
282
|
+
this.editing = { ...editing, buffer: buffer.slice(0, caret) + buffer.slice(caret + 1) };
|
|
283
|
+
} else if (matchesKey(data, Key.left)) {
|
|
284
|
+
this.editing = { ...editing, caret: Math.max(0, caret - 1) };
|
|
285
|
+
} else if (matchesKey(data, Key.right)) {
|
|
286
|
+
this.editing = { ...editing, caret: Math.min(buffer.length, caret + 1) };
|
|
287
|
+
// `Ctrl+A`/`Ctrl+E` are the emacs spellings of Home/End, which pi-tui's
|
|
288
|
+
// `Key.home`/`Key.end` do not match; both are offered because a terminal
|
|
289
|
+
// may send either.
|
|
290
|
+
} else if (matchesKey(data, Key.home) || matchesKey(data, "ctrl+a")) {
|
|
291
|
+
this.editing = { ...editing, caret: 0 };
|
|
292
|
+
} else if (matchesKey(data, Key.end) || matchesKey(data, "ctrl+e")) {
|
|
293
|
+
this.editing = { ...editing, caret: buffer.length };
|
|
294
|
+
} else if (matchesKey(data, "ctrl+u")) {
|
|
295
|
+
this.editing = { ...editing, buffer: "", caret: 0 };
|
|
296
|
+
} else {
|
|
297
|
+
// Anything else must be printable; control sequences are not text.
|
|
298
|
+
const typed = [...data].filter((char) => char >= " " && char !== "\u007f").join("");
|
|
299
|
+
const max = this.editingRow()?.view.text?.maxLength;
|
|
300
|
+
if (typed && max !== undefined && buffer.length + typed.length <= max) {
|
|
301
|
+
this.editing = {
|
|
302
|
+
...editing,
|
|
303
|
+
buffer: buffer.slice(0, caret) + typed + buffer.slice(caret),
|
|
304
|
+
caret: caret + typed.length,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
this.options.requestRender();
|
|
309
|
+
}
|
|
310
|
+
|
|
314
311
|
/**
|
|
315
312
|
* Ask for the first Extensions probe when the session had no cache. The first
|
|
316
313
|
* time the section is visible is when the rows need health; doing it at mount
|
|
@@ -358,11 +355,11 @@ export class PanelComponent implements Component {
|
|
|
358
355
|
/** Navigation shared by both sections. */
|
|
359
356
|
private navigate(data: string): void {
|
|
360
357
|
if (data === "g") {
|
|
361
|
-
this.select(this.
|
|
358
|
+
this.select(firstRow(this.activeEntries()));
|
|
362
359
|
return;
|
|
363
360
|
}
|
|
364
361
|
if (data === "G") {
|
|
365
|
-
this.select(this.
|
|
362
|
+
this.select(lastRow(this.activeEntries()));
|
|
366
363
|
return;
|
|
367
364
|
}
|
|
368
365
|
switch (navIntent(data)) {
|
|
@@ -379,10 +376,10 @@ export class PanelComponent implements Component {
|
|
|
379
376
|
this.move(Math.max(1, this.cursor.visible));
|
|
380
377
|
return;
|
|
381
378
|
case "home":
|
|
382
|
-
this.select(this.
|
|
379
|
+
this.select(firstRow(this.activeEntries()));
|
|
383
380
|
return;
|
|
384
381
|
case "end":
|
|
385
|
-
this.select(this.
|
|
382
|
+
this.select(lastRow(this.activeEntries()));
|
|
386
383
|
return;
|
|
387
384
|
}
|
|
388
385
|
}
|
|
@@ -402,6 +399,10 @@ export class PanelComponent implements Component {
|
|
|
402
399
|
}
|
|
403
400
|
|
|
404
401
|
handleInput(data: string): void {
|
|
402
|
+
if (this.editing) {
|
|
403
|
+
this.handleEditingInput(data);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
405
406
|
if (isCloseKey(data)) {
|
|
406
407
|
this.options.onClose();
|
|
407
408
|
return;
|
|
@@ -441,10 +442,23 @@ export class PanelComponent implements Component {
|
|
|
441
442
|
void this.refresh(true);
|
|
442
443
|
return;
|
|
443
444
|
}
|
|
445
|
+
// Only Project scope can hold an override, so the key does not exist in
|
|
446
|
+
// Global scope and the hint row does not offer it there.
|
|
447
|
+
if (matchesKey(data, "x") && this.options.runtime.scope() === "project") {
|
|
448
|
+
this.clearOverride();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
444
451
|
this.navigate(data);
|
|
445
452
|
}
|
|
446
453
|
|
|
447
454
|
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
455
|
+
// A click while editing leaves the editor the way `Esc` does: nothing is
|
|
456
|
+
// written behind the screen.
|
|
457
|
+
if (this.editing) {
|
|
458
|
+
this.editing = undefined;
|
|
459
|
+
this.options.requestRender();
|
|
460
|
+
return { handled: true };
|
|
461
|
+
}
|
|
448
462
|
const delta = wheelDelta(event);
|
|
449
463
|
if (delta !== undefined) {
|
|
450
464
|
this.move(delta);
|
|
@@ -464,167 +478,23 @@ export class PanelComponent implements Component {
|
|
|
464
478
|
return { handled: true };
|
|
465
479
|
}
|
|
466
480
|
|
|
467
|
-
/** The themed section bar: both section names, the active one accented. */
|
|
468
|
-
private sectionBar(width: number, counts: string): string {
|
|
469
|
-
const separator = paint(this.theme, "faint", " · ");
|
|
470
|
-
const left = ` ${sectionSegments(this.section)
|
|
471
|
-
.map((segment) => paint(this.theme, segment.color, segment.text))
|
|
472
|
-
.join(separator)}`;
|
|
473
|
-
return clip(splitRow(left, counts, width), width);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/** One section's counts as whole segments, so a narrow screen drops a count. */
|
|
477
|
-
private counts(segments: readonly SummarySegment[], width: number): string {
|
|
478
|
-
const separator = paint(this.theme, "faint", " · ");
|
|
479
|
-
const styled = segments.map((segment) => paint(this.theme, segment.color, segment.text));
|
|
480
|
-
return fitCells(styled, Math.max(1, width - 2), separator);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/** The Extensions section's counts, next to the section bar. */
|
|
484
|
-
private extensionCounts(width: number, model: PanelModel): string {
|
|
485
|
-
return this.counts(
|
|
486
|
-
summarySegments(model, this.options.runtime.scope(), {
|
|
487
|
-
filter: this.options.filter,
|
|
488
|
-
compact: width < 72,
|
|
489
|
-
}),
|
|
490
|
-
width,
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
481
|
/** Key hints for the active section; whole hints drop as the width runs out. */
|
|
495
482
|
private hint(width: number, expanded = false): string {
|
|
496
|
-
|
|
483
|
+
if (this.editing) return hintLine(this.theme, width, editHints());
|
|
497
484
|
const hints =
|
|
498
485
|
this.section === "settings"
|
|
499
|
-
?
|
|
500
|
-
:
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
"↑/↓ select",
|
|
507
|
-
];
|
|
508
|
-
return screenHint(this.theme, width, hints);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/** The transient notice row, clipped and painted like the hint it replaces. */
|
|
512
|
-
private noticeLine(width: number): string {
|
|
513
|
-
return clip(`${INDENT}${paint(this.theme, "meta", sanitizeLine(this.notice ?? ""))}`, width);
|
|
486
|
+
? settingsHints()
|
|
487
|
+
: extensionHints({
|
|
488
|
+
expanded,
|
|
489
|
+
refreshing: this.refreshing,
|
|
490
|
+
projectScope: this.options.runtime.scope() === "project",
|
|
491
|
+
});
|
|
492
|
+
return hintLine(this.theme, width, hints);
|
|
514
493
|
}
|
|
515
494
|
|
|
516
495
|
/** The key-hint row, or the notice that temporarily replaces it. */
|
|
517
496
|
private hintRow(width: number, expanded = false): string {
|
|
518
|
-
return this.notice !== undefined ? this.
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
private groupLine(entry: Extract<PanelEntry, { kind: "group" }>, width: number): string {
|
|
522
|
-
const label = paint(this.theme, "meta", sanitizeLine(entry.label));
|
|
523
|
-
const count = paint(this.theme, "faint", `(${entry.count})`);
|
|
524
|
-
return clip(` ${label} ${count}`, width);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
private rowLine(row: PanelRow, selected: boolean, width: number): string {
|
|
528
|
-
const badge = rowBadge(row);
|
|
529
|
-
return listRow(this.theme, width, {
|
|
530
|
-
selected,
|
|
531
|
-
// A reported-only row has no enable pattern, but still reserves the box
|
|
532
|
-
// column so the list stays aligned.
|
|
533
|
-
leading: row.item ? checkbox(this.theme, row.item.enabled) : " ",
|
|
534
|
-
glyph: badge.glyph ?? " ",
|
|
535
|
-
glyphRole: badge.role,
|
|
536
|
-
label: sanitizeLine(row.label),
|
|
537
|
-
labelRole: selected ? "accent" : row.item && !row.item.enabled ? "faint" : "label",
|
|
538
|
-
trailing: badge.text,
|
|
539
|
-
// The glyph carries the status colour; the word stays quiet unless it is a
|
|
540
|
-
// problem, so a clean row does not shout.
|
|
541
|
-
trailingRole: badge.role === "bad" || badge.role === "warn" ? badge.role : "faint",
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/** One setting row: a checkbox for booleans, the value right-aligned. */
|
|
546
|
-
private settingLine(view: SettingView, selected: boolean, width: number): string {
|
|
547
|
-
return listRow(this.theme, width, {
|
|
548
|
-
selected,
|
|
549
|
-
leading: view.kind === "boolean" ? checkbox(this.theme, view.value === "true") : " ",
|
|
550
|
-
label: sanitizeLine(view.label),
|
|
551
|
-
labelRole: selected ? "accent" : "label",
|
|
552
|
-
trailing: sanitizeLine(view.valueLabel),
|
|
553
|
-
trailingRole: "meta",
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/** The project note: app rows are absent because Pi has no project setter. */
|
|
558
|
-
private noteLine(width: number): string {
|
|
559
|
-
return clip(`${INDENT}${paint(this.theme, "faint", "App settings are Global only.")}`, width);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/** The saved-then-reload marker, shared by both sections. */
|
|
563
|
-
private dirtyLine(width: number): string[] {
|
|
564
|
-
if (!this.options.runtime.isDirty() && !this.options.settings.isDirty()) return [];
|
|
565
|
-
return [clip(`${INDENT}${paint(this.theme, "warn", "Saved · reload to apply")}`, width)];
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/** The origin and target file for a setting, shown in its detail pane. */
|
|
569
|
-
private originLine(view: SettingView): string {
|
|
570
|
-
const scope = this.options.runtime.scope();
|
|
571
|
-
const path =
|
|
572
|
-
view.source === "app"
|
|
573
|
-
? join(this.options.agentDir, PI_SETTINGS_FILE)
|
|
574
|
-
: configPath(scope, this.options.cwd, this.options.agentDir);
|
|
575
|
-
const file = sanitizeLine(path);
|
|
576
|
-
switch (view.origin) {
|
|
577
|
-
case "inherited":
|
|
578
|
-
return `Inherited from Global · ${file}`;
|
|
579
|
-
case "project":
|
|
580
|
-
return `Project · ${file}`;
|
|
581
|
-
case "global":
|
|
582
|
-
return `Global · ${file}`;
|
|
583
|
-
default:
|
|
584
|
-
return `Default · ${file}`;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/** The focused setting's source, description, origin, and pending changes. */
|
|
589
|
-
private settingDetail(view: SettingView | undefined, width: number, budget: number): string[] {
|
|
590
|
-
if (!view) return this.dirtyLine(width);
|
|
591
|
-
const source = view.source === "app" ? "Pi setting" : "bytefly.json";
|
|
592
|
-
const head = `${paint(this.theme, "meta", sanitizeLine(view.label))} ${paint(this.theme, "faint", "—")} ${paint(this.theme, "faint", source)}`;
|
|
593
|
-
const headLine = clip(`${INDENT}${head}`, width);
|
|
594
|
-
// The pending-changes marker survives the collapsed body: on a short terminal
|
|
595
|
-
// the value still changed, and the screen has to say so.
|
|
596
|
-
if (budget === 0) return [headLine, ...this.dirtyLine(width)];
|
|
597
|
-
|
|
598
|
-
const lines = [headLine];
|
|
599
|
-
const dirty = this.dirtyLine(width);
|
|
600
|
-
const descriptionWidth = Math.max(1, width - BODY_INDENT * 2);
|
|
601
|
-
const description = wrapTextWithAnsi(sanitizeLine(view.description), descriptionWidth).slice(
|
|
602
|
-
0,
|
|
603
|
-
Math.max(0, Math.min(2, budget - 2 - dirty.length)),
|
|
604
|
-
);
|
|
605
|
-
for (const line of description) {
|
|
606
|
-
lines.push(clip(`${INDENT}${paint(this.theme, "meta", line)}`, width));
|
|
607
|
-
}
|
|
608
|
-
lines.push(clip(`${INDENT}${paint(this.theme, "faint", this.originLine(view))}`, width));
|
|
609
|
-
lines.push(...dirty);
|
|
610
|
-
return lines;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/** The focused extension's label, findings, and the pending-changes note. */
|
|
614
|
-
private detail(row: PanelRow | undefined, width: number, budget: number, expanded: boolean): string[] {
|
|
615
|
-
const lines: string[] = [];
|
|
616
|
-
const badge = row ? rowBadge(row) : undefined;
|
|
617
|
-
if (row && badge?.text !== undefined) {
|
|
618
|
-
const head = `${paint(this.theme, "meta", sanitizeLine(row.label))} ${paint(this.theme, "faint", "—")} ${paint(this.theme, badge.role, badge.text)}`;
|
|
619
|
-
lines.push(clip(` ${head}`, width));
|
|
620
|
-
} else if (row?.item) {
|
|
621
|
-
lines.push(clip(` ${paint(this.theme, "meta", sanitizeLine(row.item.path))}`, width));
|
|
622
|
-
}
|
|
623
|
-
if (row?.report && budget > 0) {
|
|
624
|
-
lines.push(...reportDetailLines(row.report, this.theme, width, budget, expanded));
|
|
625
|
-
}
|
|
626
|
-
lines.push(...this.dirtyLine(width));
|
|
627
|
-
return lines;
|
|
497
|
+
return this.notice !== undefined ? noticeLine(this.theme, width, this.notice) : this.hint(width, expanded);
|
|
628
498
|
}
|
|
629
499
|
|
|
630
500
|
/**
|
|
@@ -653,12 +523,14 @@ export class PanelComponent implements Component {
|
|
|
653
523
|
private renderSettings(width: number, header: string): string[] {
|
|
654
524
|
const scope = this.options.runtime.scope();
|
|
655
525
|
const views = this.options.settings.views(scope);
|
|
656
|
-
const summary =
|
|
526
|
+
const summary = sectionBar(
|
|
527
|
+
this.theme,
|
|
657
528
|
width,
|
|
658
|
-
this.
|
|
529
|
+
this.section,
|
|
530
|
+
settingsSummary(this.theme, width, scope, this.options.settings.count(scope)),
|
|
659
531
|
);
|
|
660
532
|
const entries = settingEntries(views);
|
|
661
|
-
const note = scope === "project" ? this.
|
|
533
|
+
const note = scope === "project" ? projectNote(this.theme, width) : undefined;
|
|
662
534
|
|
|
663
535
|
if (entries.length === 0) {
|
|
664
536
|
return dockListFrame({
|
|
@@ -674,7 +546,14 @@ export class PanelComponent implements Component {
|
|
|
674
546
|
|
|
675
547
|
const focusedEntry = entries[this.cursor.selected];
|
|
676
548
|
const focused = focusedEntry?.kind === "setting" ? focusedEntry.setting : undefined;
|
|
677
|
-
const detail =
|
|
549
|
+
const detail = settingDetail({
|
|
550
|
+
theme: this.theme,
|
|
551
|
+
width,
|
|
552
|
+
view: focused,
|
|
553
|
+
budget: this.detailRows(false),
|
|
554
|
+
dirty: this.dirty,
|
|
555
|
+
paths: { scope, cwd: this.options.cwd, agentDir: this.options.agentDir },
|
|
556
|
+
});
|
|
678
557
|
|
|
679
558
|
this.cursor.visible = fitRows(
|
|
680
559
|
this.options.viewportRows,
|
|
@@ -689,9 +568,11 @@ export class PanelComponent implements Component {
|
|
|
689
568
|
if (note) body.push(note);
|
|
690
569
|
for (let index = start; index < end; index += 1) {
|
|
691
570
|
const entry = entries[index];
|
|
692
|
-
if (entry.kind === "group") body.push(this.
|
|
693
|
-
else if (entry.kind === "setting")
|
|
694
|
-
|
|
571
|
+
if (entry.kind === "group") body.push(groupLine(this.theme, width, entry));
|
|
572
|
+
else if (entry.kind === "setting") {
|
|
573
|
+
const editing = this.editing?.id === entry.setting.id ? this.editing : undefined;
|
|
574
|
+
body.push(settingRow(this.theme, width, entry.setting, index === this.cursor.selected, editing));
|
|
575
|
+
}
|
|
695
576
|
}
|
|
696
577
|
|
|
697
578
|
return dockListFrame({
|
|
@@ -713,7 +594,12 @@ export class PanelComponent implements Component {
|
|
|
713
594
|
|
|
714
595
|
const model = this.model();
|
|
715
596
|
const entries = entriesOf(model);
|
|
716
|
-
const summary =
|
|
597
|
+
const summary = sectionBar(
|
|
598
|
+
this.theme,
|
|
599
|
+
w,
|
|
600
|
+
this.section,
|
|
601
|
+
extensionSummary(this.theme, w, model, this.options.runtime.scope(), this.options.filter),
|
|
602
|
+
);
|
|
717
603
|
|
|
718
604
|
if (entries.length === 0) {
|
|
719
605
|
return dockListFrame({
|
|
@@ -730,7 +616,7 @@ export class PanelComponent implements Component {
|
|
|
730
616
|
const focusedEntry = entries[this.cursor.selected];
|
|
731
617
|
const focused = focusedEntry?.kind === "row" ? focusedEntry.row : undefined;
|
|
732
618
|
const expanded = focused !== undefined && this.expanded.has(focused.key);
|
|
733
|
-
const detail = this.
|
|
619
|
+
const detail = extensionDetail(this.theme, w, focused, this.detailRows(expanded), expanded, this.dirty);
|
|
734
620
|
|
|
735
621
|
this.cursor.visible = fitRows(
|
|
736
622
|
this.options.viewportRows,
|
|
@@ -744,8 +630,8 @@ export class PanelComponent implements Component {
|
|
|
744
630
|
const body: string[] = [];
|
|
745
631
|
for (let index = start; index < end; index += 1) {
|
|
746
632
|
const entry = entries[index];
|
|
747
|
-
if (entry.kind === "group") body.push(this.
|
|
748
|
-
else if (entry.kind === "row") body.push(this.
|
|
633
|
+
if (entry.kind === "group") body.push(groupLine(this.theme, w, entry));
|
|
634
|
+
else if (entry.kind === "row") body.push(extensionRow(this.theme, w, entry.row, index === this.cursor.selected));
|
|
749
635
|
}
|
|
750
636
|
|
|
751
637
|
return dockListFrame({
|