pi-powerline 0.1.0 → 0.2.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
@@ -1,58 +1,19 @@
1
1
  # pi-powerline
2
2
 
3
- Powerline-style UI extensions for [pi](https://github.com/badlogic/pi-mono) coding agent: custom editor, breadcrumb widget, footer, and header.
3
+ Powerline-style UI extensions for [pi](https://github.com/badlogic/pi-mono): custom editor, breadcrumb, footer, and header.
4
4
 
5
- ![pi-powerline screenshot](https://github.com/user-attachments/assets/9ee65cd5-8501-4502-ba69-0209b19e0499)
5
+ ![screenshot](assets/pi-powerline.png)
6
6
 
7
- ## Features
8
-
9
- **Custom editor** — Always-on bordered input area with a `❯` prompt prefix. Switches to bash-mode coloring when the prompt starts with `!`. Breadcrumb info (model → directory) can be embedded in the top border.
10
-
11
- **Breadcrumb widget** — Displays current model → working directory above the editor, shown only when breadcrumb mode is `top`.
12
-
13
- **Custom footer** — A compact status bar showing token usage (`↑input ↓output` + cache read/write), context usage % with auto-compact indicator, session cost, thinking level, git branch, and extension statuses. Updates in real-time during streaming.
14
-
15
- **Custom header** — A gradient-colored PI logo rendered with ANSI 256-color codes, replacing the built-in header.
16
-
17
- > Highly inspired by [nicobailon/pi-powerline-footer](https://github.com/nicobailon/pi-powerline-footer).
18
-
19
- ## Installation
20
-
21
- ### Local development
22
-
23
- Clone the repository and use pi's `--extension` flag:
24
-
25
- ```bash
26
- git clone <repo-url> pi-powerline
27
- cd pi-powerline
28
- pi -e ./index.ts
29
- ```
30
-
31
- Or add it to your project's `.pi/settings.json`:
32
-
33
- ```json
34
- {
35
- "extensions": ["./index.ts"]
36
- }
37
- ```
38
-
39
- ### From npm (after publishing)
7
+ ## Install
40
8
 
41
9
  ```bash
42
10
  pi install npm:pi-powerline
43
11
  ```
44
12
 
45
- Restart pi to activate.
46
-
47
- ## Usage
48
-
49
- All extensions activate automatically on session start. Each can be configured via the `/powerline` command.
50
-
51
- ### Settings
52
-
53
- Configure in `.pi/settings.json`:
13
+ ## Settings
54
14
 
55
15
  ```json
16
+ // .pi/settings.json
56
17
  {
57
18
  "breadcrumb": "inner",
58
19
  "footer": true,
@@ -60,154 +21,22 @@ Configure in `.pi/settings.json`:
60
21
  }
61
22
  ```
62
23
 
63
- | Setting | Type | Default | Description |
64
- |---------|------|---------|-------------|
65
- | `breadcrumb` | `"hide"` \| `"top"` \| `"inner"` | `"inner"` | Breadcrumb display mode |
66
- | `footer` | `boolean` | `true` | Enable custom footer |
67
- | `header` | `boolean` | `true` | Enable gradient-logo header |
68
-
69
- **Breadcrumb modes:**
70
-
71
- - `hide` — No breadcrumb display
72
- - `top` — Breadcrumb as a widget above the editor
73
- - `inner` — Breadcrumb embedded in the editor's top border
74
-
75
- ### Commands
76
-
77
- | Command | Description |
78
- |---------|-------------|
79
- | `/powerline` | Show current powerline settings |
80
- | `/powerline breadcrumb:hide` | Disable breadcrumb |
81
- | `/powerline breadcrumb:top` | Breadcrumb as top widget |
82
- | `/powerline breadcrumb:inner` | Breadcrumb in editor border |
83
- | `/powerline footer:on` | Enable custom footer |
84
- | `/powerline footer:off` | Disable custom footer |
85
- | `/powerline header:on` | Enable custom header |
86
- | `/powerline header:off` | Disable custom header |
87
-
88
- ### Nerd Fonts
89
-
90
- The breadcrumb and footer use Nerd Font icons when a compatible terminal is detected (iTerm, WezTerm, Kitty, Ghostty, Alacritty). Set `POWERLINE_NERD_FONTS=1` or `POWERLINE_NERD_FONTS=0` to explicitly enable/disable.
91
-
92
- ## Development
93
-
94
- ### Project structure
95
-
96
- ```
97
- .
98
- ├── index.ts # Single entry point (default export)
99
- ├── editor.ts # Custom editor with prompt prefix
100
- ├── breadcrumb.ts # Shared breadcrumb data & rendering helpers
101
- ├── widget.ts # Top widget (shown when breadcrumb=top)
102
- ├── footer.ts # Custom footer (token stats, git, thinking level)
103
- ├── header.ts # Gradient-logo header
104
- ├── settings.ts # Shared .pi/settings.json read/write helpers
105
- ├── tests/
106
- │ ├── editor.test.ts
107
- │ ├── footer.test.ts
108
- │ ├── header.test.ts
109
- │ └── widget.test.ts
110
- ├── .pi/
111
- │ ├── settings.json
112
- │ ├── APPEND_SYSTEM.md
113
- │ └── extensions/
114
- │ └── auto-format.ts # Auto prettier on edit/write
115
- ├── .husky/
116
- │ └── pre-commit # prettier check + bun test
117
- ├── .editorconfig
118
- ├── .prettierrc
119
- ├── .prettierignore
120
- ├── package.json
121
- └── tsconfig.json # gitignored
122
- ```
123
-
124
- ### Architecture
125
-
126
- `index.ts` is the single entry point registered in `package.json` → `"pi": { "extensions": ["./index.ts"] }`. It registers four extensions:
127
-
128
- ```ts
129
- import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
130
- import { registerEditor } from './editor.ts';
131
- import { registerFooter } from './footer.ts';
132
- import { registerHeader } from './header.ts';
133
- import { registerWidget } from './widget.ts';
134
-
135
- export default function (pi: ExtensionAPI) {
136
- registerEditor(pi);
137
- registerFooter(pi);
138
- registerHeader(pi);
139
- registerWidget(pi);
140
- }
141
- ```
142
-
143
- Settings are managed via `settings.ts` — a shared module that reads/writes `.pi/settings.json`. When `/powerline` changes a setting, it emits a `powerline_settings_changed` event that all modules listen to for live reconfiguration.
144
-
145
- ### Code quality
146
-
147
- - **Formatting**: `.pi/extensions/auto-format.ts` runs prettier automatically after edit/write tools touch `.ts` files. Prettier config: single quotes, semicolons, trailing commas, 2-space indent, 100 char width.
148
- - **Pre-commit**: `.husky/pre-commit` runs `prettier --check` + `bun test` before every commit.
149
- - Use `bun run format` to format all files, `bun run format:check` to verify.
150
-
151
- ### Editor setup
152
-
153
- Neovim's tsserver can't resolve `@mariozechner/pi-*` imports because those packages are bundled inside pi, not in `node_modules`. Create a `tsconfig.json` with path mappings pointing to the global pi installation:
154
-
155
- ```bash
156
- # Find the pi install path
157
- ls -d $(dirname $(which pi))/../lib/node_modules/@mariozechner/pi-coding-agent
158
- ```
24
+ | Setting | Values | Default |
25
+ |---------|--------|---------|
26
+ | `powerline` | `true` / `false` | `true` |
27
+ | `breadcrumb` | `"hide"` / `"top"` / `"inner"` | `"inner"` |
28
+ | `footer` | `true` / `false` | `true` |
29
+ | `header` | `true` / `false` | `true` |
159
30
 
160
- Then copy the example below and adjust paths to match your system:
161
-
162
- ```json
163
- {
164
- "compilerOptions": {
165
- "target": "ESNext",
166
- "module": "ESNext",
167
- "moduleResolution": "bundler",
168
- "strict": true,
169
- "noEmit": true,
170
- "allowImportingTsExtensions": true,
171
- "baseUrl": ".",
172
- "paths": {
173
- "@mariozechner/pi-coding-agent": [
174
- "/path/to/.nvm/versions/node/vXX/lib/node_modules/@mariozechner/pi-coding-agent/dist"
175
- ],
176
- "@mariozechner/pi-ai": [
177
- "/path/to/.nvm/.../pi-coding-agent/node_modules/@mariozechner/pi-ai/dist"
178
- ],
179
- "@mariozechner/pi-tui": [
180
- "/path/to/.nvm/.../pi-coding-agent/node_modules/@mariozechner/pi-tui/dist"
181
- ]
182
- }
183
- },
184
- "include": ["*.ts", "tests/**/*.ts"]
185
- }
186
- ```
187
-
188
- `tsconfig.json` is gitignored — each developer creates their own.
189
-
190
- ### Running tests
191
-
192
- ```bash
193
- bun test
194
- # or via npm:
195
- npm run test:bun
196
- ```
197
-
198
- Tests use bun's built-in test runner (compatible with `node:test`). Run `npm run test` for the Node.js variant.
199
-
200
- ### Testing a single extension
201
-
202
- ```bash
203
- pi -e ./index.ts
204
- ```
31
+ ## Commands
205
32
 
206
- Then verify:
207
- - **Header**: startup screen → should show gradient-colored PI logo
208
- - **Editor**: type text should see `❯` prefix with `─` borders; type `!command` → bash-mode coloring
209
- - **Breadcrumb**: check top border or widget → should show model name and folder
210
- - **Footer**: check bottom bar should show model, token stats, git branch, thinking level
33
+ | Command | Effect |
34
+ |---------|--------|
35
+ | `/powerline` | Toggle all extensions on/off |
36
+ | `/powerline info` | Show current settings |
37
+ | `/powerline breadcrumb:top\|inner\|hide` | Set breadcrumb mode |
38
+ | `/powerline footer:on\|off` | Toggle footer |
39
+ | `/powerline header:on\|off` | Toggle header |
211
40
 
212
41
  ## License
213
42
 
package/breadcrumb.ts CHANGED
@@ -22,8 +22,8 @@ export function hasNerdFonts(): boolean {
22
22
  const NERD = hasNerdFonts();
23
23
 
24
24
  export const ICON_MODEL = NERD ? '\uF4BC' : '';
25
- export const ICON_FOLDER = NERD ? '\uF115' : 'dir';
26
- export const SEP = NERD ? '\uE0B1' : '|';
25
+ export const ICON_FOLDER = NERD ? '\uF115' : '';
26
+ export const SEP = NERD ? '\uf054' : '/';
27
27
 
28
28
  // ═══════════════════════════════════════════════════════════════════════════
29
29
  // helpers
package/editor.ts CHANGED
@@ -147,8 +147,10 @@ export function updateTheme(theme: Theme): void {
147
147
  currentTheme = theme;
148
148
  }
149
149
 
150
- /** Register the custom editor extension. Editor is always enabled; breadcrumb mode controls info embedding. */
150
+ /** Register the custom editor extension. Controlled by powerline master switch + breadcrumb mode. */
151
151
  export function registerEditor(pi: ExtensionAPI) {
152
+ let editorEnabled = false;
153
+
152
154
  function createEditorFactory() {
153
155
  return (tui: any, theme: EditorTheme, keybindings: any) => {
154
156
  liveEditorTui = tui;
@@ -157,15 +159,24 @@ export function registerEditor(pi: ExtensionAPI) {
157
159
  }
158
160
 
159
161
  function enable(ctx: ExtensionContext) {
162
+ editorEnabled = true;
160
163
  liveCtx = ctx;
161
164
  currentTheme = ctx.ui.theme;
162
165
  breadcrumbMode = readPowerlineSettings(ctx.cwd).breadcrumb;
163
166
  ctx.ui.setEditorComponent(createEditorFactory());
164
167
  }
165
168
 
166
- // always enable on session start
169
+ function disable(ctx: ExtensionContext) {
170
+ editorEnabled = false;
171
+ liveEditorTui = null;
172
+ ctx.ui.setEditorComponent(undefined);
173
+ }
174
+
175
+ // enable on session start if powerline master switch is on
167
176
  pi.on('session_start', (_event, ctx) => {
168
- enable(ctx);
177
+ if (readPowerlineSettings(ctx.cwd).powerline) {
178
+ enable(ctx);
179
+ }
169
180
  });
170
181
 
171
182
  // keep widget info in sync when model/cwd changes
@@ -175,11 +186,18 @@ export function registerEditor(pi: ExtensionAPI) {
175
186
  liveEditorTui?.requestRender();
176
187
  });
177
188
 
178
- // re-render on /powerline command (settings changed)
189
+ // re-evaluate on /powerline command (settings changed)
179
190
  pi.events.on('powerline_settings_changed', (ctx) => {
180
191
  const c = ctx as ExtensionContext;
181
- breadcrumbMode = readPowerlineSettings(c.cwd).breadcrumb;
192
+ const s = readPowerlineSettings(c.cwd);
193
+ breadcrumbMode = s.breadcrumb;
182
194
  liveCtx = c;
183
- liveEditorTui?.requestRender();
195
+ if (s.powerline && !editorEnabled) {
196
+ enable(c);
197
+ } else if (!s.powerline && editorEnabled) {
198
+ disable(c);
199
+ } else if (editorEnabled) {
200
+ liveEditorTui?.requestRender();
201
+ }
184
202
  });
185
203
  }
package/footer.ts CHANGED
@@ -338,10 +338,11 @@ export function registerFooter(pi: ExtensionAPI) {
338
338
  ctx.ui.setFooter(undefined);
339
339
  }
340
340
 
341
- // enable on session start if footer setting is true
341
+ // enable on session start if powerline master switch + footer setting are both on
342
342
  pi.on('session_start', (_event, ctx) => {
343
343
  autoCompactEnabled = readAutoCompactEnabled(ctx.cwd);
344
- if (readPowerlineSettings(ctx.cwd).footer) {
344
+ const s = readPowerlineSettings(ctx.cwd);
345
+ if (s.powerline && s.footer) {
345
346
  enable(ctx);
346
347
  }
347
348
  });
@@ -355,7 +356,8 @@ export function registerFooter(pi: ExtensionAPI) {
355
356
 
356
357
  // model switch may affect reasoning support / provider count
357
358
  pi.on('model_select', (_event, ctx) => {
358
- const show = readPowerlineSettings(ctx.cwd).footer;
359
+ const s = readPowerlineSettings(ctx.cwd);
360
+ const show = s.powerline && s.footer;
359
361
  if (show && !enabled) {
360
362
  enable(ctx);
361
363
  } else if (!show && enabled) {
@@ -369,7 +371,8 @@ export function registerFooter(pi: ExtensionAPI) {
369
371
  // re-evaluate on /powerline command (settings changed)
370
372
  pi.events.on('powerline_settings_changed', (ctx) => {
371
373
  const c = ctx as ExtensionContext;
372
- const show = readPowerlineSettings(c.cwd).footer;
374
+ const s = readPowerlineSettings(c.cwd);
375
+ const show = s.powerline && s.footer;
373
376
  if (show && !enabled) {
374
377
  enable(c);
375
378
  } else if (!show && enabled) {
package/header.ts CHANGED
@@ -77,17 +77,19 @@ export function registerHeader(pi: ExtensionAPI) {
77
77
  ctx.ui.setHeader(undefined);
78
78
  }
79
79
 
80
- // auto-enable on session start if header setting is true
80
+ // auto-enable on session start if powerline master switch + header setting are both on
81
81
  pi.on('session_start', (_event, ctx) => {
82
82
  if (!ctx.hasUI) return;
83
- if (readPowerlineSettings(ctx.cwd).header) {
83
+ const s = readPowerlineSettings(ctx.cwd);
84
+ if (s.powerline && s.header) {
84
85
  enable(ctx);
85
86
  }
86
87
  });
87
88
 
88
89
  // re-evaluate on model switch
89
90
  pi.on('model_select', (_event, ctx) => {
90
- const show = readPowerlineSettings(ctx.cwd).header;
91
+ const s = readPowerlineSettings(ctx.cwd);
92
+ const show = s.powerline && s.header;
91
93
  if (show && !headerEnabled) {
92
94
  enable(ctx);
93
95
  } else if (!show && headerEnabled) {
@@ -98,7 +100,8 @@ export function registerHeader(pi: ExtensionAPI) {
98
100
  // re-evaluate on /powerline command (settings changed)
99
101
  pi.events.on('powerline_settings_changed', (ctx) => {
100
102
  const c = ctx as ExtensionContext;
101
- const show = readPowerlineSettings(c.cwd).header;
103
+ const s = readPowerlineSettings(c.cwd);
104
+ const show = s.powerline && s.header;
102
105
  if (show && !headerEnabled) {
103
106
  enable(c);
104
107
  } else if (!show && headerEnabled) {
package/index.ts CHANGED
@@ -8,6 +8,12 @@ import { readPowerlineSettings, writePowerlineSetting } from './settings.ts';
8
8
 
9
9
  export default function (pi: ExtensionAPI) {
10
10
  // flags
11
+ pi.registerFlag('powerline', {
12
+ description: 'Enable pi-powerline extensions',
13
+ type: 'boolean',
14
+ default: true,
15
+ });
16
+
11
17
  pi.registerFlag('breadcrumb', {
12
18
  description: 'Breadcrumb display mode: hide, top, inner',
13
19
  type: 'string',
@@ -37,6 +43,11 @@ export default function (pi: ExtensionAPI) {
37
43
  description: 'Configure powerline: breadcrumb, footer, header',
38
44
  getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
39
45
  const items: AutocompleteItem[] = [
46
+ {
47
+ value: 'info',
48
+ label: 'info',
49
+ description: 'Show current powerline settings',
50
+ },
40
51
  {
41
52
  value: 'breadcrumb:hide',
42
53
  label: 'breadcrumb:hide',
@@ -79,10 +90,21 @@ export default function (pi: ExtensionAPI) {
79
90
  handler: async (args, ctx) => {
80
91
  const arg = args?.trim().toLowerCase();
81
92
 
82
- // no args: show status
93
+ // no args: toggle master switch
83
94
  if (!arg) {
84
- const { breadcrumb, footer, header } = readPowerlineSettings(ctx.cwd);
95
+ const { powerline } = readPowerlineSettings(ctx.cwd);
96
+ const next = !powerline;
97
+ writePowerlineSetting(ctx.cwd, 'powerline', next);
98
+ pi.events.emit('powerline_settings_changed', ctx);
99
+ ctx.ui.notify(`powerline → ${next ? 'on' : 'off'}`, 'info');
100
+ return;
101
+ }
102
+
103
+ // show status
104
+ if (arg === 'info') {
105
+ const { powerline, breadcrumb, footer, header } = readPowerlineSettings(ctx.cwd);
85
106
  const lines = [
107
+ `powerline: ${powerline ? 'on' : 'off'}`,
86
108
  `breadcrumb: ${breadcrumb}`,
87
109
  `footer: ${footer ? 'on' : 'off'}`,
88
110
  `header: ${header ? 'on' : 'off'}`,
@@ -95,7 +117,7 @@ export default function (pi: ExtensionAPI) {
95
117
  const colonIdx = arg.indexOf(':');
96
118
  if (colonIdx === -1) {
97
119
  ctx.ui.notify(
98
- 'Usage: /powerline <breadcrumb:hide|top|inner|footer:on|off|header:on|off>',
120
+ 'Usage: /powerline <info|breadcrumb:hide|top|inner|footer:on|off|header:on|off>',
99
121
  'warning',
100
122
  );
101
123
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-powerline",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Powerline-style UI extensions for pi coding agent (custom editor, breadcrumb, footer, header)",
5
5
  "type": "module",
6
6
  "files": [
@@ -32,7 +32,7 @@
32
32
  "extensions": [
33
33
  "./index.ts"
34
34
  ],
35
- "image": "https://github.com/user-attachments/assets/9ee65cd5-8501-4502-ba69-0209b19e0499"
35
+ "image": "./assets/pi-powerline.png"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@mariozechner/pi-coding-agent": "*",
package/settings.ts CHANGED
@@ -5,12 +5,14 @@ import { join } from 'node:path';
5
5
  export type BreadcrumbMode = 'hide' | 'top' | 'inner';
6
6
 
7
7
  export interface PowerlineSettings {
8
+ powerline: boolean;
8
9
  breadcrumb: BreadcrumbMode;
9
10
  footer: boolean;
10
11
  header: boolean;
11
12
  }
12
13
 
13
14
  const DEFAULTS: PowerlineSettings = {
15
+ powerline: true,
14
16
  breadcrumb: 'inner',
15
17
  footer: true,
16
18
  header: true,
@@ -36,6 +38,7 @@ function writeSettings(cwd: string, settings: Record<string, unknown>): void {
36
38
  export function readPowerlineSettings(cwd: string): PowerlineSettings {
37
39
  const s = readSettings(cwd);
38
40
  return {
41
+ powerline: typeof s.powerline === 'boolean' ? s.powerline : DEFAULTS.powerline,
39
42
  breadcrumb: (['hide', 'top', 'inner'].includes(s.breadcrumb as string)
40
43
  ? s.breadcrumb
41
44
  : DEFAULTS.breadcrumb) as BreadcrumbMode,
@@ -54,3 +57,12 @@ export function writePowerlineSetting(
54
57
  s[key] = value;
55
58
  writeSettings(cwd, s);
56
59
  }
60
+
61
+ /** Write multiple powerline settings at once, preserving other keys. */
62
+ export function writePowerlineSettings(cwd: string, patch: Partial<PowerlineSettings>): void {
63
+ const s = readSettings(cwd);
64
+ for (const [k, v] of Object.entries(patch)) {
65
+ s[k] = v;
66
+ }
67
+ writeSettings(cwd, s);
68
+ }
package/widget.ts CHANGED
@@ -64,21 +64,22 @@ export function registerWidget(pi: ExtensionAPI) {
64
64
  ctx.ui.setWidget('powerline-status', undefined);
65
65
  }
66
66
 
67
- // enable only when breadcrumb mode is "top"
67
+ // enable only when powerline master switch is on and breadcrumb mode is "top"
68
68
  pi.on('session_start', (_event, ctx) => {
69
69
  if (!ctx.hasUI) return;
70
- const { breadcrumb } = readPowerlineSettings(ctx.cwd);
71
- if (breadcrumb === 'top') {
70
+ const s = readPowerlineSettings(ctx.cwd);
71
+ if (s.powerline && s.breadcrumb === 'top') {
72
72
  enable(ctx);
73
73
  }
74
74
  });
75
75
 
76
76
  // re-evaluate on model switch (breadcrumb setting may have changed)
77
77
  pi.on('model_select', (_event, ctx) => {
78
- const { breadcrumb } = readPowerlineSettings(ctx.cwd);
79
- if (breadcrumb === 'top' && !widgetEnabled) {
78
+ const s = readPowerlineSettings(ctx.cwd);
79
+ const show = s.powerline && s.breadcrumb === 'top';
80
+ if (show && !widgetEnabled) {
80
81
  enable(ctx);
81
- } else if (breadcrumb !== 'top' && widgetEnabled) {
82
+ } else if (!show && widgetEnabled) {
82
83
  disable(ctx);
83
84
  } else if (widgetEnabled) {
84
85
  liveCtx = ctx;
@@ -89,10 +90,11 @@ export function registerWidget(pi: ExtensionAPI) {
89
90
  // re-evaluate on /powerline command (settings changed)
90
91
  pi.events.on('powerline_settings_changed', (ctx) => {
91
92
  const c = ctx as ExtensionContext;
92
- const { breadcrumb } = readPowerlineSettings(c.cwd);
93
- if (breadcrumb === 'top' && !widgetEnabled) {
93
+ const s = readPowerlineSettings(c.cwd);
94
+ const show = s.powerline && s.breadcrumb === 'top';
95
+ if (show && !widgetEnabled) {
94
96
  enable(c);
95
- } else if (breadcrumb !== 'top' && widgetEnabled) {
97
+ } else if (!show && widgetEnabled) {
96
98
  disable(c);
97
99
  }
98
100
  });