pi-footer-manager 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -0
- package/assets/pi-footer-manager.png +0 -0
- package/footer-manager/README.md +125 -0
- package/footer-manager/built-ins.ts +144 -0
- package/footer-manager/index.ts +462 -0
- package/footer-manager/types.ts +53 -0
- package/fragments/context-gauge-text-fragment.ts +57 -0
- package/fragments/footer-timer-fragment.ts +81 -0
- package/fragments/quota-footer-fragment-text.ts +476 -0
- package/fragments/quota-footer-fragment.ts +483 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# pi-footer-manager
|
|
2
|
+
|
|
3
|
+
One footer, many extensions: build flexible Pi footers from reusable fragments with configurable layout and built-in fragments instead of competing `setFooter()` calls.
|
|
4
|
+
|
|
5
|
+
`pi-footer-manager` lets one extension own `ctx.ui.setFooter(...)` while built-in and custom fragments are arranged through a shared API, with flexible rows, regions, widths, alignment, and redraw/invalidation flow.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Included extensions
|
|
10
|
+
|
|
11
|
+
- `footer-manager/index.ts` — cooperative footer owner
|
|
12
|
+
- `fragments/footer-timer-fragment.ts` — example fragment showing elapsed work time
|
|
13
|
+
- `fragments/quota-footer-fragment.ts` — richer quota usage display for supported providers
|
|
14
|
+
- `fragments/quota-footer-fragment-text.ts` — text-focused quota usage display
|
|
15
|
+
- `fragments/context-gauge-text-fragment.ts` — text version of context usage
|
|
16
|
+
|
|
17
|
+
## How configuration works
|
|
18
|
+
|
|
19
|
+
Layout is configured under `footerManager.layout` in Pi settings.
|
|
20
|
+
|
|
21
|
+
- `separator` controls how fragments are joined inside a region
|
|
22
|
+
- `rows` is an array of footer rows
|
|
23
|
+
- each row has `regions`
|
|
24
|
+
- each region can set:
|
|
25
|
+
- `width`: fraction like `0.35` or `"auto"`
|
|
26
|
+
- `align`: `"left"`, `"center"`, or `"right"`
|
|
27
|
+
- `fragments`: fragment ids to render in that region
|
|
28
|
+
|
|
29
|
+
Project settings override global settings.
|
|
30
|
+
|
|
31
|
+
## Example: simple two-region footer
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"footerManager": {
|
|
36
|
+
"layout": {
|
|
37
|
+
"separator": " > ",
|
|
38
|
+
"rows": [
|
|
39
|
+
{
|
|
40
|
+
"regions": [
|
|
41
|
+
{ "width": 0.65, "align": "left", "fragments": ["cwd.full", "git.branch", "context.gauge"] },
|
|
42
|
+
{ "width": 0.35, "align": "right", "fragments": ["model.name", "thinking.level", "statuses"] }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Example: mostly automatic sizing
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"footerManager": {
|
|
56
|
+
"layout": {
|
|
57
|
+
"rows": [
|
|
58
|
+
{
|
|
59
|
+
"regions": [
|
|
60
|
+
{ "align": "left", "fragments": ["cwd.full", "git.branch"] },
|
|
61
|
+
{ "width": 0.35, "align": "right", "fragments": ["model.name", "statuses"] }
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
In mixed layouts, fixed fractional regions are allocated first and the remaining width goes to `"auto"` regions.
|
|
71
|
+
|
|
72
|
+
## Example: multi-row footer
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"footerManager": {
|
|
77
|
+
"layout": {
|
|
78
|
+
"rows": [
|
|
79
|
+
{
|
|
80
|
+
"regions": [
|
|
81
|
+
{ "align": "left", "fragments": ["cwd.full", "git.branch"] },
|
|
82
|
+
{ "align": "right", "fragments": ["model.name", "thinking.level"] }
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"regions": [
|
|
87
|
+
{ "align": "left", "fragments": ["context.gauge.text", "quota.current.text", "timer.work"] }
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Widths and overflow
|
|
97
|
+
|
|
98
|
+
Think of each row as a fixed-width bar split into regions:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
[ left region ][ right region ]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
For this layout:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"regions": [
|
|
109
|
+
{ "align": "left", "fragments": ["cwd.full", "git.branch"] },
|
|
110
|
+
{ "width": 0.35, "align": "right", "fragments": ["model.name", "statuses"] }
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The row behaves roughly like this:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
[ cwd.full > git.branch ][ model.name > statuses ]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Rules of thumb:
|
|
122
|
+
|
|
123
|
+
- fixed fractional regions get their width first
|
|
124
|
+
- `"auto"` regions get the remaining space
|
|
125
|
+
- if content does not fit, fragments are dropped before the final visible fragment is truncated
|
|
126
|
+
- left- and center-aligned regions drop fragments from the right
|
|
127
|
+
- right-aligned regions drop fragments from the left
|
|
128
|
+
|
|
129
|
+
Multi-row layouts behave like stacked bars:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
row 1: [ cwd.full > git.branch ][ model.name > statuses ]
|
|
133
|
+
row 2: [ context.gauge.text > timer.work ]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Built-in fragments
|
|
137
|
+
|
|
138
|
+
Main built-ins provided by `footer-manager` include:
|
|
139
|
+
|
|
140
|
+
- `cwd.full` — full path of the current working directory
|
|
141
|
+
- `git.branch` — current Git branch for the active working tree
|
|
142
|
+
- `model.name` — active model name
|
|
143
|
+
- `model.cost` — input/output token pricing for the active model
|
|
144
|
+
- `model.cacheCost` — cached token read/write pricing for the active model
|
|
145
|
+
- `cache.hit` — cache hit rate summary
|
|
146
|
+
- `cache.hit_counts` — cache hit rate with read/write token counts
|
|
147
|
+
- `thinking.level` — current reasoning/thinking level
|
|
148
|
+
- `context.gauge` — graphical context usage indicator
|
|
149
|
+
- `cost.total` — total accumulated session cost
|
|
150
|
+
- `statuses` — status items contributed through Pi status APIs
|
|
151
|
+
|
|
152
|
+
The included fragment extensions add examples like:
|
|
153
|
+
|
|
154
|
+
- `timer.work` — elapsed time for the current agent run
|
|
155
|
+
- `context.gauge.text` — text version of context usage
|
|
156
|
+
- `quota.current` — quota usage summary for supported providers
|
|
157
|
+
- `quota.current.text` — text-focused quota usage summary
|
|
158
|
+
|
|
159
|
+
## Custom fragments
|
|
160
|
+
|
|
161
|
+
Other extensions should register fragments instead of calling `ctx.ui.setFooter(...)` directly.
|
|
162
|
+
|
|
163
|
+
See detailed fragment API docs and examples in:
|
|
164
|
+
|
|
165
|
+
- [`footer-manager/README.md`](./footer-manager/README.md)
|
|
166
|
+
|
|
167
|
+
## Check
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm run check
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Publish dry run
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
npm run release:check
|
|
177
|
+
```
|
|
Binary file
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# footer-manager
|
|
2
|
+
|
|
3
|
+
`footer-manager` is the cooperative owner of Pi footer rendering.
|
|
4
|
+
|
|
5
|
+
It calls `ctx.ui.setFooter(...)` once, while other extensions contribute footer fragments through a shared registration API instead of competing to replace the whole footer. Pi cannot enforce this yet, so disable conflicting extensions that also call `ctx.ui.setFooter(...)` directly.
|
|
6
|
+
|
|
7
|
+
## Register a fragment
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
FOOTER_MANAGER_REGISTER_FRAGMENT,
|
|
12
|
+
type FooterFragmentRegistration,
|
|
13
|
+
} from "./footer-manager/types";
|
|
14
|
+
|
|
15
|
+
export default function (pi) {
|
|
16
|
+
pi.on("session_start", async () => {
|
|
17
|
+
const fragment: FooterFragmentRegistration = {
|
|
18
|
+
id: "my-extension.timer",
|
|
19
|
+
label: "Timer",
|
|
20
|
+
component: (env) => ({
|
|
21
|
+
render() {
|
|
22
|
+
return env.theme.fg("accent", "12m");
|
|
23
|
+
},
|
|
24
|
+
dispose() {},
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
pi.events.emit(FOOTER_MANAGER_REGISTER_FRAGMENT, fragment);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Fragment factories and `render()` are synchronous. Do async work inside the fragment, cache state locally, then call `env.invalidate()` when the rendered output should refresh. Use `env.separator` when a fragment needs to join multiple internal values with the current layout separator.
|
|
33
|
+
|
|
34
|
+
## Invalidate / redraw
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
env.invalidate();
|
|
38
|
+
// or
|
|
39
|
+
pi.events.emit("footer-manager:invalidate", { id: "my-extension.timer" });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Invalidations are coalesced and the manager owns `tui.requestRender()`.
|
|
43
|
+
|
|
44
|
+
## Layout configuration
|
|
45
|
+
|
|
46
|
+
Settings live under `footerManager.layout`.
|
|
47
|
+
|
|
48
|
+
- `separator` controls how fragments are joined inside a region
|
|
49
|
+
- `rows` is an array of footer rows
|
|
50
|
+
- each row has `regions`
|
|
51
|
+
- each region can set:
|
|
52
|
+
- `width`: a fraction like `0.35` or `"auto"`
|
|
53
|
+
- `align`: `"left"`, `"center"`, or `"right"`
|
|
54
|
+
- `fragments`: fragment ids to render in that region
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"footerManager": {
|
|
61
|
+
"layout": {
|
|
62
|
+
"separator": " > ",
|
|
63
|
+
"rows": [
|
|
64
|
+
{
|
|
65
|
+
"regions": [
|
|
66
|
+
{ "width": 0.65, "align": "left", "fragments": ["cwd.full", "git.branch", "context.gauge"] },
|
|
67
|
+
{ "width": 0.35, "align": "right", "fragments": ["model.name", "thinking.level", "statuses"] }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Project settings override global settings. If project `footerManager` exists but is invalid, the default layout is used; global `footerManager` is not merged as fallback.
|
|
77
|
+
|
|
78
|
+
## Widths and overflow
|
|
79
|
+
|
|
80
|
+
Widths are optional and default to `"auto"`.
|
|
81
|
+
|
|
82
|
+
- fractional regions get their width first
|
|
83
|
+
- `"auto"` regions get the remaining width
|
|
84
|
+
- if content does not fit, fragments are dropped before the last visible fragment is truncated
|
|
85
|
+
- left- and center-aligned regions drop fragments from the right
|
|
86
|
+
- right-aligned regions drop fragments from the left
|
|
87
|
+
|
|
88
|
+
Mixed example:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"regions": [
|
|
93
|
+
{ "align": "left", "fragments": ["cwd.full", "git.branch"] },
|
|
94
|
+
{ "width": 0.35, "align": "right", "fragments": ["model.name", "statuses"] }
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This behaves roughly like:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
[ cwd.full > git.branch ][ model.name > statuses ]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For rows without auto regions, widths should sum to `1`. Positive non-`1` sums are normalized with a warning. Invalid rows, invalid regions, or zero-width fully fixed rows fall back to the built-in default layout.
|
|
106
|
+
|
|
107
|
+
## Built-in fragments
|
|
108
|
+
|
|
109
|
+
- `cwd.full` — full path of the current working directory
|
|
110
|
+
- `git.branch` — current Git branch for the active working tree
|
|
111
|
+
- `model.name` — active model name
|
|
112
|
+
- `model.cost` — input/output token pricing for the active model
|
|
113
|
+
- `model.cacheCost` — cached token read/write pricing for the active model
|
|
114
|
+
- `cache.hit` — cache hit rate summary
|
|
115
|
+
- `cache.hit_counts` — cache hit rate with read/write token counts
|
|
116
|
+
- `thinking.level` — current reasoning/thinking level
|
|
117
|
+
- `context.gauge` — graphical context usage indicator
|
|
118
|
+
- `cost.total` — total accumulated session cost
|
|
119
|
+
- `statuses` — status items contributed through Pi status APIs
|
|
120
|
+
|
|
121
|
+
`statuses` preserves compatibility with extensions using `ctx.ui.setStatus(...)` by joining status values in insertion order with the layout separator.
|
|
122
|
+
|
|
123
|
+
## Example extension
|
|
124
|
+
|
|
125
|
+
`../fragments/footer-timer-fragment.ts` registers `timer.work` through the event bus and demonstrates cached state plus `env.invalidate()`. Add `"timer.work"` to a layout region to show it.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { buildSessionContext, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import type { FooterFragmentRegistration, FooterRenderEnv } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export type BuiltInFragmentsOptions = {
|
|
6
|
+
getSeparator: () => string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function formatTokens(tokens: number): string {
|
|
10
|
+
if (!Number.isFinite(tokens) || tokens <= 0) return "0";
|
|
11
|
+
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
|
|
12
|
+
if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k`;
|
|
13
|
+
return `${Math.round(tokens)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatCost(cost: number): string {
|
|
17
|
+
if (!Number.isFinite(cost) || cost <= 0) return "$0.000";
|
|
18
|
+
if (cost < 0.01) return `$${cost.toFixed(4)}`;
|
|
19
|
+
if (cost < 1) return `$${cost.toFixed(3)}`;
|
|
20
|
+
return `$${cost.toFixed(2)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatModelRate(rate: unknown): string {
|
|
24
|
+
const value = Number(rate) || 0;
|
|
25
|
+
if (value === 0) return "0";
|
|
26
|
+
if (Math.abs(value) < 0.01) return value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
|
|
27
|
+
return value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function collapseHome(cwd: string): string {
|
|
31
|
+
const home = homedir();
|
|
32
|
+
return home && (cwd === home || cwd.startsWith(home + "/")) ? `~${cwd.slice(home.length)}` : cwd;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function compactModel(ctx: ExtensionContext): string {
|
|
36
|
+
const model = ctx.model;
|
|
37
|
+
const id = typeof model?.id === "string" ? model.id : "no-model";
|
|
38
|
+
const provider = typeof model?.provider === "string" ? model.provider : undefined;
|
|
39
|
+
const base = id.split("/").filter(Boolean).pop() || id;
|
|
40
|
+
if (!provider) return base;
|
|
41
|
+
return id.toLowerCase().includes(provider.toLowerCase()) ? base : `${provider}/${base}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderModelCost(ctx: ExtensionContext): string {
|
|
45
|
+
const cost = ctx.model?.cost;
|
|
46
|
+
if (!cost) return "";
|
|
47
|
+
return `↑$${formatModelRate(cost.input)} ↓$${formatModelRate(cost.output)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderModelCacheCost(ctx: ExtensionContext): string {
|
|
51
|
+
const cost = ctx.model?.cost;
|
|
52
|
+
if (!cost) return "";
|
|
53
|
+
const read = Number(cost.cacheRead) || 0;
|
|
54
|
+
const write = Number(cost.cacheWrite) || 0;
|
|
55
|
+
if (read === 0 && write === 0) return "";
|
|
56
|
+
return `R${formatModelRate(read)} W${formatModelRate(write)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getBranchAssistantUsage(ctx: ExtensionContext): { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number } {
|
|
60
|
+
const totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
61
|
+
try {
|
|
62
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
63
|
+
if ((entry as any).type !== "message") continue;
|
|
64
|
+
const message = (entry as any).message;
|
|
65
|
+
if (message?.role !== "assistant") continue;
|
|
66
|
+
const usage = message.usage;
|
|
67
|
+
totals.input += Number(usage?.input) || 0;
|
|
68
|
+
totals.output += Number(usage?.output) || 0;
|
|
69
|
+
totals.cacheRead += Number(usage?.cacheRead) || 0;
|
|
70
|
+
totals.cacheWrite += Number(usage?.cacheWrite) || 0;
|
|
71
|
+
totals.cost += Number(usage?.cost?.total) || 0;
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
return totals;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getCacheHit(ctx: ExtensionContext): { hitPercent: number; cacheRead: number; cacheWrite: number } | undefined {
|
|
78
|
+
const { input, cacheRead, cacheWrite } = getBranchAssistantUsage(ctx);
|
|
79
|
+
if (cacheRead === 0 && cacheWrite === 0) return undefined;
|
|
80
|
+
const denominator = input + cacheRead;
|
|
81
|
+
const hitPercent = denominator > 0 ? Math.round((cacheRead / denominator) * 100) : 0;
|
|
82
|
+
return { hitPercent, cacheRead, cacheWrite };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderCacheHit(ctx: ExtensionContext): string {
|
|
86
|
+
const cache = getCacheHit(ctx);
|
|
87
|
+
return cache ? `cache ${cache.hitPercent}%` : "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderCacheHitCounts(ctx: ExtensionContext): string {
|
|
91
|
+
const cache = getCacheHit(ctx);
|
|
92
|
+
return cache ? `cache ${cache.hitPercent}% R${formatTokens(cache.cacheRead)}/W${formatTokens(cache.cacheWrite)}` : "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getThinkingLevel(ctx: ExtensionContext): string {
|
|
96
|
+
try {
|
|
97
|
+
const context = buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId());
|
|
98
|
+
return context.thinkingLevel || "off";
|
|
99
|
+
} catch {
|
|
100
|
+
return "off";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getContextInfo(ctx: ExtensionContext): { percentage: number; used?: number; total?: number } {
|
|
105
|
+
try {
|
|
106
|
+
const contextWindow = Number(ctx.model?.contextWindow) || 0;
|
|
107
|
+
if (contextWindow <= 0) return { percentage: 0 };
|
|
108
|
+
const context = buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId());
|
|
109
|
+
const lastAssistant = [...context.messages].reverse().find((m: any) => m.role === "assistant" && m.stopReason !== "aborted") as any;
|
|
110
|
+
const usage = lastAssistant?.usage;
|
|
111
|
+
if (!usage) return { percentage: 0, used: 0, total: contextWindow };
|
|
112
|
+
const used = (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
|
113
|
+
return { percentage: (used / contextWindow) * 100, used, total: contextWindow };
|
|
114
|
+
} catch {
|
|
115
|
+
return { percentage: 0 };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderContextGauge(ctx: ExtensionContext, theme: Theme): string {
|
|
120
|
+
const { percentage, used, total } = getContextInfo(ctx);
|
|
121
|
+
const width = 8;
|
|
122
|
+
const clamped = Math.max(0, Math.min(100, percentage));
|
|
123
|
+
const filled = Math.round((clamped / 100) * width);
|
|
124
|
+
const color = clamped >= 90 ? "error" : clamped >= 70 ? "warning" : clamped >= 50 ? "accent" : "success";
|
|
125
|
+
const bar = theme.fg(color, "━".repeat(filled)) + theme.fg("dim", "─".repeat(width - filled));
|
|
126
|
+
const counts = used !== undefined && total ? ` ${formatTokens(used)}/${formatTokens(total)}` : "";
|
|
127
|
+
return `${theme.fg("dim", "ctx ")}${bar} ${theme.fg("dim", `${Math.round(clamped)}%${counts}`)}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createBuiltInFragments(options: BuiltInFragmentsOptions): FooterFragmentRegistration[] {
|
|
131
|
+
return [
|
|
132
|
+
{ id: "cwd.full", label: "CWD", component: ({ ctx, theme }: FooterRenderEnv) => ({ render: () => theme.fg("accent", collapseHome(ctx.cwd || process.cwd())) }) },
|
|
133
|
+
{ id: "git.branch", label: "Git branch", component: ({ footerData, theme }: FooterRenderEnv) => ({ render: () => { const branch = footerData.getGitBranch(); return branch ? theme.fg("success", branch) : ""; } }) },
|
|
134
|
+
{ id: "model.name", label: "Model", component: ({ ctx, theme }: FooterRenderEnv) => ({ render: () => theme.fg("muted", compactModel(ctx)) }) },
|
|
135
|
+
{ id: "model.cost", label: "Model cost", component: ({ ctx, theme }: FooterRenderEnv) => ({ render: () => theme.fg("dim", renderModelCost(ctx)) }) },
|
|
136
|
+
{ id: "model.cacheCost", label: "Model cache cost", component: ({ ctx, theme }: FooterRenderEnv) => ({ render: () => theme.fg("dim", renderModelCacheCost(ctx)) }) },
|
|
137
|
+
{ id: "cache.hit", label: "Cache hit", component: ({ ctx, theme }: FooterRenderEnv) => ({ render: () => theme.fg("dim", renderCacheHit(ctx)) }) },
|
|
138
|
+
{ id: "cache.hit_counts", label: "Cache hit with counts", component: ({ ctx, theme }: FooterRenderEnv) => ({ render: () => theme.fg("dim", renderCacheHitCounts(ctx)) }) },
|
|
139
|
+
{ id: "thinking.level", label: "Thinking", component: ({ ctx, theme }: FooterRenderEnv) => ({ render: () => theme.fg("accent", getThinkingLevel(ctx)) }) },
|
|
140
|
+
{ id: "context.gauge", label: "Context", component: ({ ctx, theme }: FooterRenderEnv) => ({ render: () => renderContextGauge(ctx, theme) }) },
|
|
141
|
+
{ id: "cost.total", label: "Total cost", component: ({ ctx, theme }: FooterRenderEnv) => ({ render: () => theme.fg("dim", formatCost(getBranchAssistantUsage(ctx).cost)) }) },
|
|
142
|
+
{ id: "statuses", label: "Statuses", component: ({ footerData, theme }: FooterRenderEnv) => ({ render: () => Array.from(footerData.getExtensionStatuses().values()).filter(Boolean).join(theme.fg("dim", options.getSeparator())) }) },
|
|
143
|
+
];
|
|
144
|
+
}
|