pi-open-tui 0.2.15 → 0.2.16
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.
|
Binary file
|
|
@@ -1,328 +1,329 @@
|
|
|
1
|
-
import type { ExtensionContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { OpenTuiConfig } from "./config.ts";
|
|
4
|
-
import type { IconGlyphs } from "./icons.ts";
|
|
5
|
-
import { resolveGlyphs, resolveIconMode, runtimeSymbol } from "./icons.ts";
|
|
6
|
-
import type { GitStatus } from "./git.ts";
|
|
7
|
-
import type { RuntimeInfo } from "./runtime.ts";
|
|
8
|
-
import {
|
|
9
|
-
alignRight,
|
|
10
|
-
basenamePath,
|
|
11
|
-
cacheHitColor,
|
|
12
|
-
effortColor,
|
|
13
|
-
fitSegmentsByPriority,
|
|
14
|
-
fmtTokens,
|
|
15
|
-
formatCwd,
|
|
16
|
-
formatDuration,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
theme.fg(
|
|
37
|
-
theme.fg(
|
|
38
|
-
theme.fg("dim",
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
parts.push(theme.fg("mdLink",
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
parts.push(theme.fg("warning",
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
addStatus(git.
|
|
82
|
-
addStatus(git.
|
|
83
|
-
addStatus(git.
|
|
84
|
-
addStatus(git.
|
|
85
|
-
addStatus(git.
|
|
86
|
-
addStatus(git.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
stats.push(theme.fg("
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
.
|
|
186
|
-
.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
let
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
modelParts
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
...
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
1
|
+
import type { ExtensionContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { OpenTuiConfig } from "./config.ts";
|
|
4
|
+
import type { IconGlyphs } from "./icons.ts";
|
|
5
|
+
import { resolveGlyphs, resolveIconMode, runtimeSymbol } from "./icons.ts";
|
|
6
|
+
import type { GitStatus } from "./git.ts";
|
|
7
|
+
import type { RuntimeInfo } from "./runtime.ts";
|
|
8
|
+
import {
|
|
9
|
+
alignRight,
|
|
10
|
+
basenamePath,
|
|
11
|
+
cacheHitColor,
|
|
12
|
+
effortColor,
|
|
13
|
+
fitSegmentsByPriority,
|
|
14
|
+
fmtTokens,
|
|
15
|
+
formatCwd,
|
|
16
|
+
formatDuration,
|
|
17
|
+
formatInputBreakdown,
|
|
18
|
+
formatProviderLabel,
|
|
19
|
+
providerColor,
|
|
20
|
+
sanitizeStatus,
|
|
21
|
+
stressColor,
|
|
22
|
+
truncateBranch,
|
|
23
|
+
truncatePath,
|
|
24
|
+
type PrioritizedSegment,
|
|
25
|
+
} from "./utils.ts";
|
|
26
|
+
import type { FooterState, ModelMeta, UsageTotals } from "./state.ts";
|
|
27
|
+
import { getUsageTotals } from "./state.ts";
|
|
28
|
+
|
|
29
|
+
function renderBar(theme: Theme, pct: number, barWidth: number, ascii: boolean): string {
|
|
30
|
+
const filled = Math.max(0, Math.min(barWidth, Math.round((pct / 100) * barWidth)));
|
|
31
|
+
const empty = barWidth - filled;
|
|
32
|
+
const color = stressColor(pct);
|
|
33
|
+
const filledCell = ascii ? "#" : "█";
|
|
34
|
+
const emptyCell = ascii ? "-" : "░";
|
|
35
|
+
return (
|
|
36
|
+
theme.fg("dim", "[") +
|
|
37
|
+
theme.fg(color, filledCell.repeat(filled)) +
|
|
38
|
+
theme.fg("dim", emptyCell.repeat(empty)) +
|
|
39
|
+
theme.fg("dim", "]")
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Compact context form: icon + percentage, no bar or token counts. */
|
|
44
|
+
function renderContextCompact(theme: Theme, ctx: ExtensionContext, glyphs: IconGlyphs): string {
|
|
45
|
+
const contextUsage = ctx.getContextUsage();
|
|
46
|
+
const contextWindow = contextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
47
|
+
if (contextWindow <= 0) return "";
|
|
48
|
+
const contextPct = contextUsage?.percent ?? 0;
|
|
49
|
+
return `${theme.fg(stressColor(contextPct), glyphs.context)} ${theme.fg(stressColor(contextPct), `${contextPct.toFixed(1)}%`)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderGitSegment(
|
|
53
|
+
theme: Theme,
|
|
54
|
+
git: GitStatus,
|
|
55
|
+
glyphs: IconGlyphs,
|
|
56
|
+
segments: OpenTuiConfig["footerSegments"],
|
|
57
|
+
maxBranchLen = 20,
|
|
58
|
+
): string {
|
|
59
|
+
const parts: string[] = [];
|
|
60
|
+
if (segments.gitBranch) {
|
|
61
|
+
if (git.branch) {
|
|
62
|
+
parts.push(theme.fg("mdLink", glyphs.git));
|
|
63
|
+
parts.push(theme.fg("mdLink", truncateBranch(git.branch, maxBranchLen)));
|
|
64
|
+
} else if (git.commit?.detached) {
|
|
65
|
+
parts.push(theme.fg("warning", glyphs.git));
|
|
66
|
+
parts.push(theme.fg("warning", "HEAD"));
|
|
67
|
+
if (git.commit.oid) {
|
|
68
|
+
const shortHash = git.commit.oid.slice(0, 7);
|
|
69
|
+
const tag = git.commit.tag ? ` ${git.commit.tag}` : "";
|
|
70
|
+
parts.push(theme.fg("dim", `${shortHash}${tag}`));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (segments.gitStatus) {
|
|
76
|
+
const statusIcons: string[] = [];
|
|
77
|
+
// ponytail: always show count — `!1` not `!`, so 1 vs 100 is distinguishable.
|
|
78
|
+
const addStatus = (count: number, glyph: string, color: ThemeColor) => {
|
|
79
|
+
if (count > 0) statusIcons.push(theme.fg(color, `${glyph}${count}`));
|
|
80
|
+
};
|
|
81
|
+
addStatus(git.conflicted, glyphs.conflicted, "error");
|
|
82
|
+
addStatus(git.deleted, glyphs.deleted, "error");
|
|
83
|
+
addStatus(git.modified, glyphs.modified, "warning");
|
|
84
|
+
addStatus(git.renamed, glyphs.renamed, "warning");
|
|
85
|
+
addStatus(git.staged, glyphs.staged, "success");
|
|
86
|
+
addStatus(git.untracked, glyphs.untracked, "muted");
|
|
87
|
+
addStatus(git.stashed, glyphs.stashed, "muted");
|
|
88
|
+
|
|
89
|
+
if (git.ahead > 0 && git.behind > 0) {
|
|
90
|
+
statusIcons.push(theme.fg("warning", `${glyphs.diverged}${git.ahead}/${git.behind}`));
|
|
91
|
+
} else if (git.ahead > 0) {
|
|
92
|
+
statusIcons.push(theme.fg("success", `${glyphs.ahead}${git.ahead}`));
|
|
93
|
+
} else if (git.behind > 0) {
|
|
94
|
+
statusIcons.push(theme.fg("warning", `${glyphs.behind}${git.behind}`));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const statusBlock = statusIcons.join(" ");
|
|
98
|
+
if (statusBlock) {
|
|
99
|
+
parts.push(`${theme.fg("dim", "[")}${statusBlock}${theme.fg("dim", "]")}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return parts.join(" ");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderRuntimeSegment(
|
|
107
|
+
theme: Theme,
|
|
108
|
+
runtime: RuntimeInfo | null,
|
|
109
|
+
iconMode: OpenTuiConfig["icons"]["mode"],
|
|
110
|
+
): string {
|
|
111
|
+
if (!runtime) return "";
|
|
112
|
+
const symbol = theme.fg("success", runtimeSymbol(runtime.name, iconMode));
|
|
113
|
+
const version = runtime.version ? theme.fg("muted", runtime.version) : "";
|
|
114
|
+
const label = [symbol, version].filter(Boolean).join(" ");
|
|
115
|
+
return label;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderTimerSegment(theme: Theme, state: FooterState, glyphs: IconGlyphs): string {
|
|
119
|
+
if (state.workingSince !== undefined) {
|
|
120
|
+
return `${theme.fg("accent", glyphs.working)} ${theme.fg("dim", "working")} ${theme.fg("accent", formatDuration(Date.now() - state.workingSince))}`;
|
|
121
|
+
}
|
|
122
|
+
if (state.lastDoneIn !== undefined) {
|
|
123
|
+
return `${theme.fg("success", glyphs.done)} ${theme.fg("success", "done")} ${theme.fg("text", formatDuration(state.lastDoneIn))}`;
|
|
124
|
+
}
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderContextBar(
|
|
129
|
+
theme: Theme,
|
|
130
|
+
ctx: ExtensionContext,
|
|
131
|
+
width: number,
|
|
132
|
+
glyphs: IconGlyphs,
|
|
133
|
+
iconMode: OpenTuiConfig["icons"]["mode"],
|
|
134
|
+
): string {
|
|
135
|
+
const contextUsage = ctx.getContextUsage();
|
|
136
|
+
const contextWindow = contextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
137
|
+
const contextTokens = contextUsage?.tokens ?? 0;
|
|
138
|
+
const contextPct = contextUsage?.percent ?? 0;
|
|
139
|
+
|
|
140
|
+
// ponytail: render 0% bar once we know the window — keeps the right side
|
|
141
|
+
// populated instead of collapsing everything left in an empty session.
|
|
142
|
+
if (contextWindow <= 0) return "";
|
|
143
|
+
|
|
144
|
+
const pctText = theme.fg(stressColor(contextPct), `${contextPct.toFixed(1)}%`);
|
|
145
|
+
const ctxText = `${theme.fg("text", fmtTokens(contextTokens))}${theme.fg("dim", "/")}${theme.fg("text", fmtTokens(contextWindow))}`;
|
|
146
|
+
const contextIcon = theme.fg(stressColor(contextPct), glyphs.context);
|
|
147
|
+
// Cap the bar at `width - reserved` (with a floor of 4) so the full form
|
|
148
|
+
// never forces the left segments out before compact/drop logic kicks in.
|
|
149
|
+
const reserved = visibleWidth(contextIcon) + visibleWidth(pctText) + visibleWidth(ctxText) + 5 + 2;
|
|
150
|
+
const barWidth = Math.max(4, Math.min(12, width - reserved));
|
|
151
|
+
return `${contextIcon} ${renderBar(theme, contextPct, barWidth, resolveIconMode(iconMode) === "ascii")} ${pctText} ${theme.fg("dim", "·")} ${ctxText}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderStatsBlock(
|
|
155
|
+
theme: Theme,
|
|
156
|
+
totals: UsageTotals,
|
|
157
|
+
glyphs: IconGlyphs,
|
|
158
|
+
segments: OpenTuiConfig["footerSegments"],
|
|
159
|
+
): string {
|
|
160
|
+
const stats: string[] = [];
|
|
161
|
+
if (segments.tokens) {
|
|
162
|
+
stats.push(theme.fg("accent", `${glyphs.input} ${formatInputBreakdown(totals.input, totals.cacheRead)}`));
|
|
163
|
+
stats.push(theme.fg("success", `${glyphs.output} ${fmtTokens(totals.output)}`));
|
|
164
|
+
// ponytail: hide cache-hit rate when the provider never reported cache
|
|
165
|
+
// tokens — avoids a misleading "0%" on providers without prompt caching.
|
|
166
|
+
const hasCacheTokens = totals.cacheRead > 0 || totals.cacheWrite > 0;
|
|
167
|
+
if (hasCacheTokens && totals.latestCacheHitRate !== undefined) {
|
|
168
|
+
stats.push(theme.fg(cacheHitColor(totals.latestCacheHitRate), `${glyphs.cacheHit} ${totals.latestCacheHitRate.toFixed(1)}%`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (segments.cost) {
|
|
172
|
+
stats.push(theme.fg("warning", `${glyphs.cost} $${totals.cost.toFixed(3)}`));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return stats.join(` ${theme.fg("dim", "|")} `);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function renderExtensionStatusLines(
|
|
179
|
+
theme: Theme,
|
|
180
|
+
extensionStatuses: ReadonlyMap<string, string>,
|
|
181
|
+
glyphs: IconGlyphs,
|
|
182
|
+
width: number,
|
|
183
|
+
): string[] {
|
|
184
|
+
const statuses = Array.from(extensionStatuses.entries())
|
|
185
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
186
|
+
.map(([, text]) => sanitizeStatus(text))
|
|
187
|
+
.filter((text) => text.length > 0);
|
|
188
|
+
if (statuses.length === 0) return [];
|
|
189
|
+
|
|
190
|
+
const separator = ` ${theme.fg("dim", "|")} `;
|
|
191
|
+
const statusText = statuses.map((status) => theme.fg("muted", status)).join(separator);
|
|
192
|
+
const line = `${theme.fg("mdLink", glyphs.extensions)} ${statusText}`;
|
|
193
|
+
return wrapTextWithAnsi(line, width);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface FooterHooks {
|
|
197
|
+
setRequestRender: (fn: (() => void) | undefined) => void;
|
|
198
|
+
scheduleGitRefresh: () => void;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function installFooter(
|
|
202
|
+
ctx: ExtensionContext,
|
|
203
|
+
getState: () => FooterState,
|
|
204
|
+
getConfig: () => OpenTuiConfig,
|
|
205
|
+
getModelMeta: () => ModelMeta,
|
|
206
|
+
hooks: FooterHooks,
|
|
207
|
+
): () => void {
|
|
208
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
209
|
+
hooks.setRequestRender(() => tui.requestRender());
|
|
210
|
+
const unsubBranch = footerData.onBranchChange(() => {
|
|
211
|
+
hooks.scheduleGitRefresh();
|
|
212
|
+
tui.requestRender();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
dispose() {
|
|
217
|
+
unsubBranch();
|
|
218
|
+
hooks.setRequestRender(undefined);
|
|
219
|
+
},
|
|
220
|
+
invalidate() {},
|
|
221
|
+
render(width: number): string[] {
|
|
222
|
+
if (width <= 0) return [""];
|
|
223
|
+
const state = getState();
|
|
224
|
+
const config = getConfig();
|
|
225
|
+
const glyphs = resolveGlyphs(config.icons.mode);
|
|
226
|
+
const segments = config.footerSegments;
|
|
227
|
+
const meta = getModelMeta();
|
|
228
|
+
|
|
229
|
+
const totals = getUsageTotals(ctx);
|
|
230
|
+
|
|
231
|
+
const leftParts: PrioritizedSegment[] = [];
|
|
232
|
+
if (segments.cwd) {
|
|
233
|
+
const maxCwd = Math.min(30, Math.max(10, Math.floor(width * 0.4)));
|
|
234
|
+
const cwd = formatCwd(ctx.sessionManager.getCwd());
|
|
235
|
+
const cwdPrefix = `${theme.fg("mdLink", glyphs.cwd)} `;
|
|
236
|
+
const accent = (text: string) => theme.fg("accent", text);
|
|
237
|
+
leftParts.push({
|
|
238
|
+
text: `${cwdPrefix}${accent(truncatePath(cwd, maxCwd))}`,
|
|
239
|
+
compactText: `${cwdPrefix}${accent(truncatePath(basenamePath(cwd), maxCwd))}`,
|
|
240
|
+
priority: 0,
|
|
241
|
+
truncate: (_text, maxWidth, ellipsis) => {
|
|
242
|
+
const pathWidth = maxWidth - visibleWidth(cwdPrefix);
|
|
243
|
+
if (pathWidth <= visibleWidth(ellipsis)) {
|
|
244
|
+
return truncateToWidth(`${cwdPrefix}${accent(basenamePath(cwd))}`, maxWidth, ellipsis);
|
|
245
|
+
}
|
|
246
|
+
return `${cwdPrefix}${accent(truncatePath(basenamePath(cwd), pathWidth))}`;
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (segments.sessionName) {
|
|
251
|
+
const sessionName = ctx.sessionManager.getSessionName();
|
|
252
|
+
if (sessionName) {
|
|
253
|
+
leftParts.push({
|
|
254
|
+
text: `${theme.fg("dim", glyphs.session)} ${theme.fg("text", truncateToWidth(sessionName, 24, theme.fg("dim", "...")))}`,
|
|
255
|
+
priority: 2,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const gitSeg = renderGitSegment(theme, state.git, glyphs, segments);
|
|
260
|
+
if (gitSeg) leftParts.push({ text: gitSeg, priority: 3 });
|
|
261
|
+
if (segments.runtime) {
|
|
262
|
+
const runtimeSeg = renderRuntimeSegment(theme, state.runtime, config.icons.mode);
|
|
263
|
+
if (runtimeSeg) leftParts.push({ text: runtimeSeg, priority: 4 });
|
|
264
|
+
}
|
|
265
|
+
const timerSeg = renderTimerSegment(theme, state, glyphs);
|
|
266
|
+
if (timerSeg) leftParts.push({ text: timerSeg, priority: 1 });
|
|
267
|
+
|
|
268
|
+
// The context bar competes with the left segments for the same row:
|
|
269
|
+
// full bar first, then the compact icon+pct form, then dropped.
|
|
270
|
+
let contextText = "";
|
|
271
|
+
let contextCompact: string | undefined;
|
|
272
|
+
if (segments.context) {
|
|
273
|
+
contextText = renderContextBar(theme, ctx, width, glyphs, config.icons.mode);
|
|
274
|
+
const compact = renderContextCompact(theme, ctx, glyphs);
|
|
275
|
+
if (compact && visibleWidth(compact) < visibleWidth(contextText)) {
|
|
276
|
+
contextCompact = compact;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const allParts: PrioritizedSegment[] = [...leftParts];
|
|
280
|
+
if (contextText) {
|
|
281
|
+
// ponytail: priority 4 = sheds with runtime, before git/timer/cwd.
|
|
282
|
+
allParts.push({ text: contextText, compactText: contextCompact, priority: 4 });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const fitted = fitSegmentsByPriority(allParts, width, theme.fg("dim", "..."));
|
|
286
|
+
const fittedContext = contextText ? fitted.pop() ?? "" : "";
|
|
287
|
+
const line1 = alignRight(fitted.join(" "), fittedContext, width, theme);
|
|
288
|
+
|
|
289
|
+
const modelParts: string[] = [];
|
|
290
|
+
modelParts.push(theme.fg("mdLink", glyphs.model));
|
|
291
|
+
if (meta.provider && meta.provider !== "Unknown") {
|
|
292
|
+
modelParts.push(theme.fg(providerColor(ctx.model?.provider ?? "none"), meta.provider));
|
|
293
|
+
}
|
|
294
|
+
modelParts.push(theme.fg("text", meta.model));
|
|
295
|
+
if (meta.effort && meta.effort !== "off") {
|
|
296
|
+
modelParts.push(theme.fg(effortColor(meta.effort), `${glyphs.thinking} ${meta.effort}`));
|
|
297
|
+
}
|
|
298
|
+
const modelBlock = modelParts.join(theme.fg("dim", " · "));
|
|
299
|
+
|
|
300
|
+
const statsBlock = renderStatsBlock(
|
|
301
|
+
theme,
|
|
302
|
+
totals,
|
|
303
|
+
glyphs,
|
|
304
|
+
segments,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const line2 = alignRight(modelBlock, statsBlock, width, theme);
|
|
308
|
+
|
|
309
|
+
const mainLines = [line1, line2]
|
|
310
|
+
.map((line) => truncateToWidth(line, width, theme.fg("dim", "...")));
|
|
311
|
+
return segments.extensionStatuses
|
|
312
|
+
? [
|
|
313
|
+
...mainLines,
|
|
314
|
+
...renderExtensionStatusLines(
|
|
315
|
+
theme,
|
|
316
|
+
footerData.getExtensionStatuses(),
|
|
317
|
+
glyphs,
|
|
318
|
+
width,
|
|
319
|
+
),
|
|
320
|
+
]
|
|
321
|
+
: mainLines;
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return () => {
|
|
327
|
+
ctx.ui.setFooter(undefined);
|
|
328
|
+
};
|
|
329
|
+
}
|
|
@@ -46,7 +46,9 @@ export function getUsageTotals(ctx: ExtensionContext): UsageTotals {
|
|
|
46
46
|
const input = finiteOrZero(u.input);
|
|
47
47
|
const cacheRead = finiteOrZero(u.cacheRead);
|
|
48
48
|
const cacheWrite = finiteOrZero(u.cacheWrite);
|
|
49
|
-
|
|
49
|
+
// input matches /session's "uncached" total: cacheWrite is billed near full
|
|
50
|
+
// price (fresh content), only cacheRead is discounted repeat content.
|
|
51
|
+
totals.input += input + cacheWrite;
|
|
50
52
|
totals.output += finiteOrZero(u.output);
|
|
51
53
|
totals.cacheRead += cacheRead;
|
|
52
54
|
totals.cacheWrite += cacheWrite;
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import type { IconMode, TelemetryConfig } from "./config.ts";
|
|
13
13
|
import { resolveGlyphs } from "./icons.ts";
|
|
14
|
-
import { finiteOrZero, fmtTokens, formatDuration } from "./utils.ts";
|
|
14
|
+
import { finiteOrZero, fmtTokens, formatDuration, formatInputBreakdown } from "./utils.ts";
|
|
15
15
|
|
|
16
16
|
const STALL_THRESHOLD_MS = 1000;
|
|
17
17
|
|
|
@@ -49,6 +49,7 @@ export interface TurnTelemetry {
|
|
|
49
49
|
totalMs: number;
|
|
50
50
|
inputTokens: number;
|
|
51
51
|
outputTokens: number;
|
|
52
|
+
cacheReadTokens: number;
|
|
52
53
|
stallMs: number;
|
|
53
54
|
stallCount: number;
|
|
54
55
|
rateUsdPerMTokens: number | null;
|
|
@@ -190,11 +191,15 @@ export class TurnTelemetryTracker {
|
|
|
190
191
|
const endMs = this.now();
|
|
191
192
|
let inputTokens = 0;
|
|
192
193
|
let outputTokens = 0;
|
|
194
|
+
let cacheReadTokens = 0;
|
|
193
195
|
let totalTokens = 0;
|
|
194
196
|
let costUsd = 0;
|
|
195
197
|
for (const message of turn.messages) {
|
|
196
|
-
|
|
198
|
+
// match /session's "uncached" total: cacheWrite is fresh, near-full-price
|
|
199
|
+
// content; only cacheRead is discounted repeat content.
|
|
200
|
+
inputTokens += finiteOrZero(message.usage?.input) + finiteOrZero(message.usage?.cacheWrite);
|
|
197
201
|
outputTokens += finiteOrZero(message.usage?.output);
|
|
202
|
+
cacheReadTokens += finiteOrZero(message.usage?.cacheRead);
|
|
198
203
|
totalTokens += finiteOrZero(message.usage?.totalTokens);
|
|
199
204
|
costUsd += finiteOrZero(message.usage?.cost?.total);
|
|
200
205
|
}
|
|
@@ -211,6 +216,7 @@ export class TurnTelemetryTracker {
|
|
|
211
216
|
totalMs: endMs - turn.startMs,
|
|
212
217
|
inputTokens,
|
|
213
218
|
outputTokens,
|
|
219
|
+
cacheReadTokens,
|
|
214
220
|
stallMs: turn.stallMs,
|
|
215
221
|
stallCount: turn.stallCount,
|
|
216
222
|
rateUsdPerMTokens: validCost && validTokens
|
|
@@ -232,6 +238,7 @@ export class TurnTelemetryTracker {
|
|
|
232
238
|
|
|
233
239
|
const outputTokens = turns.reduce((sum, turn) => sum + turn.outputTokens, 0);
|
|
234
240
|
const inputTokens = turns.reduce((sum, turn) => sum + turn.inputTokens, 0);
|
|
241
|
+
const cacheReadTokens = turns.reduce((sum, turn) => sum + turn.cacheReadTokens, 0);
|
|
235
242
|
const totalTokens = turns.reduce((sum, turn) => sum + turn.totalTokens, 0);
|
|
236
243
|
const costUsd = turns.reduce((sum, turn) => sum + turn.costUsd, 0);
|
|
237
244
|
const stallMs = turns.reduce((sum, turn) => sum + turn.stallMs, 0);
|
|
@@ -248,6 +255,7 @@ export class TurnTelemetryTracker {
|
|
|
248
255
|
totalMs: this.now() - startMs,
|
|
249
256
|
inputTokens,
|
|
250
257
|
outputTokens,
|
|
258
|
+
cacheReadTokens,
|
|
251
259
|
stallMs,
|
|
252
260
|
stallCount,
|
|
253
261
|
rateUsdPerMTokens: validRate ? round(costUsd / (totalTokens / 1_000_000), 2) : null,
|
|
@@ -282,7 +290,7 @@ export function formatTurnTelemetry(
|
|
|
282
290
|
parts.push(theme.fg("success", `${glyphs.done} ${formatTurnDuration(telemetry.totalMs)}`));
|
|
283
291
|
}
|
|
284
292
|
if (config.tokens) {
|
|
285
|
-
parts.push(theme.fg("accent", `${glyphs.input} ${
|
|
293
|
+
parts.push(theme.fg("accent", `${glyphs.input} ${formatInputBreakdown(telemetry.inputTokens, telemetry.cacheReadTokens)}`));
|
|
286
294
|
parts.push(theme.fg("success", `${glyphs.output} ${fmtTokens(telemetry.outputTokens)}`));
|
|
287
295
|
}
|
|
288
296
|
if (config.stalls && telemetry.stallMs > 0) {
|
|
@@ -67,6 +67,13 @@ export function fmtTokens(n: number): string {
|
|
|
67
67
|
return `${Math.round(n / 1_000_000)}M`;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export function formatInputBreakdown(uncached: number, cacheRead: number): string {
|
|
71
|
+
const total = fmtTokens(uncached + cacheRead);
|
|
72
|
+
return cacheRead > 0
|
|
73
|
+
? `${total} (U ${fmtTokens(uncached)} + R ${fmtTokens(cacheRead)})`
|
|
74
|
+
: total;
|
|
75
|
+
}
|
|
76
|
+
|
|
70
77
|
export function formatDuration(ms: number): string {
|
|
71
78
|
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
72
79
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-open-tui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "A polished TUI for Pi coding agent: animated logo header, Starship-style footer, rounded editor with model metadata, and prompt-box user messages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|