@gavin-hu/my-pi-agent 0.13.1 → 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 +66 -41
- package/README.zh-CN.md +53 -36
- package/extensions/bytefly/README.md +321 -186
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/config.ts +4 -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/config.ts +3 -2
- 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 +33 -10
- package/extensions/bytefly/header/config.ts +22 -0
- package/extensions/bytefly/header/register.ts +2 -1
- package/extensions/bytefly/index.ts +6 -6
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +7 -12
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +22 -2
- package/extensions/bytefly/panel/format.ts +17 -21
- package/extensions/bytefly/panel/settings-runtime.ts +86 -22
- package/extensions/bytefly/panel/settings.ts +279 -46
- 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 +98 -13
- package/extensions/bytefly/panel/tui.ts +176 -166
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +17 -6
- package/extensions/bytefly/separator/config.ts +15 -0
- package/extensions/bytefly/separator/format.ts +60 -6
- package/extensions/bytefly/separator/register.ts +68 -9
- package/extensions/bytefly/separator/types.ts +8 -0
- package/extensions/bytefly/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/bytefly/working/config.ts +4 -5
- package/extensions/bytefly/working/format.ts +24 -7
- package/extensions/bytefly/working/register.ts +34 -3
- 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 +3 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +106 -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 +118 -8
- 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 +62 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +116 -1
- package/extensions/subagent/orchestrate.ts +16 -3
- package/extensions/subagent/render.ts +25 -0
- package/extensions/subagent/run.ts +113 -49
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/transcripts.ts +202 -0
- package/extensions/subagent/types.ts +23 -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 +16 -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 +3 -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 +83 -12
- package/lib/errors.ts +12 -0
- package/lib/format.ts +56 -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 +23 -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/themes/bytefly-dark.json +2 -3
- package/themes/bytefly-light.json +2 -3
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
|
@@ -17,44 +17,30 @@ 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,
|
|
37
|
+
sectionJsonPane,
|
|
46
38
|
settingDetail,
|
|
47
39
|
settingRow,
|
|
48
40
|
settingsHints,
|
|
49
41
|
settingsSummary,
|
|
50
42
|
} from "./tui/settings-view.ts";
|
|
51
43
|
|
|
52
|
-
/** Terminal rows, or `undefined` when the height is unknown. */
|
|
53
|
-
function totalRows(source: ViewportRowsSource | undefined): number | undefined {
|
|
54
|
-
const rows = typeof source === "function" ? source() : source;
|
|
55
|
-
return rows !== undefined && Number.isFinite(rows) && rows > 0 ? rows : undefined;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
44
|
/** Host seams for the panel screen, injected by the command runner. */
|
|
59
45
|
export interface PanelComponentOptions {
|
|
60
46
|
/** The session's picker runtime: items, scope, toggles, dirty flag. */
|
|
@@ -101,8 +87,12 @@ export class PanelComponent implements Component {
|
|
|
101
87
|
private probed = false;
|
|
102
88
|
/** Transient feedback (a refusal or a failed write), shown in the hint row. */
|
|
103
89
|
private notice: string | undefined;
|
|
104
|
-
/** The text row being edited
|
|
105
|
-
private editing:
|
|
90
|
+
/** The text row being edited, if any. */
|
|
91
|
+
private editing: EditingState | undefined;
|
|
92
|
+
/** True while the Settings detail pane shows the focused row's section as JSON. */
|
|
93
|
+
private json = false;
|
|
94
|
+
/** Whether the last Settings render kept the project note; the click mapping needs it. */
|
|
95
|
+
private noteShown = false;
|
|
106
96
|
/** The visible section. */
|
|
107
97
|
private section: PanelSection;
|
|
108
98
|
|
|
@@ -177,12 +167,22 @@ export class PanelComponent implements Component {
|
|
|
177
167
|
if (next === "project" && !this.options.runtime.projectAvailable()) {
|
|
178
168
|
this.notify(
|
|
179
169
|
this.section === "settings"
|
|
180
|
-
?
|
|
170
|
+
? UNTRUSTED_SETTINGS_NOTE
|
|
181
171
|
: "Project is not trusted; manage extensions in Global scope only.",
|
|
182
172
|
);
|
|
183
173
|
return;
|
|
184
174
|
}
|
|
175
|
+
// The JSON pane names a file, so a scope switch has to close it.
|
|
176
|
+
this.json = false;
|
|
177
|
+
const focused = entryKey(this.activeEntries()[this.cursor.selected]);
|
|
185
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
|
+
}
|
|
186
186
|
this.options.requestRender();
|
|
187
187
|
}
|
|
188
188
|
|
|
@@ -234,6 +234,32 @@ export class PanelComponent implements Component {
|
|
|
234
234
|
.then(() => this.options.requestRender());
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/** Delete the focused setting's leaf, so its default applies again. */
|
|
238
|
+
private resetSetting(): void {
|
|
239
|
+
const entry = this.activeEntries()[this.cursor.selected];
|
|
240
|
+
if (entry?.kind !== "setting") return;
|
|
241
|
+
const scope = this.options.runtime.scope();
|
|
242
|
+
this.chain = this.chain
|
|
243
|
+
.then(() => this.options.settings.reset(scope, entry.setting.id))
|
|
244
|
+
.catch((error: unknown) => this.notify(error instanceof Error ? error.message : String(error)))
|
|
245
|
+
.then(() => this.options.requestRender());
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Toggle the detail pane between the row and its whole section as stored. */
|
|
249
|
+
private toggleJson(): void {
|
|
250
|
+
const entry = this.activeEntries()[this.cursor.selected];
|
|
251
|
+
if (entry?.kind !== "setting") return;
|
|
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;
|
|
256
|
+
this.notify(`${entry.setting.label} is a Pi setting; bytefly.json has no section for it.`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
this.json = !this.json;
|
|
260
|
+
this.options.requestRender();
|
|
261
|
+
}
|
|
262
|
+
|
|
237
263
|
/** Store the editor's buffer; a refusal keeps it open with the reason shown. */
|
|
238
264
|
private commitEdit(): void {
|
|
239
265
|
const editing = this.editing;
|
|
@@ -248,63 +274,26 @@ export class PanelComponent implements Component {
|
|
|
248
274
|
.then(() => this.options.requestRender());
|
|
249
275
|
}
|
|
250
276
|
|
|
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
277
|
/**
|
|
261
|
-
* Editor keys. `Esc`
|
|
262
|
-
* 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.
|
|
263
280
|
*/
|
|
264
281
|
private handleEditingInput(data: string): void {
|
|
265
282
|
const editing = this.editing;
|
|
266
283
|
if (!editing) return;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
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") {
|
|
273
287
|
void this.commitEdit();
|
|
274
288
|
return;
|
|
275
289
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
}
|
|
290
|
+
if (action.kind === "cancel") {
|
|
291
|
+
this.editing = undefined;
|
|
292
|
+
this.options.requestRender();
|
|
293
|
+
return;
|
|
307
294
|
}
|
|
295
|
+
if (!action.changed) return;
|
|
296
|
+
this.editing = action.state;
|
|
308
297
|
this.options.requestRender();
|
|
309
298
|
}
|
|
310
299
|
|
|
@@ -348,6 +337,7 @@ export class PanelComponent implements Component {
|
|
|
348
337
|
private switchSection(next: PanelSection): void {
|
|
349
338
|
if (next === this.section) return;
|
|
350
339
|
this.section = next;
|
|
340
|
+
this.json = false;
|
|
351
341
|
this.prime();
|
|
352
342
|
this.options.requestRender();
|
|
353
343
|
}
|
|
@@ -362,6 +352,14 @@ export class PanelComponent implements Component {
|
|
|
362
352
|
this.select(lastRow(this.activeEntries()));
|
|
363
353
|
return;
|
|
364
354
|
}
|
|
355
|
+
if (data === "[") {
|
|
356
|
+
this.jumpGroup(-1);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (data === "]") {
|
|
360
|
+
this.jumpGroup(1);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
365
363
|
switch (navIntent(data)) {
|
|
366
364
|
case "up":
|
|
367
365
|
this.move(-1);
|
|
@@ -384,6 +382,26 @@ export class PanelComponent implements Component {
|
|
|
384
382
|
}
|
|
385
383
|
}
|
|
386
384
|
|
|
385
|
+
/** Move to the first row of the next (or previous) group. */
|
|
386
|
+
private jumpGroup(direction: -1 | 1): void {
|
|
387
|
+
const entries = this.activeEntries();
|
|
388
|
+
const headers = entries.flatMap((entry, index) => (entry.kind === "group" ? [index] : []));
|
|
389
|
+
const selected = this.cursor.selected;
|
|
390
|
+
// Step past the focused row's own header: landing on it would not move.
|
|
391
|
+
const own = headers.filter((index) => index < selected).at(-1);
|
|
392
|
+
const target =
|
|
393
|
+
direction === 1
|
|
394
|
+
? headers.find((index) => index > selected)
|
|
395
|
+
: headers.filter((index) => index < (own ?? 0)).at(-1);
|
|
396
|
+
if (target === undefined) return;
|
|
397
|
+
for (let index = target + 1; index < entries.length; index += 1) {
|
|
398
|
+
if (isSelectable(entries[index])) {
|
|
399
|
+
this.select(index);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
387
405
|
/** Settings keys: change, scope, navigation. `←`/`→` stay on sections. */
|
|
388
406
|
private handleSettingsInput(data: string): void {
|
|
389
407
|
this.clampSelection(this.activeEntries());
|
|
@@ -395,6 +413,14 @@ export class PanelComponent implements Component {
|
|
|
395
413
|
this.switchScope();
|
|
396
414
|
return;
|
|
397
415
|
}
|
|
416
|
+
if (data === "u") {
|
|
417
|
+
this.resetSetting();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (data === "J") {
|
|
421
|
+
this.toggleJson();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
398
424
|
this.navigate(data);
|
|
399
425
|
}
|
|
400
426
|
|
|
@@ -469,23 +495,24 @@ export class PanelComponent implements Component {
|
|
|
469
495
|
if (entries.length === 0) return undefined;
|
|
470
496
|
// The Settings section prints the "Global only" note first in Project scope,
|
|
471
497
|
// so the rows start one line lower there.
|
|
472
|
-
const note = this.section === "settings" && this.
|
|
498
|
+
const note = this.section === "settings" && this.noteShown ? 1 : 0;
|
|
473
499
|
const line = event.y - (BODY_START + note);
|
|
474
|
-
|
|
475
|
-
|
|
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;
|
|
476
503
|
if (!isSelectable(entries[index])) return undefined;
|
|
477
504
|
this.select(index);
|
|
478
505
|
return { handled: true };
|
|
479
506
|
}
|
|
480
507
|
|
|
481
508
|
/** Key hints for the active section; whole hints drop as the width runs out. */
|
|
482
|
-
private hint(width: number, expanded =
|
|
509
|
+
private hint(width: number, options: { expanded?: boolean; jsonOpen?: boolean } = {}): string {
|
|
483
510
|
if (this.editing) return hintLine(this.theme, width, editHints());
|
|
484
511
|
const hints =
|
|
485
512
|
this.section === "settings"
|
|
486
|
-
? settingsHints()
|
|
513
|
+
? settingsHints(options.jsonOpen === true)
|
|
487
514
|
: extensionHints({
|
|
488
|
-
expanded,
|
|
515
|
+
expanded: options.expanded === true,
|
|
489
516
|
refreshing: this.refreshing,
|
|
490
517
|
projectScope: this.options.runtime.scope() === "project",
|
|
491
518
|
});
|
|
@@ -493,21 +520,8 @@ export class PanelComponent implements Component {
|
|
|
493
520
|
}
|
|
494
521
|
|
|
495
522
|
/** The key-hint row, or the notice that temporarily replaces it. */
|
|
496
|
-
private hintRow(width: number, expanded =
|
|
497
|
-
return this.notice !== undefined ? noticeLine(this.theme, width, this.notice) : this.hint(width,
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Detail rows the focused row gets: the fixed pane when collapsed, the free
|
|
502
|
-
* height when expanded. Zero when the terminal cannot hold one body row next to
|
|
503
|
-
* the pane, so the hint row is never pushed off the screen.
|
|
504
|
-
*/
|
|
505
|
-
private detailRows(expanded: boolean): number {
|
|
506
|
-
const total = totalRows(this.options.viewportRows);
|
|
507
|
-
if (total === undefined) return expanded ? EXPANDED_DETAIL_FALLBACK : DETAIL_ROWS;
|
|
508
|
-
const available = total - FRAME_ROWS - MIN_BODY_ROWS;
|
|
509
|
-
if (available < DETAIL_ROWS + 1) return 0;
|
|
510
|
-
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);
|
|
511
525
|
}
|
|
512
526
|
|
|
513
527
|
/** Expand or collapse the focused row's findings. */
|
|
@@ -519,6 +533,13 @@ export class PanelComponent implements Component {
|
|
|
519
533
|
this.options.requestRender();
|
|
520
534
|
}
|
|
521
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
|
+
|
|
522
543
|
/** The Settings body: scope counts, the project note, rows, and detail. */
|
|
523
544
|
private renderSettings(width: number, header: string): string[] {
|
|
524
545
|
const scope = this.options.runtime.scope();
|
|
@@ -527,64 +548,57 @@ export class PanelComponent implements Component {
|
|
|
527
548
|
this.theme,
|
|
528
549
|
width,
|
|
529
550
|
this.section,
|
|
530
|
-
settingsSummary(this.theme, width, scope,
|
|
551
|
+
settingsSummary(this.theme, width, scope, views.length),
|
|
531
552
|
);
|
|
532
553
|
const entries = settingEntries(views);
|
|
533
554
|
const note = scope === "project" ? projectNote(this.theme, width) : undefined;
|
|
534
555
|
|
|
535
|
-
|
|
536
|
-
return dockListFrame({
|
|
537
|
-
header,
|
|
538
|
-
summary,
|
|
539
|
-
body: emptyState(this.theme, width, { label: "No settings found." }),
|
|
540
|
-
hint: this.hintRow(width),
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
this.retainFocus(entries);
|
|
545
|
-
this.clampSelection(entries);
|
|
546
|
-
|
|
547
|
-
const focusedEntry = entries[this.cursor.selected];
|
|
556
|
+
const focusedEntry = this.retain(entries);
|
|
548
557
|
const focused = focusedEntry?.kind === "setting" ? focusedEntry.setting : undefined;
|
|
549
|
-
|
|
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;
|
|
561
|
+
const jsonConfig = this.json ? focused?.config : undefined;
|
|
562
|
+
const detail =
|
|
563
|
+
jsonConfig && focused
|
|
564
|
+
? sectionJsonPane({
|
|
565
|
+
theme: this.theme,
|
|
566
|
+
width,
|
|
567
|
+
section: jsonConfig.section,
|
|
568
|
+
document: this.options.settings.sectionDocument(scope, jsonConfig.section),
|
|
569
|
+
budget: detailRows(true, this.options.viewportRows),
|
|
570
|
+
dirty: this.dirty,
|
|
571
|
+
paths: { scope, cwd: this.options.cwd, agentDir: this.options.agentDir },
|
|
572
|
+
})
|
|
573
|
+
: settingDetail({
|
|
574
|
+
theme: this.theme,
|
|
575
|
+
width,
|
|
576
|
+
view: focused,
|
|
577
|
+
budget: detailRows(false, this.options.viewportRows),
|
|
578
|
+
dirty: this.dirty,
|
|
579
|
+
paths: { scope, cwd: this.options.cwd, agentDir: this.options.agentDir },
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const { lines, noteShown } = panelFrame({
|
|
550
583
|
theme: this.theme,
|
|
584
|
+
viewportRows: this.options.viewportRows,
|
|
585
|
+
cursor: this.cursor,
|
|
551
586
|
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
|
-
});
|
|
557
|
-
|
|
558
|
-
this.cursor.visible = fitRows(
|
|
559
|
-
this.options.viewportRows,
|
|
560
|
-
entries.length,
|
|
561
|
-
FRAME_ROWS + detail.length + (note ? 1 : 0),
|
|
562
|
-
SCREEN_DEFAULT_ROWS,
|
|
563
|
-
);
|
|
564
|
-
this.cursor.sync(entries.length);
|
|
565
|
-
const { start, end } = this.cursor.window(entries.length);
|
|
566
|
-
|
|
567
|
-
const body: string[] = [];
|
|
568
|
-
if (note) body.push(note);
|
|
569
|
-
for (let index = start; index < end; index += 1) {
|
|
570
|
-
const entry = entries[index];
|
|
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
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
return dockListFrame({
|
|
579
587
|
header,
|
|
580
588
|
summary,
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
+
},
|
|
587
599
|
});
|
|
600
|
+
this.noteShown = noteShown;
|
|
601
|
+
return lines;
|
|
588
602
|
}
|
|
589
603
|
|
|
590
604
|
render(width: number): string[] {
|
|
@@ -610,39 +624,35 @@ export class PanelComponent implements Component {
|
|
|
610
624
|
});
|
|
611
625
|
}
|
|
612
626
|
|
|
613
|
-
this.
|
|
614
|
-
this.clampSelection(entries);
|
|
615
|
-
|
|
616
|
-
const focusedEntry = entries[this.cursor.selected];
|
|
627
|
+
const focusedEntry = this.retain(entries);
|
|
617
628
|
const focused = focusedEntry?.kind === "row" ? focusedEntry.row : undefined;
|
|
618
629
|
const expanded = focused !== undefined && this.expanded.has(focused.key);
|
|
619
|
-
const detail = extensionDetail(
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
630
|
+
const detail = extensionDetail(
|
|
631
|
+
this.theme,
|
|
632
|
+
w,
|
|
633
|
+
focused,
|
|
634
|
+
detailRows(expanded, this.options.viewportRows),
|
|
635
|
+
expanded,
|
|
636
|
+
this.dirty,
|
|
626
637
|
);
|
|
627
|
-
this.cursor.sync(entries.length);
|
|
628
|
-
const { start, end } = this.cursor.window(entries.length);
|
|
629
638
|
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return dockListFrame({
|
|
639
|
+
const { lines, noteShown } = panelFrame({
|
|
640
|
+
theme: this.theme,
|
|
641
|
+
viewportRows: this.options.viewportRows,
|
|
642
|
+
cursor: this.cursor,
|
|
643
|
+
width: w,
|
|
638
644
|
header,
|
|
639
645
|
summary,
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
+
},
|
|
646
654
|
});
|
|
655
|
+
this.noteShown = noteShown;
|
|
656
|
+
return lines;
|
|
647
657
|
}
|
|
648
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,27 +1,38 @@
|
|
|
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";
|
|
18
|
+
import type { TurnSeparatorData } from "./types.ts";
|
|
11
19
|
|
|
12
|
-
/** Renders one separator entry;
|
|
20
|
+
/** Renders one separator entry; the data comes from the stored entry. */
|
|
13
21
|
export class SeparatorComponent implements Component {
|
|
22
|
+
private readonly cache = new RenderCache();
|
|
23
|
+
|
|
14
24
|
constructor(
|
|
15
|
-
private readonly
|
|
25
|
+
private readonly data: TurnSeparatorData,
|
|
16
26
|
private readonly theme: Theme,
|
|
17
27
|
private readonly pad = 0,
|
|
18
28
|
) {}
|
|
19
29
|
|
|
20
30
|
invalidate(): void {
|
|
21
|
-
|
|
31
|
+
this.cache.invalidate();
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
render(width: number): string[] {
|
|
25
|
-
|
|
35
|
+
const w = Math.max(1, width);
|
|
36
|
+
return this.cache.lines(w, undefined, () => separatorLine(this.data, w, this.theme, this.pad));
|
|
26
37
|
}
|
|
27
38
|
}
|
|
@@ -22,6 +22,16 @@ export const CONFIG = {
|
|
|
22
22
|
dashRole: "ruleStrong",
|
|
23
23
|
/** Role for the label. */
|
|
24
24
|
labelRole: "meta",
|
|
25
|
+
/** Role for a turn with a failed tool. */
|
|
26
|
+
failureRole: "bad",
|
|
27
|
+
/** Role for an aborted turn; escape is a warning, not an error. */
|
|
28
|
+
abortedRole: "warn",
|
|
29
|
+
/** Smallest run duration printed on the receipt; shorter turns keep the label quiet. */
|
|
30
|
+
minDurationMs: 1000,
|
|
31
|
+
/** Word appended when the run was aborted. */
|
|
32
|
+
abortedLabel: "aborted",
|
|
33
|
+
/** Glyph before a failed tool count. */
|
|
34
|
+
failureGlyph: GLYPHS.cross,
|
|
25
35
|
} as const satisfies {
|
|
26
36
|
customType: string;
|
|
27
37
|
glyph: string;
|
|
@@ -29,4 +39,9 @@ export const CONFIG = {
|
|
|
29
39
|
pad: number;
|
|
30
40
|
dashRole: TuiRole;
|
|
31
41
|
labelRole: TuiRole;
|
|
42
|
+
failureRole: TuiRole;
|
|
43
|
+
abortedRole: TuiRole;
|
|
44
|
+
minDurationMs: number;
|
|
45
|
+
abortedLabel: string;
|
|
46
|
+
failureGlyph: string;
|
|
32
47
|
};
|
|
@@ -1,14 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure formatting for bytefly's turn separator line.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The line is a receipt for the settled run: the turn number, then as much of
|
|
5
|
+
* the duration, the top-level tool count, and the outcome as the width can
|
|
6
|
+
* carry. The centered `╌╌╌ turn N ╌╌╌` layout comes from the design system's
|
|
7
|
+
* `centeredRule`, so this module only builds the label and picks its role; the
|
|
8
|
+
* shared rule owns the centering and the dashes.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
13
|
+
import { formatDuration, formatToolCount } from "../../../lib/format.ts";
|
|
14
|
+
import { SEPARATORS } from "../../../lib/tui/glyphs.ts";
|
|
10
15
|
import { centeredRule } from "../../../lib/tui/rules.ts";
|
|
16
|
+
import type { TuiRole } from "../../../lib/tui/tokens.ts";
|
|
11
17
|
import { CONFIG } from "./config.ts";
|
|
18
|
+
import type { TurnSeparatorData } from "./types.ts";
|
|
19
|
+
|
|
20
|
+
/** Whether the label and `centeredRule`'s two pads leave room for a dash. */
|
|
21
|
+
function fits(label: string, width: number): boolean {
|
|
22
|
+
return visibleWidth(label) + CONFIG.pad * 2 < width;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The widest receipt that fits, shed one field at a time.
|
|
27
|
+
*
|
|
28
|
+
* The tool count goes first, then the duration; the outcome stays beside the
|
|
29
|
+
* turn as long as it fits, because a failed or aborted run is the part the rule
|
|
30
|
+
* should not hide. The turn number itself is never dropped here — a terminal
|
|
31
|
+
* too narrow even for that falls back to `centeredRule`'s plain dashed rule.
|
|
32
|
+
*/
|
|
33
|
+
export function receiptLabel(data: TurnSeparatorData, width: number): string {
|
|
34
|
+
const turn = `${CONFIG.labelPrefix} ${data.turn}`;
|
|
35
|
+
const duration =
|
|
36
|
+
data.durationMs !== undefined && data.durationMs >= CONFIG.minDurationMs
|
|
37
|
+
? formatDuration(data.durationMs)
|
|
38
|
+
: undefined;
|
|
39
|
+
const tools = data.tools !== undefined && data.tools > 0 ? formatToolCount(data.tools) : undefined;
|
|
40
|
+
const outcome = data.aborted
|
|
41
|
+
? CONFIG.abortedLabel
|
|
42
|
+
: data.failures !== undefined && data.failures > 0
|
|
43
|
+
? `${CONFIG.failureGlyph}${data.failures}`
|
|
44
|
+
: undefined;
|
|
45
|
+
|
|
46
|
+
const steps: Array<Array<string | undefined>> = [
|
|
47
|
+
[turn, duration, tools, outcome],
|
|
48
|
+
[turn, duration, outcome],
|
|
49
|
+
[turn, outcome],
|
|
50
|
+
[turn],
|
|
51
|
+
];
|
|
52
|
+
for (const parts of steps) {
|
|
53
|
+
const label = parts.filter((part): part is string => part !== undefined).join(SEPARATORS.item);
|
|
54
|
+
if (fits(label, width)) return label;
|
|
55
|
+
}
|
|
56
|
+
return turn;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The label's role: an aborted turn warns, a failed tool is an error. */
|
|
60
|
+
export function receiptRole(data: TurnSeparatorData): TuiRole {
|
|
61
|
+
if (data.aborted) return CONFIG.abortedRole;
|
|
62
|
+
if (data.failures !== undefined && data.failures > 0) return CONFIG.failureRole;
|
|
63
|
+
return CONFIG.labelRole;
|
|
64
|
+
}
|
|
12
65
|
|
|
13
66
|
/**
|
|
14
67
|
* One rendered line: the centered turn rule, colored by role.
|
|
@@ -17,14 +70,15 @@ import { CONFIG } from "./config.ts";
|
|
|
17
70
|
* component raw, so the rule insets itself symmetrically by that many columns,
|
|
18
71
|
* keeping its left edge on the transcript's margin.
|
|
19
72
|
*/
|
|
20
|
-
export function separatorLine(
|
|
73
|
+
export function separatorLine(data: TurnSeparatorData, width: number, theme: Theme, pad = 0): string[] {
|
|
21
74
|
const w = Math.max(1, Math.floor(width));
|
|
22
75
|
const inset = Math.max(0, Math.min(Math.floor(pad), Math.floor((w - 1) / 2)));
|
|
23
|
-
const
|
|
76
|
+
const ruleWidth = Math.max(1, w - inset * 2);
|
|
77
|
+
const line = centeredRule(theme, ruleWidth, receiptLabel(data, ruleWidth), {
|
|
24
78
|
glyph: CONFIG.glyph,
|
|
25
79
|
pad: CONFIG.pad,
|
|
26
80
|
fillRole: CONFIG.dashRole,
|
|
27
|
-
labelRole:
|
|
81
|
+
labelRole: receiptRole(data),
|
|
28
82
|
});
|
|
29
83
|
return [`${" ".repeat(inset)}${line}`];
|
|
30
84
|
}
|