@gavin-hu/my-pi-agent 0.9.0 → 0.11.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 +28 -2
- package/README.zh-CN.md +27 -2
- package/assets/bytefly.schema.json +17 -1
- package/assets/chrome.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 +261 -36
- 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/tools.ts +5 -2
- package/extensions/bytefly/header/component.ts +21 -10
- package/extensions/bytefly/header/config.ts +0 -3
- 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/separator/component.ts +2 -1
- package/extensions/bytefly/separator/format.ts +17 -10
- package/extensions/bytefly/separator/register.ts +3 -2
- 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 +11 -4
- package/extensions/document/paging.ts +10 -10
- package/extensions/document/render.ts +7 -7
- package/extensions/file-browse/README.md +3 -1
- package/extensions/file-browse/client.ts +9 -2
- package/extensions/file-browse/git.ts +4 -7
- package/extensions/file-browse/html.ts +27 -17
- package/extensions/file-open/README.md +6 -0
- package/extensions/goal/README.md +16 -10
- package/extensions/goal/format.ts +7 -20
- package/extensions/goal/tools.ts +5 -17
- package/extensions/goal/tui.ts +12 -40
- package/extensions/job/README.md +113 -35
- package/extensions/job/commands.ts +1 -0
- package/extensions/job/config.ts +4 -0
- package/extensions/job/format.ts +19 -7
- package/extensions/job/index.ts +25 -20
- package/extensions/job/process.ts +24 -2
- package/extensions/job/registry.ts +3 -0
- package/extensions/job/runtime.ts +253 -18
- package/extensions/job/schema.ts +34 -0
- package/extensions/job/store.ts +13 -1
- package/extensions/job/tools.ts +55 -15
- package/extensions/job/tui.ts +41 -15
- package/extensions/job/types.ts +9 -1
- package/extensions/memory/README.md +6 -0
- package/extensions/memory/format.ts +5 -3
- package/extensions/plan/README.md +32 -12
- package/extensions/plan/tools.ts +20 -5
- package/extensions/plan/web-client.ts +58 -17
- package/extensions/plan/web-page.ts +34 -9
- package/extensions/plan/web-router.ts +40 -9
- package/extensions/plan/web.ts +22 -2
- package/extensions/question/README.md +13 -1
- package/extensions/question/answers.ts +25 -18
- package/extensions/question/tools.ts +11 -6
- package/extensions/rewind/README.md +2 -0
- package/extensions/rewind/format.ts +6 -1
- package/extensions/rewind/index.ts +3 -1
- package/extensions/rewind/snapshot-run.ts +1 -1
- package/extensions/subagent/README.md +6 -0
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +56 -29
- package/extensions/todo/README.md +19 -12
- package/extensions/todo/format.ts +17 -25
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/tui.ts +3 -2
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/format.ts +2 -2
- package/extensions/watch/index.ts +28 -23
- package/extensions/watch/tools.ts +5 -3
- package/extensions/watch/tui.ts +12 -7
- package/extensions/web-access/README.md +16 -7
- package/extensions/web-access/fetch/tool.ts +1 -1
- package/extensions/web-access/search/tool.ts +3 -3
- package/extensions/wechat/README.md +6 -0
- package/extensions/worktree/README.md +6 -0
- package/lib/README.md +2 -1
- package/lib/format.ts +17 -0
- package/lib/notice.ts +87 -0
- package/lib/tui/chrome.ts +8 -4
- package/lib/tui/cursor.ts +2 -1
- package/lib/tui/glyphs.ts +10 -11
- package/lib/tui/indent.ts +166 -0
- package/lib/tui/index.ts +3 -1
- package/lib/tui/rows.ts +39 -5
- package/lib/tui/rules.ts +9 -3
- package/lib/tui/tokens.ts +14 -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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transcript rail and the shared `chrome` section of `bytefly.json`.
|
|
3
|
+
*
|
|
4
|
+
* The rail is the vertical stroke that leads a block's body — a tool result, a
|
|
5
|
+
* notice row — while the headline stays at the margin. Its glyph is the one
|
|
6
|
+
* configurable part; the geometry is fixed by `lib/tui/indent.ts`. A glyph must
|
|
7
|
+
* be exactly one visible column, or it would push the body right and drift the
|
|
8
|
+
* width math, so `normalizeChromeConfig` rejects anything else and falls back.
|
|
9
|
+
*
|
|
10
|
+
* The value is read at `session_start`/`session_tree` (nothing watches the
|
|
11
|
+
* document), held in a `RailSource` closure created once per load, and read by
|
|
12
|
+
* the renderers at render time, so no module state lives here.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
17
|
+
import { cleanString, loadConfigSection } from "../../lib/config.ts";
|
|
18
|
+
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
19
|
+
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
20
|
+
|
|
21
|
+
/** How a railed block reads at a glance. */
|
|
22
|
+
export type RailStatus = "info" | "ok" | "warn" | "bad";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Status to design role: a rail is neutral by default, so a finished or merely
|
|
26
|
+
* informational block draws no colour and only `warn` and `bad` stand out. A
|
|
27
|
+
* surface whose `info` means *active* rather than *informational* brightens it
|
|
28
|
+
* itself — a running tool result takes the accent (see `block/config.ts`).
|
|
29
|
+
*/
|
|
30
|
+
export const RAIL_ROLES: Readonly<Record<RailStatus, TuiRole>> = {
|
|
31
|
+
info: "info",
|
|
32
|
+
ok: "faint",
|
|
33
|
+
warn: "warn",
|
|
34
|
+
bad: "bad",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The rail glyph, painted in the role. `lib/tui/indent.ts` adds the gap. */
|
|
38
|
+
export function railGlyph(theme: Theme, role: TuiRole, glyph: string): string {
|
|
39
|
+
return paint(theme, role, glyph);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Transcript chrome presentation. */
|
|
43
|
+
export interface ChromeConfig {
|
|
44
|
+
/** The rail glyph: exactly one visible column. */
|
|
45
|
+
rail: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The default rail glyph, from the shared vocabulary. */
|
|
49
|
+
export const DEFAULT_RAIL_GLYPH: string = GLYPHS.divider;
|
|
50
|
+
|
|
51
|
+
export const DEFAULT_CHROME_CONFIG: ChromeConfig = { rail: DEFAULT_RAIL_GLYPH };
|
|
52
|
+
|
|
53
|
+
/** Whether `value` is a single, unstyled terminal column. */
|
|
54
|
+
function isRailGlyph(value: string): boolean {
|
|
55
|
+
// Control characters first: a bare ESC confuses a width measurement.
|
|
56
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) return false;
|
|
57
|
+
return [...value].length === 1 && visibleWidth(value) === 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Validate the `chrome` section over `base`. Total: never throws, never guesses. */
|
|
61
|
+
export function normalizeChromeConfig(raw: Record<string, unknown> | undefined, base: ChromeConfig): ChromeConfig {
|
|
62
|
+
if (!raw) return base;
|
|
63
|
+
const rail = cleanString(raw.rail, "");
|
|
64
|
+
return { rail: isRailGlyph(rail) ? rail : base.rail };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Effective chrome config for `cwd` (defaults, then the shared `chrome` section). */
|
|
68
|
+
export function loadChromeConfig(cwd: string): ChromeConfig {
|
|
69
|
+
return loadConfigSection(cwd, "chrome", DEFAULT_CHROME_CONFIG, normalizeChromeConfig);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Where a render-time surface reads the current rail glyph. */
|
|
73
|
+
export interface RailSource {
|
|
74
|
+
/** The configured glyph. */
|
|
75
|
+
resolve(): string;
|
|
76
|
+
/** Replace the glyph; a value that is not one column keeps the previous one. */
|
|
77
|
+
set(glyph: string): void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A store holding the configured rail glyph, created once per extension load. */
|
|
81
|
+
export function createRailSource(initial: string = DEFAULT_RAIL_GLYPH): RailSource {
|
|
82
|
+
let glyph = isRailGlyph(initial) ? initial : DEFAULT_RAIL_GLYPH;
|
|
83
|
+
return {
|
|
84
|
+
resolve: () => glyph,
|
|
85
|
+
set(next: string) {
|
|
86
|
+
if (isRailGlyph(next)) glyph = next;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Register the chrome lifecycle and hand back the rail source it keeps current. */
|
|
92
|
+
export function registerChrome(pi: ExtensionAPI, rail: RailSource = createRailSource()): RailSource {
|
|
93
|
+
const load = (ctx: ExtensionContext): void => rail.set(loadChromeConfig(ctx.cwd).rail);
|
|
94
|
+
pi.on("session_start", (_event, ctx) => load(ctx));
|
|
95
|
+
pi.on("session_tree", (_event, ctx) => load(ctx));
|
|
96
|
+
return rail;
|
|
97
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { sanitizeLine } from "../../../lib/format.ts";
|
|
11
12
|
import { missingFindings } from "../../../lib/requirements.ts";
|
|
12
13
|
import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
13
14
|
import { paint, type TuiRole } from "../../../lib/tui/tokens.ts";
|
|
@@ -92,9 +93,11 @@ export function registerTools(pi: ExtensionAPI, runtime: DoctorRuntime): void {
|
|
|
92
93
|
|
|
93
94
|
renderCall(args, theme, context) {
|
|
94
95
|
const input = args as DoctorArgs;
|
|
95
|
-
const scope =
|
|
96
|
+
const scope = typeof input.extension === "string" && input.extension ? sanitizeLine(input.extension) : "";
|
|
96
97
|
const text = reuseText(context?.lastComponent);
|
|
97
|
-
text.setText(
|
|
98
|
+
text.setText(
|
|
99
|
+
`${theme.fg("toolTitle", theme.bold(TOOL_NAME))}${scope ? ` ${paint(theme, "label", scope)}` : ""}${input.refresh ? paint(theme, "faint", " (refresh)") : ""}`,
|
|
100
|
+
);
|
|
98
101
|
return text;
|
|
99
102
|
},
|
|
100
103
|
|
|
@@ -14,6 +14,8 @@ import { keyText, type Theme, VERSION } from "@earendil-works/pi-coding-agent";
|
|
|
14
14
|
import { type Component, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
15
15
|
import { screenHint } from "../../../lib/tui/chrome.ts";
|
|
16
16
|
import { keyHintText } from "../../../lib/tui/glyphs.ts";
|
|
17
|
+
import { BODY_INDENT } from "../../../lib/tui/indent.ts";
|
|
18
|
+
import { RULE_GLYPH } from "../../../lib/tui/rules.ts";
|
|
17
19
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
18
20
|
import { HEADER_CONFIG, type HelpEntry } from "./config.ts";
|
|
19
21
|
|
|
@@ -23,6 +25,8 @@ export interface HeaderView {
|
|
|
23
25
|
showVersion: boolean;
|
|
24
26
|
/** Whether the terminal renders half blocks; false draws the wordmark instead. */
|
|
25
27
|
halfBlocks: boolean;
|
|
28
|
+
/** Columns of left margin: Pi's output pad, so the nameplate sits on the transcript margin. */
|
|
29
|
+
pad: number;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
/** The mark block is four columns on both rows, so the identity column aligns. */
|
|
@@ -63,8 +67,9 @@ export class ByteflyHeader implements Component {
|
|
|
63
67
|
const w = Math.max(1, width);
|
|
64
68
|
const theme = this.getTheme();
|
|
65
69
|
const view = this.view();
|
|
66
|
-
const {
|
|
67
|
-
const
|
|
70
|
+
const { nameRole, chromeRole, detailRole, chromeTagline, chromeName, chromeVersion, help } = HEADER_CONFIG;
|
|
71
|
+
const padWidth = Math.max(0, Math.min(Math.floor(view.pad), Math.max(0, w - 1)));
|
|
72
|
+
const pad = " ".repeat(padWidth);
|
|
68
73
|
const [markTop, markBottom] = markLines(theme, view.halfBlocks);
|
|
69
74
|
|
|
70
75
|
const version = view.showVersion && this.version ? paint(theme, detailRole, `v${this.version}`) : "";
|
|
@@ -92,22 +97,24 @@ export class ByteflyHeader implements Component {
|
|
|
92
97
|
this.identityLine(pad, markTop, agentCell, w),
|
|
93
98
|
this.identityLine(pad, markBottom, chromeCell, w),
|
|
94
99
|
this.rule(theme, w, pad),
|
|
95
|
-
...help.map((row) => this.helpLine(theme, w, row)),
|
|
100
|
+
...help.map((row) => this.helpLine(theme, w, row, padWidth)),
|
|
96
101
|
];
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
/**
|
|
100
105
|
* The nameplate's signature rule: an accent lead as wide as the mark, so the
|
|
101
|
-
* rule is anchored to the mark, then a muted fill to the
|
|
106
|
+
* rule is anchored to the mark, then a muted fill to the transcript's right
|
|
107
|
+
* edge. The header stops where the nameplate and turn rules stop, one pad in,
|
|
108
|
+
* instead of running to the last terminal column.
|
|
102
109
|
*/
|
|
103
110
|
private rule(theme: Theme, width: number, pad: string): string {
|
|
104
|
-
const remaining = Math.max(0, width - pad.length);
|
|
111
|
+
const remaining = Math.max(0, width - pad.length * 2);
|
|
105
112
|
const lead = Math.min(MARK_WIDTH, remaining);
|
|
106
113
|
const fill = remaining - lead;
|
|
107
114
|
return (
|
|
108
115
|
pad +
|
|
109
|
-
paint(theme, HEADER_CONFIG.ruleAccentRole,
|
|
110
|
-
paint(theme, HEADER_CONFIG.ruleRole,
|
|
116
|
+
paint(theme, HEADER_CONFIG.ruleAccentRole, RULE_GLYPH.repeat(lead)) +
|
|
117
|
+
paint(theme, HEADER_CONFIG.ruleRole, RULE_GLYPH.repeat(fill))
|
|
111
118
|
);
|
|
112
119
|
}
|
|
113
120
|
|
|
@@ -117,13 +124,17 @@ export class ByteflyHeader implements Component {
|
|
|
117
124
|
return truncateToWidth(line.replace(/ +$/, ""), width, "");
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
/**
|
|
121
|
-
|
|
127
|
+
/**
|
|
128
|
+
* One help group: whole entries only, dropped as the width runs out. The hints
|
|
129
|
+
* hang one body indent inside the header's margin, like a screen's hint footer
|
|
130
|
+
* under its title rule.
|
|
131
|
+
*/
|
|
132
|
+
private helpLine(theme: Theme, width: number, row: readonly HelpEntry[], padWidth: number): string {
|
|
122
133
|
const hints = row
|
|
123
134
|
.map(helpParts)
|
|
124
135
|
.filter((part) => part.key)
|
|
125
136
|
.map((part) => keyHintText(theme, part.key, part.description));
|
|
126
|
-
return hints.length > 0 ? screenHint(theme, width, hints) : "";
|
|
137
|
+
return hints.length > 0 ? screenHint(theme, width, hints, padWidth + BODY_INDENT) : "";
|
|
127
138
|
}
|
|
128
139
|
}
|
|
129
140
|
|
|
@@ -39,8 +39,6 @@ export interface HeaderConfig {
|
|
|
39
39
|
readonly ruleAccentRole: TuiRole;
|
|
40
40
|
/** Role for the rest of the rule. */
|
|
41
41
|
readonly ruleRole: TuiRole;
|
|
42
|
-
/** Columns of left padding, matching Pi's built-in header indent. */
|
|
43
|
-
readonly padX: number;
|
|
44
42
|
/** The tagline before the chrome name, so the relationship reads in words. */
|
|
45
43
|
readonly chromeTagline: string;
|
|
46
44
|
/** The chrome's name. */
|
|
@@ -61,7 +59,6 @@ export const HEADER_CONFIG: HeaderConfig = {
|
|
|
61
59
|
detailRole: "faint",
|
|
62
60
|
ruleAccentRole: "accent",
|
|
63
61
|
ruleRole: "rule",
|
|
64
|
-
padX: 1,
|
|
65
62
|
chromeTagline: "enhanced by",
|
|
66
63
|
chromeName: "bytefly",
|
|
67
64
|
chromeVersion: pkg.version,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { isAppleTerminalSession } from "@earendil-works/pi-tui";
|
|
12
|
+
import { DEFAULT_HOST_PAD } from "../pad.ts";
|
|
12
13
|
import { createHeader, type HeaderView } from "./component.ts";
|
|
13
14
|
import { HEADER_CONFIG } from "./config.ts";
|
|
14
15
|
|
|
@@ -17,10 +18,14 @@ export function registerHeader(pi: ExtensionAPI): void {
|
|
|
17
18
|
// Half blocks misalign in Apple Terminal, so the mark falls back to a wordmark
|
|
18
19
|
// there, mirroring Pi's own built-in header.
|
|
19
20
|
const halfBlocks = !isAppleTerminalSession();
|
|
20
|
-
const view = (): HeaderView => ({ showVersion: HEADER_CONFIG.showVersion, halfBlocks });
|
|
21
21
|
|
|
22
22
|
const apply = (ctx: ExtensionContext): void => {
|
|
23
23
|
if (ctx.mode !== "tui") return;
|
|
24
|
+
// The nameplate sits on the transcript margin, which is Pi's output pad — the
|
|
25
|
+
// value the tool frame and the notice inset by. An older host without
|
|
26
|
+
// `getSettings` keeps the default margin.
|
|
27
|
+
const pad = pi.getSettings?.().outputPad ?? DEFAULT_HOST_PAD;
|
|
28
|
+
const view = (): HeaderView => ({ showVersion: HEADER_CONFIG.showVersion, halfBlocks, pad });
|
|
24
29
|
ctx.ui.setHeader(createHeader(() => ctx.ui.theme, view));
|
|
25
30
|
};
|
|
26
31
|
|
|
@@ -1,30 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* bytefly — the package's terminal chrome and diagnostics, composed from
|
|
2
|
+
* bytefly — the package's terminal chrome and diagnostics, composed from ten
|
|
3
3
|
* feature folders: `header/` (the themed startup mark and version), `footer/`
|
|
4
4
|
* (the two-line, width-adaptive status bar with its price calculation),
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* `message/` (the user-message nameplate), `notice/` (the shared transcript
|
|
6
|
+
* notice renderer), `block/` (the unified tool-block frame), `separator/` (the
|
|
7
|
+
* transcript turn rule), `working/` (the live elapsed counter on Pi's working
|
|
8
|
+
* row), `panel/` (the unified `/bytefly` screen and its
|
|
9
|
+
* `doctor` report), `doctor/` (the `run_doctor` tool and its session-scoped
|
|
10
|
+
* probe runtime), and `notify/` (OS notifications for unattended runs).
|
|
9
11
|
*
|
|
10
|
-
* The header, footer,
|
|
11
|
-
* sessions and are always on;
|
|
12
|
-
* and
|
|
13
|
-
*
|
|
14
|
-
* and
|
|
15
|
-
*
|
|
12
|
+
* The header, footer, turn rule, and working timer are installed only in
|
|
13
|
+
* interactive (`tui`) sessions, and are always on; the user-message marker,
|
|
14
|
+
* notice, and tool blocks register at load and take effect wherever a
|
|
15
|
+
* transcript renders. `pi config` turns bytefly's chrome off. The panel and doctor work
|
|
16
|
+
* in every mode, and notify works wherever the environment can deliver it. The
|
|
17
|
+
* footer is priced from DeepSeek's published rates by default and from
|
|
18
|
+
* `bytefly.json` otherwise, the same file whose `notify` section configures
|
|
19
|
+
* notifications.
|
|
16
20
|
*
|
|
17
21
|
* Load with: pi --extension ./extensions/bytefly
|
|
18
22
|
*/
|
|
19
23
|
|
|
20
24
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
25
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
26
|
+
import { registerToolBlocks } from "./block/register.ts";
|
|
27
|
+
import { registerChrome } from "./chrome.ts";
|
|
22
28
|
import { type DoctorDeps, registerDoctor } from "./doctor/register.ts";
|
|
23
29
|
import { registerFooter } from "./footer/register.ts";
|
|
24
30
|
import { registerHeader } from "./header/register.ts";
|
|
31
|
+
import { registerUserMessage } from "./message/register.ts";
|
|
32
|
+
import { registerNotice } from "./notice/register.ts";
|
|
25
33
|
import { type NotifyDeps, registerNotify } from "./notify/register.ts";
|
|
34
|
+
import { createPadStore } from "./pad.ts";
|
|
26
35
|
import { type PanelCommandDeps, registerPanel } from "./panel/commands.ts";
|
|
27
36
|
import { registerTurnSeparator } from "./separator/register.ts";
|
|
37
|
+
import { registerWorkingTimer } from "./working/register.ts";
|
|
28
38
|
|
|
29
39
|
/** Host seams, injected by tests. */
|
|
30
40
|
export interface ByteflyDeps {
|
|
@@ -42,7 +52,17 @@ export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void
|
|
|
42
52
|
|
|
43
53
|
registerHeader(pi);
|
|
44
54
|
registerFooter(pi);
|
|
45
|
-
|
|
55
|
+
registerUserMessage(pi);
|
|
56
|
+
// One store per load: the tool frame and the notice record Pi's output pad and
|
|
57
|
+
// the turn separator reads it, so bytefly's transcript blocks share one margin.
|
|
58
|
+
const pad = createPadStore();
|
|
59
|
+
// The rail glyph is read from `bytefly.json` at session start; the tool results
|
|
60
|
+
// and the notice read it through this one store.
|
|
61
|
+
const rail = registerChrome(pi);
|
|
62
|
+
registerNotice(pi, pad, rail);
|
|
63
|
+
registerToolBlocks(pi, pad, rail);
|
|
64
|
+
registerTurnSeparator(pi, pad);
|
|
65
|
+
registerWorkingTimer(pi);
|
|
46
66
|
const doctor = registerDoctor(pi, deps.doctor);
|
|
47
67
|
registerPanel(pi, { doctor, discover: deps.extensions?.discover });
|
|
48
68
|
registerNotify(pi, deps.notify);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tuning for the user-message nameplate.
|
|
3
|
+
*
|
|
4
|
+
* Pi parses the transformed user message as Markdown and owns its colors, so
|
|
5
|
+
* this surface has no roles: it only names the label and the rule.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RULE_GLYPH } from "../../../lib/tui/rules.ts";
|
|
9
|
+
|
|
10
|
+
export const MESSAGE_CONFIG = {
|
|
11
|
+
/** The label word in the heading. */
|
|
12
|
+
label: "You",
|
|
13
|
+
/** Repeatable glyph that fills the rule row. */
|
|
14
|
+
ruleGlyph: RULE_GLYPH,
|
|
15
|
+
} as const;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user-message nameplate: a two-row heading, the `You` label over a rule
|
|
3
|
+
* that spans the width, above the message.
|
|
4
|
+
*
|
|
5
|
+
* Pi's user message component parses the transformed string as Markdown and
|
|
6
|
+
* owns its colors, so shaping text is the only lever: the surface can add no
|
|
7
|
+
* component and no color of its own. The nameplate is a level-2 heading, which
|
|
8
|
+
* Pi renders bold in `mdHeading` and prints without its `#` prefix; the bytefly
|
|
9
|
+
* themes point `mdHeading` at the accent, so the label and rule carry the accent
|
|
10
|
+
* while the message itself stays in the default text color. Markdown cannot
|
|
11
|
+
* express a two-line heading, and Pi puts a blank line after a heading block,
|
|
12
|
+
* so the label is padded to the width and the rule is appended: the heading's
|
|
13
|
+
* text wraps, putting the rule on its own row with no blank between. A blank
|
|
14
|
+
* line separates the nameplate from the message, so the message's own Markdown
|
|
15
|
+
* (a fence, a list, a second paragraph) parses exactly as it would without it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { MarkdownTransformContext } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
20
|
+
import { MESSAGE_CONFIG } from "./config.ts";
|
|
21
|
+
|
|
22
|
+
const { label, ruleGlyph } = MESSAGE_CONFIG;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The two-row nameplate: a heading whose text is the label, padded to the
|
|
26
|
+
* width, followed by a full-width rule. The wrap puts the rule on the second
|
|
27
|
+
* row; the whole nameplate is dropped when not even the label fits.
|
|
28
|
+
*/
|
|
29
|
+
export function nameplate(width: number): string | undefined {
|
|
30
|
+
const labelWidth = visibleWidth(label);
|
|
31
|
+
const w = Math.max(1, Math.floor(width));
|
|
32
|
+
if (labelWidth > w) return undefined;
|
|
33
|
+
const pad = " ".repeat(w - labelWidth);
|
|
34
|
+
return `## ${label}${pad}${ruleGlyph.repeat(w)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Put the nameplate above a user message; every other message passes through. */
|
|
38
|
+
export function styleUserMessage(markdown: string, context: MarkdownTransformContext): string {
|
|
39
|
+
if (context.messageType !== "user" || markdown.trim() === "") return markdown;
|
|
40
|
+
const head = nameplate(context.availableWidth);
|
|
41
|
+
return head ? `${head}\n\n${markdown}` : markdown;
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers bytefly's user-message markdown transformer.
|
|
3
|
+
*
|
|
4
|
+
* Registration happens at extension load, like the turn separator's entry
|
|
5
|
+
* renderer: the transformer only matters where a transcript renders, and Pi
|
|
6
|
+
* calls it for every message, so no session lifecycle is needed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { styleUserMessage } from "./format.ts";
|
|
11
|
+
|
|
12
|
+
/** Register the transformer that marks each user message. */
|
|
13
|
+
export function registerUserMessage(pi: ExtensionAPI): void {
|
|
14
|
+
pi.registerMarkdownTransformer((markdown, context) => styleUserMessage(markdown, context));
|
|
15
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transcript notice block, drawn from the shared `NoticeDetails`.
|
|
3
|
+
*
|
|
4
|
+
* Pure `noticeLines` shapes the body and hangs the rows from the transcript
|
|
5
|
+
* rail; `NoticeComponent` is the `Component` Pi mounts in place of its default
|
|
6
|
+
* custom-message box. Every field is untrusted — a job label, a matched log line
|
|
7
|
+
* — so it goes through `sanitizeLine` before it reaches the terminal, and every
|
|
8
|
+
* line is clipped to the width it is given. The rail is painted by the notice's
|
|
9
|
+
* worst severity, so a failure reads red at the edge like an errored tool
|
|
10
|
+
* result; the `source · title` headline stays at the margin.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
15
|
+
import { sanitizeLine } from "../../../lib/format.ts";
|
|
16
|
+
import type { NoticeDetails, NoticeItem, NoticeSeverity } from "../../../lib/notice.ts";
|
|
17
|
+
import { GLYPH_GAP, SEPARATORS } from "../../../lib/tui/glyphs.ts";
|
|
18
|
+
import { BODY_INDENT, indentBlockLines } from "../../../lib/tui/indent.ts";
|
|
19
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
20
|
+
import { DEFAULT_RAIL_GLYPH, railGlyph } from "../chrome.ts";
|
|
21
|
+
import { NOTICE_CONFIG } from "./config.ts";
|
|
22
|
+
|
|
23
|
+
/** Severity order, so the rail can carry the worst one. */
|
|
24
|
+
const SEVERITY_RANK: Readonly<Record<NoticeSeverity, number>> = { info: 0, ok: 1, warn: 2, bad: 3 };
|
|
25
|
+
|
|
26
|
+
/** The severity a row draws with, falling back to the notice's own. */
|
|
27
|
+
function severityOf(item: NoticeItem, fallback: NoticeSeverity): NoticeSeverity {
|
|
28
|
+
return item.severity ?? fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The worst severity across the headline and its rows; the rail's role. */
|
|
32
|
+
function worstSeverity(details: NoticeDetails): NoticeSeverity {
|
|
33
|
+
const fallback = details.severity ?? "info";
|
|
34
|
+
let worst: NoticeSeverity = fallback;
|
|
35
|
+
for (const item of details.items ?? []) {
|
|
36
|
+
const severity = severityOf(item, fallback);
|
|
37
|
+
if (SEVERITY_RANK[severity] > SEVERITY_RANK[worst]) worst = severity;
|
|
38
|
+
}
|
|
39
|
+
return worst;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One severity glyph, painted in its role. */
|
|
43
|
+
function severityGlyph(theme: Theme, severity: NoticeSeverity): string {
|
|
44
|
+
return paint(theme, NOTICE_CONFIG.glyphRoles[severity], NOTICE_CONFIG.glyphs[severity]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The notice: a `source · title` headline at the margin and body-indented rows
|
|
49
|
+
* of `glyph label · detail` behind the rail, then a `… N more` line when the
|
|
50
|
+
* rows were capped. The rail is painted by the notice's worst severity. Every
|
|
51
|
+
* line is clipped to the width it is given; `pad` is Pi's output pad, the
|
|
52
|
+
* block's own left margin.
|
|
53
|
+
*/
|
|
54
|
+
export function noticeLines(
|
|
55
|
+
details: NoticeDetails,
|
|
56
|
+
theme: Theme,
|
|
57
|
+
width: number,
|
|
58
|
+
pad = 0,
|
|
59
|
+
glyph = DEFAULT_RAIL_GLYPH,
|
|
60
|
+
): string[] {
|
|
61
|
+
const w = Math.max(1, width);
|
|
62
|
+
const sep = paint(theme, NOTICE_CONFIG.detailRole, SEPARATORS.item);
|
|
63
|
+
const source = paint(theme, NOTICE_CONFIG.sourceRole, sanitizeLine(details.source));
|
|
64
|
+
const title = paint(theme, NOTICE_CONFIG.titleRole, sanitizeLine(details.title));
|
|
65
|
+
const items = details.items ?? [];
|
|
66
|
+
const shown = items.slice(0, NOTICE_CONFIG.maxItems);
|
|
67
|
+
const fallback = details.severity ?? "info";
|
|
68
|
+
const gap = " ".repeat(GLYPH_GAP);
|
|
69
|
+
|
|
70
|
+
const rows = shown.map((item) => {
|
|
71
|
+
const label = paint(theme, NOTICE_CONFIG.titleRole, sanitizeLine(item.label));
|
|
72
|
+
const detail = item.detail ? `${sep}${paint(theme, NOTICE_CONFIG.detailRole, sanitizeLine(item.detail))}` : "";
|
|
73
|
+
return `${severityGlyph(theme, severityOf(item, fallback))}${gap}${label}${detail}`;
|
|
74
|
+
});
|
|
75
|
+
const more = items.length - shown.length;
|
|
76
|
+
if (more > 0) {
|
|
77
|
+
rows.push(`${" ".repeat(1 + GLYPH_GAP)}${paint(theme, NOTICE_CONFIG.detailRole, `… ${more} more`)}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The shared indent owns the column math: the headline at the margin, every
|
|
81
|
+
// row behind the rail, and each row clipped with the notice's ellipsis.
|
|
82
|
+
return indentBlockLines([`${source}${sep}${title}`, ...rows], w, {
|
|
83
|
+
indent: Math.max(0, Math.floor(pad)) + BODY_INDENT,
|
|
84
|
+
rail: railGlyph(theme, NOTICE_CONFIG.railRoles[worstSeverity(details)], glyph),
|
|
85
|
+
headline: true,
|
|
86
|
+
trimTrailing: false,
|
|
87
|
+
bodyClip: "…",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The notice block Pi mounts for a custom message carrying `NoticeDetails`. */
|
|
92
|
+
export class NoticeComponent implements Component {
|
|
93
|
+
constructor(
|
|
94
|
+
private readonly details: NoticeDetails,
|
|
95
|
+
private readonly theme: Theme,
|
|
96
|
+
private readonly pad = 0,
|
|
97
|
+
private readonly glyph = DEFAULT_RAIL_GLYPH,
|
|
98
|
+
) {}
|
|
99
|
+
|
|
100
|
+
invalidate(): void {
|
|
101
|
+
// The rail is rebuilt from the live theme on every render.
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
render(width: number): string[] {
|
|
105
|
+
return noticeLines(this.details, this.theme, Math.max(1, width), this.pad, this.glyph);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tuning for the transcript notice block.
|
|
3
|
+
*
|
|
4
|
+
* The notice sits at the transcript's margin — a `source · title` headline and
|
|
5
|
+
* rows behind the shared transcript rail, each row led by its own severity
|
|
6
|
+
* glyph. The glyphs come from the shared vocabulary, the roles from the shared
|
|
7
|
+
* rail map, and the rail glyph from the `chrome` config, so a notice reads like
|
|
8
|
+
* the tool results around it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { NoticeSeverity } from "../../../lib/notice.ts";
|
|
12
|
+
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
13
|
+
import type { TuiRole } from "../../../lib/tui/tokens.ts";
|
|
14
|
+
import { RAIL_ROLES } from "../chrome.ts";
|
|
15
|
+
|
|
16
|
+
export const NOTICE_CONFIG = {
|
|
17
|
+
/** Role for the source name at the head of the block. */
|
|
18
|
+
sourceRole: "accent" as TuiRole,
|
|
19
|
+
/** Role for the headline's title text and each row's label. */
|
|
20
|
+
titleRole: "label" as TuiRole,
|
|
21
|
+
/** Role for the `·` separators and each row's trailing detail. */
|
|
22
|
+
detailRole: "faint" as TuiRole,
|
|
23
|
+
/** Width-1 glyph per severity, from the shared vocabulary. */
|
|
24
|
+
glyphs: {
|
|
25
|
+
info: GLYPHS.pending,
|
|
26
|
+
ok: GLYPHS.check,
|
|
27
|
+
warn: GLYPHS.warn,
|
|
28
|
+
bad: GLYPHS.cross,
|
|
29
|
+
} as Record<NoticeSeverity, string>,
|
|
30
|
+
/** Design role per severity for a row's glyph: a check is green, a cross red. */
|
|
31
|
+
glyphRoles: {
|
|
32
|
+
info: "info",
|
|
33
|
+
ok: "ok",
|
|
34
|
+
warn: "warn",
|
|
35
|
+
bad: "bad",
|
|
36
|
+
} as Record<NoticeSeverity, TuiRole>,
|
|
37
|
+
/** The rail's neutral-by-default role per severity, shared with every rail. */
|
|
38
|
+
railRoles: RAIL_ROLES,
|
|
39
|
+
/** Rows drawn before the rest collapse into a `… N more` line. */
|
|
40
|
+
maxItems: 8,
|
|
41
|
+
} as const;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers bytefly's renderer for the shared transcript notice.
|
|
3
|
+
*
|
|
4
|
+
* Registration happens at extension load, like the turn separator's entry
|
|
5
|
+
* renderer: a message renderer only matters where a transcript renders, so no
|
|
6
|
+
* session lifecycle is needed. Pi resolves it by the exact `NOTICE_TYPE`;
|
|
7
|
+
* without one it would fall back to its default custom-message box.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { NOTICE_TYPE, type NoticeDetails } from "../../../lib/notice.ts";
|
|
12
|
+
import { createRailSource, type RailSource } from "../chrome.ts";
|
|
13
|
+
import { createPadStore, type PadSource } from "../pad.ts";
|
|
14
|
+
import { NoticeComponent } from "./component.ts";
|
|
15
|
+
|
|
16
|
+
/** Register the notice renderer; a message without details keeps Pi's default. */
|
|
17
|
+
export function registerNotice(
|
|
18
|
+
pi: ExtensionAPI,
|
|
19
|
+
pad: PadSource = createPadStore(),
|
|
20
|
+
rail: RailSource = createRailSource(),
|
|
21
|
+
): void {
|
|
22
|
+
pi.registerMessageRenderer<NoticeDetails>(NOTICE_TYPE, (message, options, theme) =>
|
|
23
|
+
message.details
|
|
24
|
+
? new NoticeComponent(message.details, theme, pad.resolve(options.outputPad), rail.resolve())
|
|
25
|
+
: undefined,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transcript's left margin, observed from Pi's built-in output padding.
|
|
3
|
+
*
|
|
4
|
+
* Pi pads its own messages, and hands the value to tool renderers
|
|
5
|
+
* (`context.outputPad`) and message renderers (`options.outputPad`) so they can
|
|
6
|
+
* pad themselves. Entry renderers get only `{ expanded }`, and Pi's chat
|
|
7
|
+
* container carries no padding of its own, so a surface that owns its output
|
|
8
|
+
* must inset itself. This tiny store is the one place the value is shared: the
|
|
9
|
+
* tool frame and the notice record the pad they are handed, and the turn
|
|
10
|
+
* separator reads the last one.
|
|
11
|
+
*
|
|
12
|
+
* There is no module state: the extension factory creates one store per load and
|
|
13
|
+
* injects it, so tests get a fresh one and a second session cannot leak either.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Where a bytefly transcript surface reads its left margin. */
|
|
17
|
+
export interface PadSource {
|
|
18
|
+
/**
|
|
19
|
+
* The host's output pad. Pass the pad Pi handed this render to record it for
|
|
20
|
+
* a surface that never receives one; omit it to read the last recorded value.
|
|
21
|
+
*/
|
|
22
|
+
resolve(hostPad?: number): number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Pi's output pad in an interactive session, before one is observed. */
|
|
26
|
+
export const DEFAULT_HOST_PAD = 1;
|
|
27
|
+
|
|
28
|
+
/** A store that follows Pi's output pad, falling back until one is observed. */
|
|
29
|
+
export function createPadStore(defaultHostPad: number = DEFAULT_HOST_PAD): PadSource {
|
|
30
|
+
const clamp = (value: number): number => Math.max(0, Math.floor(value));
|
|
31
|
+
let hostPad = Number.isFinite(defaultHostPad) ? clamp(defaultHostPad) : DEFAULT_HOST_PAD;
|
|
32
|
+
// A non-finite observation is ignored rather than resetting the margin.
|
|
33
|
+
const record = (observed: number): void => {
|
|
34
|
+
if (Number.isFinite(observed)) hostPad = clamp(observed);
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
resolve(observed?: number): number {
|
|
38
|
+
if (observed !== undefined) record(observed);
|
|
39
|
+
return hostPad;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -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
|
}
|
|
@@ -10,14 +10,21 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
10
10
|
import { centeredRule } from "../../../lib/tui/rules.ts";
|
|
11
11
|
import { CONFIG } from "./config.ts";
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
/**
|
|
14
|
+
* One rendered line: the centered turn rule, colored by role.
|
|
15
|
+
*
|
|
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.
|
|
19
|
+
*/
|
|
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}`];
|
|
23
30
|
}
|