@leo-alvarenga/pi-zen-frame 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -15
- package/extensions/components/agent-mode.ts +1 -2
- package/extensions/components/cwd.ts +2 -2
- package/extensions/components/header.ts +8 -18
- package/extensions/components/notify.ts +1 -1
- package/extensions/components/reasoning.ts +1 -1
- package/extensions/components/spinner.ts +2 -2
- package/extensions/components/token-count.ts +2 -3
- package/extensions/components/types.ts +0 -4
- package/extensions/config/constants/contracts.ts +5 -10
- package/extensions/config/constants/defaults.ts +1 -1
- package/extensions/config/constants/index.ts +1 -6
- package/extensions/config/constants/visuals.ts +8 -7
- package/extensions/config/settings.ts +2 -2
- package/extensions/config/types.ts +10 -15
- package/extensions/editor/blocky-editor.ts +69 -117
- package/extensions/editor/minimalist-editor.ts +99 -0
- package/extensions/editor/palette/index.ts +4 -0
- package/extensions/editor/palette/input.ts +85 -0
- package/extensions/editor/palette/items.ts +58 -0
- package/extensions/editor/palette/render.ts +88 -0
- package/extensions/editor/palette/types.ts +14 -0
- package/extensions/editor/render.ts +79 -0
- package/extensions/editor/spinner.ts +42 -0
- package/extensions/index.ts +9 -35
- package/extensions/renderers/registry.ts +14 -5
- package/extensions/renderers/types.ts +7 -17
- package/extensions/utils/git.ts +1 -1
- package/extensions/utils/index.ts +0 -1
- package/extensions/utils/path.ts +1 -1
- package/extensions/utils/string.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ A [`pi-coding-agent`](https://github.com/earendil-works/pi) extension that gives
|
|
|
10
10
|
- **Header Box**: Optional welcome panel with a logo and "Welcome back!" heading, model name, current working directory, Git status, and a random tip
|
|
11
11
|
- **Working Messages**: Randomized loading status messages during response generation, updated on a configurable timer
|
|
12
12
|
- **Native Theme Support**: Uses pi `ThemeColor` tokens to automatically align with your active theme
|
|
13
|
+
- **Command Palette**: `alt+p` opens a filterable list of built-in and registered slash commands
|
|
13
14
|
|
|
14
15
|
---
|
|
15
16
|
|
|
@@ -42,10 +43,7 @@ Configuration is loaded from `~/.pi/agent/pi-zen-frame.json`. All properties are
|
|
|
42
43
|
// Fallback accent color for frame border and active segments
|
|
43
44
|
"accentColor": "accent",
|
|
44
45
|
|
|
45
|
-
//
|
|
46
|
-
"zenMode": false,
|
|
47
|
-
|
|
48
|
-
// Editor-frame renderer by registered name (built-in: "blocky")
|
|
46
|
+
// Editor-frame renderer by registered name (built-in: "blocky", "minimalist")
|
|
49
47
|
"editorFrame": "blocky",
|
|
50
48
|
|
|
51
49
|
"header": {
|
|
@@ -90,27 +88,27 @@ Configuration is loaded from `~/.pi/agent/pi-zen-frame.json`. All properties are
|
|
|
90
88
|
|
|
91
89
|
The editor frame shows status indicators in two rows below the editor:
|
|
92
90
|
|
|
93
|
-
| Location | Segment | Description
|
|
94
|
-
| ------------------ | -------------- |
|
|
95
|
-
| Editor band (left) | **agent mode** | Active agent indicator pill from `pi-agent-manager`
|
|
96
|
-
| | **model** | Active model name and provider
|
|
97
|
-
| | **reasoning** | Current reasoning effort level, tinted with pi thinking tokens
|
|
91
|
+
| Location | Segment | Description |
|
|
92
|
+
| ------------------ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
93
|
+
| Editor band (left) | **agent mode** | Active agent indicator pill from `pi-agent-manager` |
|
|
94
|
+
| | **model** | Active model name and provider |
|
|
95
|
+
| | **reasoning** | Current reasoning effort level, tinted with pi thinking tokens |
|
|
98
96
|
| | **spinner** | Active phase indicator (`thinking`, `outputting`, `toolcall`, `exec`); replaces the row while streaming when `showSpinner` is enabled (default off) |
|
|
99
|
-
| Footer (left) | **cwd** | Shortened path, active Git branch, and uncommitted file counts
|
|
100
|
-
| Footer (right) | **ctx** | Context usage percentage and token counts with color alerts
|
|
97
|
+
| Footer (left) | **cwd** | Shortened path, active Git branch, and uncommitted file counts |
|
|
98
|
+
| Footer (right) | **ctx** | Context usage percentage and token counts with color alerts |
|
|
101
99
|
|
|
102
100
|
---
|
|
103
101
|
|
|
104
102
|
## Commands & Keybindings
|
|
105
103
|
|
|
106
|
-
- **
|
|
107
|
-
- **
|
|
104
|
+
- **Command Palette**: `alt+p` — filter and insert built-in or registered slash commands.
|
|
105
|
+
- **Palette Keys**: `↑`/`↓` navigate, `Enter` inserts the command, `Esc` closes.
|
|
108
106
|
|
|
109
107
|
To rebind or disable the hotkey, update `~/.pi/agent/keybindings.json`:
|
|
110
108
|
|
|
111
109
|
```jsonc
|
|
112
110
|
{
|
|
113
|
-
"piZenFrame.
|
|
111
|
+
"piZenFrame.palette": "alt+p", // Set to [] to disable the shortcut
|
|
114
112
|
}
|
|
115
113
|
```
|
|
116
114
|
|
|
@@ -125,7 +123,7 @@ extensions/
|
|
|
125
123
|
├── index.ts # Extension entry point: config initialization and events
|
|
126
124
|
├── config/ # Types, defaults, and settings normalization
|
|
127
125
|
├── components/ # Status segments, header, and frame layout helpers
|
|
128
|
-
├── editor/ #
|
|
126
|
+
├── editor/ # Editor-frame renderers (blocky / minimalist) + palette
|
|
129
127
|
├── renderers/ # Renderer registry + swap-in API (blocky / basic)
|
|
130
128
|
└── utils/ # Helpers for Git status, paths, agent modes, and token usage
|
|
131
129
|
|
|
@@ -3,7 +3,7 @@ import { capitalize } from "../utils";
|
|
|
3
3
|
import type { SegmentDef } from "./types";
|
|
4
4
|
|
|
5
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
|
+
* (when installed). Rendered as a pill using the agent's own color/icon */
|
|
7
7
|
export const agentModeSegment: SegmentDef = {
|
|
8
8
|
id: "agent-mode",
|
|
9
9
|
slot: "topLeft",
|
|
@@ -13,7 +13,6 @@ export const agentModeSegment: SegmentDef = {
|
|
|
13
13
|
const m = d.agentMode;
|
|
14
14
|
if (!m) return "";
|
|
15
15
|
|
|
16
|
-
// Agent's own color wins; accent fills in. Zen never mutes the agent.
|
|
17
16
|
const color = m.color ?? d.accentColor;
|
|
18
17
|
const label = ` ${capitalize(m.name)} `;
|
|
19
18
|
|
|
@@ -15,11 +15,11 @@ export const cwdSegment: SegmentDef = {
|
|
|
15
15
|
let git = ` ${icons.gitBranch} ${d.gitBranch}`;
|
|
16
16
|
text +=
|
|
17
17
|
theme.fg("dim", " ·") +
|
|
18
|
-
theme.fg(d.
|
|
18
|
+
theme.fg(d.accentColor, git);
|
|
19
19
|
|
|
20
20
|
if (d.gitDirty > 0) {
|
|
21
21
|
text += theme.fg(
|
|
22
|
-
|
|
22
|
+
"error",
|
|
23
23
|
` ${icons.gitDirty} ${d.gitDirty}`,
|
|
24
24
|
);
|
|
25
25
|
}
|
|
@@ -6,10 +6,7 @@ import type {
|
|
|
6
6
|
ThemeColor,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
DEFAULT_ICONS,
|
|
11
|
-
HEADER_TIPS,
|
|
12
|
-
} from "../config/constants";
|
|
9
|
+
import { DEFAULT_ICONS, HEADER_TIPS } from "../config/constants";
|
|
13
10
|
import type { Settings } from "../config/types";
|
|
14
11
|
import { fitFrameRow } from "./frame";
|
|
15
12
|
import { getShortCwd } from "../utils";
|
|
@@ -19,12 +16,7 @@ const MIN_BOX_WIDTH = 20;
|
|
|
19
16
|
const LEFT_COL_RATIO = 0.4; // logo column width / total width
|
|
20
17
|
|
|
21
18
|
/** BasicHeader preset styling — self-contained, not user-configurable. */
|
|
22
|
-
const LOGO_LINES = [
|
|
23
|
-
"█████████ ",
|
|
24
|
-
"███ ███ ",
|
|
25
|
-
"██████ ",
|
|
26
|
-
"███ ███",
|
|
27
|
-
];
|
|
19
|
+
const LOGO_LINES = ["█████████ ", "███ ███ ", "██████ ", "███ ███"];
|
|
28
20
|
const LOGO_COLOR: ThemeColor = "text";
|
|
29
21
|
const HEADING = "Welcome back!";
|
|
30
22
|
const SUBHEADING =
|
|
@@ -160,17 +152,15 @@ export function createHeader(
|
|
|
160
152
|
"",
|
|
161
153
|
"",
|
|
162
154
|
|
|
163
|
-
...wrapLines(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
).map((line) => theme.bold(theme.fg("muted", line))),
|
|
155
|
+
...wrapLines([HEADING], leftW - 2).map((line) =>
|
|
156
|
+
theme.bold(theme.fg("muted", line)),
|
|
157
|
+
),
|
|
167
158
|
|
|
168
159
|
"",
|
|
169
160
|
|
|
170
|
-
...wrapLines(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
).map((line) => theme.italic(theme.fg("muted", line))),
|
|
161
|
+
...wrapLines([SUBHEADING], leftW - 2).map((line) =>
|
|
162
|
+
theme.italic(theme.fg("muted", line)),
|
|
163
|
+
),
|
|
174
164
|
];
|
|
175
165
|
|
|
176
166
|
// Too narrow for a box → fall back to a plain centered logo.
|
|
@@ -6,7 +6,7 @@ export const notifySegment: SegmentDef = {
|
|
|
6
6
|
id: "notify",
|
|
7
7
|
slot: "topLeft",
|
|
8
8
|
render: (d, { icons, theme }) => {
|
|
9
|
-
const color = d.notifyEnabled
|
|
9
|
+
const color = d.notifyEnabled ? d.accentColor : "muted";
|
|
10
10
|
|
|
11
11
|
return theme.fg(color, ` ${d.notifyEnabled ? icons.bell : icons.bellOff} `);
|
|
12
12
|
},
|
|
@@ -12,9 +12,9 @@ export const spinnerSegment: SegmentDef = {
|
|
|
12
12
|
render: (d, { theme }) => {
|
|
13
13
|
let phase = d.spinnerPhase ?? "thinking";
|
|
14
14
|
|
|
15
|
-
// No frame.colors key for the spinner;
|
|
15
|
+
// No frame.colors key for the spinner; accent only.
|
|
16
16
|
const spinner = theme.fg(
|
|
17
|
-
d.
|
|
17
|
+
d.accentColor,
|
|
18
18
|
` ${d.spinnerFrame} `,
|
|
19
19
|
);
|
|
20
20
|
if (phase === "idle") return spinner;
|
|
@@ -44,13 +44,12 @@ export const tokenCountSegment: SegmentDef = {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const color2 = d.zenMode ? "muted" : color;
|
|
48
47
|
const used = tokens === null ? "?" : formatTokens(tokens);
|
|
49
48
|
|
|
50
49
|
return (
|
|
51
|
-
theme.fg(
|
|
50
|
+
theme.fg(color, ` ${icons.context} ctx ${pct} `) +
|
|
52
51
|
theme.fg("dim", "·") +
|
|
53
|
-
theme.fg(
|
|
52
|
+
theme.fg(color, ` ${used}/${formatWindow(window)} `) +
|
|
54
53
|
theme.fg("dim", "·") +
|
|
55
54
|
theme.fg(
|
|
56
55
|
"dim",
|
|
@@ -49,9 +49,6 @@ export interface FrameData {
|
|
|
49
49
|
spinnerPhase: SpinnerPhase | null;
|
|
50
50
|
thinkingLevel: string | undefined;
|
|
51
51
|
|
|
52
|
-
/** Master mute: when true, every segment except agentMode renders muted. */
|
|
53
|
-
zenMode: boolean;
|
|
54
|
-
|
|
55
52
|
/** To render the agent mode (from @leo-alvarenga/pi-agent-manager) segment, if any */
|
|
56
53
|
agentMode: AgentMode;
|
|
57
54
|
|
|
@@ -88,7 +85,6 @@ export interface SegmentDef {
|
|
|
88
85
|
export interface ExternalData {
|
|
89
86
|
cwd: string;
|
|
90
87
|
gitDirty: number;
|
|
91
|
-
zenMode: boolean;
|
|
92
88
|
agentMode: AgentMode;
|
|
93
89
|
theme: Theme | undefined;
|
|
94
90
|
gitBranch: string | undefined;
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-agent-manager integration (no hard dependency).
|
|
3
|
-
*/
|
|
4
|
-
/** pi-agent-manager integration (optional — no hard dependency). */
|
|
1
|
+
/** pi-agent-manager integration (optional: no hard dependency) */
|
|
5
2
|
export const PI_AGENT_MANAGER_AGENT_EVENT = "pi-agent-manager:agent-changed";
|
|
6
3
|
export const PI_AGENT_MANAGER_AGENT_DATA_KEY = "pi-agent-manager-agent";
|
|
7
4
|
|
|
8
|
-
/** pi-notify integration (optional
|
|
5
|
+
/** pi-notify integration (optional: no hard dependency) */
|
|
9
6
|
export const PI_NOTIFY_TOGGLE_EVENT = "pi-notify:toggle-changed";
|
|
10
7
|
|
|
11
|
-
/** Keybinding id users bind in keybindings.json to
|
|
12
|
-
export const
|
|
13
|
-
|
|
14
|
-
* keybindings.json; `[]` disables the shortcut, `/zen_mode` still works). */
|
|
15
|
-
export const ZEN_MODE_DEFAULT_KEY = "ctrl+shift+z";
|
|
8
|
+
/** Keybinding id users bind in keybindings.json to open the command palett. */
|
|
9
|
+
export const PALETTE_SHORTCUT_ID = "piZenFrame.palette";
|
|
10
|
+
export const PALETTE_DEFAULT_KEY = "alt+p";
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Constants, grouped by concern and re-exported from a single barrel.
|
|
3
|
-
* Import everything with `import { ... } from "../config/constants"` —
|
|
4
|
-
* resolution keeps working now that this is a directory.
|
|
5
|
-
*/
|
|
6
1
|
export * from "./file";
|
|
7
2
|
export * from "./contracts";
|
|
8
3
|
export * from "./visuals";
|
|
9
4
|
export * from "./messages";
|
|
10
|
-
export * from "./defaults";
|
|
5
|
+
export * from "./defaults";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { FrameIcons, SpinnerPhase } from "../types";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
/** Spinner animation frames per phase (pi-editor-shell style) */
|
|
4
5
|
export const SPINNER_FRAMES: Record<SpinnerPhase, string[]> = {
|
|
5
6
|
idle: ["⠃", "⠞", "⡵", "⠿", "⢹", "⠄"],
|
|
6
7
|
outputting: ["⠋", "⠙", "⠸", "⠴", "⠦", "⠇", "⠏"],
|
|
@@ -11,7 +12,7 @@ export const SPINNER_FRAMES: Record<SpinnerPhase, string[]> = {
|
|
|
11
12
|
// Sample ["░", "▒", "▓", "█", "▓", "▒"],
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
/** Nerd Font glyphs used by the blocky editor preset
|
|
15
|
+
/** Nerd Font glyphs used by the blocky editor preset */
|
|
15
16
|
export const DEFAULT_ICONS: FrameIcons = {
|
|
16
17
|
model: "",
|
|
17
18
|
folder: " ",
|
|
@@ -25,15 +26,15 @@ export const DEFAULT_ICONS: FrameIcons = {
|
|
|
25
26
|
bellOff: "",
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
/** Thinking level → theme token, mirroring pi's own border-color mapping
|
|
29
|
+
/** Thinking level → theme token, mirroring pi's own border-color mapping */
|
|
29
30
|
export const THINKING_TOKEN: Record<string, string> = {
|
|
30
|
-
off: "thinkingOff",
|
|
31
|
-
minimal: "thinkingMinimal",
|
|
32
31
|
low: "thinkingLow",
|
|
33
|
-
|
|
32
|
+
max: "thinkingMax",
|
|
33
|
+
off: "thinkingOff",
|
|
34
34
|
high: "thinkingHigh",
|
|
35
35
|
xhigh: "thinkingXhigh",
|
|
36
|
-
|
|
36
|
+
medium: "thinkingMedium",
|
|
37
|
+
minimal: "thinkingMinimal",
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
export const THEME_COLORS: Record<ThemeColor, true> = {
|
|
@@ -19,7 +19,7 @@ function loadSettingsFile(): Promise<string> {
|
|
|
19
19
|
return readFile(getResolvedSettingsFilePath(), "utf8");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** Validate a raw (possibly malformed) config against the schema shapes
|
|
22
|
+
/** Validate a raw (possibly malformed) config against the schema shapes */
|
|
23
23
|
function normalize(raw: unknown): Settings {
|
|
24
24
|
if (!raw || typeof raw !== "object") return DEFAULT_SETTINGS;
|
|
25
25
|
|
|
@@ -37,7 +37,6 @@ function normalize(raw: unknown): Settings {
|
|
|
37
37
|
const out: Settings = {
|
|
38
38
|
...DEFAULT_SETTINGS,
|
|
39
39
|
frame: { ...DEFAULT_SETTINGS.frame },
|
|
40
|
-
zenMode: bool(r.zenMode, d.zenMode ?? true),
|
|
41
40
|
editorFrame:
|
|
42
41
|
typeof r.editorFrame === "string" ? r.editorFrame : d.editorFrame,
|
|
43
42
|
};
|
|
@@ -95,6 +94,7 @@ function normalize(raw: unknown): Settings {
|
|
|
95
94
|
export async function loadSettings(): Promise<Settings> {
|
|
96
95
|
try {
|
|
97
96
|
const raw = JSON.parse(await loadSettingsFile()) as unknown;
|
|
97
|
+
|
|
98
98
|
return normalize(raw);
|
|
99
99
|
} catch {
|
|
100
100
|
return DEFAULT_SETTINGS;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
/** Streaming phase used to drive the status-animation spinner
|
|
3
|
+
/** Streaming phase used to drive the status-animation spinner */
|
|
4
4
|
export type SpinnerPhase =
|
|
5
5
|
"thinking" | "outputting" | "toolcall" | "exec" | "idle";
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ export interface BannerTip {
|
|
|
9
9
|
text: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/** Nerd-font / glyph icons shown in the band and status rows. Overridable
|
|
12
|
+
/** Nerd-font / glyph icons shown in the band and status rows. Overridable */
|
|
13
13
|
export interface FrameIcons {
|
|
14
14
|
folder: string;
|
|
15
15
|
model: string;
|
|
@@ -24,9 +24,9 @@ export interface FrameIcons {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* ``frame
|
|
27
|
+
* ``frame``: the editor frame preset. Styling and layout (prefix, colors,
|
|
28
28
|
* margins, paddings) are owned by the renderer; only these behavioral
|
|
29
|
-
* toggles are configurable
|
|
29
|
+
* toggles are configurable
|
|
30
30
|
*/
|
|
31
31
|
export interface FrameSettings {
|
|
32
32
|
enable?: boolean;
|
|
@@ -44,7 +44,7 @@ export interface FrameSettings {
|
|
|
44
44
|
showSpinner?: boolean;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** `header
|
|
47
|
+
/** `header`: the top-line header, which can show a logo or other text */
|
|
48
48
|
export interface HeaderSettings {
|
|
49
49
|
enable: boolean;
|
|
50
50
|
|
|
@@ -52,15 +52,15 @@ export interface HeaderSettings {
|
|
|
52
52
|
type?: string;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/** `workingMessage
|
|
55
|
+
/** `workingMessage`: rotating messages in pi's built-in working loader */
|
|
56
56
|
export interface WorkingMessageSettings {
|
|
57
|
-
/** Toggle the rotating messages. Default true
|
|
57
|
+
/** Toggle the rotating messages. Default true */
|
|
58
58
|
enable?: boolean;
|
|
59
59
|
|
|
60
|
-
/** How often (ms) the message is replaced. Default 3000
|
|
60
|
+
/** How often (ms) the message is replaced. Default 3000 */
|
|
61
61
|
intervalMs?: number;
|
|
62
62
|
|
|
63
|
-
/** Custom message pool; replaces the default 30
|
|
63
|
+
/** Custom message pool; replaces the default 30 */
|
|
64
64
|
messages?: string[];
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -69,14 +69,9 @@ export interface Settings {
|
|
|
69
69
|
header?: HeaderSettings;
|
|
70
70
|
workingMessage?: WorkingMessageSettings;
|
|
71
71
|
|
|
72
|
-
/** Editor-frame renderer by registered name. Default "blocky"
|
|
72
|
+
/** Editor-frame renderer by registered name. Default "blocky" */
|
|
73
73
|
editorFrame?: string;
|
|
74
74
|
|
|
75
|
-
/** Master mute: every segment except agent-mode renders muted.
|
|
76
|
-
* Default true. Toggle at runtime with `/zen_mode` or the `piZenFrame.zenMode`
|
|
77
|
-
* keybinding (default `ctrl+shift+z`). */
|
|
78
|
-
zenMode?: boolean;
|
|
79
|
-
|
|
80
75
|
/** Accent color for segment highlights; Defaults to 'accent' */
|
|
81
76
|
accentColor?: ThemeColor;
|
|
82
77
|
}
|
|
@@ -1,34 +1,29 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BlockyEditor — the editor-frame renderer. Subclasses CustomEditor so all of
|
|
3
|
-
* pi's editing/undo/app-keybinding machinery keeps working; `render` layers
|
|
4
|
-
* the bg band. Vim behavior was removed — all input passes through to the
|
|
5
|
-
* base editor. Appearance lives in `components/` (segments + frame).
|
|
6
|
-
*/
|
|
7
1
|
import {
|
|
8
2
|
CustomEditor,
|
|
9
3
|
ThemeColor,
|
|
10
4
|
type ExtensionAPI,
|
|
11
5
|
} from "@earendil-works/pi-coding-agent";
|
|
12
6
|
|
|
13
|
-
import { DEFAULT_ICONS, SPINNER_FRAMES } from "../config/constants";
|
|
14
7
|
import type { SpinnerPhase } from "../config/types";
|
|
15
|
-
import {
|
|
16
|
-
import { segmentsFor } from "../components/registry";
|
|
17
|
-
import type {
|
|
18
|
-
ExternalData,
|
|
19
|
-
FrameData,
|
|
20
|
-
SegmentContext,
|
|
21
|
-
} from "../components/types";
|
|
8
|
+
import type { ExternalData, FrameData } from "../components/types";
|
|
22
9
|
import type { EditorFrameRenderOptions } from "../renderers/types";
|
|
23
10
|
|
|
11
|
+
import {
|
|
12
|
+
handlePaletteInput,
|
|
13
|
+
loadPaletteItems,
|
|
14
|
+
renderPalette,
|
|
15
|
+
type PaletteState,
|
|
16
|
+
} from "./palette";
|
|
17
|
+
import { renderEditorFrame } from "./render";
|
|
18
|
+
import { SpinnerController } from "./spinner";
|
|
19
|
+
|
|
24
20
|
export class BlockyEditor extends CustomEditor {
|
|
25
21
|
private pi: ExtensionAPI;
|
|
26
22
|
private opts: EditorFrameRenderOptions;
|
|
27
23
|
private provider: (pi: ExtensionAPI) => ExternalData;
|
|
28
24
|
|
|
29
|
-
private
|
|
30
|
-
private
|
|
31
|
-
private spinnerTimer: ReturnType<typeof setInterval> | null = null;
|
|
25
|
+
private spinner = new SpinnerController();
|
|
26
|
+
private paletteState: PaletteState | null = null;
|
|
32
27
|
|
|
33
28
|
constructor(
|
|
34
29
|
pi: ExtensionAPI,
|
|
@@ -37,149 +32,106 @@ export class BlockyEditor extends CustomEditor {
|
|
|
37
32
|
...args: ConstructorParameters<typeof CustomEditor>
|
|
38
33
|
) {
|
|
39
34
|
super(...args);
|
|
40
|
-
|
|
41
35
|
this.pi = pi;
|
|
42
36
|
this.opts = opts;
|
|
43
37
|
this.provider = provider;
|
|
44
38
|
}
|
|
45
39
|
|
|
46
40
|
setSpinner(phase: SpinnerPhase | null): void {
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (phase && SPINNER_FRAMES[phase]) {
|
|
54
|
-
const frames = SPINNER_FRAMES[phase];
|
|
55
|
-
|
|
56
|
-
this.spinnerTimer = setInterval(() => {
|
|
57
|
-
this.spinnerIdx = (this.spinnerIdx + 1) % frames.length;
|
|
58
|
-
this.tui.requestRender();
|
|
59
|
-
}, 80);
|
|
41
|
+
if (phase) {
|
|
42
|
+
this.spinner.start(phase, () => this.tui.requestRender());
|
|
43
|
+
} else {
|
|
44
|
+
this.spinner.stop();
|
|
45
|
+
this.tui.requestRender();
|
|
60
46
|
}
|
|
61
|
-
|
|
62
|
-
this.tui.requestRender();
|
|
63
47
|
}
|
|
64
48
|
|
|
65
49
|
stopSpinner(): void {
|
|
66
|
-
this.
|
|
67
|
-
this.spinnerPhase = null;
|
|
50
|
+
this.spinner.stop();
|
|
68
51
|
}
|
|
69
52
|
|
|
70
53
|
refresh(): void {
|
|
71
54
|
this.tui.requestRender();
|
|
72
55
|
}
|
|
73
56
|
|
|
74
|
-
|
|
57
|
+
openPalette(): void {
|
|
58
|
+
const all = loadPaletteItems(this.pi);
|
|
59
|
+
|
|
60
|
+
this.paletteState = { query: "", all, items: all, selected: 0, viewTop: 0 };
|
|
61
|
+
this.tui.requestRender();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override handleInput(data: string): void {
|
|
65
|
+
if (!this.paletteState) {
|
|
66
|
+
super.handleInput(data);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const result = handlePaletteInput(this.paletteState, data);
|
|
71
|
+
|
|
72
|
+
if (result.submit) {
|
|
73
|
+
this.paletteState = null;
|
|
74
|
+
this.setText(result.submit);
|
|
75
|
+
super.handleInput("\r");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.paletteState = result.next;
|
|
80
|
+
this.tui.requestRender();
|
|
81
|
+
}
|
|
75
82
|
|
|
76
83
|
render(width: number): string[] {
|
|
84
|
+
if (this.paletteState) {
|
|
85
|
+
const ext = this.provider(this.pi);
|
|
86
|
+
if (ext.theme) {
|
|
87
|
+
return renderPalette(this.paletteState, width, ext.theme);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
77
91
|
const frame = this.opts.frame;
|
|
78
92
|
|
|
79
93
|
const padX = Math.min(2, Math.max(0, Math.floor(width / 2)));
|
|
80
|
-
|
|
81
94
|
const marginX = Math.min(1, Math.max(0, Math.floor(width / 2)));
|
|
82
95
|
|
|
83
96
|
const contentWidth = width - marginX * 2;
|
|
84
97
|
const innerWidth = width - padX * 2 - marginX * 2;
|
|
98
|
+
|
|
85
99
|
if (innerWidth < 8) return super.render(width);
|
|
86
100
|
|
|
87
101
|
const ext = this.provider(this.pi);
|
|
102
|
+
|
|
103
|
+
// No theme (non-TUI) or frame disabled or too narrow → plain editor.
|
|
104
|
+
if (!ext.theme || !frame.enable || contentWidth < (frame.minWidth ?? 20)) {
|
|
105
|
+
return super.render(width);
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
const d: FrameData = {
|
|
89
109
|
cwd: ext.cwd,
|
|
90
110
|
context: ext.context,
|
|
91
|
-
zenMode: ext.zenMode,
|
|
92
111
|
gitDirty: ext.gitDirty,
|
|
93
112
|
agentMode: ext.agentMode,
|
|
94
113
|
gitBranch: ext.gitBranch,
|
|
95
114
|
modelName: ext.modelName,
|
|
96
115
|
spinnerPhase: ext.spinnerPhase,
|
|
97
116
|
modelProvider: ext.modelProvider,
|
|
117
|
+
notifyEnabled: ext.notifyEnabled,
|
|
98
118
|
thinkingLevel: ext.thinkingLevel,
|
|
99
|
-
spinnerFrame: this.
|
|
119
|
+
spinnerFrame: this.spinner.frame(),
|
|
100
120
|
accentColor: this.opts.accentColor,
|
|
101
|
-
notifyEnabled: ext.notifyEnabled,
|
|
102
121
|
};
|
|
103
122
|
|
|
104
123
|
let prefix = "┃";
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return super.render(width);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const inner = super.render(innerWidth);
|
|
118
|
-
|
|
119
|
-
let bottomIdx = inner.length - 1;
|
|
120
|
-
for (let i = inner.length - 1; i >= 0; i--) {
|
|
121
|
-
if (isBorderRow(inner[i]!)) {
|
|
122
|
-
bottomIdx = i;
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const content = inner.slice(1, bottomIdx);
|
|
128
|
-
const autocomplete = inner.slice(bottomIdx + 1);
|
|
129
|
-
|
|
130
|
-
const bgSgr = ext.theme.getBgAnsi("customMessageBg");
|
|
131
|
-
const paint = (row: string) =>
|
|
132
|
-
bgSgr + row.split("\x1b[0m").join(`\x1b[0m${bgSgr}`) + "\x1b[0m";
|
|
133
|
-
|
|
134
|
-
const ctx: SegmentContext = {
|
|
135
|
-
cfg: frame,
|
|
136
|
-
theme: ext.theme,
|
|
137
|
-
icons: DEFAULT_ICONS,
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const box = composeBand(content, autocomplete, {
|
|
141
|
-
padX,
|
|
142
|
-
paint,
|
|
124
|
+
const prefixColor: ThemeColor =
|
|
125
|
+
ext.agentMode?.color ?? this.opts.accentColor ?? "text";
|
|
126
|
+
prefix = ext.theme.fg(prefixColor, prefix);
|
|
127
|
+
|
|
128
|
+
return renderEditorFrame(
|
|
129
|
+
(innerWidth) => super.render(innerWidth),
|
|
130
|
+
this.opts,
|
|
131
|
+
ext,
|
|
132
|
+
d,
|
|
143
133
|
prefix,
|
|
144
|
-
width
|
|
145
|
-
marginX: 1,
|
|
146
|
-
paddingTop: 1,
|
|
147
|
-
paddingBottom: 1,
|
|
148
|
-
boxBottom: fitInfoRow(
|
|
149
|
-
segmentsFor("topLeft", d, ctx),
|
|
150
|
-
segmentsFor("topRight", d, ctx),
|
|
151
|
-
innerWidth,
|
|
152
|
-
),
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const pseudoFooter = fitInfoRow(
|
|
156
|
-
segmentsFor("bottomLeft", d, ctx),
|
|
157
|
-
segmentsFor("bottomRight", d, ctx),
|
|
158
|
-
contentWidth,
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
// Blank rows OUTSIDE the band, above/below it.
|
|
162
|
-
const marginRow = " ".repeat(contentWidth);
|
|
163
|
-
const marginTop: string[] = [];
|
|
164
|
-
const marginBottom: string[] = [];
|
|
165
|
-
|
|
166
|
-
return [...marginTop, ...box, marginRow, pseudoFooter, ...marginBottom].map(
|
|
167
|
-
(row) => " ".repeat(marginX) + row + " ".repeat(marginX),
|
|
134
|
+
width,
|
|
168
135
|
);
|
|
169
136
|
}
|
|
170
|
-
|
|
171
|
-
private spinnerFrame(): string {
|
|
172
|
-
if (!this.spinnerPhase) return "";
|
|
173
|
-
|
|
174
|
-
const frames = SPINNER_FRAMES[this.spinnerPhase]!;
|
|
175
|
-
|
|
176
|
-
return frames[this.spinnerIdx % frames.length]!;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private clearSpinnerTimer(): void {
|
|
180
|
-
if (this.spinnerTimer) {
|
|
181
|
-
clearInterval(this.spinnerTimer);
|
|
182
|
-
this.spinnerTimer = null;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
137
|
}
|