pi-powerline 0.3.0 → 0.4.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 +36 -7
- package/header.ts +365 -16
- package/index.ts +31 -3
- package/package.json +7 -3
- package/settings.ts +36 -4
package/README.md
CHANGED
|
@@ -14,21 +14,49 @@ pi install npm:pi-powerline
|
|
|
14
14
|
|
|
15
15
|
## Settings
|
|
16
16
|
|
|
17
|
+
Settings are read from both global and project files. Project settings override global settings.
|
|
18
|
+
|
|
19
|
+
| Location | Scope |
|
|
20
|
+
|----------|-------|
|
|
21
|
+
| `~/.pi/agent/settings.json` | Global |
|
|
22
|
+
| `.pi/settings.json` | Current project |
|
|
23
|
+
|
|
17
24
|
```json
|
|
18
25
|
// .pi/settings.json
|
|
19
26
|
{
|
|
27
|
+
"powerline": true,
|
|
20
28
|
"breadcrumb": "inner",
|
|
21
29
|
"footer": true,
|
|
22
|
-
"header": true
|
|
30
|
+
"header": true,
|
|
31
|
+
"header-info": false
|
|
23
32
|
}
|
|
24
33
|
```
|
|
25
34
|
|
|
26
|
-
| Setting | Values | Default |
|
|
27
|
-
|
|
28
|
-
| `powerline` | `true` / `false` | `true` |
|
|
29
|
-
| `breadcrumb` | `"hide"` / `"top"` / `"inner"` | `"inner"` |
|
|
30
|
-
| `footer` | `true` / `false` | `true` |
|
|
31
|
-
| `header` | `true` / `false` | `true` |
|
|
35
|
+
| Setting | Values | Default | Effect |
|
|
36
|
+
|---------|--------|---------|--------|
|
|
37
|
+
| `powerline` | `true` / `false` | `true` | Master switch for all pi-powerline UI extensions |
|
|
38
|
+
| `breadcrumb` | `"hide"` / `"top"` / `"inner"` | `"inner"` | Breadcrumb placement |
|
|
39
|
+
| `footer` | `true` / `false` | `true` | Enable custom footer |
|
|
40
|
+
| `header` | `true` / `false` | `true` | Enable custom gradient-logo header |
|
|
41
|
+
| `header-info` | `true` / `false` | `false` | Show header diagnostic info on startup/reload |
|
|
42
|
+
|
|
43
|
+
### Header info
|
|
44
|
+
|
|
45
|
+
`header-info` adds diagnostic sections under the header:
|
|
46
|
+
|
|
47
|
+
- `Context` — loaded system prompt context files, such as `AGENTS.md` and `.pi/APPEND_SYSTEM.md`
|
|
48
|
+
- `Skills` — loaded skills
|
|
49
|
+
- `Prompts` — loaded prompt commands
|
|
50
|
+
- `Extensions` — loaded extension packages or paths
|
|
51
|
+
|
|
52
|
+
It is only rendered for `startup` and `reload`, never for new sessions. It also requires Pi's `quietStartup` setting to be `true`:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"quietStartup": true,
|
|
57
|
+
"header-info": true
|
|
58
|
+
}
|
|
59
|
+
```
|
|
32
60
|
|
|
33
61
|
## Commands
|
|
34
62
|
|
|
@@ -39,6 +67,7 @@ pi install npm:pi-powerline
|
|
|
39
67
|
| `/powerline breadcrumb:top\|inner\|hide` | Set breadcrumb mode |
|
|
40
68
|
| `/powerline footer:on\|off` | Toggle footer |
|
|
41
69
|
| `/powerline header:on\|off` | Toggle header |
|
|
70
|
+
| `/powerline header-info:on\|off` | Toggle header diagnostic info |
|
|
42
71
|
|
|
43
72
|
## License
|
|
44
73
|
|
package/header.ts
CHANGED
|
@@ -4,8 +4,19 @@
|
|
|
4
4
|
* Shows a gradient-colored PI logo.
|
|
5
5
|
* Controlled by .pi/settings.json → header (boolean, default true).
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
7
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
10
|
+
import type {
|
|
11
|
+
ExtensionAPI,
|
|
12
|
+
ExtensionContext,
|
|
13
|
+
BeforeAgentStartEvent,
|
|
14
|
+
SessionStartEvent,
|
|
15
|
+
SlashCommandInfo,
|
|
16
|
+
Theme,
|
|
17
|
+
} from '@mariozechner/pi-coding-agent';
|
|
8
18
|
import { VERSION } from '@mariozechner/pi-coding-agent';
|
|
19
|
+
import { truncateToWidth, wrapTextWithAnsi } from '@mariozechner/pi-tui';
|
|
9
20
|
import { readPowerlineSettings } from './settings.ts';
|
|
10
21
|
|
|
11
22
|
/** Left-to-right ANSI gradient coloring. Spaces are left uncolored. */
|
|
@@ -39,6 +50,60 @@ function gradientLine(line: string): string {
|
|
|
39
50
|
return result;
|
|
40
51
|
}
|
|
41
52
|
|
|
53
|
+
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
54
|
+
|
|
55
|
+
function visibleLength(line: string): number {
|
|
56
|
+
return line.replace(ANSI_PATTERN, '').length;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function centerTruncate(line: string, width: number): string {
|
|
60
|
+
if (width <= 0) return '';
|
|
61
|
+
|
|
62
|
+
const length = visibleLength(line);
|
|
63
|
+
if (length <= width) return line;
|
|
64
|
+
|
|
65
|
+
const reset = '\x1b[0m';
|
|
66
|
+
const start = Math.floor((length - width) / 2);
|
|
67
|
+
const end = start + width;
|
|
68
|
+
let activeAnsi = '';
|
|
69
|
+
let result = '';
|
|
70
|
+
let visibleIdx = 0;
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < line.length; ) {
|
|
73
|
+
const ansi = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
|
|
74
|
+
if (ansi) {
|
|
75
|
+
const code = ansi[0];
|
|
76
|
+
activeAnsi = code === reset ? '' : code;
|
|
77
|
+
if (visibleIdx >= start && visibleIdx < end) {
|
|
78
|
+
result += code;
|
|
79
|
+
}
|
|
80
|
+
i += code.length;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const char = Array.from(line.slice(i))[0] ?? '';
|
|
85
|
+
if (visibleIdx >= start && visibleIdx < end) {
|
|
86
|
+
if (!result && activeAnsi) result += activeAnsi;
|
|
87
|
+
result += char;
|
|
88
|
+
}
|
|
89
|
+
visibleIdx++;
|
|
90
|
+
i += char.length;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result.includes('\x1b[') ? result + reset : result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function centerLine(line: string, width: number): string {
|
|
97
|
+
const centeredLine = centerTruncate(line, width);
|
|
98
|
+
const padding = Math.max(0, Math.floor((width - visibleLength(centeredLine)) / 2));
|
|
99
|
+
return ' '.repeat(padding) + centeredLine;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function centerWrappedLines(line: string, width: number): string[] {
|
|
103
|
+
if (width <= 0) return [''];
|
|
104
|
+
return wrapTextWithAnsi(line, width).map((wrappedLine) => centerLine(wrappedLine, width));
|
|
105
|
+
}
|
|
106
|
+
|
|
42
107
|
const PI_LOGO = [
|
|
43
108
|
'██████████ ',
|
|
44
109
|
'████ ████ ',
|
|
@@ -48,44 +113,328 @@ const PI_LOGO = [
|
|
|
48
113
|
'████ ████',
|
|
49
114
|
];
|
|
50
115
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
116
|
+
function formatReasonStatus(theme: Theme, reason: SessionStartEvent['reason']): string {
|
|
117
|
+
switch (reason) {
|
|
118
|
+
case 'startup':
|
|
119
|
+
return theme.fg('warning', 'Welcome');
|
|
120
|
+
case 'reload':
|
|
121
|
+
return theme.fg('success', 'Reloaded');
|
|
122
|
+
case 'new':
|
|
123
|
+
return theme.fg('success', 'New Session Started');
|
|
124
|
+
default:
|
|
125
|
+
return theme.fg('dim', reason);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface HeaderInfo {
|
|
130
|
+
themeName: string;
|
|
131
|
+
cwd: string;
|
|
132
|
+
commands: string[];
|
|
133
|
+
prompts: string[];
|
|
134
|
+
skills: string[];
|
|
135
|
+
extensions: string[];
|
|
136
|
+
contextItems: string[];
|
|
137
|
+
contextCount: number;
|
|
138
|
+
themesCount: number;
|
|
139
|
+
skillsCount: number;
|
|
140
|
+
promptsCount: number;
|
|
141
|
+
extensionsCount: number;
|
|
142
|
+
commandsCount: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderBullet(theme: Theme, value: string, width: number): string[] {
|
|
146
|
+
if (width <= 0) return [''];
|
|
147
|
+
|
|
148
|
+
const bulletWidth = Math.min(4, width);
|
|
149
|
+
const textWidth = Math.max(1, width - bulletWidth);
|
|
150
|
+
const wrapped = wrapTextWithAnsi(theme.fg('muted', value), textWidth);
|
|
151
|
+
const bullet = theme.fg('dim', ' • ');
|
|
152
|
+
const indent = ' '.repeat(bulletWidth);
|
|
153
|
+
|
|
154
|
+
return wrapped.map((line, index) => `${index === 0 ? bullet : indent}${line}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderInfoSection(theme: Theme, title: string, items: string[], width: number): string[] {
|
|
158
|
+
if (width <= 0) return [''];
|
|
159
|
+
|
|
160
|
+
const values = items.length ? items : ['none'];
|
|
161
|
+
return [
|
|
162
|
+
truncateToWidth(theme.fg('accent', `[${title}]`), width, ''),
|
|
163
|
+
...values.flatMap((item) => renderBullet(theme, item, width)),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderLogo(
|
|
168
|
+
theme: Theme,
|
|
169
|
+
reason: SessionStartEvent['reason'],
|
|
170
|
+
width: number,
|
|
171
|
+
info?: HeaderInfo,
|
|
172
|
+
): string[] {
|
|
173
|
+
const logoWidth = Math.max(...PI_LOGO.map((line) => line.length));
|
|
174
|
+
const lines = PI_LOGO.map((line) =>
|
|
175
|
+
centerLine(gradientLine(line.padEnd(logoWidth)) + '\x1b[0m', width),
|
|
176
|
+
);
|
|
177
|
+
const subtitle = `${theme.fg('muted', 'pi agent')}${theme.fg('dim', ` v${VERSION}`)}`;
|
|
178
|
+
const result = [
|
|
179
|
+
'',
|
|
180
|
+
...lines,
|
|
181
|
+
...centerWrappedLines(subtitle, width),
|
|
182
|
+
...centerWrappedLines(formatReasonStatus(theme, reason), width),
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
if (!info) return result;
|
|
186
|
+
|
|
187
|
+
const counts = [
|
|
188
|
+
`context: ${info.contextCount}`,
|
|
189
|
+
`themes: ${info.themesCount}`,
|
|
190
|
+
`skills: ${info.skillsCount}`,
|
|
191
|
+
`prompts: ${info.promptsCount}`,
|
|
192
|
+
`extensions: ${info.extensionsCount}`,
|
|
193
|
+
`commands: ${info.commandsCount}`,
|
|
194
|
+
].join(' | ');
|
|
195
|
+
|
|
196
|
+
return [
|
|
197
|
+
...result,
|
|
198
|
+
'',
|
|
199
|
+
...centerWrappedLines(theme.fg('dim', counts), width),
|
|
200
|
+
'',
|
|
201
|
+
...renderInfoSection(theme, 'Context', info.contextItems, width),
|
|
202
|
+
'',
|
|
203
|
+
...renderInfoSection(theme, 'Skills', info.skills, width),
|
|
204
|
+
'',
|
|
205
|
+
...renderInfoSection(
|
|
206
|
+
theme,
|
|
207
|
+
'Prompts',
|
|
208
|
+
info.prompts.map((name) => `/${name}`),
|
|
209
|
+
width,
|
|
210
|
+
),
|
|
211
|
+
'',
|
|
212
|
+
...renderInfoSection(theme, 'Extensions', info.extensions, width),
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getCommandNames(
|
|
217
|
+
commands: SlashCommandInfo[],
|
|
218
|
+
source?: SlashCommandInfo['source'],
|
|
219
|
+
): string[] {
|
|
220
|
+
return commands
|
|
221
|
+
.filter((command) => !source || command.source === source)
|
|
222
|
+
.map((command) => command.name)
|
|
223
|
+
.sort((a, b) => a.localeCompare(b));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function countUniqueSources(
|
|
227
|
+
commands: SlashCommandInfo[],
|
|
228
|
+
source: SlashCommandInfo['source'],
|
|
229
|
+
): number {
|
|
230
|
+
return new Set(
|
|
231
|
+
commands
|
|
232
|
+
.filter((command) => command.source === source)
|
|
233
|
+
.map((command) => command.sourceInfo?.path || command.sourceInfo?.source || command.name),
|
|
234
|
+
).size;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function formatRelativePath(cwd: string, filePath: string): string {
|
|
238
|
+
return relative(cwd, filePath) || '.';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function formatDisplayPath(cwd: string, filePath: string): string {
|
|
242
|
+
const home = homedir();
|
|
243
|
+
const rel = relative(cwd, filePath);
|
|
244
|
+
if (!rel || (!rel.startsWith('..') && !rel.startsWith('/'))) return rel || '.';
|
|
245
|
+
if (filePath === home) return '~';
|
|
246
|
+
if (filePath.startsWith(`${home}/`)) return `~/${relative(home, filePath)}`;
|
|
247
|
+
return filePath;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function discoverContextItems(cwd: string): string[] {
|
|
251
|
+
const candidates = ['AGENTS.md', 'AGENTS.MD', 'CLAUDE.md', 'CLAUDE.MD'];
|
|
252
|
+
const items: string[] = [];
|
|
253
|
+
const seen = new Set<string>();
|
|
254
|
+
let currentDir = resolve(cwd);
|
|
255
|
+
const root = resolve('/');
|
|
256
|
+
|
|
257
|
+
while (true) {
|
|
258
|
+
for (const filename of candidates) {
|
|
259
|
+
const filePath = join(currentDir, filename);
|
|
260
|
+
if (existsSync(filePath) && !seen.has(filePath)) {
|
|
261
|
+
items.unshift(formatRelativePath(cwd, filePath));
|
|
262
|
+
seen.add(filePath);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (currentDir === root) break;
|
|
268
|
+
const parentDir = resolve(currentDir, '..');
|
|
269
|
+
if (parentDir === currentDir) break;
|
|
270
|
+
currentDir = parentDir;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const appendSystemPath = join(cwd, '.pi', 'APPEND_SYSTEM.md');
|
|
274
|
+
if (existsSync(appendSystemPath)) items.push(formatRelativePath(cwd, appendSystemPath));
|
|
275
|
+
return items;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function normalizeSystemPromptItems(ctx: ExtensionContext, event: BeforeAgentStartEvent): string[] {
|
|
279
|
+
const files = event.systemPromptOptions.contextFiles ?? [];
|
|
280
|
+
const nextItems = files.map((file: { path: string }) => formatRelativePath(ctx.cwd, file.path));
|
|
281
|
+
if (event.systemPromptOptions.customPrompt) nextItems.unshift('custom system prompt');
|
|
282
|
+
if (event.systemPromptOptions.appendSystemPrompt) nextItems.push('append system prompt');
|
|
283
|
+
return nextItems;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getDirectory(path: string): string {
|
|
287
|
+
try {
|
|
288
|
+
return statSync(path).isDirectory() ? path : dirname(path);
|
|
289
|
+
} catch {
|
|
290
|
+
return dirname(path);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function readPackageLabel(startPath: string): string | undefined {
|
|
295
|
+
let currentDir = getDirectory(startPath);
|
|
296
|
+
const root = resolve('/');
|
|
297
|
+
|
|
298
|
+
while (true) {
|
|
299
|
+
const packagePath = join(currentDir, 'package.json');
|
|
300
|
+
if (existsSync(packagePath)) {
|
|
301
|
+
try {
|
|
302
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8')) as {
|
|
303
|
+
name?: string;
|
|
304
|
+
version?: string;
|
|
305
|
+
};
|
|
306
|
+
if (pkg.name) return `${pkg.name}${pkg.version ? ` (v${pkg.version})` : ''}`;
|
|
307
|
+
} catch {
|
|
308
|
+
// ignore invalid package metadata
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (currentDir === root) break;
|
|
313
|
+
const parentDir = resolve(currentDir, '..');
|
|
314
|
+
if (parentDir === currentDir) break;
|
|
315
|
+
currentDir = parentDir;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getExtensionItems(cwd: string, commands: SlashCommandInfo[]): string[] {
|
|
322
|
+
const extensions = new Map<string, string>();
|
|
323
|
+
|
|
324
|
+
for (const command of commands) {
|
|
325
|
+
if (command.source !== 'extension') continue;
|
|
326
|
+
|
|
327
|
+
const sourcePath = command.sourceInfo?.baseDir ?? command.sourceInfo?.path;
|
|
328
|
+
const key = sourcePath || command.sourceInfo?.source || command.name;
|
|
329
|
+
const label =
|
|
330
|
+
(sourcePath ? readPackageLabel(sourcePath) : undefined) ??
|
|
331
|
+
(sourcePath ? formatDisplayPath(cwd, sourcePath) : undefined) ??
|
|
332
|
+
command.sourceInfo?.source ??
|
|
333
|
+
command.name;
|
|
334
|
+
extensions.set(key, label);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return Array.from(extensions.values()).sort((a, b) => a.localeCompare(b));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function shouldShowHeaderInfo(ctx: ExtensionContext, reason: SessionStartEvent['reason']): boolean {
|
|
341
|
+
if (reason !== 'startup' && reason !== 'reload') return false;
|
|
342
|
+
const settings = readPowerlineSettings(ctx.cwd);
|
|
343
|
+
if (!settings.quietStartup) return false;
|
|
344
|
+
return settings['header-info'];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function collectHeaderInfo(
|
|
348
|
+
pi: ExtensionAPI,
|
|
349
|
+
ctx: ExtensionContext,
|
|
350
|
+
theme: Theme,
|
|
351
|
+
contextItems: string[],
|
|
352
|
+
skillItems: string[],
|
|
353
|
+
): HeaderInfo {
|
|
354
|
+
const commands = typeof pi.getCommands === 'function' ? pi.getCommands() : [];
|
|
355
|
+
const allThemes = typeof ctx.ui.getAllThemes === 'function' ? ctx.ui.getAllThemes() : [];
|
|
356
|
+
const themeName = theme.name ?? ctx.ui.theme?.name ?? 'current';
|
|
357
|
+
const extensions = getExtensionItems(ctx.cwd, commands);
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
themeName,
|
|
361
|
+
cwd: ctx.cwd,
|
|
362
|
+
commands: getCommandNames(commands),
|
|
363
|
+
prompts: getCommandNames(commands, 'prompt'),
|
|
364
|
+
skills: skillItems,
|
|
365
|
+
extensions,
|
|
366
|
+
contextItems,
|
|
367
|
+
contextCount: contextItems.length,
|
|
368
|
+
themesCount: allThemes.length,
|
|
369
|
+
skillsCount: skillItems.length,
|
|
370
|
+
promptsCount: countUniqueSources(commands, 'prompt'),
|
|
371
|
+
extensionsCount: extensions.length,
|
|
372
|
+
commandsCount: commands.length,
|
|
373
|
+
};
|
|
55
374
|
}
|
|
56
375
|
|
|
57
376
|
/** Register the custom header extension. */
|
|
58
377
|
export function registerHeader(pi: ExtensionAPI) {
|
|
59
378
|
let headerEnabled = false;
|
|
379
|
+
let currentReason: SessionStartEvent['reason'] = 'startup';
|
|
380
|
+
let liveTui: any = null;
|
|
381
|
+
let contextItems: string[] = [];
|
|
382
|
+
let skillItems: string[] = [];
|
|
60
383
|
|
|
61
|
-
function createHeaderComponent() {
|
|
62
|
-
return (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
384
|
+
function createHeaderComponent(ctx: ExtensionContext, reason: SessionStartEvent['reason']) {
|
|
385
|
+
return (tui: any, theme: Theme) => {
|
|
386
|
+
liveTui = tui;
|
|
387
|
+
return {
|
|
388
|
+
render(width: number): string[] {
|
|
389
|
+
return renderLogo(
|
|
390
|
+
theme,
|
|
391
|
+
reason,
|
|
392
|
+
width,
|
|
393
|
+
shouldShowHeaderInfo(ctx, reason)
|
|
394
|
+
? collectHeaderInfo(pi, ctx, theme, contextItems, skillItems)
|
|
395
|
+
: undefined,
|
|
396
|
+
);
|
|
397
|
+
},
|
|
398
|
+
invalidate() {},
|
|
399
|
+
};
|
|
400
|
+
};
|
|
68
401
|
}
|
|
69
402
|
|
|
70
|
-
function enable(ctx: ExtensionContext) {
|
|
403
|
+
function enable(ctx: ExtensionContext, reason = currentReason) {
|
|
71
404
|
headerEnabled = true;
|
|
72
|
-
|
|
405
|
+
currentReason = reason;
|
|
406
|
+
ctx.ui.setHeader(createHeaderComponent(ctx, reason));
|
|
73
407
|
}
|
|
74
408
|
|
|
75
409
|
function disable(ctx: ExtensionContext) {
|
|
76
410
|
headerEnabled = false;
|
|
411
|
+
liveTui = null;
|
|
77
412
|
ctx.ui.setHeader(undefined);
|
|
78
413
|
}
|
|
79
414
|
|
|
80
415
|
// auto-enable on session start if powerline master switch + header setting are both on
|
|
81
|
-
pi.on('session_start', (
|
|
416
|
+
pi.on('session_start', (event, ctx) => {
|
|
82
417
|
if (!ctx.hasUI) return;
|
|
418
|
+
const commands = typeof pi.getCommands === 'function' ? pi.getCommands() : [];
|
|
419
|
+
contextItems = discoverContextItems(ctx.cwd);
|
|
420
|
+
skillItems = getCommandNames(commands, 'skill');
|
|
83
421
|
const s = readPowerlineSettings(ctx.cwd);
|
|
84
422
|
if (s.powerline && s.header) {
|
|
85
|
-
enable(ctx);
|
|
423
|
+
enable(ctx, event.reason);
|
|
86
424
|
}
|
|
87
425
|
});
|
|
88
426
|
|
|
427
|
+
// Refresh with Pi's exact system prompt sources once a prompt is submitted.
|
|
428
|
+
pi.on('before_agent_start', (event, ctx) => {
|
|
429
|
+
const nextItems = normalizeSystemPromptItems(ctx, event);
|
|
430
|
+
const nextSkills = (event.systemPromptOptions.skills ?? [])
|
|
431
|
+
.map((skill) => skill.name)
|
|
432
|
+
.sort((a, b) => a.localeCompare(b));
|
|
433
|
+
if (nextItems.length > 0) contextItems = nextItems;
|
|
434
|
+
skillItems = nextSkills;
|
|
435
|
+
liveTui?.requestRender();
|
|
436
|
+
});
|
|
437
|
+
|
|
89
438
|
// re-evaluate on model switch
|
|
90
439
|
pi.on('model_select', (_event, ctx) => {
|
|
91
440
|
const s = readPowerlineSettings(ctx.cwd);
|
package/index.ts
CHANGED
|
@@ -32,6 +32,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
32
32
|
default: true,
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
pi.registerFlag('header-info', {
|
|
36
|
+
description: 'Show diagnostic info in custom header',
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
default: false,
|
|
39
|
+
});
|
|
40
|
+
|
|
35
41
|
// register all sub-extensions
|
|
36
42
|
registerEditor(pi);
|
|
37
43
|
registerFooter(pi);
|
|
@@ -83,6 +89,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
83
89
|
label: 'header:off',
|
|
84
90
|
description: 'Disable custom header',
|
|
85
91
|
},
|
|
92
|
+
{
|
|
93
|
+
value: 'header-info:on',
|
|
94
|
+
label: 'header-info:on',
|
|
95
|
+
description: 'Show diagnostic info in header',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
value: 'header-info:off',
|
|
99
|
+
label: 'header-info:off',
|
|
100
|
+
description: 'Hide diagnostic info in header',
|
|
101
|
+
},
|
|
86
102
|
];
|
|
87
103
|
if (!prefix) return items;
|
|
88
104
|
return items.filter((i) => i.value.startsWith(prefix));
|
|
@@ -102,12 +118,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
102
118
|
|
|
103
119
|
// show status
|
|
104
120
|
if (arg === 'info') {
|
|
105
|
-
const
|
|
121
|
+
const settings = readPowerlineSettings(ctx.cwd);
|
|
122
|
+
const { powerline, breadcrumb, footer, header } = settings;
|
|
106
123
|
const lines = [
|
|
107
124
|
`powerline: ${powerline ? 'on' : 'off'}`,
|
|
108
125
|
`breadcrumb: ${breadcrumb}`,
|
|
109
126
|
`footer: ${footer ? 'on' : 'off'}`,
|
|
110
127
|
`header: ${header ? 'on' : 'off'}`,
|
|
128
|
+
`header-info: ${settings['header-info'] ? 'on' : 'off'}`,
|
|
111
129
|
];
|
|
112
130
|
ctx.ui.notify(lines.join('\n'), 'info');
|
|
113
131
|
return;
|
|
@@ -117,7 +135,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
117
135
|
const colonIdx = arg.indexOf(':');
|
|
118
136
|
if (colonIdx === -1) {
|
|
119
137
|
ctx.ui.notify(
|
|
120
|
-
'Usage: /powerline <info|breadcrumb:hide|top|inner|footer:on|off|header:on|off>',
|
|
138
|
+
'Usage: /powerline <info|breadcrumb:hide|top|inner|footer:on|off|header:on|off|header-info:on|off>',
|
|
121
139
|
'warning',
|
|
122
140
|
);
|
|
123
141
|
return;
|
|
@@ -158,9 +176,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
158
176
|
msg = `header → ${val}`;
|
|
159
177
|
break;
|
|
160
178
|
}
|
|
179
|
+
case 'header-info': {
|
|
180
|
+
if (val !== 'on' && val !== 'off') {
|
|
181
|
+
ctx.ui.notify('header-info must be: on or off', 'warning');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
writePowerlineSetting(ctx.cwd, 'header-info', val === 'on');
|
|
185
|
+
pi.events.emit('powerline_settings_changed', ctx);
|
|
186
|
+
msg = `header-info → ${val}`;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
161
189
|
default:
|
|
162
190
|
ctx.ui.notify(
|
|
163
|
-
'Usage: /powerline <breadcrumb:hide|top|inner|footer:on|off|header:on|off>',
|
|
191
|
+
'Usage: /powerline <breadcrumb:hide|top|inner|footer:on|off|header:on|off|header-info:on|off>',
|
|
164
192
|
'warning',
|
|
165
193
|
);
|
|
166
194
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-powerline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Powerline-style UI extensions for pi coding agent (custom editor, breadcrumb, footer, header)",
|
|
5
5
|
"homepage": "https://github.com/jwu/pi-powerline#readme",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"typecheck": "bun tsc --noEmit --ignoreDeprecations 6.0",
|
|
32
32
|
"format": "prettier --write '**/*.ts'",
|
|
33
33
|
"format:check": "prettier --check '**/*.ts'",
|
|
34
|
-
"prepare": "[ -d .git ] &&
|
|
34
|
+
"prepare": "[ -d .git ] && simple-git-hooks || true",
|
|
35
35
|
"release": "GH_TOKEN=$(gh auth token) semantic-release --no-ci",
|
|
36
36
|
"release:dry": "GH_TOKEN=$(gh auth token) semantic-release --no-ci --dry-run"
|
|
37
37
|
},
|
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
]
|
|
67
67
|
]
|
|
68
68
|
},
|
|
69
|
+
"simple-git-hooks": {
|
|
70
|
+
"pre-commit": "bun prettier --check '**/*.ts' && bun test",
|
|
71
|
+
"commit-msg": "bun commitlint --edit \"$1\""
|
|
72
|
+
},
|
|
69
73
|
"pi": {
|
|
70
74
|
"extensions": [
|
|
71
75
|
"./index.ts"
|
|
@@ -86,9 +90,9 @@
|
|
|
86
90
|
"@semantic-release/github": "^12.0.6",
|
|
87
91
|
"@semantic-release/npm": "^13.1.5",
|
|
88
92
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
89
|
-
"husky": "^9.1.7",
|
|
90
93
|
"prettier": "^3.8.3",
|
|
91
94
|
"semantic-release": "^25.0.3",
|
|
95
|
+
"simple-git-hooks": "^2.13.1",
|
|
92
96
|
"typescript": "^6.0.3"
|
|
93
97
|
}
|
|
94
98
|
}
|
package/settings.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// shared settings read/write helpers for pi-powerline
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
|
|
5
6
|
export type BreadcrumbMode = 'hide' | 'top' | 'inner';
|
|
@@ -9,6 +10,8 @@ export interface PowerlineSettings {
|
|
|
9
10
|
breadcrumb: BreadcrumbMode;
|
|
10
11
|
footer: boolean;
|
|
11
12
|
header: boolean;
|
|
13
|
+
'header-info': boolean;
|
|
14
|
+
quietStartup: boolean;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
const DEFAULTS: PowerlineSettings = {
|
|
@@ -16,22 +19,48 @@ const DEFAULTS: PowerlineSettings = {
|
|
|
16
19
|
breadcrumb: 'inner',
|
|
17
20
|
footer: true,
|
|
18
21
|
header: true,
|
|
22
|
+
'header-info': false,
|
|
23
|
+
quietStartup: false,
|
|
19
24
|
};
|
|
20
25
|
|
|
21
|
-
function
|
|
22
|
-
|
|
26
|
+
function getSettingsPath(): string {
|
|
27
|
+
return join(process.env.HOME ?? homedir(), '.pi', 'agent', 'settings.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getProjectSettingsPath(cwd: string): string {
|
|
31
|
+
return join(cwd, '.pi', 'settings.json');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readSettingsFile(settingsPath: string): Record<string, unknown> {
|
|
23
35
|
if (!existsSync(settingsPath)) return {};
|
|
24
36
|
try {
|
|
25
|
-
|
|
37
|
+
const value = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
38
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
39
|
+
? (value as Record<string, unknown>)
|
|
40
|
+
: {};
|
|
26
41
|
} catch {
|
|
27
42
|
return {};
|
|
28
43
|
}
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
function mergeSettings(
|
|
47
|
+
globalSettings: Record<string, unknown>,
|
|
48
|
+
projectSettings: Record<string, unknown>,
|
|
49
|
+
): Record<string, unknown> {
|
|
50
|
+
return { ...globalSettings, ...projectSettings };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function readSettings(cwd: string = process.cwd()): Record<string, unknown> {
|
|
54
|
+
return mergeSettings(
|
|
55
|
+
readSettingsFile(getSettingsPath()),
|
|
56
|
+
readSettingsFile(getProjectSettingsPath(cwd)),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
function writeSettings(cwd: string, settings: Record<string, unknown>): void {
|
|
32
61
|
const settingsDir = join(cwd, '.pi');
|
|
33
62
|
if (!existsSync(settingsDir)) mkdirSync(settingsDir, { recursive: true });
|
|
34
|
-
writeFileSync(
|
|
63
|
+
writeFileSync(getProjectSettingsPath(cwd), JSON.stringify(settings, null, 2) + '\n');
|
|
35
64
|
}
|
|
36
65
|
|
|
37
66
|
/** Read powerline settings, validating and applying defaults. */
|
|
@@ -44,6 +73,9 @@ export function readPowerlineSettings(cwd: string): PowerlineSettings {
|
|
|
44
73
|
: DEFAULTS.breadcrumb) as BreadcrumbMode,
|
|
45
74
|
footer: typeof s.footer === 'boolean' ? s.footer : DEFAULTS.footer,
|
|
46
75
|
header: typeof s.header === 'boolean' ? s.header : DEFAULTS.header,
|
|
76
|
+
'header-info':
|
|
77
|
+
typeof s['header-info'] === 'boolean' ? s['header-info'] : DEFAULTS['header-info'],
|
|
78
|
+
quietStartup: typeof s.quietStartup === 'boolean' ? s.quietStartup : DEFAULTS.quietStartup,
|
|
47
79
|
};
|
|
48
80
|
}
|
|
49
81
|
|