pi-powerline-footer 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/README.md +75 -0
- package/colors.ts +126 -0
- package/git-status.ts +242 -0
- package/icons.ts +156 -0
- package/index.ts +412 -0
- package/install.mjs +30 -0
- package/package.json +27 -0
- package/presets.ts +80 -0
- package/segments.ts +440 -0
- package/separators.ts +57 -0
- package/types.ts +129 -0
package/index.ts
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import type { ExtensionAPI, ReadonlyFooterDataProvider } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
3
|
+
import { visibleWidth } from "@mariozechner/pi-tui";
|
|
4
|
+
|
|
5
|
+
import type { SegmentContext, StatusLinePreset } from "./types.js";
|
|
6
|
+
import { getPreset, PRESETS } from "./presets.js";
|
|
7
|
+
import { getSeparator } from "./separators.js";
|
|
8
|
+
import { renderSegment } from "./segments.js";
|
|
9
|
+
import { getGitStatus, invalidateGitStatus, invalidateGitBranch } from "./git-status.js";
|
|
10
|
+
import { ansi, getFgAnsiCode } from "./colors.js";
|
|
11
|
+
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
// Configuration
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
15
|
+
|
|
16
|
+
interface PowerlineConfig {
|
|
17
|
+
preset: StatusLinePreset;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let config: PowerlineConfig = {
|
|
21
|
+
preset: "default",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
+
// Status Line Builder (for top border)
|
|
26
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
+
|
|
28
|
+
/** Build just the status content (segments with separators, no borders) */
|
|
29
|
+
function buildStatusContent(ctx: SegmentContext, presetDef: ReturnType<typeof getPreset>): string {
|
|
30
|
+
const separatorDef = getSeparator(presetDef.separator);
|
|
31
|
+
const sepAnsi = getFgAnsiCode("sep");
|
|
32
|
+
|
|
33
|
+
// Collect visible segment contents
|
|
34
|
+
const leftParts: string[] = [];
|
|
35
|
+
for (const segId of presetDef.leftSegments) {
|
|
36
|
+
const rendered = renderSegment(segId, ctx);
|
|
37
|
+
if (rendered.visible && rendered.content) {
|
|
38
|
+
leftParts.push(rendered.content);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const rightParts: string[] = [];
|
|
43
|
+
for (const segId of presetDef.rightSegments) {
|
|
44
|
+
const rendered = renderSegment(segId, ctx);
|
|
45
|
+
if (rendered.visible && rendered.content) {
|
|
46
|
+
rightParts.push(rendered.content);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (leftParts.length === 0 && rightParts.length === 0) {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Build content with powerline separators (no background)
|
|
55
|
+
const sep = separatorDef.left;
|
|
56
|
+
const allParts = [...leftParts, ...rightParts];
|
|
57
|
+
return " " + allParts.join(` ${sepAnsi}${sep}${ansi.reset} `) + ansi.reset + " ";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
61
|
+
// Extension
|
|
62
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
63
|
+
|
|
64
|
+
export default function powerlineFooter(pi: ExtensionAPI) {
|
|
65
|
+
let enabled = true;
|
|
66
|
+
let sessionStartTime = Date.now();
|
|
67
|
+
let currentCtx: any = null;
|
|
68
|
+
let footerDataRef: ReadonlyFooterDataProvider | null = null;
|
|
69
|
+
let getThinkingLevelFn: (() => string) | null = null;
|
|
70
|
+
let isStreaming = false;
|
|
71
|
+
let tuiRef: any = null; // Store TUI reference for forcing re-renders
|
|
72
|
+
|
|
73
|
+
// Track session start
|
|
74
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
75
|
+
sessionStartTime = Date.now();
|
|
76
|
+
currentCtx = ctx;
|
|
77
|
+
|
|
78
|
+
// Store thinking level getter if available
|
|
79
|
+
if (typeof ctx.getThinkingLevel === 'function') {
|
|
80
|
+
getThinkingLevelFn = () => ctx.getThinkingLevel();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (enabled && ctx.hasUI) {
|
|
84
|
+
setupCustomEditor(ctx);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Check if a bash command might change git branch
|
|
89
|
+
const mightChangeGitBranch = (cmd: string): boolean => {
|
|
90
|
+
const gitBranchPatterns = [
|
|
91
|
+
/\bgit\s+(checkout|switch|branch\s+-[dDmM]|merge|rebase|pull|reset|worktree)/,
|
|
92
|
+
/\bgit\s+stash\s+(pop|apply)/,
|
|
93
|
+
];
|
|
94
|
+
return gitBranchPatterns.some(p => p.test(cmd));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Invalidate git status on file changes, trigger re-render on potential branch changes
|
|
98
|
+
pi.on("tool_result", async (event, _ctx) => {
|
|
99
|
+
if (event.toolName === "write" || event.toolName === "edit") {
|
|
100
|
+
invalidateGitStatus();
|
|
101
|
+
}
|
|
102
|
+
// Check for bash commands that might change git branch
|
|
103
|
+
if (event.toolName === "bash" && event.input?.command) {
|
|
104
|
+
const cmd = String(event.input.command);
|
|
105
|
+
if (mightChangeGitBranch(cmd)) {
|
|
106
|
+
// Invalidate caches since working tree state changes with branch
|
|
107
|
+
invalidateGitStatus();
|
|
108
|
+
invalidateGitBranch();
|
|
109
|
+
// Small delay to let git update, then re-render
|
|
110
|
+
setTimeout(() => tuiRef?.requestRender(), 100);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Also catch user escape commands (! prefix)
|
|
116
|
+
// Note: This fires BEFORE execution, so we use a longer delay and multiple re-renders
|
|
117
|
+
// to ensure we catch the update after the command completes.
|
|
118
|
+
pi.on("user_bash", async (event, _ctx) => {
|
|
119
|
+
if (mightChangeGitBranch(event.command)) {
|
|
120
|
+
// Invalidate immediately so next render fetches fresh data
|
|
121
|
+
invalidateGitStatus();
|
|
122
|
+
invalidateGitBranch();
|
|
123
|
+
// Multiple staggered re-renders to catch fast and slow commands
|
|
124
|
+
setTimeout(() => tuiRef?.requestRender(), 100);
|
|
125
|
+
setTimeout(() => tuiRef?.requestRender(), 300);
|
|
126
|
+
setTimeout(() => tuiRef?.requestRender(), 500);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Track streaming state (footer only shows status during streaming)
|
|
131
|
+
pi.on("stream_start", async () => {
|
|
132
|
+
isStreaming = true;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
pi.on("stream_end", async () => {
|
|
136
|
+
isStreaming = false;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Command to toggle/configure
|
|
140
|
+
pi.registerCommand("powerline", {
|
|
141
|
+
description: "Configure powerline status (toggle, preset)",
|
|
142
|
+
handler: async (args, ctx) => {
|
|
143
|
+
// Update context reference (command ctx may have more methods)
|
|
144
|
+
currentCtx = ctx;
|
|
145
|
+
|
|
146
|
+
if (!args) {
|
|
147
|
+
// Toggle
|
|
148
|
+
enabled = !enabled;
|
|
149
|
+
if (enabled) {
|
|
150
|
+
setupCustomEditor(ctx);
|
|
151
|
+
ctx.ui.notify("Powerline status enabled", "info");
|
|
152
|
+
} else {
|
|
153
|
+
// setFooter(undefined) internally calls the old footer's dispose()
|
|
154
|
+
ctx.ui.setEditorComponent(undefined);
|
|
155
|
+
ctx.ui.setFooter(undefined);
|
|
156
|
+
footerDataRef = null;
|
|
157
|
+
tuiRef = null;
|
|
158
|
+
ctx.ui.notify("Default editor restored", "info");
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if args is a preset name
|
|
164
|
+
const preset = args.trim().toLowerCase() as StatusLinePreset;
|
|
165
|
+
if (preset in PRESETS) {
|
|
166
|
+
config.preset = preset;
|
|
167
|
+
if (enabled) {
|
|
168
|
+
setupCustomEditor(ctx);
|
|
169
|
+
}
|
|
170
|
+
ctx.ui.notify(`Preset set to: ${preset}`, "info");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Show available presets
|
|
175
|
+
const presetList = Object.keys(PRESETS).join(", ");
|
|
176
|
+
ctx.ui.notify(`Available presets: ${presetList}`, "info");
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
function buildSegmentContext(ctx: any, width: number): SegmentContext {
|
|
181
|
+
const presetDef = getPreset(config.preset);
|
|
182
|
+
|
|
183
|
+
// Build usage stats and get thinking level from session
|
|
184
|
+
let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
|
|
185
|
+
let lastAssistant: AssistantMessage | undefined;
|
|
186
|
+
let thinkingLevelFromSession = "off";
|
|
187
|
+
|
|
188
|
+
const sessionEvents = ctx.sessionManager?.getBranch?.() ?? [];
|
|
189
|
+
for (const e of sessionEvents) {
|
|
190
|
+
// Check for thinking level change entries
|
|
191
|
+
if (e.type === "thinking_level_change" && e.thinkingLevel) {
|
|
192
|
+
thinkingLevelFromSession = e.thinkingLevel;
|
|
193
|
+
}
|
|
194
|
+
if (e.type === "message" && e.message.role === "assistant") {
|
|
195
|
+
const m = e.message as AssistantMessage;
|
|
196
|
+
if (m.stopReason === "error" || m.stopReason === "aborted") {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
input += m.usage.input;
|
|
200
|
+
output += m.usage.output;
|
|
201
|
+
cacheRead += m.usage.cacheRead;
|
|
202
|
+
cacheWrite += m.usage.cacheWrite;
|
|
203
|
+
cost += m.usage.cost.total;
|
|
204
|
+
lastAssistant = m;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Calculate context percentage (total tokens used in last turn)
|
|
209
|
+
const contextTokens = lastAssistant
|
|
210
|
+
? lastAssistant.usage.input + lastAssistant.usage.output +
|
|
211
|
+
lastAssistant.usage.cacheRead + lastAssistant.usage.cacheWrite
|
|
212
|
+
: 0;
|
|
213
|
+
const contextWindow = ctx.model?.contextWindow || 0;
|
|
214
|
+
const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
215
|
+
|
|
216
|
+
// Get git status (cached)
|
|
217
|
+
const gitBranch = footerDataRef?.getGitBranch() ?? null;
|
|
218
|
+
const gitStatus = getGitStatus(gitBranch);
|
|
219
|
+
|
|
220
|
+
// Check if using OAuth subscription
|
|
221
|
+
const usingSubscription = ctx.model
|
|
222
|
+
? ctx.modelRegistry?.isUsingOAuth?.(ctx.model) ?? false
|
|
223
|
+
: false;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
model: ctx.model,
|
|
227
|
+
thinkingLevel: thinkingLevelFromSession || getThinkingLevelFn?.() || "off",
|
|
228
|
+
sessionId: ctx.sessionManager?.getSessionId?.(),
|
|
229
|
+
usageStats: { input, output, cacheRead, cacheWrite, cost },
|
|
230
|
+
contextPercent,
|
|
231
|
+
contextWindow,
|
|
232
|
+
autoCompactEnabled: ctx.settingsManager?.getCompactionSettings?.()?.enabled ?? true,
|
|
233
|
+
usingSubscription,
|
|
234
|
+
sessionStartTime,
|
|
235
|
+
git: gitStatus,
|
|
236
|
+
extensionStatuses: footerDataRef?.getExtensionStatuses() ?? new Map(),
|
|
237
|
+
options: presetDef.segmentOptions ?? {},
|
|
238
|
+
width,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function setupCustomEditor(ctx: any) {
|
|
243
|
+
// Import CustomEditor dynamically and create wrapper
|
|
244
|
+
import("@mariozechner/pi-coding-agent").then(({ CustomEditor }) => {
|
|
245
|
+
ctx.ui.setEditorComponent((tui: any, theme: any, keybindings: any) => {
|
|
246
|
+
// Create custom editor that overrides render for status in top border
|
|
247
|
+
const editor = new CustomEditor(theme, keybindings);
|
|
248
|
+
|
|
249
|
+
// Store original render
|
|
250
|
+
const originalRender = editor.render.bind(editor);
|
|
251
|
+
|
|
252
|
+
// Override render to match oh-my-pi design with rounded box:
|
|
253
|
+
// ╭─ status content ────────────────────╮
|
|
254
|
+
// │ input text here │
|
|
255
|
+
// ╰─ ─╯
|
|
256
|
+
// + autocomplete items (if showing)
|
|
257
|
+
editor.render = (width: number): string[] => {
|
|
258
|
+
const bc = (s: string) => `${getFgAnsiCode("border")}${s}${ansi.reset}`;
|
|
259
|
+
|
|
260
|
+
// Box drawing chars
|
|
261
|
+
const topLeft = bc("╭─");
|
|
262
|
+
const topRight = bc("─╮");
|
|
263
|
+
const bottomLeft = bc("╰─");
|
|
264
|
+
const bottomRight = bc("─╯");
|
|
265
|
+
const vertical = bc("│");
|
|
266
|
+
|
|
267
|
+
// Content area is width - 6 (3 chars border on each side)
|
|
268
|
+
const contentWidth = Math.max(1, width - 6);
|
|
269
|
+
const lines = originalRender(contentWidth);
|
|
270
|
+
|
|
271
|
+
if (lines.length === 0 || !currentCtx) return lines;
|
|
272
|
+
|
|
273
|
+
// Find where the bottom border is (last line that's all ─ chars)
|
|
274
|
+
// Lines after it are autocomplete items
|
|
275
|
+
let bottomBorderIndex = lines.length - 1;
|
|
276
|
+
for (let i = lines.length - 1; i >= 1; i--) {
|
|
277
|
+
const stripped = lines[i]?.replace(/\x1b\[[0-9;]*m/g, "") || "";
|
|
278
|
+
if (stripped.length > 0 && /^─+$/.test(stripped)) {
|
|
279
|
+
bottomBorderIndex = i;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const result: string[] = [];
|
|
285
|
+
|
|
286
|
+
// Top border: ╭─ status ────────────╮
|
|
287
|
+
const presetDef = getPreset(config.preset);
|
|
288
|
+
const segmentCtx = buildSegmentContext(currentCtx, width);
|
|
289
|
+
const statusContent = buildStatusContent(segmentCtx, presetDef);
|
|
290
|
+
const statusWidth = visibleWidth(statusContent);
|
|
291
|
+
const topFillWidth = width - 4; // Reserve 4 for corners (╭─ and ─╮)
|
|
292
|
+
|
|
293
|
+
if (statusWidth <= topFillWidth) {
|
|
294
|
+
const fillWidth = topFillWidth - statusWidth;
|
|
295
|
+
result.push(topLeft + statusContent + bc("─".repeat(fillWidth)) + topRight);
|
|
296
|
+
} else {
|
|
297
|
+
// Status too wide - truncate by removing segments from the end
|
|
298
|
+
// Build progressively shorter content until it fits
|
|
299
|
+
const allSegments = [...presetDef.leftSegments, ...presetDef.rightSegments];
|
|
300
|
+
let truncatedContent = "";
|
|
301
|
+
|
|
302
|
+
for (let numSegments = allSegments.length - 1; numSegments >= 1; numSegments--) {
|
|
303
|
+
const limitedPreset = {
|
|
304
|
+
...presetDef,
|
|
305
|
+
leftSegments: presetDef.leftSegments.slice(0, numSegments),
|
|
306
|
+
rightSegments: [],
|
|
307
|
+
};
|
|
308
|
+
truncatedContent = buildStatusContent(segmentCtx, limitedPreset);
|
|
309
|
+
const truncWidth = visibleWidth(truncatedContent);
|
|
310
|
+
if (truncWidth <= topFillWidth - 1) { // -1 for ellipsis
|
|
311
|
+
truncatedContent += "…";
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const truncWidth = visibleWidth(truncatedContent);
|
|
317
|
+
if (truncWidth <= topFillWidth) {
|
|
318
|
+
const fillWidth = topFillWidth - truncWidth;
|
|
319
|
+
result.push(topLeft + truncatedContent + bc("─".repeat(fillWidth)) + topRight);
|
|
320
|
+
} else {
|
|
321
|
+
// Still too wide, show minimal
|
|
322
|
+
result.push(topLeft + bc("─".repeat(Math.max(0, topFillWidth))) + topRight);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Content lines (between top border at 0 and bottom border)
|
|
327
|
+
for (let i = 1; i < bottomBorderIndex; i++) {
|
|
328
|
+
const line = lines[i] || "";
|
|
329
|
+
const lineWidth = visibleWidth(line);
|
|
330
|
+
const padding = " ".repeat(Math.max(0, contentWidth - lineWidth));
|
|
331
|
+
|
|
332
|
+
const isLastContent = i === bottomBorderIndex - 1;
|
|
333
|
+
if (isLastContent) {
|
|
334
|
+
// Last content line: ╰─ content ─╯
|
|
335
|
+
result.push(`${bottomLeft} ${line}${padding} ${bottomRight}`);
|
|
336
|
+
} else {
|
|
337
|
+
// Middle lines: │ content │
|
|
338
|
+
result.push(`${vertical} ${line}${padding} ${vertical}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// If only had top/bottom borders (empty editor), add the bottom
|
|
343
|
+
if (bottomBorderIndex === 1) {
|
|
344
|
+
const padding = " ".repeat(contentWidth);
|
|
345
|
+
result.push(`${bottomLeft} ${padding} ${bottomRight}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Append any autocomplete lines that come after the bottom border
|
|
349
|
+
for (let i = bottomBorderIndex + 1; i < lines.length; i++) {
|
|
350
|
+
result.push(lines[i] || "");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return result;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
return editor;
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Also set up footer data provider access via a minimal footer
|
|
360
|
+
ctx.ui.setFooter((tui: any, _theme: any, footerData: ReadonlyFooterDataProvider) => {
|
|
361
|
+
footerDataRef = footerData;
|
|
362
|
+
tuiRef = tui; // Store TUI reference for re-renders on git branch changes
|
|
363
|
+
const unsub = footerData.onBranchChange(() => tui.requestRender());
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
dispose: unsub,
|
|
367
|
+
invalidate() {
|
|
368
|
+
// Re-render when thinking level or other settings change
|
|
369
|
+
tui.requestRender();
|
|
370
|
+
},
|
|
371
|
+
render(width: number): string[] {
|
|
372
|
+
// Only show status in footer during streaming (editor hidden)
|
|
373
|
+
// When editor is visible, status shows in editor top border instead
|
|
374
|
+
if (!isStreaming || !currentCtx) return [];
|
|
375
|
+
|
|
376
|
+
const presetDef = getPreset(config.preset);
|
|
377
|
+
const segmentCtx = buildSegmentContext(currentCtx, width);
|
|
378
|
+
const statusContent = buildStatusContent(segmentCtx, presetDef);
|
|
379
|
+
|
|
380
|
+
if (!statusContent) return [];
|
|
381
|
+
|
|
382
|
+
// Single line with status content, padded/truncated to width
|
|
383
|
+
const statusWidth = visibleWidth(statusContent);
|
|
384
|
+
if (statusWidth <= width) {
|
|
385
|
+
return [statusContent + " ".repeat(width - statusWidth)];
|
|
386
|
+
} else {
|
|
387
|
+
// Truncate by removing segments (same logic as editor)
|
|
388
|
+
const allSegments = [...presetDef.leftSegments, ...presetDef.rightSegments];
|
|
389
|
+
let truncatedContent = "";
|
|
390
|
+
|
|
391
|
+
for (let numSegments = allSegments.length - 1; numSegments >= 1; numSegments--) {
|
|
392
|
+
const limitedPreset = {
|
|
393
|
+
...presetDef,
|
|
394
|
+
leftSegments: presetDef.leftSegments.slice(0, numSegments),
|
|
395
|
+
rightSegments: [],
|
|
396
|
+
};
|
|
397
|
+
truncatedContent = buildStatusContent(segmentCtx, limitedPreset);
|
|
398
|
+
const truncWidth = visibleWidth(truncatedContent);
|
|
399
|
+
if (truncWidth <= width - 1) {
|
|
400
|
+
truncatedContent += "…";
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return [truncatedContent];
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
package/install.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, copyFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const targetDir = join(homedir(), ".pi", "agent", "extensions", "powerline-footer");
|
|
10
|
+
|
|
11
|
+
// Files to copy (TypeScript sources + docs)
|
|
12
|
+
const files = readdirSync(__dirname).filter(
|
|
13
|
+
(f) => f.endsWith(".ts") || f === "README.md" || f === "CHANGELOG.md"
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
// Create target directory
|
|
17
|
+
mkdirSync(targetDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
// Copy files
|
|
20
|
+
let copied = 0;
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const src = join(__dirname, file);
|
|
23
|
+
const dest = join(targetDir, file);
|
|
24
|
+
copyFileSync(src, dest);
|
|
25
|
+
copied++;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(`✓ Installed pi-powerline-footer to ${targetDir}`);
|
|
29
|
+
console.log(` Copied ${copied} files`);
|
|
30
|
+
console.log(`\nRestart pi to activate the extension.`);
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-powerline-footer",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Powerline-style status bar extension for pi coding agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pi-powerline-footer": "./install.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"*.ts",
|
|
11
|
+
"*.md",
|
|
12
|
+
"install.mjs"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"pi",
|
|
16
|
+
"coding-agent",
|
|
17
|
+
"powerline",
|
|
18
|
+
"status-bar",
|
|
19
|
+
"extension"
|
|
20
|
+
],
|
|
21
|
+
"author": "Nico Bailon",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/nicobailon/pi-powerline-footer"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/presets.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { PresetDef, StatusLinePreset } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
4
|
+
default: {
|
|
5
|
+
leftSegments: ["pi", "model", "thinking", "path", "git", "context_pct", "token_total", "cost", "extension_statuses"],
|
|
6
|
+
rightSegments: [],
|
|
7
|
+
separator: "powerline-thin",
|
|
8
|
+
segmentOptions: {
|
|
9
|
+
model: { showThinkingLevel: false },
|
|
10
|
+
path: { mode: "basename" },
|
|
11
|
+
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
minimal: {
|
|
16
|
+
leftSegments: ["path", "git"],
|
|
17
|
+
rightSegments: ["context_pct"],
|
|
18
|
+
separator: "slash",
|
|
19
|
+
segmentOptions: {
|
|
20
|
+
path: { mode: "basename" },
|
|
21
|
+
git: { showBranch: true, showStaged: false, showUnstaged: false, showUntracked: false },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
compact: {
|
|
26
|
+
leftSegments: ["model", "git"],
|
|
27
|
+
rightSegments: ["cost", "context_pct"],
|
|
28
|
+
separator: "powerline-thin",
|
|
29
|
+
segmentOptions: {
|
|
30
|
+
model: { showThinkingLevel: false },
|
|
31
|
+
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: false },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
full: {
|
|
36
|
+
leftSegments: ["pi", "hostname", "model", "thinking", "path", "git", "subagents"],
|
|
37
|
+
rightSegments: ["token_in", "token_out", "cache_read", "cost", "context_pct", "time_spent", "time", "extension_statuses"],
|
|
38
|
+
separator: "powerline",
|
|
39
|
+
segmentOptions: {
|
|
40
|
+
model: { showThinkingLevel: false },
|
|
41
|
+
path: { mode: "abbreviated", maxLength: 50 },
|
|
42
|
+
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
|
|
43
|
+
time: { format: "24h", showSeconds: false },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
nerd: {
|
|
48
|
+
leftSegments: ["pi", "hostname", "model", "thinking", "path", "git", "session", "subagents"],
|
|
49
|
+
rightSegments: ["token_in", "token_out", "cache_read", "cache_write", "cost", "context_pct", "context_total", "time_spent", "time", "extension_statuses"],
|
|
50
|
+
separator: "powerline",
|
|
51
|
+
segmentOptions: {
|
|
52
|
+
model: { showThinkingLevel: false },
|
|
53
|
+
path: { mode: "abbreviated", maxLength: 60 },
|
|
54
|
+
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
|
|
55
|
+
time: { format: "24h", showSeconds: true },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
ascii: {
|
|
60
|
+
leftSegments: ["model", "path", "git"],
|
|
61
|
+
rightSegments: ["token_total", "cost", "context_pct"],
|
|
62
|
+
separator: "ascii",
|
|
63
|
+
segmentOptions: {
|
|
64
|
+
model: { showThinkingLevel: true },
|
|
65
|
+
path: { mode: "abbreviated", maxLength: 40 },
|
|
66
|
+
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
custom: {
|
|
71
|
+
leftSegments: ["model", "path", "git"],
|
|
72
|
+
rightSegments: ["token_total", "cost", "context_pct"],
|
|
73
|
+
separator: "powerline-thin",
|
|
74
|
+
segmentOptions: {},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function getPreset(name: StatusLinePreset): PresetDef {
|
|
79
|
+
return PRESETS[name] ?? PRESETS.default;
|
|
80
|
+
}
|