@leo-alvarenga/pi-zen-frame 0.20.0 → 0.22.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 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 | 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 |
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 |
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: "blocky",
6
+ editorFrame: "linear",
7
7
 
8
8
  header: {
9
+ type: "zen",
9
10
  enable: false,
10
- type: "basic",
11
11
  },
12
12
 
13
13
  workingMessage: {
@@ -174,3 +174,16 @@ export const HEADER_TIPS: BannerTip[] = [
174
174
  text: "For multi-file changes, ask for a plan first (files touched + order) and approve it before any edits — cheap insurance against wrong-direction refactors",
175
175
  },
176
176
  ];
177
+
178
+ export const GREETINGS: string[] = [
179
+ "Welcome back!",
180
+ "Ready when you are",
181
+ "New moment, new beginning",
182
+ "The present is yours",
183
+ "Stillness before the work",
184
+ "Breathe. Then begin",
185
+ "Here and now",
186
+ "Empty mind, open hands",
187
+ "The path continues",
188
+ "Begin with intention",
189
+ ];
@@ -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: ["⠃", "⠞", "⡵", "⠿", "⢹", "⠄"],
@@ -48,7 +48,7 @@ export interface FrameSettings {
48
48
  export interface HeaderSettings {
49
49
  enable: boolean;
50
50
 
51
- /** Header renderer by registered name. Default "basic". */
51
+ /** Header renderer by registered name. Default "zen" */
52
52
  type?: string;
53
53
  }
54
54
 
@@ -4,18 +4,18 @@ import {
4
4
  type ExtensionAPI,
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
 
7
- import type { SpinnerPhase } from "../config/types";
8
- import type { ExternalData, FrameData } from "../components/types";
9
- import type { EditorFrameRenderOptions } from "../renderers/types";
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 "./palette";
16
+ } from "../palette";
17
17
  import { renderEditorFrame } from "./render";
18
- import { SpinnerController } from "./spinner";
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 "../config/constants";
2
- import { composeBand, fitInfoRow, isBorderRow } from "../components/frame";
3
- import { segmentsFor } from "../components/registry";
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 "../components/types";
9
- import type { EditorFrameRenderOptions } from "../renderers/types";
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,3 @@
1
+ export { BlockyEditor } from "./blocky";
2
+ export { MinimalistEditor } from "./minimalist";
3
+ export { LinearEditor } from "./linear";
@@ -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 "../config/types";
7
- import type { ExternalData } from "../components/types";
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 "../renderers/types";
11
+ } from "../../renderers/types";
12
12
 
13
13
  import {
14
14
  handlePaletteInput,
15
15
  loadPaletteItems,
16
16
  renderPalette,
17
17
  type PaletteState,
18
- } from "./palette";
18
+ } from "../palette";
19
19
 
20
20
  export class MinimalistEditor
21
21
  extends CustomEditor
@@ -1,52 +1,34 @@
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 { DEFAULT_ICONS, HEADER_TIPS } from "../config/constants";
10
- import type { Settings } from "../config/types";
11
- import { fitFrameRow } from "./frame";
12
- import { getShortCwd } from "../utils";
13
-
14
- /** Below which terminal width the box is skipped (plain logo). */
8
+ import {
9
+ DEFAULT_ICONS,
10
+ GREETINGS,
11
+ HEADER_TIPS,
12
+ LOGO_COLOR,
13
+ LOGO_LINES,
14
+ } from "../../config/constants";
15
+ import type { Settings } from "../../config/types";
16
+ import { fitFrameRow } from "../../components/frame";
17
+ import { getShortCwd, splitRow, wrapLines } from "../../utils";
18
+ import { HeaderEnv } from "../types";
19
+
20
+ /** Below which terminal width the box is skipped (plain logo) */
15
21
  const MIN_BOX_WIDTH = 20;
16
22
  const LEFT_COL_RATIO = 0.4; // logo column width / total width
17
23
 
18
- /** BasicHeader preset styling — self-contained, not user-configurable. */
19
- const LOGO_LINES = ["█████████ ", "███ ███ ", "██████ ", "███ ███"];
20
- const LOGO_COLOR: ThemeColor = "text";
21
- const HEADING = "Welcome back!";
24
+ const HEADING = GREETINGS[Math.floor(Math.random() * GREETINGS.length)]!;
22
25
  const SUBHEADING =
23
26
  "Ready for your next session? Terminal warm, context clean, tools ready to execute";
24
27
 
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
28
  const RANDOM_TIP =
36
29
  HEADER_TIPS[Math.floor(Math.random() * HEADER_TIPS.length)]?.text ?? "";
37
30
 
38
- function wrapLines(
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(
31
+ export function createBoxHeader(
50
32
  _tui: TUI,
51
33
  theme: Theme,
52
34
  pi: ExtensionAPI,
@@ -66,36 +48,13 @@ export function createHeader(
66
48
  const accentColor = settings.accentColor ?? "accent";
67
49
 
68
50
  const logo = {
69
- lines: LOGO_LINES,
70
51
  color: LOGO_COLOR,
52
+ lines: LOGO_LINES,
71
53
  };
72
54
 
73
55
  const border = (s: string, fg?: ThemeColor) => theme.fg(fg ?? accentColor, s);
74
56
 
75
- /** Space-pad `text` so it sits horizontally centered within `inner` cols. */
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. */
57
+ /** Right column halves: top = model(+provider), bottom = cwd + git */
99
58
  function infoRows(env: HeaderEnv, width: number): string[] {
100
59
  const icons = DEFAULT_ICONS;
101
60
  const parts: string[] = [];
@@ -103,6 +62,7 @@ export function createHeader(
103
62
  if (env.cwd) {
104
63
  parts.push(theme.fg("muted", `${icons.folder} ${getShortCwd(env.cwd)}`));
105
64
  }
65
+
106
66
  if (env.gitBranch) {
107
67
  parts.push(theme.fg(accentColor, `${icons.gitBranch} ${env.gitBranch}`));
108
68
 
@@ -137,6 +97,8 @@ export function createHeader(
137
97
  }
138
98
 
139
99
  return {
100
+ invalidate() {},
101
+
140
102
  render(width: number): string[] {
141
103
  const env = getEnv(pi);
142
104
 
@@ -163,12 +125,12 @@ export function createHeader(
163
125
  ),
164
126
  ];
165
127
 
166
- // Too narrow for a box → fall back to a plain centered logo.
128
+ // Too narrow for a box → fall back to a plain centered logo
167
129
  if (width < MIN_BOX_WIDTH) {
168
130
  return leftLines;
169
131
  }
170
132
 
171
- // No truncation: overflow wraps onto following lines until it all fits.
133
+ // No truncation: overflow wraps onto following lines until it all fits
172
134
  const height = Math.max(leftLines.length, rightLines.length);
173
135
  const logoTop = Math.max(0, Math.floor((height - leftLines.length) / 2));
174
136
 
@@ -180,7 +142,7 @@ export function createHeader(
180
142
  const lines: string[] = [""];
181
143
 
182
144
  lines.push(fitFrameRow("╭", "╮", "", "", width, border));
183
- lines.push(splitRow(leftW, rightW, "", ""));
145
+ lines.push(splitRow(leftW, rightW, "", "", border));
184
146
 
185
147
  for (let i = 0; i < height; i++) {
186
148
  const l = i - logoTop;
@@ -189,14 +151,13 @@ export function createHeader(
189
151
  const r = i - rightTop;
190
152
  const rightLine = r >= 0 && r < rightLines.length ? rightLines[r]! : "";
191
153
 
192
- lines.push(splitRow(leftW, rightW, leftLine, rightLine));
154
+ lines.push(splitRow(leftW, rightW, leftLine, rightLine, border));
193
155
  }
194
156
 
195
- lines.push(splitRow(leftW, rightW, "", ""));
157
+ lines.push(splitRow(leftW, rightW, "", "", border));
196
158
  lines.push(fitFrameRow("╰", "╯", "", "", width, border));
197
159
 
198
160
  return lines;
199
161
  },
200
- invalidate() {},
201
162
  };
202
163
  }
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export { createBoxHeader } from "./box";
3
+ export { createZenHeader } from "./zen";
@@ -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
+ }
@@ -0,0 +1,51 @@
1
+ import type { TUI } from "@earendil-works/pi-tui";
2
+ import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
3
+
4
+ import { GREETINGS, LOGO_COLOR, LOGO_LINES } from "../../config/constants";
5
+ import type { Settings } from "../../config/types";
6
+ import type { HeaderEnv } from "../types";
7
+ import { center, wrapLines } from "../../utils";
8
+ import { QUOTES } from "./quotes";
9
+
10
+ const TOP_PAD = 2;
11
+
12
+ const GREETING = GREETINGS[Math.floor(Math.random() * GREETINGS.length)]!;
13
+ const QUOTE = QUOTES[Math.floor(Math.random() * QUOTES.length)]!;
14
+
15
+ export function createZenHeader(
16
+ _tui: TUI,
17
+ theme: Theme,
18
+ _pi: ExtensionAPI,
19
+ settings: Settings,
20
+ _getEnv: (pi: ExtensionAPI) => HeaderEnv,
21
+ ) {
22
+ if (!settings.header?.enable) {
23
+ return { render: (): string[] => [], invalidate() {} };
24
+ }
25
+
26
+ return {
27
+ invalidate() {},
28
+
29
+ render(width: number): string[] {
30
+ const lines: string[] = [];
31
+
32
+ for (let i = 0; i < TOP_PAD; i++) lines.push("");
33
+
34
+ for (const line of LOGO_LINES) {
35
+ lines.push(center(theme.fg(LOGO_COLOR, line), width));
36
+ }
37
+
38
+ lines.push("");
39
+ lines.push(center(theme.bold(GREETING), width));
40
+ lines.push("");
41
+
42
+ const quoteText = `"${QUOTE.text}" — ${QUOTE.author}`;
43
+ for (const l of wrapLines([quoteText], Math.min(width - 4, 80))) {
44
+ lines.push(center(theme.italic(theme.fg("muted", l)), width));
45
+ }
46
+
47
+ lines.push("");
48
+ return lines;
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,187 @@
1
+ export const QUOTES = [
2
+ {
3
+ text: "Do not dwell in the past, do not dream of the future, concentrate the mind on the present moment.",
4
+ author: "Buddha",
5
+ },
6
+ {
7
+ text: "The present moment is the only moment available to us, and it is the door to all moments.",
8
+ author: "Thich Nhat Hanh",
9
+ },
10
+ {
11
+ text: "You have power over your mind, not outside events. Realize this, and you will find strength.",
12
+ author: "Marcus Aurelius",
13
+ },
14
+ {
15
+ text: "Knowing others is wisdom. Knowing yourself is enlightenment.",
16
+ author: "Lao Tzu",
17
+ },
18
+ {
19
+ text: "Out beyond ideas of wrongdoing and rightdoing, there is a field. I'll meet you there.",
20
+ author: "Rumi",
21
+ },
22
+ {
23
+ text: "Life is what happens to you while you're busy making other plans.",
24
+ author: "John Lennon",
25
+ },
26
+ { text: "The obstacle is the path.", author: "Zen proverb" },
27
+ {
28
+ text: "He who is not every day conquering some fear has not learned the secret of life.",
29
+ author: "Ralph Waldo Emerson",
30
+ },
31
+ {
32
+ text: "Make the present moment your friend rather than your enemy.",
33
+ author: "Eckhart Tolle",
34
+ },
35
+ {
36
+ text: "Waste no more time arguing about what a good person should be. Be one.",
37
+ author: "Marcus Aurelius",
38
+ },
39
+ {
40
+ text: "The quieter you become, the more you can hear.",
41
+ author: "Ram Dass",
42
+ },
43
+ {
44
+ text: "Nature does not hurry, yet everything is accomplished.",
45
+ author: "Lao Tzu",
46
+ },
47
+ {
48
+ text: "Man suffers only because he takes seriously what the gods made for fun.",
49
+ author: "Alan Watts",
50
+ },
51
+ {
52
+ text: "If you realize that all things change, there is nothing you will try to hold on to.",
53
+ author: "Lao Tzu",
54
+ },
55
+ {
56
+ text: "Before enlightenment, chop wood, carry water. After enlightenment, chop wood, carry water.",
57
+ author: "Zen proverb",
58
+ },
59
+ {
60
+ text: "It is not death that a person should fear, but rather they should fear never beginning to live.",
61
+ author: "Marcus Aurelius",
62
+ },
63
+ {
64
+ text: "The soul that sees beauty may sometimes walk alone.",
65
+ author: "Johann Wolfgang von Goethe",
66
+ },
67
+ {
68
+ text: "Wherever you are is always the right place. There is never a need to fix anything, to hush the noise or turn off the light.",
69
+ author: "John Kabat-Zinn",
70
+ },
71
+ {
72
+ text: "When you realize there is nothing lacking, the whole world belongs to you.",
73
+ author: "Lao Tzu",
74
+ },
75
+ {
76
+ text: "The first step in the journey of a thousand miles is taken beneath your feet.",
77
+ author: "Lao Tzu",
78
+ },
79
+ {
80
+ text: "Do not be too timid and squeamish about your actions. All life is an experiment.",
81
+ author: "Ralph Waldo Emerson",
82
+ },
83
+ {
84
+ text: "Peace comes from within. Do not seek it without.",
85
+ author: "Buddha",
86
+ },
87
+ {
88
+ text: "Not knowing how near the truth is, people seek it far away.",
89
+ author: "Hakuin Ekaku",
90
+ },
91
+ {
92
+ text: "The Tao that can be told is not the eternal Tao.",
93
+ author: "Lao Tzu",
94
+ },
95
+ {
96
+ text: "You must be the change you wish to see in the world.",
97
+ author: "Mahatma Gandhi",
98
+ },
99
+ { text: "What you seek is seeking you.", author: "Rumi" },
100
+ { text: "Sell your cleverness and buy bewilderment.", author: "Rumi" },
101
+ {
102
+ text: "To understand everything is to forgive everything.",
103
+ author: "Osho",
104
+ },
105
+ {
106
+ text: "Begin at once to live, and count each separate day as a separate life.",
107
+ author: "Seneca",
108
+ },
109
+ {
110
+ text: "Happiness is not something ready made. It comes from your own actions.",
111
+ author: "Dalai Lama",
112
+ },
113
+ {
114
+ text: "We are what we repeatedly do. Excellence, then, is not an act but a habit.",
115
+ author: "Aristotle",
116
+ },
117
+ {
118
+ text: "The mind is everything. What you think you become.",
119
+ author: "Buddha",
120
+ },
121
+ {
122
+ text: "In the beginner's mind there are many possibilities, but in the expert's mind there are few.",
123
+ author: "Shunryu Suzuki",
124
+ },
125
+ {
126
+ text: "No snowflake ever falls in the wrong place.",
127
+ author: "Zen proverb",
128
+ },
129
+ {
130
+ text: "Look within. Within is the fountain of good, and it will ever bubble up, if thou wilt ever dig.",
131
+ author: "Marcus Aurelius",
132
+ },
133
+ {
134
+ text: "There is no way to happiness. Happiness is the way.",
135
+ author: "Thich Nhat Hanh",
136
+ },
137
+ {
138
+ text: "The two most powerful warriors are patience and time.",
139
+ author: "Leo Tolstoy",
140
+ },
141
+ {
142
+ text: "A man who dares to waste one hour of life has not discovered the value of life.",
143
+ author: "Charles Darwin",
144
+ },
145
+ {
146
+ text: "The only way to do great work is to love what you do.",
147
+ author: "Steve Jobs",
148
+ },
149
+ { text: "Everything you can imagine is real.", author: "Pablo Picasso" },
150
+ {
151
+ text: "I have just three things to teach: simplicity, patience, compassion.",
152
+ author: "Lao Tzu",
153
+ },
154
+ { text: "Act without expectation.", author: "Lao Tzu" },
155
+ {
156
+ text: "Man stands in his own shadow and wonders why it is dark.",
157
+ author: "Zen proverb",
158
+ },
159
+ {
160
+ text: "To the mind that is still, the whole universe surrenders.",
161
+ author: "Lao Tzu",
162
+ },
163
+ {
164
+ text: "Better than a thousand hollow words is one word that brings peace.",
165
+ author: "Buddha",
166
+ },
167
+ {
168
+ text: "Yesterday I was clever, so I wanted to change the world. Today I am wise, so I am changing myself.",
169
+ author: "Rumi",
170
+ },
171
+ {
172
+ text: "It is the mark of an educated mind to be able to entertain a thought without accepting it.",
173
+ author: "Aristotle",
174
+ },
175
+ {
176
+ text: "Accept what is, let go of what was, have faith in what will be.",
177
+ author: "Sonia Ricotti",
178
+ },
179
+ {
180
+ text: "The greatest weapon against stress is our ability to choose one thought over another.",
181
+ author: "William James",
182
+ },
183
+ {
184
+ text: "The moment you stop accepting challenges is the moment you stop moving forward.",
185
+ author: "Epictetus",
186
+ },
187
+ ];
@@ -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
- capitalize,
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
- let agentMode: AgentMode = null;
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
- let workingMessageTimer: ReturnType<typeof setInterval> | null = null;
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 ?? "basic") ?? getHeader("basic")!;
198
+ getHeader(settings.header.type ?? "zen") ?? getHeader("zen")!;
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
- import { createHeader } from "../components/header";
8
- import { BlockyEditor } from "../editor/blocky-editor";
9
- import { MinimalistEditor } from "../editor/minimalist-editor";
10
- import { CustomEditor } from "@earendil-works/pi-coding-agent";
7
+
8
+ import { BlockyEditor, LinearEditor, MinimalistEditor } from "../editor";
9
+ import { createBoxHeader, createZenHeader } from "../headers";
11
10
 
12
11
  const editorFrames = new Map<string, EditorFrameFactory>();
13
12
  const headers = new Map<string, HeaderRendererFactory>();
@@ -47,4 +46,11 @@ registerEditorFrame(
47
46
  new MinimalistEditor(pi, provider, opts, ...args),
48
47
  );
49
48
 
50
- registerHeader("basic", createHeader);
49
+ registerEditorFrame(
50
+ "linear",
51
+ (pi, provider, opts, ...args) =>
52
+ new LinearEditor(pi, provider, opts, ...args),
53
+ );
54
+
55
+ registerHeader("box", createBoxHeader);
56
+ registerHeader("zen", createZenHeader);
@@ -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
- import type { HeaderEnv } from "../components/header";
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.20.0",
5
+ "version": "0.22.0",
6
6
  "license": "MIT",
7
7
  "repository": "github.com/leo-alvarenga/pi-mono",
8
8
  "publishConfig": {