pi-powerline 0.3.1 → 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 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 type { ExtensionAPI, ExtensionContext, Theme } from '@mariozechner/pi-coding-agent';
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 renderLogo(theme: Theme): string[] {
52
- const lines = PI_LOGO.map((line) => ' ' + gradientLine(line) + '\x1b[0m');
53
- const subtitle = `${theme.fg('muted', ' pi agent')}${theme.fg('dim', ` v${VERSION}`)}`;
54
- return ['', ...lines, subtitle];
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 (_tui: any, theme: Theme) => ({
63
- render(_width: number): string[] {
64
- return renderLogo(theme);
65
- },
66
- invalidate() {},
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
- ctx.ui.setHeader(createHeaderComponent());
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', (_event, ctx) => {
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 { powerline, breadcrumb, footer, header } = readPowerlineSettings(ctx.cwd);
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.1",
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": {
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 readSettings(cwd: string): Record<string, unknown> {
22
- const settingsPath = join(cwd, '.pi', 'settings.json');
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
- return JSON.parse(readFileSync(settingsPath, 'utf-8'));
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(join(settingsDir, 'settings.json'), JSON.stringify(settings, null, 2) + '\n');
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