@leo-alvarenga/pi-zen-frame 0.20.0 → 0.21.2
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 +9 -9
- package/extensions/config/constants/defaults.ts +2 -2
- package/extensions/config/constants/visuals.ts +9 -0
- package/extensions/config/types.ts +1 -1
- package/extensions/editor/{blocky-editor.ts → blocky/blocky-editor.ts} +5 -5
- package/extensions/editor/blocky/index.ts +1 -0
- package/extensions/editor/{render.ts → blocky/render.ts} +5 -5
- package/extensions/editor/index.ts +3 -0
- package/extensions/editor/linear/components/index.ts +44 -0
- package/extensions/editor/linear/index.ts +1 -0
- package/extensions/editor/linear/linear-editor.ts +122 -0
- package/extensions/editor/linear/render.ts +84 -0
- package/extensions/editor/{minimalist-editor.ts → minimalist/index.ts} +4 -4
- package/extensions/{components/header.ts → headers/box/index.ts} +23 -63
- package/extensions/headers/index.ts +2 -0
- package/extensions/headers/types.ts +9 -0
- package/extensions/index.ts +29 -17
- package/extensions/renderers/registry.ts +10 -5
- package/extensions/renderers/types.ts +2 -1
- package/extensions/utils/data.ts +11 -0
- package/extensions/utils/index.ts +3 -1
- package/extensions/utils/tui.ts +36 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Configuration is loaded from `~/.pi/agent/pi-zen-frame.json`. All properties are
|
|
|
43
43
|
// Fallback accent color for frame border and active segments
|
|
44
44
|
"accentColor": "accent",
|
|
45
45
|
|
|
46
|
-
// Editor-frame renderer by registered name (built-in: "blocky", "minimalist")
|
|
46
|
+
// Editor-frame renderer by registered name (built-in: "blocky", "linear", "minimalist")
|
|
47
47
|
"editorFrame": "blocky",
|
|
48
48
|
|
|
49
49
|
"header": {
|
|
@@ -88,14 +88,14 @@ Configuration is loaded from `~/.pi/agent/pi-zen-frame.json`. All properties are
|
|
|
88
88
|
|
|
89
89
|
The editor frame shows status indicators in two rows below the editor:
|
|
90
90
|
|
|
91
|
-
| Location
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
|
|
|
97
|
-
| Footer (left)
|
|
98
|
-
| Footer (right)
|
|
91
|
+
| Location | Segment | Description |
|
|
92
|
+
| -------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
93
|
+
| Top | **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 |
|
|
96
|
+
| | **spinner** | Active phase indicator (`thinking`, `outputting`, `toolcall`, `exec`); replaces the row while streaming when `showSpinner` is enabled (default off) |
|
|
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 |
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
|
@@ -3,11 +3,11 @@ import { WORKING_MESSAGES } from "./messages";
|
|
|
3
3
|
|
|
4
4
|
export const DEFAULT_SETTINGS: Settings = {
|
|
5
5
|
accentColor: "accent",
|
|
6
|
-
editorFrame: "
|
|
6
|
+
editorFrame: "linear",
|
|
7
7
|
|
|
8
8
|
header: {
|
|
9
|
+
type: "box",
|
|
9
10
|
enable: false,
|
|
10
|
-
type: "basic",
|
|
11
11
|
},
|
|
12
12
|
|
|
13
13
|
workingMessage: {
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { FrameIcons, SpinnerPhase } from "../types";
|
|
3
3
|
|
|
4
|
+
export const LOGO_LINES = [
|
|
5
|
+
"█████████ ",
|
|
6
|
+
"███ ███ ",
|
|
7
|
+
"██████ ",
|
|
8
|
+
"███ ███",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export const LOGO_COLOR: ThemeColor = "text";
|
|
12
|
+
|
|
4
13
|
/** Spinner animation frames per phase (pi-editor-shell style) */
|
|
5
14
|
export const SPINNER_FRAMES: Record<SpinnerPhase, string[]> = {
|
|
6
15
|
idle: ["⠃", "⠞", "⡵", "⠿", "⢹", "⠄"],
|
|
@@ -4,18 +4,18 @@ import {
|
|
|
4
4
|
type ExtensionAPI,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
|
|
7
|
-
import type { SpinnerPhase } from "
|
|
8
|
-
import type { ExternalData, FrameData } from "
|
|
9
|
-
import type { EditorFrameRenderOptions } from "
|
|
7
|
+
import type { SpinnerPhase } from "../../config/types";
|
|
8
|
+
import type { ExternalData, FrameData } from "../../components/types";
|
|
9
|
+
import type { EditorFrameRenderOptions } from "../../renderers/types";
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
handlePaletteInput,
|
|
13
13
|
loadPaletteItems,
|
|
14
14
|
renderPalette,
|
|
15
15
|
type PaletteState,
|
|
16
|
-
} from "
|
|
16
|
+
} from "../palette";
|
|
17
17
|
import { renderEditorFrame } from "./render";
|
|
18
|
-
import { SpinnerController } from "
|
|
18
|
+
import { SpinnerController } from "../spinner";
|
|
19
19
|
|
|
20
20
|
export class BlockyEditor extends CustomEditor {
|
|
21
21
|
private pi: ExtensionAPI;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BlockyEditor } from "./blocky-editor";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { DEFAULT_ICONS } from "
|
|
2
|
-
import { composeBand, fitInfoRow, isBorderRow } from "
|
|
3
|
-
import { segmentsFor } from "
|
|
1
|
+
import { DEFAULT_ICONS } from "../../config/constants";
|
|
2
|
+
import { composeBand, fitInfoRow, isBorderRow } from "../../components/frame";
|
|
3
|
+
import { segmentsFor } from "../../components/registry";
|
|
4
4
|
import type {
|
|
5
5
|
ExternalData,
|
|
6
6
|
FrameData,
|
|
7
7
|
SegmentContext,
|
|
8
|
-
} from "
|
|
9
|
-
import type { EditorFrameRenderOptions } from "
|
|
8
|
+
} from "../../components/types";
|
|
9
|
+
import type { EditorFrameRenderOptions } from "../../renderers/types";
|
|
10
10
|
|
|
11
11
|
export function renderEditorFrame(
|
|
12
12
|
getInnerLines: (innerWidth: number) => string[],
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { agentModeSegment } from "../../../components/agent-mode";
|
|
2
|
+
import { cwdSegment } from "../../../components/cwd";
|
|
3
|
+
import { modelSegment } from "../../../components/model";
|
|
4
|
+
import { notifySegment } from "../../../components/notify";
|
|
5
|
+
import { reasoningSegment } from "../../../components/reasoning";
|
|
6
|
+
import { spinnerSegment } from "../../../components/spinner";
|
|
7
|
+
import { tokenCountSegment } from "../../../components/token-count";
|
|
8
|
+
import type {
|
|
9
|
+
FrameData,
|
|
10
|
+
SegmentContext,
|
|
11
|
+
SegmentDef,
|
|
12
|
+
Slot,
|
|
13
|
+
} from "../../../components/types";
|
|
14
|
+
|
|
15
|
+
// Re-wire global segments onto LinearEditor's slot layout:
|
|
16
|
+
// topLeft / topRight → above-band info row
|
|
17
|
+
// bottomLeft → below-band info row
|
|
18
|
+
const segments: SegmentDef[] = [
|
|
19
|
+
{ ...agentModeSegment, slot: "topLeft" },
|
|
20
|
+
{ ...notifySegment, slot: "topLeft" },
|
|
21
|
+
{ ...modelSegment, slot: "topLeft" },
|
|
22
|
+
{ ...reasoningSegment, slot: "topLeft" },
|
|
23
|
+
{ ...spinnerSegment, slot: "topLeft" },
|
|
24
|
+
{ ...tokenCountSegment, slot: "topRight" },
|
|
25
|
+
{ ...cwdSegment, slot: "bottomLeft" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function linearSegmentsFor(
|
|
29
|
+
slot: Slot,
|
|
30
|
+
d: FrameData,
|
|
31
|
+
ctx: SegmentContext,
|
|
32
|
+
): string {
|
|
33
|
+
const defs = segments.filter(
|
|
34
|
+
(s) => s.slot === slot && (!s.enabled || s.enabled(d, ctx.cfg)),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const replacer = defs.find((s) => s.replaces?.(d, ctx.cfg));
|
|
38
|
+
const active = replacer ? [replacer] : defs;
|
|
39
|
+
|
|
40
|
+
return active
|
|
41
|
+
.map((s) => s.render(d, ctx))
|
|
42
|
+
.filter((t) => t !== "")
|
|
43
|
+
.join(ctx.theme.fg("dim", "·"));
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LinearEditor } from "./linear-editor";
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CustomEditor,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import type { SpinnerPhase } from "../../config/types";
|
|
7
|
+
import type { ExternalData, FrameData } from "../../components/types";
|
|
8
|
+
import type {
|
|
9
|
+
EditorFrameRenderer,
|
|
10
|
+
EditorFrameRenderOptions,
|
|
11
|
+
} from "../../renderers/types";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
handlePaletteInput,
|
|
15
|
+
loadPaletteItems,
|
|
16
|
+
renderPalette,
|
|
17
|
+
type PaletteState,
|
|
18
|
+
} from "../palette";
|
|
19
|
+
import { SpinnerController } from "../spinner";
|
|
20
|
+
import { renderLinearFrame } from "./render";
|
|
21
|
+
|
|
22
|
+
export class LinearEditor extends CustomEditor implements EditorFrameRenderer {
|
|
23
|
+
private pi: ExtensionAPI;
|
|
24
|
+
private opts: EditorFrameRenderOptions;
|
|
25
|
+
private spinner = new SpinnerController();
|
|
26
|
+
private paletteState: PaletteState | null = null;
|
|
27
|
+
private provider: (pi: ExtensionAPI) => ExternalData;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
pi: ExtensionAPI,
|
|
31
|
+
provider: (pi: ExtensionAPI) => ExternalData,
|
|
32
|
+
opts: EditorFrameRenderOptions,
|
|
33
|
+
...args: ConstructorParameters<typeof CustomEditor>
|
|
34
|
+
) {
|
|
35
|
+
super(...args);
|
|
36
|
+
|
|
37
|
+
this.pi = pi;
|
|
38
|
+
this.opts = opts;
|
|
39
|
+
this.provider = provider;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
refresh(): void {
|
|
43
|
+
this.tui.requestRender();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
openPalette(): void {
|
|
47
|
+
const all = loadPaletteItems(this.pi);
|
|
48
|
+
this.paletteState = { query: "", all, items: all, selected: 0, viewTop: 0 };
|
|
49
|
+
this.tui.requestRender();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setSpinner(phase: SpinnerPhase | null): void {
|
|
53
|
+
if (phase) {
|
|
54
|
+
this.spinner.start(phase, () => this.tui.requestRender());
|
|
55
|
+
} else {
|
|
56
|
+
this.spinner.stop();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
stopSpinner(): void {
|
|
61
|
+
this.spinner.stop();
|
|
62
|
+
this.tui.requestRender();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override handleInput(data: string): void {
|
|
66
|
+
if (!this.paletteState) {
|
|
67
|
+
super.handleInput(data);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = handlePaletteInput(this.paletteState, data);
|
|
72
|
+
if (result.submit) {
|
|
73
|
+
this.paletteState = null;
|
|
74
|
+
this.setText(result.submit);
|
|
75
|
+
super.handleInput("\r");
|
|
76
|
+
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.paletteState = result.next;
|
|
81
|
+
this.tui.requestRender();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override render(width: number): string[] {
|
|
85
|
+
if (this.paletteState) {
|
|
86
|
+
const ext = this.provider(this.pi);
|
|
87
|
+
if (ext.theme) return renderPalette(this.paletteState, width, ext.theme);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { frame } = this.opts;
|
|
91
|
+
const marginX = Math.min(1, Math.max(0, Math.floor(width / 2)));
|
|
92
|
+
const contentWidth = width - marginX * 2;
|
|
93
|
+
|
|
94
|
+
if (contentWidth < (frame.minWidth ?? 20)) return super.render(width);
|
|
95
|
+
|
|
96
|
+
const ext = this.provider(this.pi);
|
|
97
|
+
if (!ext.theme || !frame.enable) return super.render(width);
|
|
98
|
+
|
|
99
|
+
const d: FrameData = {
|
|
100
|
+
cwd: ext.cwd,
|
|
101
|
+
context: ext.context,
|
|
102
|
+
gitDirty: ext.gitDirty,
|
|
103
|
+
agentMode: ext.agentMode,
|
|
104
|
+
gitBranch: ext.gitBranch,
|
|
105
|
+
modelName: ext.modelName,
|
|
106
|
+
spinnerPhase: ext.spinnerPhase,
|
|
107
|
+
modelProvider: ext.modelProvider,
|
|
108
|
+
notifyEnabled: ext.notifyEnabled,
|
|
109
|
+
thinkingLevel: ext.thinkingLevel,
|
|
110
|
+
spinnerFrame: this.spinner.frame(),
|
|
111
|
+
accentColor: this.opts.accentColor,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return renderLinearFrame(
|
|
115
|
+
(innerWidth) => super.render(innerWidth),
|
|
116
|
+
this.opts,
|
|
117
|
+
ext,
|
|
118
|
+
d,
|
|
119
|
+
width,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { DEFAULT_ICONS } from "../../config/constants";
|
|
2
|
+
import { fitInfoRow, isBorderRow } from "../../components/frame";
|
|
3
|
+
import type {
|
|
4
|
+
ExternalData,
|
|
5
|
+
FrameData,
|
|
6
|
+
SegmentContext,
|
|
7
|
+
} from "../../components/types";
|
|
8
|
+
import type { EditorFrameRenderOptions } from "../../renderers/types";
|
|
9
|
+
import { linearSegmentsFor } from "./components";
|
|
10
|
+
|
|
11
|
+
const LINEAR_PREFIX = "❯";
|
|
12
|
+
|
|
13
|
+
export function renderLinearFrame(
|
|
14
|
+
getInnerLines: (innerWidth: number) => string[],
|
|
15
|
+
opts: EditorFrameRenderOptions,
|
|
16
|
+
ext: ExternalData,
|
|
17
|
+
d: FrameData,
|
|
18
|
+
width: number,
|
|
19
|
+
): string[] {
|
|
20
|
+
const marginX = Math.min(1, Math.max(0, Math.floor(width / 2)));
|
|
21
|
+
const contentWidth = width - marginX * 2;
|
|
22
|
+
|
|
23
|
+
const innerWidth = contentWidth - LINEAR_PREFIX.length;
|
|
24
|
+
|
|
25
|
+
const inner = getInnerLines(innerWidth);
|
|
26
|
+
|
|
27
|
+
let bottomIdx = inner.length - 1;
|
|
28
|
+
for (let i = inner.length - 1; i >= 0; i--) {
|
|
29
|
+
if (isBorderRow(inner[i]!)) {
|
|
30
|
+
bottomIdx = i;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const content = inner.slice(1, bottomIdx);
|
|
36
|
+
const autocomplete = inner.slice(bottomIdx + 1);
|
|
37
|
+
|
|
38
|
+
const ctx: SegmentContext = {
|
|
39
|
+
cfg: opts.frame,
|
|
40
|
+
theme: ext.theme!,
|
|
41
|
+
icons: DEFAULT_ICONS,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const blankRow = " ".repeat(width);
|
|
45
|
+
const regularPrefix = " ".repeat(marginX);
|
|
46
|
+
const borderRow = ext.theme!.fg("dim", "─".repeat(width));
|
|
47
|
+
const accentPrefix = ext.theme!.fg(opts.accentColor, LINEAR_PREFIX);
|
|
48
|
+
|
|
49
|
+
const withMargin = (row: string) => regularPrefix + row + regularPrefix;
|
|
50
|
+
|
|
51
|
+
const rows: string[] = [
|
|
52
|
+
blankRow,
|
|
53
|
+
|
|
54
|
+
withMargin(
|
|
55
|
+
fitInfoRow(
|
|
56
|
+
linearSegmentsFor("topLeft", d, ctx),
|
|
57
|
+
linearSegmentsFor("topRight", d, ctx),
|
|
58
|
+
contentWidth,
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
|
|
62
|
+
borderRow,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
content.forEach((row, i) => {
|
|
66
|
+
rows.push(withMargin((i === 0 ? accentPrefix : regularPrefix) + row));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
autocomplete.forEach((row) => rows.push(withMargin(regularPrefix + row)));
|
|
70
|
+
|
|
71
|
+
rows.push(
|
|
72
|
+
borderRow,
|
|
73
|
+
|
|
74
|
+
withMargin(
|
|
75
|
+
fitInfoRow(
|
|
76
|
+
linearSegmentsFor("bottomLeft", d, ctx),
|
|
77
|
+
linearSegmentsFor("bottomRight", d, ctx),
|
|
78
|
+
contentWidth,
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return rows;
|
|
84
|
+
}
|
|
@@ -3,19 +3,19 @@ import {
|
|
|
3
3
|
type ExtensionAPI,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
|
-
import type { SpinnerPhase } from "
|
|
7
|
-
import type { ExternalData } from "
|
|
6
|
+
import type { SpinnerPhase } from "../../config/types";
|
|
7
|
+
import type { ExternalData } from "../../components/types";
|
|
8
8
|
import type {
|
|
9
9
|
EditorFrameRenderer,
|
|
10
10
|
EditorFrameRenderOptions,
|
|
11
|
-
} from "
|
|
11
|
+
} from "../../renderers/types";
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
14
|
handlePaletteInput,
|
|
15
15
|
loadPaletteItems,
|
|
16
16
|
renderPalette,
|
|
17
17
|
type PaletteState,
|
|
18
|
-
} from "
|
|
18
|
+
} from "../palette";
|
|
19
19
|
|
|
20
20
|
export class MinimalistEditor
|
|
21
21
|
extends CustomEditor
|
|
@@ -1,52 +1,33 @@
|
|
|
1
1
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
2
|
-
import { visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
2
|
import type {
|
|
4
3
|
ExtensionAPI,
|
|
5
4
|
Theme,
|
|
6
5
|
ThemeColor,
|
|
7
6
|
} from "@earendil-works/pi-coding-agent";
|
|
8
7
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_ICONS,
|
|
10
|
+
HEADER_TIPS,
|
|
11
|
+
LOGO_COLOR,
|
|
12
|
+
LOGO_LINES,
|
|
13
|
+
} from "../../config/constants";
|
|
14
|
+
import type { Settings } from "../../config/types";
|
|
15
|
+
import { fitFrameRow } from "../../components/frame";
|
|
16
|
+
import { getShortCwd, splitRow, wrapLines } from "../../utils";
|
|
17
|
+
import { HeaderEnv } from "../types";
|
|
18
|
+
|
|
19
|
+
/** Below which terminal width the box is skipped (plain logo) */
|
|
15
20
|
const MIN_BOX_WIDTH = 20;
|
|
16
21
|
const LEFT_COL_RATIO = 0.4; // logo column width / total width
|
|
17
22
|
|
|
18
|
-
/** BasicHeader preset styling — self-contained, not user-configurable. */
|
|
19
|
-
const LOGO_LINES = ["█████████ ", "███ ███ ", "██████ ", "███ ███"];
|
|
20
|
-
const LOGO_COLOR: ThemeColor = "text";
|
|
21
23
|
const HEADING = "Welcome back!";
|
|
22
24
|
const SUBHEADING =
|
|
23
25
|
"Ready for your next session? Terminal warm, context clean, tools ready to execute";
|
|
24
26
|
|
|
25
|
-
/** Live env snapshot the header renders in the right column. */
|
|
26
|
-
export interface HeaderEnv {
|
|
27
|
-
gitBranch: string | undefined;
|
|
28
|
-
gitDirty: number;
|
|
29
|
-
cwd: string;
|
|
30
|
-
|
|
31
|
-
/** Combined display name, e.g. `"Model (Provider)"` (provider embedded). */
|
|
32
|
-
modelName: string | undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
27
|
const RANDOM_TIP =
|
|
36
28
|
HEADER_TIPS[Math.floor(Math.random() * HEADER_TIPS.length)]?.text ?? "";
|
|
37
29
|
|
|
38
|
-
function
|
|
39
|
-
lines: string[],
|
|
40
|
-
maxLen: number,
|
|
41
|
-
style?: (line: string) => string,
|
|
42
|
-
): string[] {
|
|
43
|
-
const wrapped = lines.map((l) => wrapTextWithAnsi(l, maxLen)).flat();
|
|
44
|
-
if (style) return wrapped.map((l) => (l.length ? style(l) : l));
|
|
45
|
-
|
|
46
|
-
return wrapped;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function createHeader(
|
|
30
|
+
export function createBoxHeader(
|
|
50
31
|
_tui: TUI,
|
|
51
32
|
theme: Theme,
|
|
52
33
|
pi: ExtensionAPI,
|
|
@@ -66,36 +47,13 @@ export function createHeader(
|
|
|
66
47
|
const accentColor = settings.accentColor ?? "accent";
|
|
67
48
|
|
|
68
49
|
const logo = {
|
|
69
|
-
lines: LOGO_LINES,
|
|
70
50
|
color: LOGO_COLOR,
|
|
51
|
+
lines: LOGO_LINES,
|
|
71
52
|
};
|
|
72
53
|
|
|
73
54
|
const border = (s: string, fg?: ThemeColor) => theme.fg(fg ?? accentColor, s);
|
|
74
55
|
|
|
75
|
-
/**
|
|
76
|
-
function center(text: string, inner: number): string {
|
|
77
|
-
const w = visibleWidth(text);
|
|
78
|
-
const left = Math.max(0, Math.floor((inner - w) / 2));
|
|
79
|
-
const right = Math.max(0, inner - w - left);
|
|
80
|
-
|
|
81
|
-
return " ".repeat(left) + text + " ".repeat(right);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Claude-style split row: │ logo (leftW) │ info (rightW) │, exactly `width` cols. */
|
|
85
|
-
function splitRow(
|
|
86
|
-
leftW: number,
|
|
87
|
-
rightW: number,
|
|
88
|
-
left: string,
|
|
89
|
-
right: string,
|
|
90
|
-
): string {
|
|
91
|
-
const left2 = center(left, leftW);
|
|
92
|
-
const right2 = " " + right;
|
|
93
|
-
const pad = " ".repeat(Math.max(0, rightW - visibleWidth(right2)));
|
|
94
|
-
|
|
95
|
-
return border("│") + left2 + border("│") + right2 + pad + border("│");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Right column halves: top = model(+provider), bottom = cwd + git. */
|
|
56
|
+
/** Right column halves: top = model(+provider), bottom = cwd + git */
|
|
99
57
|
function infoRows(env: HeaderEnv, width: number): string[] {
|
|
100
58
|
const icons = DEFAULT_ICONS;
|
|
101
59
|
const parts: string[] = [];
|
|
@@ -103,6 +61,7 @@ export function createHeader(
|
|
|
103
61
|
if (env.cwd) {
|
|
104
62
|
parts.push(theme.fg("muted", `${icons.folder} ${getShortCwd(env.cwd)}`));
|
|
105
63
|
}
|
|
64
|
+
|
|
106
65
|
if (env.gitBranch) {
|
|
107
66
|
parts.push(theme.fg(accentColor, `${icons.gitBranch} ${env.gitBranch}`));
|
|
108
67
|
|
|
@@ -137,6 +96,8 @@ export function createHeader(
|
|
|
137
96
|
}
|
|
138
97
|
|
|
139
98
|
return {
|
|
99
|
+
invalidate() {},
|
|
100
|
+
|
|
140
101
|
render(width: number): string[] {
|
|
141
102
|
const env = getEnv(pi);
|
|
142
103
|
|
|
@@ -163,12 +124,12 @@ export function createHeader(
|
|
|
163
124
|
),
|
|
164
125
|
];
|
|
165
126
|
|
|
166
|
-
// Too narrow for a box → fall back to a plain centered logo
|
|
127
|
+
// Too narrow for a box → fall back to a plain centered logo
|
|
167
128
|
if (width < MIN_BOX_WIDTH) {
|
|
168
129
|
return leftLines;
|
|
169
130
|
}
|
|
170
131
|
|
|
171
|
-
// No truncation: overflow wraps onto following lines until it all fits
|
|
132
|
+
// No truncation: overflow wraps onto following lines until it all fits
|
|
172
133
|
const height = Math.max(leftLines.length, rightLines.length);
|
|
173
134
|
const logoTop = Math.max(0, Math.floor((height - leftLines.length) / 2));
|
|
174
135
|
|
|
@@ -180,7 +141,7 @@ export function createHeader(
|
|
|
180
141
|
const lines: string[] = [""];
|
|
181
142
|
|
|
182
143
|
lines.push(fitFrameRow("╭", "╮", "", "", width, border));
|
|
183
|
-
lines.push(splitRow(leftW, rightW, "", ""));
|
|
144
|
+
lines.push(splitRow(leftW, rightW, "", "", border));
|
|
184
145
|
|
|
185
146
|
for (let i = 0; i < height; i++) {
|
|
186
147
|
const l = i - logoTop;
|
|
@@ -189,14 +150,13 @@ export function createHeader(
|
|
|
189
150
|
const r = i - rightTop;
|
|
190
151
|
const rightLine = r >= 0 && r < rightLines.length ? rightLines[r]! : "";
|
|
191
152
|
|
|
192
|
-
lines.push(splitRow(leftW, rightW, leftLine, rightLine));
|
|
153
|
+
lines.push(splitRow(leftW, rightW, leftLine, rightLine, border));
|
|
193
154
|
}
|
|
194
155
|
|
|
195
|
-
lines.push(splitRow(leftW, rightW, "", ""));
|
|
156
|
+
lines.push(splitRow(leftW, rightW, "", "", border));
|
|
196
157
|
lines.push(fitFrameRow("╰", "╯", "", "", width, border));
|
|
197
158
|
|
|
198
159
|
return lines;
|
|
199
160
|
},
|
|
200
|
-
invalidate() {},
|
|
201
161
|
};
|
|
202
162
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Live env snapshot the header renders in the right column */
|
|
2
|
+
export interface HeaderEnv {
|
|
3
|
+
cwd: string;
|
|
4
|
+
gitDirty: number;
|
|
5
|
+
gitBranch: string | undefined;
|
|
6
|
+
|
|
7
|
+
/** Combined display name, e.g. `"Model (Provider)"` (provider embedded) */
|
|
8
|
+
modelName: string | undefined;
|
|
9
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -31,27 +31,27 @@ import type { Settings, SpinnerPhase } from "./config/types";
|
|
|
31
31
|
import type { AgentMode, AgentState, ExternalData } from "./components/types";
|
|
32
32
|
import { getEditorFrame, getHeader } from "./renderers/registry";
|
|
33
33
|
import type { EditorFrameRenderer } from "./renderers/types";
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
readAgentModeFromSession,
|
|
37
|
-
readGit,
|
|
38
|
-
type GitInfo,
|
|
39
|
-
} from "./utils";
|
|
40
|
-
import { type HeaderEnv } from "./components/header";
|
|
34
|
+
import { capitalize, getData, type GitInfo } from "./utils";
|
|
35
|
+
import { type HeaderEnv } from "./headers";
|
|
41
36
|
import { getUsage } from "./utils/token";
|
|
42
37
|
|
|
43
|
-
let editor: EditorFrameRenderer | null = null;
|
|
44
|
-
let currentCtx: ExtensionContext | null = null;
|
|
45
38
|
let settings: Settings = DEFAULT_SETTINGS;
|
|
39
|
+
|
|
40
|
+
let editor: EditorFrameRenderer | null = null;
|
|
46
41
|
let spinnerPhase: SpinnerPhase | null = null;
|
|
47
|
-
|
|
42
|
+
|
|
43
|
+
let currentCtx: ExtensionContext | null = null;
|
|
44
|
+
|
|
48
45
|
let notifyEnabled = false;
|
|
46
|
+
let agentMode: AgentMode = null;
|
|
47
|
+
|
|
49
48
|
let git: GitInfo = {
|
|
50
49
|
branch: undefined,
|
|
51
50
|
dirty: 0,
|
|
52
51
|
};
|
|
53
|
-
|
|
52
|
+
|
|
54
53
|
let workingMessageBag: string[] = [];
|
|
54
|
+
let workingMessageTimer: ReturnType<typeof setInterval> | null = null;
|
|
55
55
|
|
|
56
56
|
/** Pop a random message; reshuffles when the pool is exhausted (no back-to-back repeats). */
|
|
57
57
|
function nextWorkingMessage(): string {
|
|
@@ -91,15 +91,15 @@ function provideExternal(pi: ExtensionAPI): ExternalData {
|
|
|
91
91
|
return {
|
|
92
92
|
agentMode,
|
|
93
93
|
spinnerPhase,
|
|
94
|
+
notifyEnabled,
|
|
94
95
|
cwd: ctx?.cwd ?? "",
|
|
95
96
|
gitDirty: git.dirty,
|
|
96
|
-
gitBranch: git.branch,
|
|
97
97
|
theme: ctx?.ui.theme,
|
|
98
|
+
gitBranch: git.branch,
|
|
98
99
|
context: getUsage(ctx ?? undefined),
|
|
99
100
|
thinkingLevel: pi.getThinkingLevel(),
|
|
100
101
|
modelName: model?.name ?? model?.id ?? "Unknown",
|
|
101
102
|
modelProvider: capitalize(model?.provider ?? "unknown"),
|
|
102
|
-
notifyEnabled,
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -116,12 +116,21 @@ function paletteKeys(): string[] {
|
|
|
116
116
|
readFileSync(join(getAgentDir(), "keybindings.json"), "utf8"),
|
|
117
117
|
) as Record<string, unknown>;
|
|
118
118
|
const v = raw[PALETTE_SHORTCUT_ID];
|
|
119
|
+
|
|
119
120
|
if (typeof v === "string") return [v];
|
|
120
121
|
if (Array.isArray(v) && v.every((k) => typeof k === "string")) return v;
|
|
121
122
|
} catch {}
|
|
123
|
+
|
|
122
124
|
return [PALETTE_DEFAULT_KEY];
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
function refreshData(ctx: ExtensionContext) {
|
|
128
|
+
const data = getData(ctx);
|
|
129
|
+
|
|
130
|
+
git = data.git;
|
|
131
|
+
agentMode = data.agentMode;
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
// ── extension entry ──────────────────────────────────────────────────────
|
|
126
135
|
|
|
127
136
|
export default async function (pi: ExtensionAPI) {
|
|
@@ -155,14 +164,17 @@ export default async function (pi: ExtensionAPI) {
|
|
|
155
164
|
pi.events.on(PI_NOTIFY_TOGGLE_EVENT, (event) => {
|
|
156
165
|
const enabled = (event as { enabled?: unknown } | undefined)?.enabled;
|
|
157
166
|
if (typeof enabled === "boolean") notifyEnabled = enabled;
|
|
167
|
+
|
|
158
168
|
editor?.refresh();
|
|
159
169
|
});
|
|
160
170
|
|
|
171
|
+
pi.on("turn_end", async (_, ctx) => {
|
|
172
|
+
refreshData(ctx);
|
|
173
|
+
});
|
|
174
|
+
|
|
161
175
|
pi.on("session_start", async (_event, ctx) => {
|
|
176
|
+
refreshData(ctx);
|
|
162
177
|
currentCtx = ctx;
|
|
163
|
-
agentMode = readAgentModeFromSession(ctx) || agentMode;
|
|
164
|
-
|
|
165
|
-
git = readGit(ctx.cwd);
|
|
166
178
|
|
|
167
179
|
ctx.ui.setWorkingIndicator({
|
|
168
180
|
intervalMs: 80,
|
|
@@ -183,7 +195,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
183
195
|
|
|
184
196
|
if (settings.header?.enable) {
|
|
185
197
|
const headerFactory =
|
|
186
|
-
getHeader(settings.header.type ?? "
|
|
198
|
+
getHeader(settings.header.type ?? "box") ?? getHeader("box")!;
|
|
187
199
|
ctx.ui.setHeader((_tui, theme) =>
|
|
188
200
|
headerFactory(_tui, theme, pi, settings, getHeaderEnv),
|
|
189
201
|
);
|
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
* getEditorFrame/getHeader
|
|
5
5
|
*/
|
|
6
6
|
import type { EditorFrameFactory, HeaderRendererFactory } from "./types";
|
|
7
|
-
|
|
8
|
-
import { BlockyEditor } from "../editor
|
|
9
|
-
import {
|
|
10
|
-
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
import { BlockyEditor, LinearEditor, MinimalistEditor } from "../editor";
|
|
9
|
+
import { createBoxHeader } from "../headers";
|
|
11
10
|
|
|
12
11
|
const editorFrames = new Map<string, EditorFrameFactory>();
|
|
13
12
|
const headers = new Map<string, HeaderRendererFactory>();
|
|
@@ -47,4 +46,10 @@ registerEditorFrame(
|
|
|
47
46
|
new MinimalistEditor(pi, provider, opts, ...args),
|
|
48
47
|
);
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
registerEditorFrame(
|
|
50
|
+
"linear",
|
|
51
|
+
(pi, provider, opts, ...args) =>
|
|
52
|
+
new LinearEditor(pi, provider, opts, ...args),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
registerHeader("box", createBoxHeader);
|
|
@@ -7,7 +7,8 @@ import type {
|
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { FrameSettings, Settings, SpinnerPhase } from "../config/types";
|
|
9
9
|
import type { ExternalData } from "../components/types";
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
import type { HeaderEnv } from "../headers";
|
|
11
12
|
|
|
12
13
|
/** What pi's TUI needs from any header implementation */
|
|
13
14
|
export interface HeaderRenderer {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { readGit } from "./git";
|
|
4
|
+
import { readAgentModeFromSession } from "./agent";
|
|
5
|
+
|
|
6
|
+
export function getData(ctx: ExtensionContext) {
|
|
7
|
+
return {
|
|
8
|
+
git: readGit(ctx.cwd),
|
|
9
|
+
agentMode: readAgentModeFromSession(ctx),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -5,9 +5,11 @@ import { THEME_COLORS } from "../config/constants";
|
|
|
5
5
|
* Shared helpers, grouped by domain and re-exported from a single barrel.
|
|
6
6
|
* Import everything with `import { ... } from "../utils"`.
|
|
7
7
|
*/
|
|
8
|
-
export * from "./agent";
|
|
9
8
|
export * from "./git";
|
|
9
|
+
export * from "./tui";
|
|
10
|
+
export * from "./data";
|
|
10
11
|
export * from "./path";
|
|
12
|
+
export * from "./agent";
|
|
11
13
|
export { capitalize } from "@leo-alvarenga/pi-ext-core";
|
|
12
14
|
|
|
13
15
|
export function isThemeColor(value: string): value is ThemeColor {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export function wrapLines(
|
|
4
|
+
lines: string[],
|
|
5
|
+
maxLen: number,
|
|
6
|
+
style?: (line: string) => string,
|
|
7
|
+
): string[] {
|
|
8
|
+
const wrapped = lines.map((l) => wrapTextWithAnsi(l, maxLen)).flat();
|
|
9
|
+
if (style) return wrapped.map((l) => (l.length ? style(l) : l));
|
|
10
|
+
|
|
11
|
+
return wrapped;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Space-pad `text` so it sits horizontally centered within `inner` cols. */
|
|
15
|
+
export function center(text: string, inner: number): string {
|
|
16
|
+
const w = visibleWidth(text);
|
|
17
|
+
const left = Math.max(0, Math.floor((inner - w) / 2));
|
|
18
|
+
const right = Math.max(0, inner - w - left);
|
|
19
|
+
|
|
20
|
+
return " ".repeat(left) + text + " ".repeat(right);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Claude-style split row: │ logo (leftW) │ info (rightW) │ exactly `width` cols */
|
|
24
|
+
export function splitRow(
|
|
25
|
+
leftW: number,
|
|
26
|
+
rightW: number,
|
|
27
|
+
left: string,
|
|
28
|
+
right: string,
|
|
29
|
+
border: (s: string) => string,
|
|
30
|
+
): string {
|
|
31
|
+
const left2 = center(left, leftW);
|
|
32
|
+
const right2 = " " + right;
|
|
33
|
+
const pad = " ".repeat(Math.max(0, rightW - visibleWidth(right2)));
|
|
34
|
+
|
|
35
|
+
return border("│") + left2 + border("│") + right2 + pad + border("│");
|
|
36
|
+
}
|
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.21.2",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github.com/leo-alvarenga/pi-mono",
|
|
8
8
|
"publishConfig": {
|