pi-open-tui 0.2.14 → 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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/assets/preview_dashboard_1.png +0 -0
- package/extensions/open-tui/footer.ts +329 -328
- package/extensions/open-tui/settings-command.ts +72 -14
- package/extensions/open-tui/state.ts +13 -8
- package/extensions/open-tui/telemetry.ts +303 -298
- package/extensions/open-tui/utils.ts +372 -361
- package/package.json +1 -1
|
@@ -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
|
+
}
|