pi-agent-extensions 0.4.0 → 0.4.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/extensions/btw/index.ts
CHANGED
|
@@ -29,10 +29,8 @@ import {
|
|
|
29
29
|
} from "@mariozechner/pi-coding-agent";
|
|
30
30
|
import {
|
|
31
31
|
type Component,
|
|
32
|
-
Container,
|
|
33
32
|
Key,
|
|
34
33
|
matchesKey,
|
|
35
|
-
Text,
|
|
36
34
|
wrapTextWithAnsi,
|
|
37
35
|
visibleWidth,
|
|
38
36
|
type TUI,
|
|
@@ -48,8 +46,8 @@ import {
|
|
|
48
46
|
|
|
49
47
|
|
|
50
48
|
/**
|
|
51
|
-
* Overlay component that displays the BTW answer.
|
|
52
|
-
*
|
|
49
|
+
* Overlay component that displays the BTW question and answer.
|
|
50
|
+
* The full page scrolls together so large input and output remain usable.
|
|
53
51
|
*/
|
|
54
52
|
class BtwOverlay implements Component {
|
|
55
53
|
private tui: TUI;
|
|
@@ -60,6 +58,7 @@ class BtwOverlay implements Component {
|
|
|
60
58
|
private scrollOffset = 0;
|
|
61
59
|
private cachedWidth?: number;
|
|
62
60
|
private cachedLines?: string[];
|
|
61
|
+
private maxScrollOffset = 0;
|
|
63
62
|
|
|
64
63
|
constructor(
|
|
65
64
|
tui: TUI,
|
|
@@ -76,7 +75,6 @@ class BtwOverlay implements Component {
|
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
handleInput(data: string): void {
|
|
79
|
-
// Dismiss overlay
|
|
80
78
|
if (
|
|
81
79
|
matchesKey(data, Key.escape) ||
|
|
82
80
|
matchesKey(data, Key.ctrl("c")) ||
|
|
@@ -87,7 +85,8 @@ class BtwOverlay implements Component {
|
|
|
87
85
|
return;
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
|
|
88
|
+
const pageStep = Math.max(4, (this.tui.height ?? 24) - 8);
|
|
89
|
+
|
|
91
90
|
if (matchesKey(data, Key.up) || data === "k") {
|
|
92
91
|
if (this.scrollOffset > 0) {
|
|
93
92
|
this.scrollOffset--;
|
|
@@ -97,21 +96,21 @@ class BtwOverlay implements Component {
|
|
|
97
96
|
return;
|
|
98
97
|
}
|
|
99
98
|
if (matchesKey(data, Key.down) || data === "j") {
|
|
100
|
-
this.scrollOffset
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
if (this.scrollOffset < this.maxScrollOffset) {
|
|
100
|
+
this.scrollOffset++;
|
|
101
|
+
this.invalidate();
|
|
102
|
+
this.tui.requestRender();
|
|
103
|
+
}
|
|
103
104
|
return;
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
-
// Page up/down
|
|
107
106
|
if (matchesKey(data, Key.pageUp)) {
|
|
108
|
-
this.scrollOffset = Math.max(0, this.scrollOffset -
|
|
107
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - pageStep);
|
|
109
108
|
this.invalidate();
|
|
110
109
|
this.tui.requestRender();
|
|
111
110
|
return;
|
|
112
111
|
}
|
|
113
112
|
if (matchesKey(data, Key.pageDown)) {
|
|
114
|
-
this.scrollOffset
|
|
113
|
+
this.scrollOffset = Math.min(this.maxScrollOffset, this.scrollOffset + pageStep);
|
|
115
114
|
this.invalidate();
|
|
116
115
|
this.tui.requestRender();
|
|
117
116
|
return;
|
|
@@ -129,13 +128,24 @@ class BtwOverlay implements Component {
|
|
|
129
128
|
}
|
|
130
129
|
|
|
131
130
|
const theme = this.theme;
|
|
132
|
-
const boxWidth = Math.min(width - 2,
|
|
133
|
-
const contentWidth = boxWidth -
|
|
131
|
+
const boxWidth = Math.min(width - 2, 144);
|
|
132
|
+
const contentWidth = Math.max(24, boxWidth - 8);
|
|
134
133
|
|
|
135
134
|
const horizontalLine = (count: number) => "─".repeat(count);
|
|
135
|
+
const meter = (value: number, total: number, size: number) => {
|
|
136
|
+
if (total <= 0) return "░".repeat(size);
|
|
137
|
+
const filled = Math.max(1, Math.min(size, Math.round((value / total) * size)));
|
|
138
|
+
return "█".repeat(filled) + "░".repeat(Math.max(0, size - filled));
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const fitInline = (text: string, targetWidth: number): string => {
|
|
142
|
+
if (targetWidth <= 0) return "";
|
|
143
|
+
const wrapped = wrapTextWithAnsi(text, targetWidth);
|
|
144
|
+
return wrapped[0] ?? "";
|
|
145
|
+
};
|
|
136
146
|
|
|
137
147
|
const boxLine = (content: string, leftPad: number = 2): string => {
|
|
138
|
-
const paddedContent = " ".repeat(leftPad) + content;
|
|
148
|
+
const paddedContent = " ".repeat(leftPad) + fitInline(content, Math.max(0, boxWidth - leftPad - 3));
|
|
139
149
|
const contentLen = visibleWidth(paddedContent);
|
|
140
150
|
const rightPad = Math.max(0, boxWidth - contentLen - 2);
|
|
141
151
|
return theme.fg("border", "│") + paddedContent + " ".repeat(rightPad) + theme.fg("border", "│");
|
|
@@ -150,73 +160,69 @@ class BtwOverlay implements Component {
|
|
|
150
160
|
return line + " ".repeat(Math.max(0, width - len));
|
|
151
161
|
};
|
|
152
162
|
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// Title
|
|
159
|
-
const title = theme.fg("accent", theme.bold("btw"));
|
|
160
|
-
lines.push(padToWidth(boxLine(title)));
|
|
161
|
-
|
|
162
|
-
// Separator
|
|
163
|
-
lines.push(padToWidth(theme.fg("accent", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
164
|
-
|
|
165
|
-
// Question
|
|
166
|
-
const questionLabel = theme.fg("muted", "Q: ") + theme.fg("text", this.question);
|
|
167
|
-
const wrappedQuestion = wrapTextWithAnsi(questionLabel, contentWidth);
|
|
168
|
-
for (const line of wrappedQuestion) {
|
|
169
|
-
lines.push(padToWidth(boxLine(line)));
|
|
170
|
-
}
|
|
163
|
+
const sectionTitle = (label: string, meta: string) => {
|
|
164
|
+
const text = `${theme.fg("accent", theme.bold(label))}${theme.fg("muted", ` · ${meta}`)}`;
|
|
165
|
+
return boxLine(text, 2);
|
|
166
|
+
};
|
|
171
167
|
|
|
172
|
-
|
|
168
|
+
const pushWrappedSection = (
|
|
169
|
+
bodyLines: string[],
|
|
170
|
+
label: string,
|
|
171
|
+
meta: string,
|
|
172
|
+
text: string,
|
|
173
|
+
prefix: string,
|
|
174
|
+
) => {
|
|
175
|
+
bodyLines.push(sectionTitle(label, meta));
|
|
176
|
+
bodyLines.push(emptyBoxLine());
|
|
177
|
+
for (const paragraph of text.split("\n")) {
|
|
178
|
+
if (paragraph.trim() === "") {
|
|
179
|
+
bodyLines.push(boxLine("", 2));
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const wrapped = wrapTextWithAnsi(paragraph, Math.max(12, contentWidth - visibleWidth(prefix)));
|
|
183
|
+
for (const line of wrapped) {
|
|
184
|
+
bodyLines.push(boxLine(`${prefix}${line}`, 2));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
173
188
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
lines.push(padToWidth(emptyBoxLine()));
|
|
189
|
+
const questionWords = this.question.trim().split(/\s+/).filter(Boolean).length;
|
|
190
|
+
const answerParagraphs = this.answer.split("\n").filter((line) => line.trim() !== "").length;
|
|
177
191
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const wrapped = wrapTextWithAnsi(paragraph, contentWidth);
|
|
185
|
-
answerLines.push(...wrapped);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
192
|
+
const bodyLines: string[] = [];
|
|
193
|
+
pushWrappedSection(bodyLines, "Question", `${questionWords} words`, this.question, theme.fg("muted", "› "));
|
|
194
|
+
bodyLines.push(emptyBoxLine());
|
|
195
|
+
bodyLines.push(boxLine(theme.fg("border", horizontalLine(Math.max(10, contentWidth - 6))), 3));
|
|
196
|
+
bodyLines.push(emptyBoxLine());
|
|
197
|
+
pushWrappedSection(bodyLines, "Answer", `${answerParagraphs} paragraphs`, this.answer, "");
|
|
188
198
|
|
|
189
|
-
// Clamp scroll offset
|
|
190
199
|
const termHeight = this.tui.height ?? 24;
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this.scrollOffset = Math.max(0, answerLines.length - maxVisibleAnswerLines);
|
|
200
|
+
const fixedLines = 7;
|
|
201
|
+
const maxVisibleBodyLines = Math.max(4, termHeight - fixedLines);
|
|
202
|
+
this.maxScrollOffset = Math.max(0, bodyLines.length - maxVisibleBodyLines);
|
|
203
|
+
if (this.scrollOffset > this.maxScrollOffset) {
|
|
204
|
+
this.scrollOffset = this.maxScrollOffset;
|
|
197
205
|
}
|
|
198
206
|
|
|
199
|
-
const
|
|
207
|
+
const visibleBodyLines = bodyLines.slice(
|
|
200
208
|
this.scrollOffset,
|
|
201
|
-
this.scrollOffset +
|
|
209
|
+
this.scrollOffset + maxVisibleBodyLines,
|
|
202
210
|
);
|
|
203
211
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (answerLines.length > maxVisibleAnswerLines) {
|
|
210
|
-
const scrollInfo = theme.fg("dim", `[${this.scrollOffset + 1}-${Math.min(this.scrollOffset + maxVisibleAnswerLines, answerLines.length)}/${answerLines.length}]`);
|
|
211
|
-
lines.push(padToWidth(boxLine(scrollInfo)));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
lines.push(padToWidth(emptyBoxLine()));
|
|
212
|
+
const scrollCurrent = Math.min(bodyLines.length, this.scrollOffset + maxVisibleBodyLines);
|
|
213
|
+
const scrollInfo = this.maxScrollOffset > 0
|
|
214
|
+
? `${this.scrollOffset + 1}-${scrollCurrent}/${bodyLines.length}`
|
|
215
|
+
: "full";
|
|
216
|
+
const progress = meter(scrollCurrent, Math.max(bodyLines.length, 1), 10);
|
|
215
217
|
|
|
216
|
-
|
|
218
|
+
const lines: string[] = [];
|
|
219
|
+
lines.push(padToWidth(theme.fg("accent", "╭" + horizontalLine(boxWidth - 2) + "╮")));
|
|
220
|
+
lines.push(padToWidth(boxLine(`${theme.fg("accent", theme.bold("BTW"))}${theme.fg("muted", " · side question")}`, 2)));
|
|
221
|
+
lines.push(padToWidth(boxLine(theme.fg("dim", "An editorial-style reading pane for long prompts and answers."), 2)));
|
|
217
222
|
lines.push(padToWidth(theme.fg("accent", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
218
|
-
|
|
219
|
-
lines.push(padToWidth(
|
|
223
|
+
lines.push(...visibleBodyLines.map(padToWidth));
|
|
224
|
+
lines.push(padToWidth(theme.fg("accent", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
225
|
+
lines.push(padToWidth(boxLine(`${theme.fg("accent", progress)} ${theme.fg("muted", scrollInfo)}${theme.fg("dim", " · Esc dismiss · ↑↓ / j k · PgUp PgDn")}`, 2)));
|
|
220
226
|
lines.push(padToWidth(theme.fg("accent", "╰" + horizontalLine(boxWidth - 2) + "╯")));
|
|
221
227
|
|
|
222
228
|
this.cachedWidth = width;
|
|
@@ -348,6 +354,14 @@ async function runBtwCommand(
|
|
|
348
354
|
// Step 2: Show the answer in an overlay
|
|
349
355
|
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
350
356
|
return new BtwOverlay(tui, theme, question, answerResult, done);
|
|
357
|
+
}, {
|
|
358
|
+
overlay: true,
|
|
359
|
+
overlayOptions: {
|
|
360
|
+
anchor: "center",
|
|
361
|
+
width: "92%",
|
|
362
|
+
maxHeight: "92%",
|
|
363
|
+
margin: { top: 1, bottom: 1, left: 1, right: 1 },
|
|
364
|
+
},
|
|
351
365
|
});
|
|
352
366
|
|
|
353
367
|
// Nothing persisted — fully ephemeral
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext,
|
|
2
|
-
import { type Component, type Theme, type TUI, visibleWidth } from "@mariozechner/pi-tui";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ReadonlyFooterDataProvider } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { type Component, type Theme, type TUI, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
3
3
|
import * as child_process from "child_process";
|
|
4
4
|
|
|
5
5
|
class PowerlineFooter implements Component {
|
|
@@ -9,6 +9,14 @@ class PowerlineFooter implements Component {
|
|
|
9
9
|
private ctx: ExtensionContext;
|
|
10
10
|
private interval?: ReturnType<typeof setInterval>;
|
|
11
11
|
private sessionStartTime: number;
|
|
12
|
+
private disposed = false;
|
|
13
|
+
private cwd: string;
|
|
14
|
+
private lastRenderedLine = "";
|
|
15
|
+
private lastShortDir: string;
|
|
16
|
+
private lastModelShort = "unknown";
|
|
17
|
+
private lastContextInfo = "";
|
|
18
|
+
private lastCostInfo = "";
|
|
19
|
+
private lastSessionName = "";
|
|
12
20
|
|
|
13
21
|
// Cached git state (updated async every 10s)
|
|
14
22
|
private gitStatusExtras: string = "";
|
|
@@ -19,33 +27,40 @@ class PowerlineFooter implements Component {
|
|
|
19
27
|
this.footerData = footerData;
|
|
20
28
|
this.ctx = ctx;
|
|
21
29
|
this.sessionStartTime = Date.now();
|
|
30
|
+
this.cwd = ctx.cwd;
|
|
31
|
+
this.lastShortDir = this.formatShortDir(this.cwd);
|
|
22
32
|
|
|
23
33
|
this.fetchAsyncData();
|
|
24
34
|
|
|
25
35
|
this.interval = setInterval(() => {
|
|
36
|
+
if (this.disposed) return;
|
|
26
37
|
this.fetchAsyncData();
|
|
27
38
|
this.tui.requestRender();
|
|
28
39
|
}, 10000);
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
dispose() {
|
|
43
|
+
this.disposed = true;
|
|
32
44
|
if (this.interval) {
|
|
33
45
|
clearInterval(this.interval);
|
|
46
|
+
this.interval = undefined;
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
private fetchAsyncData() {
|
|
38
|
-
|
|
51
|
+
if (this.disposed) return;
|
|
52
|
+
const cwd = this.cwd;
|
|
39
53
|
const branch = this.footerData.getGitBranch();
|
|
40
54
|
|
|
41
55
|
if (branch) {
|
|
42
56
|
child_process.exec("git --no-optional-locks status --porcelain", { encoding: "utf8", cwd }, (err, stdout) => {
|
|
43
|
-
if (err) return;
|
|
57
|
+
if (this.disposed || err) return;
|
|
44
58
|
const staged = (stdout.match(/^[AMDRC]/gm) || []).length;
|
|
45
59
|
const unstaged = (stdout.match(/^.[MD]/gm) || []).length;
|
|
46
60
|
const untracked = (stdout.match(/^\?\?/gm) || []).length;
|
|
47
61
|
|
|
48
62
|
child_process.exec("git --no-optional-locks rev-list --count --left-right @{u}...HEAD", { encoding: "utf8", cwd }, (err2, revListOut) => {
|
|
63
|
+
if (this.disposed) return;
|
|
49
64
|
let ahead = 0;
|
|
50
65
|
let behind = 0;
|
|
51
66
|
if (!err2 && revListOut && revListOut.includes("\t")) {
|
|
@@ -72,6 +87,93 @@ class PowerlineFooter implements Component {
|
|
|
72
87
|
invalidate() {}
|
|
73
88
|
handleInput(_data: string) {}
|
|
74
89
|
|
|
90
|
+
private formatShortDir(cwd: string): string {
|
|
91
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
92
|
+
let shortDir = cwd;
|
|
93
|
+
if (homeDir && shortDir.startsWith(homeDir)) {
|
|
94
|
+
shortDir = "~" + shortDir.slice(homeDir.length);
|
|
95
|
+
}
|
|
96
|
+
const parts = shortDir.split("/");
|
|
97
|
+
if (parts.length > 4) {
|
|
98
|
+
shortDir = parts.slice(parts.length - 4).join("/");
|
|
99
|
+
}
|
|
100
|
+
return shortDir;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private getShortDir(): string {
|
|
104
|
+
try {
|
|
105
|
+
this.lastShortDir = this.formatShortDir(this.ctx.cwd);
|
|
106
|
+
} catch {}
|
|
107
|
+
return this.lastShortDir;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private getModelShort(): string {
|
|
111
|
+
try {
|
|
112
|
+
const model = this.ctx.model;
|
|
113
|
+
this.lastModelShort = model?.name || model?.id || "unknown";
|
|
114
|
+
} catch {}
|
|
115
|
+
return this.lastModelShort;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private getContextDetails(reset: string, green: string, yellow: string, red: string, peach: string, dim: string) {
|
|
119
|
+
try {
|
|
120
|
+
const model = this.ctx.model;
|
|
121
|
+
const contextUsage = this.ctx.getContextUsage();
|
|
122
|
+
let contextInfo = "";
|
|
123
|
+
let costInfo = "";
|
|
124
|
+
|
|
125
|
+
if (contextUsage) {
|
|
126
|
+
const used = contextUsage.tokens;
|
|
127
|
+
const total = contextUsage.contextWindow;
|
|
128
|
+
const pct = contextUsage.percent;
|
|
129
|
+
if (used !== null && pct !== null) {
|
|
130
|
+
const remaining = 100 - pct;
|
|
131
|
+
|
|
132
|
+
let ctxColor = green;
|
|
133
|
+
if (remaining < 20) {
|
|
134
|
+
ctxColor = red;
|
|
135
|
+
} else if (remaining < 50) {
|
|
136
|
+
ctxColor = yellow;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const pctDisplay = pct % 1 === 0 ? pct.toFixed(0) : pct.toFixed(1);
|
|
140
|
+
contextInfo = `${ctxColor}${this.formatTokens(used)}/${this.formatTokens(total)} (${pctDisplay}%)${reset}`;
|
|
141
|
+
} else {
|
|
142
|
+
contextInfo = `${green}?/${this.formatTokens(total)}${reset}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let totalCost = 0;
|
|
147
|
+
for (const entry of this.ctx.sessionManager.getEntries()) {
|
|
148
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
149
|
+
totalCost += entry.message.usage?.cost?.total ?? 0;
|
|
150
|
+
}
|
|
151
|
+
const usingSubscription = model ? this.ctx.modelRegistry.isUsingOAuth(model) : false;
|
|
152
|
+
if (totalCost > 0 || usingSubscription) {
|
|
153
|
+
const formattedCost = `$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
|
|
154
|
+
costInfo = ` ${dim}|${reset} ${peach}${formattedCost}${reset}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.lastContextInfo = contextInfo;
|
|
158
|
+
this.lastCostInfo = costInfo;
|
|
159
|
+
} catch {}
|
|
160
|
+
|
|
161
|
+
return { contextInfo: this.lastContextInfo, costInfo: this.lastCostInfo };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private getSessionName(): string {
|
|
165
|
+
try {
|
|
166
|
+
this.lastSessionName = this.ctx.sessionManager.getSessionName() ?? "";
|
|
167
|
+
} catch {}
|
|
168
|
+
return this.lastSessionName;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private fitToWidth(line: string, width: number): string {
|
|
172
|
+
if (width <= 0) return "";
|
|
173
|
+
if (visibleWidth(line) <= width) return line;
|
|
174
|
+
return truncateToWidth(line, width, "...");
|
|
175
|
+
}
|
|
176
|
+
|
|
75
177
|
private formatTokens(num: number): string {
|
|
76
178
|
if (num >= 1_000_000) {
|
|
77
179
|
return (num / 1_000_000).toFixed(1) + "M";
|
|
@@ -95,19 +197,11 @@ class PowerlineFooter implements Component {
|
|
|
95
197
|
const PEACH = "\x1b[38;5;216m";
|
|
96
198
|
const OVERLAY2 = "\x1b[38;5;103m";
|
|
97
199
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
101
|
-
let shortDir = cwd;
|
|
102
|
-
if (homeDir && shortDir.startsWith(homeDir)) {
|
|
103
|
-
shortDir = "~" + shortDir.slice(homeDir.length);
|
|
104
|
-
}
|
|
105
|
-
const parts = shortDir.split("/");
|
|
106
|
-
if (parts.length > 4) {
|
|
107
|
-
shortDir = parts.slice(parts.length - 4).join("/");
|
|
200
|
+
if (this.disposed) {
|
|
201
|
+
return [this.fitToWidth(this.lastRenderedLine || "", width)];
|
|
108
202
|
}
|
|
109
203
|
|
|
110
|
-
|
|
204
|
+
const shortDir = this.getShortDir();
|
|
111
205
|
let gitInfo = "";
|
|
112
206
|
const branch = this.footerData.getGitBranch();
|
|
113
207
|
if (branch) {
|
|
@@ -117,46 +211,9 @@ class PowerlineFooter implements Component {
|
|
|
117
211
|
}
|
|
118
212
|
}
|
|
119
213
|
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const modelShort = model?.name || model?.id || "unknown";
|
|
123
|
-
|
|
124
|
-
// --- Context usage ---
|
|
125
|
-
const contextUsage = this.ctx.getContextUsage();
|
|
126
|
-
let contextInfo = "";
|
|
127
|
-
let costInfo = "";
|
|
128
|
-
|
|
129
|
-
if (contextUsage) {
|
|
130
|
-
const used = contextUsage.tokens;
|
|
131
|
-
const total = contextUsage.contextWindow;
|
|
132
|
-
const pct = contextUsage.percent;
|
|
133
|
-
const remaining = 100 - pct;
|
|
134
|
-
|
|
135
|
-
let ctxColor = GREEN;
|
|
136
|
-
if (remaining < 20) {
|
|
137
|
-
ctxColor = RED;
|
|
138
|
-
} else if (remaining < 50) {
|
|
139
|
-
ctxColor = YELLOW;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
contextInfo = `${ctxColor}${this.formatTokens(used)}/${this.formatTokens(total)} (${pct}%)${RESET}`;
|
|
214
|
+
const modelShort = this.getModelShort();
|
|
215
|
+
const { contextInfo, costInfo } = this.getContextDetails(RESET, GREEN, YELLOW, RED, PEACH, DIM);
|
|
143
216
|
|
|
144
|
-
// Cost estimation from model pricing
|
|
145
|
-
if (model?.cost && contextUsage.usageTokens > 0) {
|
|
146
|
-
// Rough estimate: treat usageTokens as input, trailingTokens as recent output
|
|
147
|
-
const inputTokens = contextUsage.usageTokens;
|
|
148
|
-
const outputTokens = contextUsage.trailingTokens;
|
|
149
|
-
const cost =
|
|
150
|
-
(inputTokens * model.cost.input) / 1_000_000 +
|
|
151
|
-
(outputTokens * model.cost.output) / 1_000_000;
|
|
152
|
-
|
|
153
|
-
if (cost > 0.001) {
|
|
154
|
-
costInfo = ` ${DIM}|${RESET} ${PEACH}$${cost.toFixed(2)}${RESET}`;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// --- Session duration ---
|
|
160
217
|
let durationInfo = "";
|
|
161
218
|
const durationMs = Date.now() - this.sessionStartTime;
|
|
162
219
|
if (durationMs > 0) {
|
|
@@ -174,7 +231,6 @@ class PowerlineFooter implements Component {
|
|
|
174
231
|
durationInfo = ` ${DIM}|${RESET} ${OVERLAY2}${durFmt}${RESET}`;
|
|
175
232
|
}
|
|
176
233
|
|
|
177
|
-
// --- Python env ---
|
|
178
234
|
let envInfo = "";
|
|
179
235
|
if (process.env.CONDA_DEFAULT_ENV) {
|
|
180
236
|
envInfo = ` ${SKY}(${process.env.CONDA_DEFAULT_ENV})${RESET}`;
|
|
@@ -183,16 +239,31 @@ class PowerlineFooter implements Component {
|
|
|
183
239
|
envInfo = ` ${SKY}(${venvName})${RESET}`;
|
|
184
240
|
}
|
|
185
241
|
|
|
186
|
-
// --- Time ---
|
|
187
242
|
const date = new Date();
|
|
188
243
|
const currentTime = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
189
|
-
|
|
190
|
-
// --- Session name ---
|
|
191
|
-
const sessionName = this.ctx.sessionManager.getSessionName();
|
|
244
|
+
const sessionName = this.getSessionName();
|
|
192
245
|
const sessionInfo = sessionName ? `${MAUVE}[${sessionName}]${RESET} ` : "";
|
|
193
246
|
|
|
194
|
-
|
|
195
|
-
|
|
247
|
+
const left = `${sessionInfo}${BOLD}${BLUE} ${shortDir}${RESET}${gitInfo}`;
|
|
248
|
+
const right = `${OVERLAY2}${modelShort}${RESET} ${contextInfo}${costInfo}${durationInfo}${envInfo} ${DIM}${currentTime}${RESET}`;
|
|
249
|
+
const leftWidth = visibleWidth(left);
|
|
250
|
+
const rightWidth = visibleWidth(right);
|
|
251
|
+
const minPadding = 1;
|
|
252
|
+
let line = "";
|
|
253
|
+
|
|
254
|
+
if (leftWidth + minPadding + rightWidth <= width) {
|
|
255
|
+
line = left + " ".repeat(width - leftWidth - rightWidth) + right;
|
|
256
|
+
} else {
|
|
257
|
+
const availableForLeft = Math.max(0, width - rightWidth - minPadding);
|
|
258
|
+
if (availableForLeft > 0) {
|
|
259
|
+
const truncatedLeft = this.fitToWidth(left, availableForLeft);
|
|
260
|
+
const truncatedLeftWidth = visibleWidth(truncatedLeft);
|
|
261
|
+
line = truncatedLeft + " ".repeat(Math.max(minPadding, width - truncatedLeftWidth - rightWidth)) + right;
|
|
262
|
+
} else {
|
|
263
|
+
line = this.fitToWidth(right, width);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
196
267
|
const statuses = this.footerData.getExtensionStatuses();
|
|
197
268
|
if (statuses.size > 0) {
|
|
198
269
|
const parts: string[] = [];
|
|
@@ -200,13 +271,13 @@ class PowerlineFooter implements Component {
|
|
|
200
271
|
if (text) parts.push(text);
|
|
201
272
|
}
|
|
202
273
|
if (parts.length > 0) {
|
|
203
|
-
|
|
274
|
+
line = this.fitToWidth(`${line} ${DIM}|${RESET} ${parts.join(" ")}`, width);
|
|
204
275
|
}
|
|
205
276
|
}
|
|
206
277
|
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
return [
|
|
278
|
+
const fittedLine = this.fitToWidth(line, width);
|
|
279
|
+
this.lastRenderedLine = fittedLine;
|
|
280
|
+
return [fittedLine];
|
|
210
281
|
}
|
|
211
282
|
}
|
|
212
283
|
|
|
@@ -491,7 +491,7 @@ export default function whimsicalExtension(pi: ExtensionAPI) {
|
|
|
491
491
|
|
|
492
492
|
pi.on("turn_start", async (_event, ctx) => {
|
|
493
493
|
await ensureStateLoaded();
|
|
494
|
-
if (!state.enabled) return;
|
|
494
|
+
if (!state.enabled || !ctx.hasUI) return;
|
|
495
495
|
|
|
496
496
|
stopActiveTicker();
|
|
497
497
|
|
|
@@ -517,6 +517,11 @@ export default function whimsicalExtension(pi: ExtensionAPI) {
|
|
|
517
517
|
}, SPINNER_FRAME_INTERVAL_MS);
|
|
518
518
|
});
|
|
519
519
|
|
|
520
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
521
|
+
stopActiveTicker();
|
|
522
|
+
if (ctx.hasUI) ctx.ui.setWorkingMessage();
|
|
523
|
+
});
|
|
524
|
+
|
|
520
525
|
// Keep running until the next turn_start replaces cadence.
|
|
521
526
|
pi.on("turn_end", async () => {
|
|
522
527
|
// no-op by design
|