pi-zentui 0.1.7 → 0.1.9
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 +31 -16
- package/extensions/zentui/config.ts +110 -139
- package/extensions/zentui/footer.ts +107 -0
- package/extensions/zentui/format.ts +87 -0
- package/extensions/zentui/index.ts +87 -242
- package/extensions/zentui/runtime.ts +531 -603
- package/extensions/zentui/selector-border.ts +106 -0
- package/extensions/zentui/settings-command.ts +121 -0
- package/extensions/zentui/state.ts +41 -0
- package/extensions/zentui/style.ts +221 -0
- package/extensions/zentui/ui.ts +19 -3
- package/extensions/zentui/user-message.ts +57 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -20,12 +20,12 @@ Zentui brings two popular aesthetics to Pi:
|
|
|
20
20
|
- ` dirname` — current directory with icon
|
|
21
21
|
- `on branch` — git branch with icon
|
|
22
22
|
- `[!?↑]` — git status indicators (modified, untracked, ahead/behind, stashed, etc.)
|
|
23
|
-
- `via v5.5.0` — runtime detection with version and Starship
|
|
23
|
+
- `via v5.5.0` — runtime detection with version and Starship-style Nerd Font runtime/language modules
|
|
24
24
|
- Right side shows context usage, token counts, and cost
|
|
25
25
|
|
|
26
26
|
### Editor (Opencode-inspired)
|
|
27
27
|
|
|
28
|
-
- Bordered input box with accent-
|
|
28
|
+
- Bordered input box with theme accent rail and thinking-level border color
|
|
29
29
|
- Model name and provider displayed inside the editor frame
|
|
30
30
|
- Thinking level indicator when enabled
|
|
31
31
|
- Prompt-box-style user messages matching the ZentUI input chrome
|
|
@@ -47,7 +47,7 @@ Zentui brings two popular aesthetics to Pi:
|
|
|
47
47
|
|
|
48
48
|
### Runtime Detection
|
|
49
49
|
|
|
50
|
-
Detects Starship Nerd Font runtime/language modules, uses the Starship Nerd Font symbols, and
|
|
50
|
+
Detects Starship Nerd Font runtime/language modules, uses the Starship Nerd Font symbols, and keeps Starship-style defaults such as `bold green` for Node.js. By default Zentui maps those styles through your active Pi theme; switch the Starship/footer color source to `terminal` in `/zentui` if you want your terminal colorscheme to supply the exact ANSI colors.
|
|
51
51
|
|
|
52
52
|
| Runtime/language | Detection examples |
|
|
53
53
|
| ---------------- | ------------------------------------------------------------ |
|
|
@@ -148,15 +148,21 @@ On first run, Zentui creates a config file at:
|
|
|
148
148
|
"typechanged": "T"
|
|
149
149
|
},
|
|
150
150
|
"colors": {
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"gitStatus": "
|
|
154
|
-
"contextNormal": "
|
|
155
|
-
"contextWarning": "
|
|
156
|
-
"contextError": "
|
|
157
|
-
"tokens": "
|
|
158
|
-
"cost": "
|
|
159
|
-
"separator": "
|
|
151
|
+
"cwd": "bold cyan",
|
|
152
|
+
"gitBranch": "bold purple",
|
|
153
|
+
"gitStatus": "bold red",
|
|
154
|
+
"contextNormal": "dimmed",
|
|
155
|
+
"contextWarning": "bold yellow",
|
|
156
|
+
"contextError": "bold red",
|
|
157
|
+
"tokens": "dimmed",
|
|
158
|
+
"cost": "bold green",
|
|
159
|
+
"separator": "dimmed",
|
|
160
|
+
"runtimePrefix": ""
|
|
161
|
+
},
|
|
162
|
+
"colorSources": {
|
|
163
|
+
"starship": "theme",
|
|
164
|
+
"editor": "theme",
|
|
165
|
+
"userMessages": "theme"
|
|
160
166
|
}
|
|
161
167
|
}
|
|
162
168
|
```
|
|
@@ -165,12 +171,21 @@ On first run, Zentui creates a config file at:
|
|
|
165
171
|
|
|
166
172
|
### Color values
|
|
167
173
|
|
|
168
|
-
|
|
174
|
+
Use `/zentui` inside Pi to switch color sources between Pi theme colors and terminal colors:
|
|
175
|
+
|
|
176
|
+
- `starship` — footer/runtime/git/context/cost colors
|
|
177
|
+
- `editor + previous messages` — input editor and previous user-message rails/borders
|
|
178
|
+
|
|
179
|
+
Both settings default to `theme`. The config still stores editor and previous user-message sources separately as `editor` and `userMessages`, but `/zentui` changes them together so the prompt chrome stays consistent.
|
|
180
|
+
|
|
181
|
+
Color values can use terminal-palette style strings, hex colors, or Pi theme color tokens:
|
|
169
182
|
|
|
170
|
-
-
|
|
171
|
-
-
|
|
183
|
+
- Named terminal colors: `bold purple`, `yellow`, `bright-cyan`
|
|
184
|
+
- 256-color / hex: `bold 149`, `fg:202`, `#89b4fa`
|
|
185
|
+
- Backgrounds and modifiers: `bg:blue fg:bright-green`, `underline bg:#bf5700`
|
|
186
|
+
- Pi theme tokens: `accent`, `borderMuted`, `syntaxKeyword`
|
|
172
187
|
|
|
173
|
-
|
|
188
|
+
Use the `colors` config key for these values.
|
|
174
189
|
|
|
175
190
|
## Requirements
|
|
176
191
|
|
|
@@ -3,6 +3,13 @@ import { join } from "node:path";
|
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
export type ColorSpec = string;
|
|
6
|
+
export type ColorSource = "theme" | "terminal";
|
|
7
|
+
|
|
8
|
+
export type ColorSourcesConfig = {
|
|
9
|
+
starship: ColorSource;
|
|
10
|
+
editor: ColorSource;
|
|
11
|
+
userMessages: ColorSource;
|
|
12
|
+
};
|
|
6
13
|
|
|
7
14
|
const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
|
|
8
15
|
const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
|
|
@@ -25,8 +32,8 @@ export type PolishedTuiConfig = {
|
|
|
25
32
|
typechanged: string;
|
|
26
33
|
};
|
|
27
34
|
colors: {
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
cwd: ColorSpec;
|
|
36
|
+
gitBranch: ColorSpec;
|
|
30
37
|
gitStatus: ColorSpec;
|
|
31
38
|
contextNormal: ColorSpec;
|
|
32
39
|
contextWarning: ColorSpec;
|
|
@@ -34,59 +41,13 @@ export type PolishedTuiConfig = {
|
|
|
34
41
|
tokens: ColorSpec;
|
|
35
42
|
cost: ColorSpec;
|
|
36
43
|
separator: ColorSpec;
|
|
44
|
+
runtimePrefix: ColorSpec;
|
|
37
45
|
};
|
|
46
|
+
colorSources: ColorSourcesConfig;
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
export const configPath = join(getAgentDir(), "zentui.json");
|
|
41
50
|
|
|
42
|
-
const themeColorTokens = new Set([
|
|
43
|
-
"accent",
|
|
44
|
-
"border",
|
|
45
|
-
"borderAccent",
|
|
46
|
-
"borderMuted",
|
|
47
|
-
"success",
|
|
48
|
-
"error",
|
|
49
|
-
"warning",
|
|
50
|
-
"muted",
|
|
51
|
-
"dim",
|
|
52
|
-
"text",
|
|
53
|
-
"thinkingText",
|
|
54
|
-
"userMessageText",
|
|
55
|
-
"customMessageText",
|
|
56
|
-
"customMessageLabel",
|
|
57
|
-
"toolTitle",
|
|
58
|
-
"toolOutput",
|
|
59
|
-
"mdHeading",
|
|
60
|
-
"mdLink",
|
|
61
|
-
"mdLinkUrl",
|
|
62
|
-
"mdCode",
|
|
63
|
-
"mdCodeBlock",
|
|
64
|
-
"mdCodeBlockBorder",
|
|
65
|
-
"mdQuote",
|
|
66
|
-
"mdQuoteBorder",
|
|
67
|
-
"mdHr",
|
|
68
|
-
"mdListBullet",
|
|
69
|
-
"toolDiffAdded",
|
|
70
|
-
"toolDiffRemoved",
|
|
71
|
-
"toolDiffContext",
|
|
72
|
-
"syntaxComment",
|
|
73
|
-
"syntaxKeyword",
|
|
74
|
-
"syntaxFunction",
|
|
75
|
-
"syntaxVariable",
|
|
76
|
-
"syntaxString",
|
|
77
|
-
"syntaxNumber",
|
|
78
|
-
"syntaxType",
|
|
79
|
-
"syntaxOperator",
|
|
80
|
-
"syntaxPunctuation",
|
|
81
|
-
"thinkingOff",
|
|
82
|
-
"thinkingMinimal",
|
|
83
|
-
"thinkingLow",
|
|
84
|
-
"thinkingMedium",
|
|
85
|
-
"thinkingHigh",
|
|
86
|
-
"thinkingXhigh",
|
|
87
|
-
"bashMode",
|
|
88
|
-
]);
|
|
89
|
-
|
|
90
51
|
export const defaultConfig: PolishedTuiConfig = {
|
|
91
52
|
projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
|
|
92
53
|
icons: {
|
|
@@ -105,72 +66,22 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
105
66
|
typechanged: "T",
|
|
106
67
|
},
|
|
107
68
|
colors: {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
gitStatus: "
|
|
111
|
-
contextNormal: "
|
|
112
|
-
contextWarning: "
|
|
113
|
-
contextError: "
|
|
114
|
-
tokens: "
|
|
115
|
-
cost: "
|
|
116
|
-
separator: "
|
|
69
|
+
cwd: "bold cyan",
|
|
70
|
+
gitBranch: "bold purple",
|
|
71
|
+
gitStatus: "bold red",
|
|
72
|
+
contextNormal: "dimmed",
|
|
73
|
+
contextWarning: "bold yellow",
|
|
74
|
+
contextError: "bold red",
|
|
75
|
+
tokens: "dimmed",
|
|
76
|
+
cost: "bold green",
|
|
77
|
+
separator: "dimmed",
|
|
78
|
+
runtimePrefix: "",
|
|
79
|
+
},
|
|
80
|
+
colorSources: {
|
|
81
|
+
starship: "theme",
|
|
82
|
+
editor: "theme",
|
|
83
|
+
userMessages: "theme",
|
|
117
84
|
},
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
function isHexColor(value: string): boolean {
|
|
121
|
-
return /^#(?:[0-9a-fA-F]{6})$/.test(value);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function hexToAnsi(hex: string, isBackground = false): string {
|
|
125
|
-
const normalized = hex.slice(1);
|
|
126
|
-
const r = Number.parseInt(normalized.slice(0, 2), 16);
|
|
127
|
-
const g = Number.parseInt(normalized.slice(2, 4), 16);
|
|
128
|
-
const b = Number.parseInt(normalized.slice(4, 6), 16);
|
|
129
|
-
return `\x1b[${isBackground ? 48 : 38};2;${r};${g};${b}m`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const terminalColorCodes = new Map([
|
|
133
|
-
["black", 30],
|
|
134
|
-
["red", 31],
|
|
135
|
-
["green", 32],
|
|
136
|
-
["yellow", 33],
|
|
137
|
-
["blue", 34],
|
|
138
|
-
["purple", 35],
|
|
139
|
-
["cyan", 36],
|
|
140
|
-
["white", 37],
|
|
141
|
-
["bright-black", 90],
|
|
142
|
-
["bright-red", 91],
|
|
143
|
-
["bright-green", 92],
|
|
144
|
-
["bright-yellow", 93],
|
|
145
|
-
["bright-blue", 94],
|
|
146
|
-
["bright-purple", 95],
|
|
147
|
-
["bright-cyan", 96],
|
|
148
|
-
["bright-white", 97],
|
|
149
|
-
]);
|
|
150
|
-
|
|
151
|
-
const terminalStyleModifiers = new Map([
|
|
152
|
-
["bold", 1],
|
|
153
|
-
["dim", 2],
|
|
154
|
-
["dimmed", 2],
|
|
155
|
-
["italic", 3],
|
|
156
|
-
["underline", 4],
|
|
157
|
-
]);
|
|
158
|
-
|
|
159
|
-
function terminalColorToAnsi(color: string): string | undefined {
|
|
160
|
-
const normalized = color.toLowerCase();
|
|
161
|
-
const colorCode = terminalColorCodes.get(normalized);
|
|
162
|
-
if (colorCode !== undefined) return `${colorCode}`;
|
|
163
|
-
|
|
164
|
-
if (/^(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(normalized)) {
|
|
165
|
-
return `38;5;${normalized}`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (isHexColor(normalized)) return hexToAnsi(normalized).slice(2, -1);
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
type ThemeLike = {
|
|
173
|
-
fg(color: string, text: string): string;
|
|
174
85
|
};
|
|
175
86
|
|
|
176
87
|
type ConfigRecord = Record<string, unknown>;
|
|
@@ -191,34 +102,73 @@ function parseProjectRefreshIntervalMs(value: unknown): number {
|
|
|
191
102
|
: defaultConfig.projectRefreshIntervalMs;
|
|
192
103
|
}
|
|
193
104
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
if (isHexColor(color)) {
|
|
199
|
-
return `${hexToAnsi(color)}${text}\x1b[39m`;
|
|
200
|
-
}
|
|
201
|
-
return theme.fg("text", text);
|
|
105
|
+
function stringValue(record: Record<string, unknown>, key: string): string | undefined {
|
|
106
|
+
const value = record[key];
|
|
107
|
+
return typeof value === "string" ? value : undefined;
|
|
202
108
|
}
|
|
203
109
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
110
|
+
function colorSourceValue(
|
|
111
|
+
record: Record<string, unknown>,
|
|
112
|
+
key: keyof ColorSourcesConfig,
|
|
113
|
+
): ColorSource {
|
|
114
|
+
const value = record[key];
|
|
115
|
+
return value === "terminal" || value === "theme" ? value : defaultConfig.colorSources[key];
|
|
116
|
+
}
|
|
208
117
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
118
|
+
function definedColors(
|
|
119
|
+
colors: Partial<Record<keyof PolishedTuiConfig["colors"], string | undefined>>,
|
|
120
|
+
): Partial<PolishedTuiConfig["colors"]> {
|
|
121
|
+
return Object.fromEntries(
|
|
122
|
+
Object.entries(colors).filter(
|
|
123
|
+
(entry): entry is [keyof PolishedTuiConfig["colors"], string] => typeof entry[1] === "string",
|
|
124
|
+
),
|
|
125
|
+
) as Partial<PolishedTuiConfig["colors"]>;
|
|
126
|
+
}
|
|
215
127
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
128
|
+
function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiConfig["colors"]> {
|
|
129
|
+
return definedColors({
|
|
130
|
+
cwd: stringValue(record, "cwd") ?? stringValue(record, "cwdText"),
|
|
131
|
+
gitBranch: stringValue(record, "gitBranch") ?? stringValue(record, "git"),
|
|
132
|
+
gitStatus: stringValue(record, "gitStatus"),
|
|
133
|
+
contextNormal: stringValue(record, "contextNormal"),
|
|
134
|
+
contextWarning: stringValue(record, "contextWarning"),
|
|
135
|
+
contextError: stringValue(record, "contextError"),
|
|
136
|
+
tokens: stringValue(record, "tokens"),
|
|
137
|
+
cost: stringValue(record, "cost"),
|
|
138
|
+
separator: stringValue(record, "separator"),
|
|
139
|
+
runtimePrefix: stringValue(record, "runtimePrefix"),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeColorSources(record: Record<string, unknown>): ColorSourcesConfig {
|
|
144
|
+
return {
|
|
145
|
+
starship: colorSourceValue(record, "starship"),
|
|
146
|
+
editor: colorSourceValue(record, "editor"),
|
|
147
|
+
userMessages: colorSourceValue(record, "userMessages"),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isColorSourceKey(value: string): value is keyof ColorSourcesConfig {
|
|
152
|
+
return value === "starship" || value === "editor" || value === "userMessages";
|
|
153
|
+
}
|
|
220
154
|
|
|
221
|
-
|
|
155
|
+
function validColorSourceEntries(record: Record<string, unknown>): Partial<ColorSourcesConfig> {
|
|
156
|
+
return Object.fromEntries(
|
|
157
|
+
Object.entries(record).filter((entry): entry is [keyof ColorSourcesConfig, ColorSource] => {
|
|
158
|
+
const [key, value] = entry;
|
|
159
|
+
return isColorSourceKey(key) && (value === "theme" || value === "terminal");
|
|
160
|
+
}),
|
|
161
|
+
) as Partial<ColorSourcesConfig>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readConfigRecord(path = configPath): ConfigRecord {
|
|
165
|
+
try {
|
|
166
|
+
if (!existsSync(path)) return {};
|
|
167
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
168
|
+
return isRecord(parsed) ? parsed : {};
|
|
169
|
+
} catch {
|
|
170
|
+
return {};
|
|
171
|
+
}
|
|
222
172
|
}
|
|
223
173
|
|
|
224
174
|
export function ensureConfigExists(): void {
|
|
@@ -235,8 +185,11 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
235
185
|
const config = isRecord(parsed) ? parsed : {};
|
|
236
186
|
const icons = isRecord(config.icons) ? (config.icons as Partial<PolishedTuiConfig["icons"]>) : {};
|
|
237
187
|
const colors = isRecord(config.colors)
|
|
238
|
-
? (config.colors as
|
|
188
|
+
? normalizeColors(config.colors as Record<string, unknown>)
|
|
239
189
|
: {};
|
|
190
|
+
const colorSources = isRecord(config.colorSources)
|
|
191
|
+
? normalizeColorSources(config.colorSources as Record<string, unknown>)
|
|
192
|
+
: defaultConfig.colorSources;
|
|
240
193
|
return {
|
|
241
194
|
projectRefreshIntervalMs: parseProjectRefreshIntervalMs(config.projectRefreshIntervalMs),
|
|
242
195
|
icons: {
|
|
@@ -247,6 +200,7 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
247
200
|
...defaultConfig.colors,
|
|
248
201
|
...colors,
|
|
249
202
|
},
|
|
203
|
+
colorSources,
|
|
250
204
|
};
|
|
251
205
|
}
|
|
252
206
|
|
|
@@ -258,3 +212,20 @@ export function loadConfig(): PolishedTuiConfig {
|
|
|
258
212
|
return defaultConfig;
|
|
259
213
|
}
|
|
260
214
|
}
|
|
215
|
+
|
|
216
|
+
export function saveColorSourcesPatch(
|
|
217
|
+
patch: Partial<ColorSourcesConfig>,
|
|
218
|
+
path = configPath,
|
|
219
|
+
): PolishedTuiConfig {
|
|
220
|
+
const record = readConfigRecord(path);
|
|
221
|
+
const existing = isRecord(record.colorSources)
|
|
222
|
+
? validColorSourceEntries(record.colorSources as Record<string, unknown>)
|
|
223
|
+
: {};
|
|
224
|
+
record.colorSources = {
|
|
225
|
+
...defaultConfig.colorSources,
|
|
226
|
+
...existing,
|
|
227
|
+
...validColorSourceEntries(patch),
|
|
228
|
+
};
|
|
229
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
230
|
+
return mergeConfig(record);
|
|
231
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { PolishedTuiConfig } from "./config";
|
|
4
|
+
import { formatCwdLabel, formatRuntimeSegment } from "./format";
|
|
5
|
+
import type { FooterState } from "./state";
|
|
6
|
+
import { renderStyleForSource } from "./style";
|
|
7
|
+
|
|
8
|
+
export function installFooter(
|
|
9
|
+
ctx: ExtensionContext,
|
|
10
|
+
state: FooterState,
|
|
11
|
+
getConfig: () => PolishedTuiConfig,
|
|
12
|
+
hooks: {
|
|
13
|
+
setRequestRender: (fn: (() => void) | undefined) => void;
|
|
14
|
+
scheduleProjectRefresh: (ctx: ExtensionContext) => void;
|
|
15
|
+
},
|
|
16
|
+
): void {
|
|
17
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
18
|
+
hooks.setRequestRender(() => tui.requestRender());
|
|
19
|
+
const unsubscribeBranch = footerData.onBranchChange(() => {
|
|
20
|
+
hooks.scheduleProjectRefresh(ctx);
|
|
21
|
+
tui.requestRender();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
dispose: () => {
|
|
26
|
+
unsubscribeBranch();
|
|
27
|
+
hooks.setRequestRender(undefined);
|
|
28
|
+
},
|
|
29
|
+
invalidate() {},
|
|
30
|
+
render(width: number): string[] {
|
|
31
|
+
if (width <= 0) return [""];
|
|
32
|
+
const config = getConfig();
|
|
33
|
+
const colorSource = config.colorSources.starship;
|
|
34
|
+
const separator = renderStyleForSource(theme, colorSource, config.colors.separator, " | ");
|
|
35
|
+
const innerWidth = Math.max(1, width - 2);
|
|
36
|
+
const cwdLabel = renderStyleForSource(
|
|
37
|
+
theme,
|
|
38
|
+
colorSource,
|
|
39
|
+
config.colors.cwd,
|
|
40
|
+
formatCwdLabel(ctx.cwd, config.icons.cwd),
|
|
41
|
+
);
|
|
42
|
+
const branch = state.branch;
|
|
43
|
+
const contextUsage = ctx.getContextUsage();
|
|
44
|
+
const contextColor =
|
|
45
|
+
contextUsage?.percent !== null && contextUsage?.percent !== undefined
|
|
46
|
+
? contextUsage.percent >= 90
|
|
47
|
+
? config.colors.contextError
|
|
48
|
+
: contextUsage.percent >= 70
|
|
49
|
+
? config.colors.contextWarning
|
|
50
|
+
: config.colors.contextNormal
|
|
51
|
+
: config.colors.contextNormal;
|
|
52
|
+
const gitColor = (text: string) =>
|
|
53
|
+
renderStyleForSource(theme, colorSource, config.colors.gitBranch, text);
|
|
54
|
+
const gitStatusColor = (text: string) =>
|
|
55
|
+
renderStyleForSource(theme, colorSource, config.colors.gitStatus, text);
|
|
56
|
+
const gitIcon = gitColor(config.icons.git);
|
|
57
|
+
const allStatus = [
|
|
58
|
+
state.conflicted > 0 ? config.icons.conflicted : "",
|
|
59
|
+
state.stashed ? config.icons.stashed : "",
|
|
60
|
+
state.deleted > 0 ? config.icons.deleted : "",
|
|
61
|
+
state.renamed > 0 ? config.icons.renamed : "",
|
|
62
|
+
state.modified > 0 ? config.icons.modified : "",
|
|
63
|
+
state.typechanged > 0 ? config.icons.typechanged : "",
|
|
64
|
+
state.staged > 0 ? config.icons.staged : "",
|
|
65
|
+
state.untracked > 0 ? config.icons.untracked : "",
|
|
66
|
+
].join("");
|
|
67
|
+
const aheadBehind =
|
|
68
|
+
state.ahead > 0 && state.behind > 0
|
|
69
|
+
? config.icons.diverged
|
|
70
|
+
: state.ahead > 0
|
|
71
|
+
? config.icons.ahead
|
|
72
|
+
: state.behind > 0
|
|
73
|
+
? config.icons.behind
|
|
74
|
+
: "";
|
|
75
|
+
const statusBlock =
|
|
76
|
+
allStatus || aheadBehind ? gitStatusColor(`[${allStatus}${aheadBehind}]`) : "";
|
|
77
|
+
const branchLabel = branch
|
|
78
|
+
? `on ${gitIcon} ${gitColor(branch)}${statusBlock ? ` ${statusBlock}` : ""}`
|
|
79
|
+
: "";
|
|
80
|
+
const runtimeLabel = formatRuntimeSegment(
|
|
81
|
+
theme,
|
|
82
|
+
state.runtime,
|
|
83
|
+
config.colors.runtimePrefix,
|
|
84
|
+
colorSource,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const left = [cwdLabel, branchLabel, runtimeLabel].filter(Boolean).join(" ");
|
|
88
|
+
const right = [
|
|
89
|
+
renderStyleForSource(theme, colorSource, contextColor, state.contextLabel),
|
|
90
|
+
renderStyleForSource(theme, colorSource, config.colors.tokens, state.tokenLabel),
|
|
91
|
+
renderStyleForSource(theme, colorSource, config.colors.cost, state.costLabel),
|
|
92
|
+
].join(separator);
|
|
93
|
+
|
|
94
|
+
const leftWidth = visibleWidth(left);
|
|
95
|
+
const rightWidth = visibleWidth(right);
|
|
96
|
+
const content =
|
|
97
|
+
leftWidth >= innerWidth
|
|
98
|
+
? truncateToWidth(left, innerWidth, "")
|
|
99
|
+
: leftWidth + 1 + rightWidth <= innerWidth
|
|
100
|
+
? `${left}${" ".repeat(innerWidth - leftWidth - rightWidth)}${right}`
|
|
101
|
+
: truncateToWidth(left, innerWidth, "");
|
|
102
|
+
const framed = width > 2 ? ` ${truncateToWidth(content, width - 2, "")} ` : content;
|
|
103
|
+
return [truncateToWidth(framed, width, "")];
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { ColorSource, ColorSpec } from "./config";
|
|
4
|
+
import type { RuntimeInfo } from "./runtime";
|
|
5
|
+
import { renderStyleForSource } from "./style";
|
|
6
|
+
|
|
7
|
+
export type UsageTotals = {
|
|
8
|
+
input: number;
|
|
9
|
+
output: number;
|
|
10
|
+
cost: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function formatCount(value: number): string {
|
|
14
|
+
if (value < 1000) return `${value}`;
|
|
15
|
+
if (value < 10_000) return `${(value / 1000).toFixed(1)}k`;
|
|
16
|
+
return `${Math.round(value / 1000)}k`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatProviderLabel(provider: string | undefined): string {
|
|
20
|
+
if (!provider) return "Unknown";
|
|
21
|
+
|
|
22
|
+
const known: Record<string, string> = {
|
|
23
|
+
anthropic: "Anthropic",
|
|
24
|
+
gemini: "Google",
|
|
25
|
+
google: "Google",
|
|
26
|
+
ollama: "Ollama",
|
|
27
|
+
openai: "OpenAI",
|
|
28
|
+
"openai-codex": "OpenAI",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
known[provider] ?? provider.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getUsageTotals(ctx: ExtensionContext): UsageTotals {
|
|
37
|
+
let input = 0;
|
|
38
|
+
let output = 0;
|
|
39
|
+
let cost = 0;
|
|
40
|
+
|
|
41
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
42
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
43
|
+
const message = entry.message as AssistantMessage;
|
|
44
|
+
input += message.usage?.input ?? 0;
|
|
45
|
+
output += message.usage?.output ?? 0;
|
|
46
|
+
cost += message.usage?.cost?.total ?? 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { input, output, cost };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildTokenLabel(totals: UsageTotals): string {
|
|
53
|
+
return `↑${formatCount(totals.input)} ↓${formatCount(totals.output)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildCostLabel(totals: UsageTotals): string {
|
|
57
|
+
return `$${totals.cost.toFixed(3)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildContextLabel(ctx: ExtensionContext): string {
|
|
61
|
+
const usage = ctx.getContextUsage();
|
|
62
|
+
const contextWindow = ctx.model?.contextWindow ?? usage?.contextWindow;
|
|
63
|
+
|
|
64
|
+
if (!usage || !contextWindow || contextWindow <= 0) return "--";
|
|
65
|
+
|
|
66
|
+
const percent =
|
|
67
|
+
usage.percent === null ? "?" : `${Math.max(0, Math.min(999, Math.round(usage.percent)))}%`;
|
|
68
|
+
return `${percent}/${formatCount(contextWindow)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function formatRuntimeSegment(
|
|
72
|
+
theme: Pick<Theme, "fg">,
|
|
73
|
+
runtime: RuntimeInfo | undefined,
|
|
74
|
+
prefixStyle: ColorSpec,
|
|
75
|
+
colorSource: ColorSource,
|
|
76
|
+
): string {
|
|
77
|
+
if (!runtime) return "";
|
|
78
|
+
const label = runtime.version ? `${runtime.symbol} ${runtime.version}` : runtime.symbol;
|
|
79
|
+
return `${renderStyleForSource(theme, colorSource, prefixStyle, "via")} ${renderStyleForSource(theme, colorSource, runtime.style, label)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function formatCwdLabel(cwd: string, cwdIcon: string): string {
|
|
83
|
+
const normalized = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
84
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
85
|
+
const last = parts[parts.length - 1] ?? cwd;
|
|
86
|
+
return cwdIcon ? `${cwdIcon} ${last}` : last;
|
|
87
|
+
}
|