@leo-alvarenga/pi-zen-frame 0.12.2 → 0.14.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 +8 -34
- package/extensions/components/agent-mode.ts +8 -7
- package/extensions/components/cwd.ts +6 -9
- package/extensions/components/frame.ts +58 -63
- package/extensions/components/header.ts +17 -11
- package/extensions/components/model.ts +3 -6
- package/extensions/components/reasoning.ts +5 -4
- package/extensions/components/registry.ts +2 -2
- package/extensions/components/token-count.ts +7 -9
- package/extensions/components/types.ts +12 -18
- package/extensions/config/constants/defaults.ts +3 -21
- package/extensions/config/constants/visuals.ts +1 -1
- package/extensions/config/settings.ts +4 -52
- package/extensions/config/types.ts +12 -49
- package/extensions/editor/{frame-editor.ts → blocky-editor.ts} +57 -71
- package/extensions/index.ts +20 -9
- package/extensions/renderers/registry.ts +41 -0
- package/extensions/renderers/types.ts +57 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,13 +45,13 @@ Configuration is loaded from `~/.pi/agent/pi-zen-frame.json`. All properties are
|
|
|
45
45
|
// Master mute: renders all segments in muted tones except agent-mode
|
|
46
46
|
"zenMode": true,
|
|
47
47
|
|
|
48
|
+
// Editor-frame renderer by registered name (built-in: "blocky")
|
|
49
|
+
"editorFrame": "blocky",
|
|
50
|
+
|
|
48
51
|
"header": {
|
|
49
52
|
"enable": true,
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"subheading": "Ready for your next session? Terminal warm, context clean, tools ready to execute",
|
|
53
|
-
"logoColor": "text",
|
|
54
|
-
"accentColor": "customMessageLabel",
|
|
53
|
+
// Header renderer by registered name (built-in: "basic")
|
|
54
|
+
"type": "basic",
|
|
55
55
|
},
|
|
56
56
|
|
|
57
57
|
"frame": {
|
|
@@ -60,15 +60,6 @@ Configuration is loaded from `~/.pi/agent/pi-zen-frame.json`. All properties are
|
|
|
60
60
|
// Minimum terminal width required to render the border frame
|
|
61
61
|
"minWidth": 20,
|
|
62
62
|
|
|
63
|
-
// Padding inside the frame
|
|
64
|
-
"paddingTop": 1,
|
|
65
|
-
"paddingBottom": 1,
|
|
66
|
-
"paddingX": 1,
|
|
67
|
-
|
|
68
|
-
// Outer margin around the frame
|
|
69
|
-
"marginTop": 0,
|
|
70
|
-
"marginBottom": 0,
|
|
71
|
-
|
|
72
63
|
// Toggle individual status segments
|
|
73
64
|
"showCwd": true,
|
|
74
65
|
"showModel": true,
|
|
@@ -76,24 +67,6 @@ Configuration is loaded from `~/.pi/agent/pi-zen-frame.json`. All properties are
|
|
|
76
67
|
"showThinking": true,
|
|
77
68
|
"showSpinner": false,
|
|
78
69
|
"showAgentMode": true,
|
|
79
|
-
|
|
80
|
-
// Color options: "border", "accentColor", or "agentMode"
|
|
81
|
-
"borderColor": "border",
|
|
82
|
-
|
|
83
|
-
// Input prompt prefix glyph and color settings
|
|
84
|
-
"prefix": "❯",
|
|
85
|
-
"prefixColor": "muted", // Options: "agentMode", "frameBorder", or any ThemeColor
|
|
86
|
-
|
|
87
|
-
// Override default Nerd-Font glyphs
|
|
88
|
-
"icons": {
|
|
89
|
-
// Keys: folder, model, context, thinking, gitDirty, gitBranch
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
// Per-segment foreground color overrides (supersedes default theme colors)
|
|
93
|
-
"colors": {
|
|
94
|
-
"model": "accent",
|
|
95
|
-
"cwd": "accent",
|
|
96
|
-
},
|
|
97
70
|
},
|
|
98
71
|
|
|
99
72
|
// Randomized status messages shown while streaming responses
|
|
@@ -149,8 +122,9 @@ Run `/reload` after modifying your keybindings.
|
|
|
149
122
|
extensions/
|
|
150
123
|
├── index.ts # Extension entry point: config initialization and events
|
|
151
124
|
├── config/ # Types, defaults, and settings normalization
|
|
152
|
-
├── components/ # Header, frame border, and status segment modules
|
|
153
|
-
├── editor/ #
|
|
125
|
+
├── components/ # Header (BasicHeader), frame border, and status segment modules
|
|
126
|
+
├── editor/ # BlockyEditor editor-frame renderer
|
|
127
|
+
├── renderers/ # Renderer registry + swap-in API (blocky / basic)
|
|
154
128
|
└── utils/ # Helpers for Git status, paths, and agent modes
|
|
155
129
|
|
|
156
130
|
```
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { capitalize } from "../utils";
|
|
2
|
+
|
|
2
3
|
import type { SegmentDef } from "./types";
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
* Rendered as a pill using the agent's own color/icon. */
|
|
5
|
+
/** Pseudo-footer (left): the current Agent Mode from pi-agent-manager
|
|
6
|
+
* (when installed). Rendered as a pill using the agent's own color/icon. */
|
|
6
7
|
export const agentModeSegment: SegmentDef = {
|
|
7
8
|
id: "agent-mode",
|
|
8
|
-
slot: "
|
|
9
|
+
slot: "topLeft",
|
|
9
10
|
enabled: (_d, cfg) => cfg.showAgentMode !== false,
|
|
10
11
|
|
|
11
|
-
render: (d, { theme
|
|
12
|
+
render: (d, { theme }) => {
|
|
12
13
|
const m = d.agentMode;
|
|
13
14
|
if (!m) return "";
|
|
14
15
|
|
|
15
|
-
// Agent's own color wins;
|
|
16
|
-
const color = m.color ??
|
|
17
|
-
const label =
|
|
16
|
+
// Agent's own color wins; accent fills in. Zen never mutes the agent.
|
|
17
|
+
const color = m.color ?? d.accentColor;
|
|
18
|
+
const label = ` ${capitalize(m.name)} `;
|
|
18
19
|
|
|
19
20
|
try {
|
|
20
21
|
return theme.fg(color, label);
|
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
import type { SegmentDef } from "./types";
|
|
2
2
|
import { getShortCwd } from "../utils";
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/** Pseudo-footer (right): working directory + git branch (with dirty count). */
|
|
5
5
|
export const cwdSegment: SegmentDef = {
|
|
6
6
|
id: "cwd",
|
|
7
|
-
slot: "
|
|
7
|
+
slot: "bottomLeft",
|
|
8
8
|
enabled: (_d, cfg) => cfg.showCwd !== false,
|
|
9
|
-
render: (d, { theme, icons
|
|
9
|
+
render: (d, { theme, icons }) => {
|
|
10
10
|
const folder = getShortCwd(d.cwd);
|
|
11
11
|
|
|
12
|
-
let text = theme.fg(
|
|
13
|
-
segColor("cwd", "muted"),
|
|
14
|
-
`${icons.folder} ${folder}`,
|
|
15
|
-
);
|
|
12
|
+
let text = theme.fg("muted", `${icons.folder} ${folder}`);
|
|
16
13
|
|
|
17
14
|
if (d.gitBranch) {
|
|
18
15
|
let git = ` ${icons.gitBranch} ${d.gitBranch}`;
|
|
19
|
-
text += theme.fg(
|
|
16
|
+
text += theme.fg(d.zenMode ? "muted" : d.accentColor, git);
|
|
20
17
|
|
|
21
18
|
if (d.gitDirty > 0) {
|
|
22
19
|
text += theme.fg(
|
|
23
|
-
|
|
20
|
+
d.zenMode ? "muted" : "error",
|
|
24
21
|
` ${icons.gitDirty} ${d.gitDirty}`,
|
|
25
22
|
);
|
|
26
23
|
}
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* corner glyphs, embeds the segment text into the top/bottom border, and
|
|
4
|
-
* inserts the configurable blank padding lines inside the box.
|
|
2
|
+
* Row layout for the borderless band editor.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `width` columns
|
|
4
|
+
* composeBand assembles the bg-filled band: padding rows, content (prefix on
|
|
5
|
+
* the first row), autocomplete, one blank row, then the box-bottom segments.
|
|
6
|
+
* Every output row is exactly `width` columns. fitFrameRow/fitInfoRow lay out
|
|
7
|
+
* the header and status rows.
|
|
9
8
|
*/
|
|
10
9
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
10
|
|
|
12
|
-
export interface FrameOptions {
|
|
13
|
-
width: number;
|
|
14
|
-
prefix?: string;
|
|
15
|
-
paddingX: number;
|
|
16
|
-
paddingTop: number;
|
|
17
|
-
paddingBottom: number;
|
|
18
|
-
border: (str: string) => string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
11
|
const SGR_RE = /\x1b\[[0-9;]*m/g;
|
|
22
12
|
const CURSOR_MARKER_RE = /\x1b_pi:c\x07/g;
|
|
23
13
|
|
|
@@ -26,7 +16,7 @@ function plain(row: string): string {
|
|
|
26
16
|
}
|
|
27
17
|
|
|
28
18
|
/** A border row is all `─`, or a `─── ↑/↓ N more ───` scroll indicator. */
|
|
29
|
-
function isBorderRow(row: string): boolean {
|
|
19
|
+
export function isBorderRow(row: string): boolean {
|
|
30
20
|
const t = plain(row).trim();
|
|
31
21
|
|
|
32
22
|
if (t === "") return true;
|
|
@@ -35,6 +25,58 @@ function isBorderRow(row: string): boolean {
|
|
|
35
25
|
return /^─── [↑↓] \d+ more ─*$/.test(t);
|
|
36
26
|
}
|
|
37
27
|
|
|
28
|
+
export interface BandOptions {
|
|
29
|
+
padX: number;
|
|
30
|
+
width: number;
|
|
31
|
+
prefix: string;
|
|
32
|
+
marginX: number;
|
|
33
|
+
boxBottom: string;
|
|
34
|
+
paddingTop: number;
|
|
35
|
+
paddingBottom: number;
|
|
36
|
+
paint: (row: string) => string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Borderless bg band: padding, content (prefix on first row), autocomplete,
|
|
40
|
+
* blank row, box-bottom segments. Every row is exactly `width` columns. */
|
|
41
|
+
export function composeBand(
|
|
42
|
+
content: string[],
|
|
43
|
+
autocomplete: string[],
|
|
44
|
+
opts: BandOptions,
|
|
45
|
+
): string[] {
|
|
46
|
+
const {
|
|
47
|
+
padX,
|
|
48
|
+
paint,
|
|
49
|
+
width,
|
|
50
|
+
prefix,
|
|
51
|
+
marginX,
|
|
52
|
+
boxBottom,
|
|
53
|
+
paddingTop,
|
|
54
|
+
paddingBottom,
|
|
55
|
+
} = opts;
|
|
56
|
+
|
|
57
|
+
const lead = Math.max(0, padX - visibleWidth(prefix));
|
|
58
|
+
|
|
59
|
+
const inset = (row: string) =>
|
|
60
|
+
paint(prefix + " ".repeat(lead) + row + " ".repeat(padX));
|
|
61
|
+
|
|
62
|
+
const fill = () => inset(paint(" ".repeat(Math.max(0, width - 2 * padX))));
|
|
63
|
+
|
|
64
|
+
const rows: string[] = [];
|
|
65
|
+
for (let i = 0; i < paddingTop; i++) rows.push(fill());
|
|
66
|
+
|
|
67
|
+
content.forEach((row) => {
|
|
68
|
+
rows.push(inset(row));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
autocomplete.forEach((row) => rows.push(inset(row)));
|
|
72
|
+
|
|
73
|
+
rows.push(fill());
|
|
74
|
+
rows.push(inset(boxBottom));
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < paddingBottom; i++) rows.push(fill());
|
|
77
|
+
return rows;
|
|
78
|
+
}
|
|
79
|
+
|
|
38
80
|
/** Lay one border row: cap + leftText + ─fill + rightText + cap, always
|
|
39
81
|
* exactly `width` columns. Right text truncates first, then left text. */
|
|
40
82
|
export function fitFrameRow(
|
|
@@ -91,50 +133,3 @@ export function fitInfoRow(
|
|
|
91
133
|
);
|
|
92
134
|
return leftText2 + " ".repeat(fill) + rightText2;
|
|
93
135
|
}
|
|
94
|
-
|
|
95
|
-
function wrapContent(row: string, paddingX: number): string {
|
|
96
|
-
if (paddingX <= 0) return row;
|
|
97
|
-
|
|
98
|
-
return " ".repeat(paddingX) + row + " ".repeat(paddingX);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function renderFrame(inner: string[], opts: FrameOptions): string[] {
|
|
102
|
-
const { border, width, paddingX, prefix } = opts;
|
|
103
|
-
if (inner.length === 0) return inner;
|
|
104
|
-
|
|
105
|
-
// The real bottom border is the last border-like row (autocomplete rows
|
|
106
|
-
// are appended *after* it, so the last row is NOT necessarily the bottom).
|
|
107
|
-
let bottomIdx = inner.length - 1;
|
|
108
|
-
|
|
109
|
-
for (let i = inner.length - 1; i >= 0; i--) {
|
|
110
|
-
if (isBorderRow(inner[i]!)) {
|
|
111
|
-
bottomIdx = i;
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const result: string[] = [];
|
|
117
|
-
const padRow = " ".repeat(Math.max(0, width));
|
|
118
|
-
const fullBorder = border("─".repeat(width));
|
|
119
|
-
|
|
120
|
-
result.push(fullBorder);
|
|
121
|
-
|
|
122
|
-
for (let i = 0; i < opts.paddingTop; i++) result.push(padRow);
|
|
123
|
-
|
|
124
|
-
for (let i = 1; i < bottomIdx; i++) {
|
|
125
|
-
const decorator = i === 1 ? (prefix ?? "") : "";
|
|
126
|
-
const content = decorator + inner[i]!;
|
|
127
|
-
|
|
128
|
-
result.push(wrapContent(content, paddingX - visibleWidth(decorator)));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
for (let i = 0; i < opts.paddingBottom; i++) result.push(padRow);
|
|
132
|
-
|
|
133
|
-
result.push(fullBorder);
|
|
134
|
-
|
|
135
|
-
for (let i = bottomIdx + 1; i < inner.length; i++) {
|
|
136
|
-
result.push(wrapContent(inner[i]!, paddingX));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return result;
|
|
140
|
-
}
|
|
@@ -8,7 +8,6 @@ import type {
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_ICONS,
|
|
11
|
-
DEFAULT_SETTINGS,
|
|
12
11
|
HEADER_TIPS,
|
|
13
12
|
} from "../config/constants";
|
|
14
13
|
import type { Settings } from "../config/types";
|
|
@@ -19,6 +18,18 @@ import { getShortCwd } from "../utils";
|
|
|
19
18
|
const MIN_BOX_WIDTH = 20;
|
|
20
19
|
const LEFT_COL_RATIO = 0.4; // logo column width / total width
|
|
21
20
|
|
|
21
|
+
/** BasicHeader preset styling — self-contained, not user-configurable. */
|
|
22
|
+
const LOGO_LINES = [
|
|
23
|
+
"█████████ ",
|
|
24
|
+
"███ ███ ",
|
|
25
|
+
"██████ ",
|
|
26
|
+
"███ ███",
|
|
27
|
+
];
|
|
28
|
+
const LOGO_COLOR: ThemeColor = "text";
|
|
29
|
+
const HEADING = "Welcome back!";
|
|
30
|
+
const SUBHEADING =
|
|
31
|
+
"Ready for your next session? Terminal warm, context clean, tools ready to execute";
|
|
32
|
+
|
|
22
33
|
/** Live env snapshot the header renders in the right column. */
|
|
23
34
|
export interface HeaderEnv {
|
|
24
35
|
gitBranch: string | undefined;
|
|
@@ -60,12 +71,11 @@ export function createHeader(
|
|
|
60
71
|
};
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
const accentColor =
|
|
64
|
-
settings.header?.accentColor ?? settings.accentColor ?? "accent";
|
|
74
|
+
const accentColor = settings.accentColor ?? "accent";
|
|
65
75
|
|
|
66
76
|
const logo = {
|
|
67
|
-
lines:
|
|
68
|
-
color:
|
|
77
|
+
lines: LOGO_LINES,
|
|
78
|
+
color: LOGO_COLOR,
|
|
69
79
|
};
|
|
70
80
|
|
|
71
81
|
const border = (s: string, fg?: ThemeColor) => theme.fg(fg ?? accentColor, s);
|
|
@@ -151,18 +161,14 @@ export function createHeader(
|
|
|
151
161
|
"",
|
|
152
162
|
|
|
153
163
|
...wrapLines(
|
|
154
|
-
[
|
|
164
|
+
[HEADING],
|
|
155
165
|
leftW - 2,
|
|
156
166
|
).map((line) => theme.bold(theme.fg("muted", line))),
|
|
157
167
|
|
|
158
168
|
"",
|
|
159
169
|
|
|
160
170
|
...wrapLines(
|
|
161
|
-
[
|
|
162
|
-
settings.header?.subheading ??
|
|
163
|
-
DEFAULT_SETTINGS.header?.subheading ??
|
|
164
|
-
"",
|
|
165
|
-
],
|
|
171
|
+
[SUBHEADING],
|
|
166
172
|
leftW - 2,
|
|
167
173
|
).map((line) => theme.italic(theme.fg("muted", line))),
|
|
168
174
|
];
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import type { SegmentDef } from "./types";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** Box bottom (left): the active model name. */
|
|
4
4
|
export const modelSegment: SegmentDef = {
|
|
5
5
|
id: "model",
|
|
6
6
|
slot: "topLeft",
|
|
7
7
|
enabled: (_d, cfg) => cfg.showModel !== false,
|
|
8
|
-
render: (d, { theme
|
|
8
|
+
render: (d, { theme }) => {
|
|
9
9
|
if (!d.modelName) return "";
|
|
10
10
|
|
|
11
|
-
return theme.fg(
|
|
12
|
-
segColor("model", d.accentColor),
|
|
13
|
-
` ${icons.model} ${d.modelName} `,
|
|
14
|
-
);
|
|
11
|
+
return ` ${theme.fg("text", d.modelName)} ${theme.fg("muted", `(${d.modelProvider})`)} `;
|
|
15
12
|
},
|
|
16
13
|
};
|
|
@@ -3,19 +3,20 @@ import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import type { SegmentDef } from "./types";
|
|
4
4
|
import { THINKING_TOKEN } from "../config/constants";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
6
|
+
/** Box bottom (left): current thinking level, tinted with its own theme
|
|
7
|
+
* token — the same color pi applies to the border at that level. */
|
|
8
8
|
export const reasoningSegment: SegmentDef = {
|
|
9
9
|
id: "reasoning",
|
|
10
10
|
slot: "topLeft",
|
|
11
11
|
enabled: (_d, cfg) => cfg.showThinking !== false,
|
|
12
|
-
render: (d, { theme, icons
|
|
12
|
+
render: (d, { theme, icons }) => {
|
|
13
13
|
if (!d.thinkingLevel) return "";
|
|
14
|
+
|
|
14
15
|
const token = (THINKING_TOKEN[d.thinkingLevel] ??
|
|
15
16
|
"thinkingText") as ThemeColor;
|
|
16
17
|
|
|
17
18
|
return theme.fg(
|
|
18
|
-
|
|
19
|
+
d.zenMode ? "muted" : token,
|
|
19
20
|
` ${icons.thinking} ${d.thinkingLevel} `,
|
|
20
21
|
);
|
|
21
22
|
},
|
|
@@ -13,11 +13,11 @@ import { agentModeSegment } from "./agent-mode";
|
|
|
13
13
|
import type { FrameData, SegmentContext, SegmentDef, Slot } from "./types";
|
|
14
14
|
|
|
15
15
|
export const segments: SegmentDef[] = [
|
|
16
|
+
agentModeSegment,
|
|
16
17
|
modelSegment,
|
|
17
18
|
reasoningSegment,
|
|
18
19
|
spinnerSegment,
|
|
19
20
|
tokenCountSegment,
|
|
20
|
-
agentModeSegment,
|
|
21
21
|
cwdSegment,
|
|
22
22
|
];
|
|
23
23
|
|
|
@@ -38,5 +38,5 @@ export function segmentsFor(
|
|
|
38
38
|
return active
|
|
39
39
|
.map((s) => s.render(d, ctx))
|
|
40
40
|
.filter((t) => t !== "")
|
|
41
|
-
.join(ctx.
|
|
41
|
+
.join(ctx.theme.fg("dim", "·"));
|
|
42
42
|
}
|
|
@@ -19,12 +19,12 @@ function formatWindow(n: number): string {
|
|
|
19
19
|
return `${(n / 1_000).toFixed(0)}k`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
22
|
+
/** Box bottom (right): context window usage (percent + used/window tokens). */
|
|
23
23
|
export const tokenCountSegment: SegmentDef = {
|
|
24
24
|
id: "token-count",
|
|
25
|
-
slot: "
|
|
25
|
+
slot: "bottomRight",
|
|
26
26
|
enabled: (_d, cfg) => cfg.showContext !== false,
|
|
27
|
-
render: (d, {
|
|
27
|
+
render: (d, { theme, icons }) => {
|
|
28
28
|
const c = d.context;
|
|
29
29
|
if (!c) return "";
|
|
30
30
|
|
|
@@ -41,15 +41,13 @@ export const tokenCountSegment: SegmentDef = {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
const color2 = d.zenMode ? "muted" : color;
|
|
44
45
|
const used = c.tokens === null ? "?" : formatTokens(c.tokens);
|
|
45
46
|
|
|
46
47
|
return (
|
|
47
|
-
theme.fg(
|
|
48
|
-
|
|
49
|
-
theme.fg(
|
|
50
|
-
segColor("context", color),
|
|
51
|
-
` ${used}/${formatWindow(c.window)} `,
|
|
52
|
-
)
|
|
48
|
+
theme.fg(color2, ` ${icons.context} ctx ${pct} `) +
|
|
49
|
+
theme.fg("dim", "·") +
|
|
50
|
+
theme.fg(color2, ` ${used}/${formatWindow(c.window)} `)
|
|
53
51
|
);
|
|
54
52
|
},
|
|
55
53
|
};
|
|
@@ -10,14 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
|
|
13
|
-
import type {
|
|
14
|
-
FrameColors,
|
|
15
|
-
FrameIcons,
|
|
16
|
-
FrameSettings,
|
|
17
|
-
SpinnerPhase,
|
|
18
|
-
} from "../config/types";
|
|
13
|
+
import type { FrameIcons, FrameSettings, SpinnerPhase } from "../config/types";
|
|
19
14
|
|
|
20
|
-
/** Where
|
|
15
|
+
/** Where a segment renders. "top*" → the box-bottom row inside the band,
|
|
16
|
+
* "bottom*" → the pseudo-footer row below it (names kept from the old
|
|
17
|
+
* border layout). */
|
|
21
18
|
export type Slot = "topLeft" | "topRight" | "bottomLeft" | "bottomRight";
|
|
22
19
|
|
|
23
20
|
export type AgentConfig = {
|
|
@@ -47,6 +44,7 @@ export interface FrameData {
|
|
|
47
44
|
accentColor: ThemeColor;
|
|
48
45
|
gitBranch: string | undefined;
|
|
49
46
|
modelName: string | undefined;
|
|
47
|
+
modelProvider: string | undefined;
|
|
50
48
|
spinnerPhase: SpinnerPhase | null;
|
|
51
49
|
thinkingLevel: string | undefined;
|
|
52
50
|
|
|
@@ -68,11 +66,6 @@ export interface SegmentContext {
|
|
|
68
66
|
theme: Theme;
|
|
69
67
|
icons: FrameIcons;
|
|
70
68
|
cfg: FrameSettings;
|
|
71
|
-
border: (str: string) => string;
|
|
72
|
-
|
|
73
|
-
/** Resolve a segment's fg: zen-mode mutes everything except agentMode;
|
|
74
|
-
* else `frame.colors.<key>` supersedes the segment default. */
|
|
75
|
-
segColor: (key: keyof FrameColors, fallback: ThemeColor) => ThemeColor;
|
|
76
69
|
}
|
|
77
70
|
|
|
78
71
|
/** A segment renders an already-ANSI-styled string, or "" to render nothing. */
|
|
@@ -93,14 +86,15 @@ export interface SegmentDef {
|
|
|
93
86
|
* this with its own live state (mode / count / spinner frame) in render().
|
|
94
87
|
*/
|
|
95
88
|
export interface ExternalData {
|
|
96
|
-
modelName: string | undefined;
|
|
97
|
-
thinkingLevel: string | undefined;
|
|
98
|
-
spinnerPhase: SpinnerPhase | null;
|
|
99
|
-
context: FrameData["context"];
|
|
100
89
|
cwd: string;
|
|
101
|
-
gitBranch: string | undefined;
|
|
102
90
|
gitDirty: number;
|
|
91
|
+
zenMode: boolean;
|
|
103
92
|
agentMode: AgentMode;
|
|
104
93
|
theme: Theme | undefined;
|
|
105
|
-
|
|
94
|
+
gitBranch: string | undefined;
|
|
95
|
+
modelName: string | undefined;
|
|
96
|
+
context: FrameData["context"];
|
|
97
|
+
modelProvider: string | undefined;
|
|
98
|
+
thinkingLevel: string | undefined;
|
|
99
|
+
spinnerPhase: SpinnerPhase | null;
|
|
106
100
|
}
|
|
@@ -3,17 +3,11 @@ import { WORKING_MESSAGES } from "./messages";
|
|
|
3
3
|
export const DEFAULT_SETTINGS: Settings = {
|
|
4
4
|
zenMode: false,
|
|
5
5
|
accentColor: "accent",
|
|
6
|
+
editorFrame: "blocky",
|
|
6
7
|
|
|
7
8
|
header: {
|
|
8
9
|
enable: false,
|
|
9
|
-
|
|
10
|
-
accentColor: "accent",
|
|
11
|
-
|
|
12
|
-
heading: "Welcome back!",
|
|
13
|
-
subheading:
|
|
14
|
-
"Ready for your next session? Terminal warm, context clean, tools ready to execute",
|
|
15
|
-
|
|
16
|
-
logo: ["█████████ ", "███ ███ ", "██████ ", "███ ███"],
|
|
10
|
+
type: "basic",
|
|
17
11
|
},
|
|
18
12
|
|
|
19
13
|
workingMessage: {
|
|
@@ -23,25 +17,13 @@ export const DEFAULT_SETTINGS: Settings = {
|
|
|
23
17
|
},
|
|
24
18
|
|
|
25
19
|
frame: {
|
|
26
|
-
icons: {},
|
|
27
|
-
paddingX: 1,
|
|
28
20
|
enable: true,
|
|
29
21
|
minWidth: 20,
|
|
30
|
-
marginTop: 0,
|
|
31
22
|
showCwd: true,
|
|
32
|
-
paddingTop: 1,
|
|
33
|
-
marginBottom: 0,
|
|
34
23
|
showModel: true,
|
|
35
|
-
paddingBottom: 1,
|
|
36
|
-
showSpinner: false,
|
|
37
24
|
showContext: true,
|
|
38
25
|
showThinking: true,
|
|
39
26
|
showAgentMode: true,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
colors: {},
|
|
43
|
-
// ponytail: colors were dormant (nothing read them); now wired — keep
|
|
44
|
-
// defaults unset so segments keep their natural colors and zen-mode is
|
|
45
|
-
// the mute switch.
|
|
27
|
+
showSpinner: false,
|
|
46
28
|
},
|
|
47
29
|
};
|
|
@@ -11,7 +11,7 @@ export const SPINNER_FRAMES: Record<SpinnerPhase, string[]> = {
|
|
|
11
11
|
// Sample ["░", "▒", "▓", "█", "▓", "▒"],
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
/** Nerd Font
|
|
14
|
+
/** Nerd Font glyphs used by the blocky editor preset. */
|
|
15
15
|
export const DEFAULT_ICONS: FrameIcons = {
|
|
16
16
|
model: "",
|
|
17
17
|
folder: " ",
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
DEFAULT_SETTINGS,
|
|
9
9
|
WORKING_MESSAGES,
|
|
10
10
|
} from "./constants";
|
|
11
|
-
import { Settings
|
|
11
|
+
import type { Settings } from "./types";
|
|
12
12
|
import { isThemeColor } from "../utils";
|
|
13
13
|
|
|
14
14
|
function getResolvedSettingsFilePath(): string {
|
|
@@ -31,9 +31,6 @@ function normalize(raw: unknown): Settings {
|
|
|
31
31
|
const bool = (v: unknown, fallback: boolean | undefined): boolean =>
|
|
32
32
|
typeof v === "boolean" ? v : (fallback ?? true);
|
|
33
33
|
|
|
34
|
-
const str = (v: unknown, fallback: string): string =>
|
|
35
|
-
typeof v === "string" ? v : fallback;
|
|
36
|
-
|
|
37
34
|
const d = DEFAULT_SETTINGS;
|
|
38
35
|
const r = raw as Record<string, unknown>;
|
|
39
36
|
|
|
@@ -41,6 +38,8 @@ function normalize(raw: unknown): Settings {
|
|
|
41
38
|
...DEFAULT_SETTINGS,
|
|
42
39
|
frame: { ...DEFAULT_SETTINGS.frame },
|
|
43
40
|
zenMode: bool(r.zenMode, d.zenMode ?? true),
|
|
41
|
+
editorFrame:
|
|
42
|
+
typeof r.editorFrame === "string" ? r.editorFrame : d.editorFrame,
|
|
44
43
|
};
|
|
45
44
|
|
|
46
45
|
if (typeof r.frame === "object" && r.frame) {
|
|
@@ -49,46 +48,12 @@ function normalize(raw: unknown): Settings {
|
|
|
49
48
|
out.frame = {
|
|
50
49
|
enable: bool(f.enable, d.frame?.enable),
|
|
51
50
|
minWidth: num(f.minWidth, d.frame?.minWidth ?? 20),
|
|
52
|
-
paddingTop: num(f.paddingTop, d.frame?.paddingTop ?? 1),
|
|
53
|
-
paddingBottom: num(f.paddingBottom, d.frame?.paddingBottom ?? 1),
|
|
54
|
-
paddingX: num(f.paddingX, d.frame?.paddingX ?? 1),
|
|
55
|
-
marginTop: num(f.marginTop, d.frame?.marginTop ?? 0),
|
|
56
|
-
marginBottom: num(f.marginBottom, d.frame?.marginBottom ?? 0),
|
|
57
51
|
showModel: bool(f.showModel, d.frame?.showModel),
|
|
58
52
|
showThinking: bool(f.showThinking, d.frame?.showThinking),
|
|
59
53
|
showContext: bool(f.showContext, d.frame?.showContext),
|
|
60
54
|
showCwd: bool(f.showCwd, d.frame?.showCwd),
|
|
61
55
|
showAgentMode: bool(f.showAgentMode, d.frame?.showAgentMode),
|
|
62
56
|
showSpinner: bool(f.showSpinner, d.frame?.showSpinner),
|
|
63
|
-
icons:
|
|
64
|
-
typeof f.icons === "object" && f.icons
|
|
65
|
-
? { ...d.frame?.icons, ...(f.icons as Record<string, unknown>) }
|
|
66
|
-
: d.frame?.icons,
|
|
67
|
-
borderColor:
|
|
68
|
-
typeof f.borderColor === "string" &&
|
|
69
|
-
(isThemeColor(f.borderColor) || f.borderColor === "agentMode")
|
|
70
|
-
? f.borderColor
|
|
71
|
-
: (d.frame?.borderColor ?? "border"),
|
|
72
|
-
|
|
73
|
-
colors:
|
|
74
|
-
typeof f.colors === "object" && f.colors
|
|
75
|
-
? (Object.fromEntries(
|
|
76
|
-
Object.entries(f.colors as Record<string, unknown>).filter(
|
|
77
|
-
([, v]) => typeof v === "string" && isThemeColor(v),
|
|
78
|
-
),
|
|
79
|
-
) as Partial<FrameColors>)
|
|
80
|
-
: d.frame?.colors,
|
|
81
|
-
|
|
82
|
-
prefix:
|
|
83
|
-
typeof f.prefix === "string" ? f.prefix : (d.frame?.prefix ?? "❯"),
|
|
84
|
-
|
|
85
|
-
prefixColor:
|
|
86
|
-
typeof f.prefixColor === "string" &&
|
|
87
|
-
(f.prefixColor === "agentMode" ||
|
|
88
|
-
f.prefixColor === "frameBorder" ||
|
|
89
|
-
isThemeColor(f.prefixColor))
|
|
90
|
-
? f.prefixColor
|
|
91
|
-
: d.frame?.prefixColor, // unset → text
|
|
92
57
|
};
|
|
93
58
|
}
|
|
94
59
|
|
|
@@ -96,21 +61,8 @@ function normalize(raw: unknown): Settings {
|
|
|
96
61
|
const h = r.header as Record<string, unknown>;
|
|
97
62
|
|
|
98
63
|
out.header = {
|
|
64
|
+
type: typeof h.type === "string" ? h.type : (d.header?.type ?? "basic"),
|
|
99
65
|
enable: bool(h.enable, d.header?.enable),
|
|
100
|
-
heading: str(h.heading, d.header?.heading ?? ""),
|
|
101
|
-
subheading: str(h.subheading, d.header?.subheading ?? ""),
|
|
102
|
-
logoColor:
|
|
103
|
-
typeof h.logoColor === "string" && isThemeColor(h.logoColor)
|
|
104
|
-
? h.logoColor
|
|
105
|
-
: (d.header?.logoColor ?? "accent"),
|
|
106
|
-
accentColor:
|
|
107
|
-
typeof h.accentColor === "string" && isThemeColor(h.accentColor)
|
|
108
|
-
? h.accentColor
|
|
109
|
-
: (d.header?.accentColor ?? "accent"),
|
|
110
|
-
logo:
|
|
111
|
-
Array.isArray(h.logo) && h.logo.every((x) => typeof x === "string")
|
|
112
|
-
? (h.logo as string[])
|
|
113
|
-
: (d.header?.logo ?? []),
|
|
114
66
|
};
|
|
115
67
|
}
|
|
116
68
|
|
|
@@ -9,17 +9,7 @@ export interface BannerTip {
|
|
|
9
9
|
text: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
border: ThemeColor;
|
|
14
|
-
background: ThemeColor;
|
|
15
|
-
cwd: ThemeColor;
|
|
16
|
-
model: ThemeColor;
|
|
17
|
-
context: ThemeColor;
|
|
18
|
-
thinking: ThemeColor;
|
|
19
|
-
agentMode: ThemeColor;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Nerd-font / glyph icons shown in the frame border. Each is overridable. */
|
|
12
|
+
/** Nerd-font / glyph icons shown in the band and status rows. Overridable. */
|
|
23
13
|
export interface FrameIcons {
|
|
24
14
|
folder: string;
|
|
25
15
|
model: string;
|
|
@@ -30,9 +20,9 @@ export interface FrameIcons {
|
|
|
30
20
|
}
|
|
31
21
|
|
|
32
22
|
/**
|
|
33
|
-
* ``frame`` — the
|
|
34
|
-
*
|
|
35
|
-
*
|
|
23
|
+
* ``frame`` — the editor frame preset. Styling and layout (prefix, colors,
|
|
24
|
+
* margins, paddings) are owned by the renderer; only these behavioral
|
|
25
|
+
* toggles are configurable.
|
|
36
26
|
*/
|
|
37
27
|
export interface FrameSettings {
|
|
38
28
|
enable?: boolean;
|
|
@@ -40,52 +30,22 @@ export interface FrameSettings {
|
|
|
40
30
|
/** Below this terminal width the frame is skipped entirely (passthrough) */
|
|
41
31
|
minWidth?: number;
|
|
42
32
|
|
|
43
|
-
/** Blank lines inside the box above the content. Default 1. */
|
|
44
|
-
paddingTop?: number;
|
|
45
|
-
|
|
46
|
-
/** Blank lines inside the box below the content. Default 1. */
|
|
47
|
-
paddingBottom?: number;
|
|
48
|
-
|
|
49
|
-
/** Horizontal inner padding for the content rows. Default 1. */
|
|
50
|
-
paddingX?: number;
|
|
51
|
-
|
|
52
|
-
/** Blank rows OUTSIDE the box, above its top border. Default 0. */
|
|
53
|
-
marginTop?: number;
|
|
54
|
-
|
|
55
|
-
/** Blank rows OUTSIDE the box, below its bottom border. Default 0. */
|
|
56
|
-
marginBottom?: number;
|
|
57
|
-
|
|
58
33
|
showCwd?: boolean;
|
|
59
34
|
showModel?: boolean;
|
|
60
35
|
showContext?: boolean;
|
|
61
36
|
showThinking?: boolean;
|
|
62
37
|
showAgentMode?: boolean;
|
|
63
38
|
|
|
64
|
-
/** Show the status-animation spinner segment while streaming. Default false
|
|
39
|
+
/** Show the status-animation spinner segment while streaming. Default false */
|
|
65
40
|
showSpinner?: boolean;
|
|
66
|
-
|
|
67
|
-
icons?: Partial<FrameIcons>;
|
|
68
|
-
colors?: Partial<FrameColors>;
|
|
69
|
-
|
|
70
|
-
/** ThemeColor used as the fg for all editor border characters; If set to `"agentMode"`, will follow the current Agent color. Default "border". */
|
|
71
|
-
borderColor?: ThemeColor | "agentMode";
|
|
72
|
-
|
|
73
|
-
/** Text shown before the editor content. Default "❯". */
|
|
74
|
-
prefix?: string;
|
|
75
|
-
|
|
76
|
-
/** ThemeColor used as the fg for the prefix text; If set to `"agentMode"`, will follow the current Agent color;
|
|
77
|
-
* If set to `"frameBorder"`, will follow the border color. Default: text color. */
|
|
78
|
-
prefixColor?: "agentMode" | "frameBorder" | ThemeColor;
|
|
79
41
|
}
|
|
80
42
|
|
|
81
43
|
/** `header` — the top-line header, which can show a logo or other text */
|
|
82
44
|
export interface HeaderSettings {
|
|
83
|
-
logo: string[];
|
|
84
45
|
enable: boolean;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
accentColor: ThemeColor;
|
|
46
|
+
|
|
47
|
+
/** Header renderer by registered name. Default "basic". */
|
|
48
|
+
type?: string;
|
|
89
49
|
}
|
|
90
50
|
|
|
91
51
|
/** `workingMessage` — rotating messages in pi's built-in working loader. */
|
|
@@ -105,11 +65,14 @@ export interface Settings {
|
|
|
105
65
|
header?: HeaderSettings;
|
|
106
66
|
workingMessage?: WorkingMessageSettings;
|
|
107
67
|
|
|
68
|
+
/** Editor-frame renderer by registered name. Default "blocky". */
|
|
69
|
+
editorFrame?: string;
|
|
70
|
+
|
|
108
71
|
/** Master mute: every segment except agent-mode renders muted.
|
|
109
72
|
* Default true. Toggle at runtime with `/zen_mode` or the `piZenFrame.zenMode`
|
|
110
73
|
* keybinding (default `ctrl+shift+z`). */
|
|
111
74
|
zenMode?: boolean;
|
|
112
75
|
|
|
113
|
-
/** Accent color for
|
|
76
|
+
/** Accent color for segment highlights; Defaults to 'accent' */
|
|
114
77
|
accentColor?: ThemeColor;
|
|
115
78
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* BlockyEditor — the editor-frame renderer. Subclasses CustomEditor so all of
|
|
3
3
|
* pi's editing/undo/app-keybinding machinery keeps working; `render` layers
|
|
4
|
-
* the
|
|
4
|
+
* the bg band. Vim behavior was removed — all input passes through to the
|
|
5
5
|
* base editor. Appearance lives in `components/` (segments + frame).
|
|
6
6
|
*/
|
|
7
7
|
import {
|
|
@@ -11,24 +11,19 @@ import {
|
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
12
|
|
|
13
13
|
import { DEFAULT_ICONS, SPINNER_FRAMES } from "../config/constants";
|
|
14
|
-
import type {
|
|
15
|
-
import {
|
|
14
|
+
import type { SpinnerPhase } from "../config/types";
|
|
15
|
+
import { composeBand, fitInfoRow, isBorderRow } from "../components/frame";
|
|
16
16
|
import { segmentsFor } from "../components/registry";
|
|
17
17
|
import type {
|
|
18
18
|
ExternalData,
|
|
19
19
|
FrameData,
|
|
20
20
|
SegmentContext,
|
|
21
21
|
} from "../components/types";
|
|
22
|
-
import {
|
|
22
|
+
import type { EditorFrameRenderOptions } from "../renderers/types";
|
|
23
23
|
|
|
24
|
-
export
|
|
25
|
-
frame: FrameSettings;
|
|
26
|
-
accentColor: ThemeColor;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class FrameEditor extends CustomEditor {
|
|
24
|
+
export class BlockyEditor extends CustomEditor {
|
|
30
25
|
private pi: ExtensionAPI;
|
|
31
|
-
private opts:
|
|
26
|
+
private opts: EditorFrameRenderOptions;
|
|
32
27
|
private provider: (pi: ExtensionAPI) => ExternalData;
|
|
33
28
|
|
|
34
29
|
private spinnerIdx = 0;
|
|
@@ -38,7 +33,7 @@ export class FrameEditor extends CustomEditor {
|
|
|
38
33
|
constructor(
|
|
39
34
|
pi: ExtensionAPI,
|
|
40
35
|
provider: (pi: ExtensionAPI) => ExternalData,
|
|
41
|
-
opts:
|
|
36
|
+
opts: EditorFrameRenderOptions,
|
|
42
37
|
...args: ConstructorParameters<typeof CustomEditor>
|
|
43
38
|
) {
|
|
44
39
|
super(...args);
|
|
@@ -81,103 +76,94 @@ export class FrameEditor extends CustomEditor {
|
|
|
81
76
|
render(width: number): string[] {
|
|
82
77
|
const frame = this.opts.frame;
|
|
83
78
|
|
|
84
|
-
const padX = Math.min(
|
|
85
|
-
frame.paddingX ?? 1,
|
|
86
|
-
Math.max(0, Math.floor((width - 2) / 2)),
|
|
87
|
-
);
|
|
79
|
+
const padX = Math.min(2, Math.max(0, Math.floor(width / 2)));
|
|
88
80
|
|
|
89
|
-
const
|
|
81
|
+
const marginX = Math.min(1, Math.max(0, Math.floor(width / 2)));
|
|
82
|
+
|
|
83
|
+
const contentWidth = width - marginX * 2;
|
|
84
|
+
const innerWidth = width - padX * 2 - marginX * 2;
|
|
90
85
|
if (innerWidth < 8) return super.render(width);
|
|
91
86
|
|
|
92
87
|
const ext = this.provider(this.pi);
|
|
93
88
|
const d: FrameData = {
|
|
94
89
|
cwd: ext.cwd,
|
|
95
90
|
context: ext.context,
|
|
91
|
+
zenMode: ext.zenMode,
|
|
96
92
|
gitDirty: ext.gitDirty,
|
|
97
93
|
agentMode: ext.agentMode,
|
|
98
94
|
gitBranch: ext.gitBranch,
|
|
99
95
|
modelName: ext.modelName,
|
|
100
96
|
spinnerPhase: ext.spinnerPhase,
|
|
97
|
+
modelProvider: ext.modelProvider,
|
|
101
98
|
thinkingLevel: ext.thinkingLevel,
|
|
102
99
|
spinnerFrame: this.spinnerFrame(),
|
|
103
100
|
accentColor: this.opts.accentColor,
|
|
104
|
-
zenMode: ext.zenMode,
|
|
105
101
|
};
|
|
106
102
|
|
|
107
|
-
let prefix =
|
|
103
|
+
let prefix = "┃";
|
|
108
104
|
|
|
109
105
|
if (ext.theme?.fg) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (frame.borderColor === "agentMode") {
|
|
113
|
-
borderColor = ext.agentMode?.color ?? "accent";
|
|
114
|
-
} else {
|
|
115
|
-
borderColor = frame.borderColor ?? "accent";
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let prefixColor: ThemeColor = "text";
|
|
119
|
-
if (frame.prefixColor === "agentMode") {
|
|
120
|
-
prefixColor = ext.agentMode?.color ?? "accent";
|
|
121
|
-
} else if (frame.prefixColor === "frameBorder") {
|
|
122
|
-
prefixColor = borderColor;
|
|
123
|
-
} else if (frame.prefixColor && isThemeColor(frame.prefixColor)) {
|
|
124
|
-
prefixColor = frame.prefixColor;
|
|
125
|
-
}
|
|
126
|
-
|
|
106
|
+
const prefixColor: ThemeColor = ext.agentMode?.color ?? "text";
|
|
127
107
|
prefix = ext.theme.fg(prefixColor, prefix);
|
|
128
|
-
|
|
129
|
-
this.borderColor = (s: string) => ext.theme!.fg(borderColor, s);
|
|
130
108
|
}
|
|
131
109
|
|
|
132
110
|
// No theme (non-TUI) or frame disabled or too narrow → plain editor.
|
|
133
|
-
if (!ext.theme || !frame.enable ||
|
|
111
|
+
if (!ext.theme || !frame.enable || contentWidth < (frame.minWidth ?? 20)) {
|
|
134
112
|
return super.render(width);
|
|
135
113
|
}
|
|
136
114
|
|
|
137
115
|
const inner = super.render(innerWidth);
|
|
116
|
+
|
|
117
|
+
let bottomIdx = inner.length - 1;
|
|
118
|
+
for (let i = inner.length - 1; i >= 0; i--) {
|
|
119
|
+
if (isBorderRow(inner[i]!)) {
|
|
120
|
+
bottomIdx = i;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const content = inner.slice(1, bottomIdx);
|
|
126
|
+
const autocomplete = inner.slice(bottomIdx + 1);
|
|
127
|
+
|
|
128
|
+
const bgSgr = ext.theme.getBgAnsi("customMessageBg");
|
|
129
|
+
const paint = (row: string) =>
|
|
130
|
+
bgSgr + row.split("\x1b[0m").join(`\x1b[0m${bgSgr}`) + "\x1b[0m";
|
|
131
|
+
|
|
138
132
|
const ctx: SegmentContext = {
|
|
139
133
|
cfg: frame,
|
|
140
134
|
theme: ext.theme,
|
|
141
|
-
|
|
142
|
-
icons: {
|
|
143
|
-
...DEFAULT_ICONS,
|
|
144
|
-
...frame.icons,
|
|
145
|
-
},
|
|
146
|
-
segColor: (key, fallback) =>
|
|
147
|
-
d.zenMode && key !== "agentMode"
|
|
148
|
-
? "muted"
|
|
149
|
-
: (frame.colors?.[key] ?? fallback),
|
|
135
|
+
icons: DEFAULT_ICONS,
|
|
150
136
|
};
|
|
151
137
|
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
138
|
+
const box = composeBand(content, autocomplete, {
|
|
139
|
+
padX,
|
|
140
|
+
paint,
|
|
141
|
+
prefix,
|
|
142
|
+
width: contentWidth,
|
|
143
|
+
marginX: 1,
|
|
144
|
+
paddingTop: 1,
|
|
145
|
+
paddingBottom: 1,
|
|
146
|
+
boxBottom: fitInfoRow(
|
|
147
|
+
segmentsFor("topLeft", d, ctx),
|
|
148
|
+
segmentsFor("topRight", d, ctx),
|
|
149
|
+
innerWidth,
|
|
150
|
+
),
|
|
151
|
+
});
|
|
157
152
|
|
|
158
|
-
const
|
|
153
|
+
const pseudoFooter = fitInfoRow(
|
|
159
154
|
segmentsFor("bottomLeft", d, ctx),
|
|
160
155
|
segmentsFor("bottomRight", d, ctx),
|
|
161
|
-
|
|
156
|
+
contentWidth,
|
|
162
157
|
);
|
|
163
158
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
border: this.borderColor,
|
|
169
|
-
paddingTop: frame.paddingTop ?? 1,
|
|
170
|
-
paddingBottom: frame.paddingBottom ?? 1,
|
|
171
|
-
});
|
|
159
|
+
// Blank rows OUTSIDE the band, above/below it.
|
|
160
|
+
const marginRow = " ".repeat(contentWidth);
|
|
161
|
+
const marginTop: string[] = [];
|
|
162
|
+
const marginBottom: string[] = [];
|
|
172
163
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const marginTop = Array(Math.max(0, frame.marginTop ?? 0)).fill(marginRow);
|
|
176
|
-
const marginBottom = Array(Math.max(0, frame.marginBottom ?? 0)).fill(
|
|
177
|
-
marginRow,
|
|
164
|
+
return [...marginTop, ...box, marginRow, pseudoFooter, ...marginBottom].map(
|
|
165
|
+
(row) => " ".repeat(marginX) + row + " ".repeat(marginX),
|
|
178
166
|
);
|
|
179
|
-
|
|
180
|
-
return [...marginTop, topLine, ...box, bottomLine, ...marginBottom];
|
|
181
167
|
}
|
|
182
168
|
|
|
183
169
|
private spinnerFrame(): string {
|
package/extensions/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-zen-frame entry. Thin wiring layer:
|
|
3
3
|
* - loads config, subscribes to pi lifecycle + mode/agent-mode events
|
|
4
|
-
* - installs
|
|
5
|
-
* model switch — the old instance's spinner
|
|
4
|
+
* - installs the configured editor-frame renderer via setEditorComponent
|
|
5
|
+
* (factory is re-run on model switch — the old instance's spinner
|
|
6
|
+
* timer is stopped first)
|
|
6
7
|
* - assembles ExternalData per paint (model, thinking, context, git, ...)
|
|
7
8
|
*
|
|
8
9
|
* All appearance logic lives in components/ (segments + frame); the editor
|
|
@@ -27,16 +28,17 @@ import {
|
|
|
27
28
|
import { loadSettings } from "./config/settings";
|
|
28
29
|
import type { Settings, SpinnerPhase } from "./config/types";
|
|
29
30
|
import type { AgentMode, AgentState, ExternalData } from "./components/types";
|
|
30
|
-
import {
|
|
31
|
+
import { getEditorFrame, getHeader } from "./renderers/registry";
|
|
32
|
+
import type { EditorFrameRenderer } from "./renderers/types";
|
|
31
33
|
import {
|
|
32
34
|
capitalize,
|
|
33
35
|
readAgentModeFromSession,
|
|
34
36
|
readGit,
|
|
35
37
|
type GitInfo,
|
|
36
38
|
} from "./utils";
|
|
37
|
-
import {
|
|
39
|
+
import { type HeaderEnv } from "./components/header";
|
|
38
40
|
|
|
39
|
-
let editor:
|
|
41
|
+
let editor: EditorFrameRenderer | null = null;
|
|
40
42
|
let currentCtx: ExtensionContext | null = null;
|
|
41
43
|
let settings: Settings = DEFAULT_SETTINGS;
|
|
42
44
|
let spinnerPhase: SpinnerPhase | null = null;
|
|
@@ -94,7 +96,8 @@ function provideExternal(pi: ExtensionAPI): ExternalData {
|
|
|
94
96
|
gitBranch: git.branch,
|
|
95
97
|
theme: ctx?.ui.theme,
|
|
96
98
|
thinkingLevel: pi.getThinkingLevel(),
|
|
97
|
-
modelName:
|
|
99
|
+
modelName: model?.name ?? model?.id ?? "Unknown",
|
|
100
|
+
modelProvider: capitalize(model?.provider ?? "unknown"),
|
|
98
101
|
context: usage
|
|
99
102
|
? {
|
|
100
103
|
tokens: usage.tokens,
|
|
@@ -201,8 +204,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
201
204
|
}
|
|
202
205
|
|
|
203
206
|
if (settings.header?.enable) {
|
|
207
|
+
const headerFactory =
|
|
208
|
+
getHeader(settings.header.type ?? "basic") ?? getHeader("basic")!;
|
|
204
209
|
ctx.ui.setHeader((_tui, theme) =>
|
|
205
|
-
|
|
210
|
+
headerFactory(_tui, theme, pi, settings, getHeaderEnv),
|
|
206
211
|
);
|
|
207
212
|
}
|
|
208
213
|
|
|
@@ -217,13 +222,19 @@ export default async function (pi: ExtensionAPI) {
|
|
|
217
222
|
ctx.ui.setEditorComponent((...args) => {
|
|
218
223
|
editor?.stopSpinner();
|
|
219
224
|
|
|
220
|
-
|
|
225
|
+
const frameFactory =
|
|
226
|
+
getEditorFrame(settings.editorFrame ?? "blocky") ??
|
|
227
|
+
getEditorFrame("blocky")!;
|
|
228
|
+
|
|
229
|
+
editor = frameFactory(
|
|
221
230
|
pi,
|
|
222
231
|
provideExternal,
|
|
223
232
|
{
|
|
224
233
|
frame: settings.frame ?? DEFAULT_SETTINGS.frame!,
|
|
225
234
|
accentColor:
|
|
226
|
-
settings.accentColor ??
|
|
235
|
+
settings.accentColor ??
|
|
236
|
+
DEFAULT_SETTINGS.accentColor ??
|
|
237
|
+
"accent",
|
|
227
238
|
},
|
|
228
239
|
...args,
|
|
229
240
|
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer registry — the swap-in point. Register an editor-frame or header
|
|
3
|
+
* implementation by name; the entry point resolves the configured name via
|
|
4
|
+
* getEditorFrame/getHeader. Built-ins are registered here so the entry point
|
|
5
|
+
* only ever talks to the registry — add a new look by registering it.
|
|
6
|
+
*/
|
|
7
|
+
import type { EditorFrameFactory, HeaderRendererFactory } from "./types";
|
|
8
|
+
import { createHeader } from "../components/header";
|
|
9
|
+
import { BlockyEditor } from "../editor/blocky-editor";
|
|
10
|
+
|
|
11
|
+
const editorFrames = new Map<string, EditorFrameFactory>();
|
|
12
|
+
const headers = new Map<string, HeaderRendererFactory>();
|
|
13
|
+
|
|
14
|
+
export function registerEditorFrame(
|
|
15
|
+
name: string,
|
|
16
|
+
factory: EditorFrameFactory,
|
|
17
|
+
): void {
|
|
18
|
+
editorFrames.set(name, factory);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function registerHeader(
|
|
22
|
+
name: string,
|
|
23
|
+
factory: HeaderRendererFactory,
|
|
24
|
+
): void {
|
|
25
|
+
headers.set(name, factory);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getEditorFrame(name: string): EditorFrameFactory | undefined {
|
|
29
|
+
return editorFrames.get(name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getHeader(name: string): HeaderRendererFactory | undefined {
|
|
33
|
+
return headers.get(name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── built-ins ─────────────────────────────────────────────────────────────
|
|
37
|
+
registerEditorFrame("blocky", (pi, provider, opts, ...args) =>
|
|
38
|
+
new BlockyEditor(pi, provider, opts, ...args),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
registerHeader("basic", createHeader);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer contracts. A "renderer" is a named, swappable implementation of
|
|
3
|
+
* the editor frame (the blocky band) or the header (the welcome panel).
|
|
4
|
+
* Implementations register by name in registry.ts; the entry point resolves
|
|
5
|
+
* the configured name and instantiates. Signatures mirror pi's
|
|
6
|
+
* setEditorComponent / setHeader factories.
|
|
7
|
+
*/
|
|
8
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
9
|
+
import type {
|
|
10
|
+
CustomEditor,
|
|
11
|
+
ExtensionAPI,
|
|
12
|
+
Theme,
|
|
13
|
+
ThemeColor,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { FrameSettings, Settings, SpinnerPhase } from "../config/types";
|
|
16
|
+
import type { ExternalData } from "../components/types";
|
|
17
|
+
import type { HeaderEnv } from "../components/header";
|
|
18
|
+
|
|
19
|
+
// ── Header ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** What pi's TUI needs from any header implementation. */
|
|
22
|
+
export interface HeaderRenderer {
|
|
23
|
+
render(width: number): string[];
|
|
24
|
+
invalidate(): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Builds a HeaderRenderer; matches ctx.ui.setHeader's factory signature. */
|
|
28
|
+
export type HeaderRendererFactory = (
|
|
29
|
+
tui: TUI,
|
|
30
|
+
theme: Theme,
|
|
31
|
+
pi: ExtensionAPI,
|
|
32
|
+
settings: Settings,
|
|
33
|
+
getEnv: (pi: ExtensionAPI) => HeaderEnv,
|
|
34
|
+
) => HeaderRenderer;
|
|
35
|
+
|
|
36
|
+
// ── Editor frame ──────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/** Per-renderer options handed to the frame factory (frame config + accent). */
|
|
39
|
+
export interface EditorFrameRenderOptions {
|
|
40
|
+
frame: FrameSettings;
|
|
41
|
+
accentColor: ThemeColor;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Surface the entry point drives on the active editor-frame renderer. */
|
|
45
|
+
export interface EditorFrameRenderer extends CustomEditor {
|
|
46
|
+
setSpinner(phase: SpinnerPhase | null): void;
|
|
47
|
+
stopSpinner(): void;
|
|
48
|
+
refresh(): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Builds an EditorFrameRenderer; `...args` forward to CustomEditor. */
|
|
52
|
+
export type EditorFrameFactory = (
|
|
53
|
+
pi: ExtensionAPI,
|
|
54
|
+
provider: (pi: ExtensionAPI) => ExternalData,
|
|
55
|
+
opts: EditorFrameRenderOptions,
|
|
56
|
+
...args: ConstructorParameters<typeof CustomEditor>
|
|
57
|
+
) => EditorFrameRenderer;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@leo-alvarenga/pi-zen-frame",
|
|
3
3
|
"description": "A pi-coding-agent extension that gives the TUI editor a simple yet polished look and feel",
|
|
4
4
|
"author": "Leonardo A. Alvarenga",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.14.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github.com/leo-alvarenga/pi-mono",
|
|
8
8
|
"publishConfig": {
|