@gavin-hu/my-pi-agent 0.8.1 → 0.10.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 +35 -8
- package/README.zh-CN.md +33 -8
- package/assets/bytefly.schema.json +17 -1
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
- package/extensions/bytefly/README.md +306 -68
- package/extensions/bytefly/block/component.ts +282 -0
- package/extensions/bytefly/block/config.ts +24 -0
- package/extensions/bytefly/block/fallback.ts +120 -0
- package/extensions/bytefly/block/register.ts +164 -0
- package/extensions/bytefly/chrome.ts +97 -0
- package/extensions/bytefly/doctor/format.ts +36 -10
- package/extensions/bytefly/doctor/tools.ts +10 -6
- package/extensions/bytefly/footer/config.ts +1 -1
- package/extensions/bytefly/footer/render/format.ts +5 -18
- package/extensions/bytefly/footer/render/layout.ts +2 -1
- package/extensions/bytefly/footer/render/segments.ts +51 -78
- package/extensions/bytefly/header/component.ts +49 -21
- package/extensions/bytefly/header/config.ts +28 -21
- package/extensions/bytefly/header/register.ts +6 -1
- package/extensions/bytefly/index.ts +32 -12
- package/extensions/bytefly/message/config.ts +15 -0
- package/extensions/bytefly/message/format.ts +42 -0
- package/extensions/bytefly/message/register.ts +15 -0
- package/extensions/bytefly/notice/component.ts +107 -0
- package/extensions/bytefly/notice/config.ts +41 -0
- package/extensions/bytefly/notice/register.ts +27 -0
- package/extensions/bytefly/pad.ts +42 -0
- package/extensions/bytefly/panel/commands.ts +69 -35
- package/extensions/bytefly/panel/format.ts +8 -8
- package/extensions/bytefly/panel/tui.ts +45 -70
- package/extensions/bytefly/separator/component.ts +2 -1
- package/extensions/bytefly/separator/config.ts +16 -8
- package/extensions/bytefly/separator/format.ts +18 -38
- package/extensions/bytefly/separator/register.ts +3 -2
- package/extensions/bytefly/separator/types.ts +0 -10
- package/extensions/bytefly/working/config.ts +20 -0
- package/extensions/bytefly/working/format.ts +25 -0
- package/extensions/bytefly/working/register.ts +88 -0
- package/extensions/document/README.md +12 -5
- package/extensions/document/paging.ts +10 -10
- package/extensions/document/render.ts +17 -16
- package/extensions/document/tool.ts +1 -1
- package/extensions/file-browse/README.md +54 -10
- package/extensions/file-browse/client.ts +110 -2
- package/extensions/file-browse/config.ts +12 -0
- package/extensions/file-browse/diff.ts +187 -0
- package/extensions/file-browse/git.ts +180 -8
- package/extensions/file-browse/html.ts +401 -25
- package/extensions/file-browse/index.ts +15 -4
- package/extensions/file-browse/markdown.ts +33 -0
- package/extensions/file-browse/router.ts +94 -2
- package/extensions/file-browse/server.ts +4 -1
- package/extensions/file-open/README.md +6 -0
- package/extensions/goal/README.md +16 -10
- package/extensions/goal/format.ts +13 -25
- package/extensions/goal/tools.ts +11 -22
- package/extensions/goal/tui.ts +12 -40
- package/extensions/job/README.md +22 -13
- package/extensions/job/format.ts +36 -13
- package/extensions/job/index.ts +26 -21
- package/extensions/job/tools.ts +15 -12
- package/extensions/job/tui.ts +148 -222
- package/extensions/job/ui.ts +24 -49
- package/extensions/memory/README.md +7 -1
- package/extensions/memory/format.ts +5 -3
- package/extensions/memory/tools.ts +4 -3
- package/extensions/memory/tui.ts +32 -36
- package/extensions/plan/README.md +86 -36
- package/extensions/plan/blocks.ts +44 -2
- package/extensions/plan/feedback.ts +20 -5
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/runtime.ts +3 -2
- package/extensions/plan/tools.ts +43 -27
- package/extensions/plan/tui.ts +21 -27
- package/extensions/plan/web-client.ts +386 -40
- package/extensions/plan/web-page.ts +138 -33
- package/extensions/plan/web-router.ts +46 -13
- package/extensions/plan/web.ts +63 -12
- package/extensions/question/README.md +14 -2
- package/extensions/question/answers.ts +25 -18
- package/extensions/question/tools.ts +22 -16
- package/extensions/question/tui.ts +132 -76
- package/extensions/rewind/README.md +3 -1
- package/extensions/rewind/format.ts +35 -2
- package/extensions/rewind/index.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +66 -62
- package/extensions/rewind/snapshot-run.ts +1 -1
- package/extensions/rewind/status.ts +3 -2
- package/extensions/subagent/README.md +6 -0
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +108 -66
- package/extensions/todo/README.md +20 -13
- package/extensions/todo/format.ts +47 -34
- package/extensions/todo/tools.ts +6 -4
- package/extensions/todo/tui.ts +46 -26
- package/extensions/watch/README.md +16 -8
- package/extensions/watch/format.ts +33 -13
- package/extensions/watch/index.ts +29 -24
- package/extensions/watch/tools.ts +16 -9
- package/extensions/watch/tui.ts +142 -196
- package/extensions/watch/ui.ts +24 -46
- package/extensions/web-access/README.md +16 -7
- package/extensions/web-access/fetch/tool.ts +12 -11
- package/extensions/web-access/search/tool.ts +16 -13
- package/extensions/wechat/README.md +8 -2
- package/extensions/wechat/index.ts +4 -3
- package/extensions/wechat/login-screen.ts +8 -5
- package/extensions/wechat/status-screen.ts +41 -22
- package/extensions/wechat/tools.ts +6 -5
- package/extensions/worktree/README.md +6 -0
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/index.ts +1 -1
- package/extensions/worktree/runtime.ts +1 -1
- package/lib/README.md +11 -7
- package/lib/format.ts +17 -0
- package/lib/notice.ts +87 -0
- package/lib/requirements.ts +2 -2
- package/lib/{tui.ts → tui/chrome.ts} +17 -34
- package/lib/tui/columns.ts +96 -0
- package/lib/{list-cursor.ts → tui/cursor.ts} +58 -4
- package/lib/tui/gauge.ts +78 -0
- package/lib/{ui.ts → tui/glyphs.ts} +10 -11
- package/lib/tui/indent.ts +166 -0
- package/lib/tui/index.ts +27 -0
- package/lib/tui/line-pane.ts +173 -0
- package/lib/tui/rows.ts +143 -0
- package/lib/tui/rules.ts +94 -0
- package/lib/tui/status-controller.ts +107 -0
- package/lib/tui/tokens.ts +72 -0
- package/lib/web/base.ts +2 -1
- package/lib/web/components.ts +3 -1
- package/lib/web/layout.ts +5 -1
- package/package.json +7 -3
- package/themes/bytefly-dark.json +7 -8
- package/themes/bytefly-light.json +10 -11
- package/assets/bytefly-chrome.png +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `/bytefly` surface: one
|
|
3
|
-
* `/extensions` and `/doctor` registered against the same handler.
|
|
2
|
+
* The `/bytefly` surface: one command, `/bytefly [doctor] [extension] [refresh]`.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Without `doctor` it opens the extension screen: the dock in `tui`, the
|
|
5
|
+
* grouped listing elsewhere. With `doctor` it produces the doctor report — the
|
|
6
|
+
* same text `run_doctor` returns — for the whole set or one named extension; in
|
|
7
|
+
* `tui` the report is the dock screen. Discovery, the doctor probe, and the
|
|
8
|
+
* dirty-to-reload confirm all live here. The doctor runtime is injected by the
|
|
9
|
+
* composition root, so `run_doctor` and the screen keep reading one cache.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -14,10 +14,10 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
14
14
|
import { sanitizeLine } from "../../../lib/format.ts";
|
|
15
15
|
import { withRailsSuppressed } from "../../../lib/rails.ts";
|
|
16
16
|
import { REQUIREMENTS, type ExtensionReport } from "../../../lib/requirements.ts";
|
|
17
|
-
import { formatExtension, hasProblems } from "../doctor/format.ts";
|
|
17
|
+
import { formatExtension, formatReport, hasProblems } from "../doctor/format.ts";
|
|
18
18
|
import type { DoctorRuntime } from "../doctor/runtime.ts";
|
|
19
19
|
import { discoverExtensions, type DiscoveryDeps } from "../picker/discovery.ts";
|
|
20
|
-
import { createExtensionsPickerRuntime } from "../picker/runtime.ts";
|
|
20
|
+
import { createExtensionsPickerRuntime, type ExtensionsPickerRuntime } from "../picker/runtime.ts";
|
|
21
21
|
import type { Discovery } from "../picker/types.ts";
|
|
22
22
|
import { formatListing } from "./format.ts";
|
|
23
23
|
import { buildPanelModel, extensionName } from "./join.ts";
|
|
@@ -42,20 +42,63 @@ function selectableNames(discovery: Discovery): Set<string> {
|
|
|
42
42
|
return names;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/** Argument completions: the `doctor` subcommand, every declared extension, then `refresh`. */
|
|
46
46
|
function completions(prefix: string): Array<{ value: string; label: string }> | null {
|
|
47
|
-
const options = [...Object.keys(REQUIREMENTS), "refresh"].filter((option) => option.startsWith(prefix));
|
|
47
|
+
const options = ["doctor", ...Object.keys(REQUIREMENTS), "refresh"].filter((option) => option.startsWith(prefix));
|
|
48
48
|
return options.length > 0 ? options.map((value) => ({ value, label: value })) : null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
51
|
+
/** Which bytefly surface the arguments select. */
|
|
52
|
+
type PanelMode = "screen" | "doctor";
|
|
53
|
+
|
|
54
|
+
/** `/bytefly [doctor] [extension] [refresh]`, parsed. */
|
|
55
|
+
interface PanelArgs {
|
|
56
|
+
mode: PanelMode;
|
|
57
|
+
/** The extension to narrow to, when one is named. */
|
|
58
|
+
name?: string;
|
|
59
|
+
/** Re-probe the doctor instead of reusing the session cache. */
|
|
60
|
+
refresh: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Parse `/bytefly [doctor] [extension] [refresh]`; extra tokens are ignored. */
|
|
64
|
+
function parseArgs(args: string): PanelArgs {
|
|
65
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
66
|
+
const refresh = parts.includes("refresh");
|
|
67
|
+
const rest = parts.filter((part) => part !== "refresh");
|
|
68
|
+
const mode: PanelMode = rest[0] === "doctor" ? "doctor" : "screen";
|
|
69
|
+
return { mode, name: rest[mode === "doctor" ? 1 : 0], refresh };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Print one extension's full findings, or say why it has none. The screen's
|
|
74
|
+
* named form and the `doctor` subcommand share this text path.
|
|
75
|
+
*/
|
|
76
|
+
function notifyExtension(
|
|
77
|
+
ctx: ExtensionCommandContext,
|
|
78
|
+
name: string,
|
|
79
|
+
reports: readonly ExtensionReport[],
|
|
80
|
+
runtime: ExtensionsPickerRuntime,
|
|
81
|
+
): void {
|
|
82
|
+
const report = reports.find((entry) => entry.extension === name);
|
|
83
|
+
if (report) {
|
|
84
|
+
ctx.ui.notify(formatExtension(report), hasProblems([report]) ? "warning" : "info");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const item = runtime.items().find((entry) => extensionName(entry.path) === name);
|
|
88
|
+
ctx.ui.notify(
|
|
89
|
+
item
|
|
90
|
+
? `bytefly: ${sanitizeLine(item.path)} — no doctor report`
|
|
91
|
+
: `bytefly: no extension named "${sanitizeLine(name)}"`,
|
|
92
|
+
"info",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Register the single `/bytefly` command. */
|
|
52
97
|
export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
53
98
|
const discover = deps.discover ?? discoverExtensions;
|
|
54
99
|
|
|
55
100
|
const handler = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
56
|
-
const
|
|
57
|
-
const refresh = parts.includes("refresh");
|
|
58
|
-
const name = parts.find((part) => part !== "refresh");
|
|
101
|
+
const { mode, name, refresh } = parseArgs(args);
|
|
59
102
|
|
|
60
103
|
const cwd = ctx.cwd ?? process.cwd();
|
|
61
104
|
const agentDir = getAgentDir();
|
|
@@ -88,18 +131,11 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
88
131
|
|
|
89
132
|
if (ctx.mode !== "tui") {
|
|
90
133
|
if (name !== undefined) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const item = runtime.items().find((entry) => extensionName(entry.path) === name);
|
|
97
|
-
ctx.ui.notify(
|
|
98
|
-
item
|
|
99
|
-
? `bytefly: ${sanitizeLine(item.path)} — no doctor report`
|
|
100
|
-
: `bytefly: no extension named "${sanitizeLine(name)}"`,
|
|
101
|
-
"info",
|
|
102
|
-
);
|
|
134
|
+
notifyExtension(ctx, name, reports, runtime);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (mode === "doctor") {
|
|
138
|
+
ctx.ui.notify(formatReport(reports), hasProblems(reports) ? "warning" : "info");
|
|
103
139
|
return;
|
|
104
140
|
}
|
|
105
141
|
const model = buildPanelModel({ items: runtime.items(), reports, agentDir });
|
|
@@ -136,12 +172,10 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
136
172
|
if (reload) await ctx.reload();
|
|
137
173
|
};
|
|
138
174
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
pi.registerCommand(command, { description, getArgumentCompletions: completions, handler });
|
|
146
|
-
}
|
|
175
|
+
pi.registerCommand("bytefly", {
|
|
176
|
+
description:
|
|
177
|
+
"Open the bytefly extension screen: /bytefly [extension] [refresh]; /bytefly doctor [extension] [refresh] prints the doctor report",
|
|
178
|
+
getArgumentCompletions: completions,
|
|
179
|
+
handler,
|
|
180
|
+
});
|
|
147
181
|
}
|
|
@@ -8,18 +8,18 @@
|
|
|
8
8
|
* came from a probe is sanitized by `doctor/format.ts`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type {
|
|
12
|
-
import { GLYPHS } from "../../../lib/
|
|
11
|
+
import type { TuiRole } from "../../../lib/tui/tokens.ts";
|
|
12
|
+
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
13
13
|
import { reportSeverity, reportSummary } from "../doctor/format.ts";
|
|
14
14
|
import type { PanelModel, PanelRow } from "./types.ts";
|
|
15
15
|
|
|
16
16
|
/** The settings scope the screen shows. */
|
|
17
17
|
export type PanelScope = "global" | "project";
|
|
18
18
|
|
|
19
|
-
/** One segment of the headline, with its
|
|
19
|
+
/** One segment of the headline, with its design role for the TUI. */
|
|
20
20
|
export interface SummarySegment {
|
|
21
21
|
text: string;
|
|
22
|
-
color:
|
|
22
|
+
color: TuiRole;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/** Row and problem counts over a model. */
|
|
@@ -67,19 +67,19 @@ export function summarySegments(model: PanelModel, scope: PanelScope, options: S
|
|
|
67
67
|
const counts = panelCounts(model);
|
|
68
68
|
const segments: SummarySegment[] = [{ text: scope === "global" ? "Global" : "Project", color: "accent" }];
|
|
69
69
|
if (options.filter !== undefined) {
|
|
70
|
-
segments.push({ text: `${options.filter} only`, color: "
|
|
70
|
+
segments.push({ text: `${options.filter} only`, color: "meta" });
|
|
71
71
|
} else {
|
|
72
72
|
const noun = options.compact ? "ext" : counts.extensions === 1 ? "extension" : "extensions";
|
|
73
|
-
segments.push({ text: `${counts.extensions} ${noun}`, color: "
|
|
73
|
+
segments.push({ text: `${counts.extensions} ${noun}`, color: "meta" });
|
|
74
74
|
if (counts.disabled > 0) {
|
|
75
|
-
segments.push({ text: `${counts.disabled} ${options.compact ? "off" : "disabled"}`, color: "
|
|
75
|
+
segments.push({ text: `${counts.disabled} ${options.compact ? "off" : "disabled"}`, color: "meta" });
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
const problems: string[] = [];
|
|
79
79
|
if (counts.missing > 0) problems.push(`${counts.missing} missing`);
|
|
80
80
|
if (counts.degraded > 0) problems.push(`${counts.degraded} degraded`);
|
|
81
81
|
if (problems.length > 0) {
|
|
82
|
-
segments.push({ text: problems.join(", "), color: counts.missing > 0 ? "
|
|
82
|
+
segments.push({ text: problems.join(", "), color: counts.missing > 0 ? "bad" : "warn" });
|
|
83
83
|
}
|
|
84
84
|
return segments;
|
|
85
85
|
}
|
|
@@ -5,33 +5,29 @@
|
|
|
5
5
|
* The component is derived: it rebuilds the merged model from the picker
|
|
6
6
|
* runtime and the current reports on every render, keeps the focused row's key
|
|
7
7
|
* across rebuilds, and requests a repaint only after an input changes state.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The screen is drawn from the design system — a label-first `titleRule`, one
|
|
9
|
+
* `listRow` grammar, one `checkbox` — and its scroll math from
|
|
10
|
+
* `lib/tui/cursor.ts`. A row without a report renders no glyph and no summary,
|
|
11
|
+
* never a false `ok`.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
14
|
-
Key,
|
|
15
|
-
matchesKey,
|
|
16
|
-
truncateToWidth,
|
|
17
|
-
visibleWidth,
|
|
18
|
-
type Component,
|
|
19
|
-
type TuiMouseEvent,
|
|
20
|
-
type TuiMouseEventResult,
|
|
21
|
-
} from "@earendil-works/pi-tui";
|
|
15
|
+
import { Key, matchesKey, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
22
16
|
import { sanitizeLine } from "../../../lib/format.ts";
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
selectionMarker,
|
|
30
|
-
wheelDelta,
|
|
31
|
-
} from "../../../lib/list-cursor.ts";
|
|
17
|
+
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../../lib/tui/chrome.ts";
|
|
18
|
+
import { clip, fitCells } from "../../../lib/tui/columns.ts";
|
|
19
|
+
import { fitRows, formatRange, isCloseKey, ListCursor, navIntent, wheelDelta } from "../../../lib/tui/cursor.ts";
|
|
20
|
+
import { checkbox, emptyState, listRow } from "../../../lib/tui/rows.ts";
|
|
21
|
+
import { titleRule } from "../../../lib/tui/rules.ts";
|
|
22
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
32
23
|
import type { ExtensionReport } from "../../../lib/requirements.ts";
|
|
33
|
-
import {
|
|
34
|
-
|
|
24
|
+
import {
|
|
25
|
+
detailLines as reportDetailLines,
|
|
26
|
+
reportSeverity,
|
|
27
|
+
reportSummary,
|
|
28
|
+
severityGlyphChar,
|
|
29
|
+
severityRole,
|
|
30
|
+
} from "../doctor/format.ts";
|
|
35
31
|
import type { ExtensionsPickerRuntime } from "../picker/runtime.ts";
|
|
36
32
|
import { summarySegments } from "./format.ts";
|
|
37
33
|
import { buildPanelModel } from "./join.ts";
|
|
@@ -283,50 +279,37 @@ export class PanelComponent implements Component {
|
|
|
283
279
|
return { handled: true };
|
|
284
280
|
}
|
|
285
281
|
|
|
282
|
+
/** The headline: whole segments only, so a narrow screen drops a count. */
|
|
286
283
|
private summary(width: number, model: PanelModel): string {
|
|
287
284
|
const segments = summarySegments(model, this.options.runtime.scope(), {
|
|
288
285
|
filter: this.options.filter,
|
|
289
286
|
compact: width < 72,
|
|
290
287
|
});
|
|
291
|
-
const separator = this.theme
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
const styled = this.theme.fg(segment.color, segment.text);
|
|
295
|
-
if (kept.length > 0 && visibleWidth(` ${[...kept, styled].join(separator)}`) > width) break;
|
|
296
|
-
kept.push(styled);
|
|
297
|
-
}
|
|
298
|
-
return truncateToWidth(` ${kept.join(separator)}`, width, "…");
|
|
288
|
+
const separator = paint(this.theme, "faint", " · ");
|
|
289
|
+
const styled = segments.map((segment) => paint(this.theme, segment.color, segment.text));
|
|
290
|
+
return clip(` ${fitCells(styled, Math.max(1, width - 2), separator)}`, width);
|
|
299
291
|
}
|
|
300
292
|
|
|
301
293
|
private groupLine(entry: Extract<PanelEntry, { kind: "group" }>, width: number): string {
|
|
302
|
-
const label = this.theme
|
|
303
|
-
const count = this.theme
|
|
304
|
-
return
|
|
294
|
+
const label = paint(this.theme, "meta", sanitizeLine(entry.label));
|
|
295
|
+
const count = paint(this.theme, "faint", `(${entry.count})`);
|
|
296
|
+
return clip(` ${label} ${count}`, width);
|
|
305
297
|
}
|
|
306
298
|
|
|
307
299
|
private rowLine(row: PanelRow, selected: boolean, width: number): string {
|
|
308
|
-
const checkbox = row.item
|
|
309
|
-
? row.item.enabled
|
|
310
|
-
? this.theme.fg("success", "[x]")
|
|
311
|
-
: this.theme.fg("dim", "[ ]")
|
|
312
|
-
: this.theme.fg("dim", " ");
|
|
313
300
|
const severity = row.report ? reportSeverity(row.report) : undefined;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
: row.item
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const name = truncateToWidth(styled, available, "…");
|
|
327
|
-
const pad = " ".repeat(Math.max(0, available - visibleWidth(name)));
|
|
328
|
-
const tone = severity === "missing" ? "error" : severity === "degraded" ? "warning" : "dim";
|
|
329
|
-
return truncateToWidth(`${head}${name}${pad} ${this.theme.fg(tone, summary)}`, width, "…");
|
|
301
|
+
return listRow(this.theme, width, {
|
|
302
|
+
selected,
|
|
303
|
+
// A reported-only row has no enable pattern, but still reserves the box
|
|
304
|
+
// column so the list stays aligned.
|
|
305
|
+
leading: row.item ? checkbox(this.theme, row.item.enabled) : " ",
|
|
306
|
+
glyph: severity ? severityGlyphChar(severity) : " ",
|
|
307
|
+
glyphRole: severity ? severityRole(severity) : "faint",
|
|
308
|
+
label: sanitizeLine(row.label),
|
|
309
|
+
labelRole: selected ? "accent" : row.item && !row.item.enabled ? "faint" : "label",
|
|
310
|
+
trailing: row.report ? reportSummary(row.report) : undefined,
|
|
311
|
+
trailingRole: severity === "missing" ? "bad" : severity === "degraded" ? "warn" : "faint",
|
|
312
|
+
});
|
|
330
313
|
}
|
|
331
314
|
|
|
332
315
|
/** The focused row's label, findings, and the pending-changes note. */
|
|
@@ -334,19 +317,14 @@ export class PanelComponent implements Component {
|
|
|
334
317
|
const lines: string[] = [];
|
|
335
318
|
if (row?.report) {
|
|
336
319
|
const tail = reportSummary(row.report);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
` ${this.theme.fg("muted", sanitizeLine(row.label))} ${this.theme.fg("dim", "—")} ${this.theme.fg(row.report.enabled ? "muted" : "warning", tail)}`,
|
|
340
|
-
width,
|
|
341
|
-
"…",
|
|
342
|
-
),
|
|
343
|
-
);
|
|
320
|
+
const head = `${paint(this.theme, "meta", sanitizeLine(row.label))} ${paint(this.theme, "faint", "—")} ${paint(this.theme, row.report.enabled ? "meta" : "warn", tail)}`;
|
|
321
|
+
lines.push(clip(` ${head}`, width));
|
|
344
322
|
if (budget > 0) lines.push(...reportDetailLines(row.report, this.theme, width, budget, this.showDetail));
|
|
345
323
|
} else if (row?.item) {
|
|
346
|
-
lines.push(
|
|
324
|
+
lines.push(clip(` ${paint(this.theme, "meta", sanitizeLine(row.item.path))}`, width));
|
|
347
325
|
}
|
|
348
326
|
if (this.options.runtime.isDirty()) {
|
|
349
|
-
lines.push(
|
|
327
|
+
lines.push(clip(` ${paint(this.theme, "warn", "Saved · reload to apply")}`, width));
|
|
350
328
|
}
|
|
351
329
|
return lines;
|
|
352
330
|
}
|
|
@@ -356,14 +334,13 @@ export class PanelComponent implements Component {
|
|
|
356
334
|
const model = this.model();
|
|
357
335
|
const entries = entriesOf(model);
|
|
358
336
|
const summary = this.summary(w, model);
|
|
337
|
+
const header = titleRule(this.theme, w, "bytefly");
|
|
359
338
|
|
|
360
339
|
if (entries.length === 0) {
|
|
361
340
|
return dockListFrame({
|
|
362
|
-
|
|
363
|
-
width: w,
|
|
364
|
-
title: "bytefly",
|
|
341
|
+
header,
|
|
365
342
|
summary,
|
|
366
|
-
body:
|
|
343
|
+
body: emptyState(this.theme, w, { label: "No extensions found." }),
|
|
367
344
|
hint: screenHint(this.theme, w, ["Esc close"]),
|
|
368
345
|
});
|
|
369
346
|
}
|
|
@@ -394,9 +371,7 @@ export class PanelComponent implements Component {
|
|
|
394
371
|
}
|
|
395
372
|
|
|
396
373
|
return dockListFrame({
|
|
397
|
-
|
|
398
|
-
width: w,
|
|
399
|
-
title: "bytefly",
|
|
374
|
+
header,
|
|
400
375
|
summary,
|
|
401
376
|
body,
|
|
402
377
|
range: this.cursor.clipped(entries.length)
|
|
@@ -14,6 +14,7 @@ export class SeparatorComponent implements Component {
|
|
|
14
14
|
constructor(
|
|
15
15
|
private readonly turn: number,
|
|
16
16
|
private readonly theme: Theme,
|
|
17
|
+
private readonly pad = 0,
|
|
17
18
|
) {}
|
|
18
19
|
|
|
19
20
|
invalidate(): void {
|
|
@@ -21,6 +22,6 @@ export class SeparatorComponent implements Component {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
render(width: number): string[] {
|
|
24
|
-
return separatorLine(this.turn, width, this.theme);
|
|
25
|
+
return separatorLine(this.turn, width, this.theme, this.pad);
|
|
25
26
|
}
|
|
26
27
|
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Tuning for bytefly's turn separator.
|
|
3
3
|
*
|
|
4
4
|
* Edit this file to change how the line between completed turns looks. The
|
|
5
|
-
*
|
|
6
|
-
* theme automatically.
|
|
5
|
+
* glyph and the colors are plain design-system roles, so the line follows the
|
|
6
|
+
* active theme automatically.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { GLYPHS } from "../../../lib/
|
|
9
|
+
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
10
|
+
import type { TuiRole } from "../../../lib/tui/tokens.ts";
|
|
10
11
|
|
|
11
12
|
export const CONFIG = {
|
|
12
13
|
/** Custom entry type. Changing it orphans separators recorded by an older build. */
|
|
@@ -17,8 +18,15 @@ export const CONFIG = {
|
|
|
17
18
|
labelPrefix: "turn",
|
|
18
19
|
/** Spaces on each side of the label. */
|
|
19
20
|
pad: 1,
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
} as const
|
|
21
|
+
/** Role for the dashes. */
|
|
22
|
+
dashRole: "ruleStrong",
|
|
23
|
+
/** Role for the label. */
|
|
24
|
+
labelRole: "meta",
|
|
25
|
+
} as const satisfies {
|
|
26
|
+
customType: string;
|
|
27
|
+
glyph: string;
|
|
28
|
+
labelPrefix: string;
|
|
29
|
+
pad: number;
|
|
30
|
+
dashRole: TuiRole;
|
|
31
|
+
labelRole: TuiRole;
|
|
32
|
+
};
|
|
@@ -1,50 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure formatting for bytefly's turn separator line.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The centered `╌╌╌ turn N ╌╌╌` layout comes from the design system's
|
|
5
|
+
* `centeredRule`, so this module only names the turn; the shared rule owns the
|
|
6
|
+
* centering, the degradation, and the colors.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
10
|
+
import { centeredRule } from "../../../lib/tui/rules.ts";
|
|
11
11
|
import { CONFIG } from "./config.ts";
|
|
12
|
-
import type { SeparatorParts } from "./types.ts";
|
|
13
|
-
|
|
14
|
-
/** ` turn N ` — the label printed in the middle of the line. */
|
|
15
|
-
export function separatorLabel(turn: number): string {
|
|
16
|
-
const pad = " ".repeat(CONFIG.pad);
|
|
17
|
-
return `${pad}${CONFIG.labelPrefix} ${turn}${pad}`;
|
|
18
|
-
}
|
|
19
12
|
|
|
20
13
|
/**
|
|
21
|
-
*
|
|
14
|
+
* One rendered line: the centered turn rule, colored by role.
|
|
22
15
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
16
|
+
* `pad` is Pi's output pad. Pi gives an entry renderer no pad and adds its
|
|
17
|
+
* component raw, so the rule insets itself symmetrically by that many columns,
|
|
18
|
+
* keeping its left edge on the transcript's margin.
|
|
25
19
|
*/
|
|
26
|
-
export function
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const left = Math.floor(remaining / 2);
|
|
37
|
-
const right = remaining - left;
|
|
38
|
-
return {
|
|
39
|
-
left: CONFIG.glyph.repeat(left),
|
|
40
|
-
label,
|
|
41
|
-
right: CONFIG.glyph.repeat(right),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** One rendered line: colored dashes around a colored label. */
|
|
46
|
-
export function separatorLine(turn: number, width: number, theme: Theme): string[] {
|
|
47
|
-
const { left, label, right } = separatorParts(turn, width);
|
|
48
|
-
const dashes = (text: string) => (text ? theme.fg(CONFIG.dashColor, text) : "");
|
|
49
|
-
return [dashes(left) + (label ? theme.fg(CONFIG.labelColor, label) : "") + dashes(right)];
|
|
20
|
+
export function separatorLine(turn: number, width: number, theme: Theme, pad = 0): string[] {
|
|
21
|
+
const w = Math.max(1, Math.floor(width));
|
|
22
|
+
const inset = Math.max(0, Math.min(Math.floor(pad), Math.floor((w - 1) / 2)));
|
|
23
|
+
const line = centeredRule(theme, Math.max(1, w - inset * 2), `${CONFIG.labelPrefix} ${turn}`, {
|
|
24
|
+
glyph: CONFIG.glyph,
|
|
25
|
+
pad: CONFIG.pad,
|
|
26
|
+
fillRole: CONFIG.dashRole,
|
|
27
|
+
labelRole: CONFIG.labelRole,
|
|
28
|
+
});
|
|
29
|
+
return [`${" ".repeat(inset)}${line}`];
|
|
50
30
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { createPadStore, type PadSource } from "../pad.ts";
|
|
15
16
|
import { SeparatorComponent } from "./component.ts";
|
|
16
17
|
import { CONFIG } from "./config.ts";
|
|
17
18
|
import type { TurnSeparatorData } from "./types.ts";
|
|
@@ -26,11 +27,11 @@ function countSeparators(entries: readonly { type: string; customType?: string }
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/** Register the separator renderer and append one on every settled turn. */
|
|
29
|
-
export function registerTurnSeparator(pi: ExtensionAPI): void {
|
|
30
|
+
export function registerTurnSeparator(pi: ExtensionAPI, pad: PadSource = createPadStore()): void {
|
|
30
31
|
pi.registerEntryRenderer<TurnSeparatorData>(CONFIG.customType, (entry, _options, theme) => {
|
|
31
32
|
const turn = entry.data?.turn;
|
|
32
33
|
if (typeof turn !== "number" || !Number.isFinite(turn)) return undefined;
|
|
33
|
-
return new SeparatorComponent(turn, theme);
|
|
34
|
+
return new SeparatorComponent(turn, theme, pad.resolve());
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
pi.on("agent_settled", (_event, ctx) => {
|
|
@@ -10,13 +10,3 @@ export interface TurnSeparatorData {
|
|
|
10
10
|
/** 1-based index of the completed user turn this separator follows. */
|
|
11
11
|
turn: number;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
/** A separator line split into its dashes and label. */
|
|
15
|
-
export interface SeparatorParts {
|
|
16
|
-
/** Dashes before the label. */
|
|
17
|
-
left: string;
|
|
18
|
-
/** The centred `turn N` label. */
|
|
19
|
-
label: string;
|
|
20
|
-
/** Dashes after the label. */
|
|
21
|
-
right: string;
|
|
22
|
-
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tuning for bytefly's live working timer.
|
|
3
|
+
*
|
|
4
|
+
* While Pi's working row is up, the counter is written into the working message
|
|
5
|
+
* Pi renders beside its spinner. Edit this file to change the word or how often
|
|
6
|
+
* the count advances.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const WORKING_CONFIG = {
|
|
10
|
+
/** Word before the count. */
|
|
11
|
+
working: "Working",
|
|
12
|
+
/** Between the word and the count. */
|
|
13
|
+
ellipsis: "…",
|
|
14
|
+
/** How often the count advances, in milliseconds. */
|
|
15
|
+
intervalMs: 1000,
|
|
16
|
+
} as const satisfies {
|
|
17
|
+
working: string;
|
|
18
|
+
ellipsis: string;
|
|
19
|
+
intervalMs: number;
|
|
20
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure text for the live working timer.
|
|
3
|
+
*
|
|
4
|
+
* Whole seconds, because the counter advances on a one-second tick: `0s` at the
|
|
5
|
+
* start, `42s`, `1m05s`, `1h02m`. Negative and non-finite input reads as `0s`,
|
|
6
|
+
* so a clock that steps backwards cannot print a negative count.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { WORKING_CONFIG } from "./config.ts";
|
|
10
|
+
|
|
11
|
+
/** Compact elapsed time in whole seconds: `0s`, `42s`, `1m05s`, `1h02m`. */
|
|
12
|
+
export function formatElapsed(ms: number): string {
|
|
13
|
+
const total = Number.isFinite(ms) && ms > 0 ? Math.floor(ms / 1000) : 0;
|
|
14
|
+
const seconds = total % 60;
|
|
15
|
+
const minutes = Math.floor(total / 60) % 60;
|
|
16
|
+
const hours = Math.floor(total / 3600);
|
|
17
|
+
if (hours > 0) return `${hours}h${String(minutes).padStart(2, "0")}m`;
|
|
18
|
+
if (minutes > 0) return `${minutes}m${String(seconds).padStart(2, "0")}s`;
|
|
19
|
+
return `${seconds}s`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The working-row message for an elapsed time, e.g. `Working… 4s`. */
|
|
23
|
+
export function workingCounter(ms: number): string {
|
|
24
|
+
return `${WORKING_CONFIG.working}${WORKING_CONFIG.ellipsis} ${formatElapsed(ms)}`;
|
|
25
|
+
}
|