rinco-pi-hud 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/LICENSE +21 -0
- package/NOTICE +18 -0
- package/README.md +204 -0
- package/docs/CONTRIBUTING.md +110 -0
- package/docs/README.zh-CN.md +204 -0
- package/docs/footer.md +139 -0
- package/extensions/hud/commands/settings.ts +765 -0
- package/extensions/hud/config/config-counts.ts +87 -0
- package/extensions/hud/config/config.ts +65 -0
- package/extensions/hud/config/file-io.ts +128 -0
- package/extensions/hud/config/normalize.ts +374 -0
- package/extensions/hud/config/persist.ts +277 -0
- package/extensions/hud/config/schema.ts +303 -0
- package/extensions/hud/footer/format.ts +279 -0
- package/extensions/hud/footer/index.ts +543 -0
- package/extensions/hud/footer/layout.ts +183 -0
- package/extensions/hud/footer/render-labels.ts +305 -0
- package/extensions/hud/index.ts +466 -0
- package/extensions/hud/segments/extension-status.ts +88 -0
- package/extensions/hud/segments/git.ts +384 -0
- package/extensions/hud/segments/mcp-status.ts +25 -0
- package/extensions/hud/segments/package-version.ts +651 -0
- package/extensions/hud/segments/project-state.ts +67 -0
- package/extensions/hud/segments/runtime-definitions.ts +695 -0
- package/extensions/hud/segments/runtime.ts +167 -0
- package/extensions/hud/segments/skill-activity.ts +152 -0
- package/extensions/hud/session/context.ts +19 -0
- package/extensions/hud/session/lifecycle.ts +60 -0
- package/extensions/hud/session/live-context.ts +75 -0
- package/extensions/hud/state/index.ts +67 -0
- package/extensions/hud/state/project-refresh.ts +104 -0
- package/extensions/hud/state/telemetry.ts +261 -0
- package/extensions/hud/telemetry/codex-usage/LICENSE +21 -0
- package/extensions/hud/telemetry/codex-usage/app-server-client.ts +231 -0
- package/extensions/hud/telemetry/codex-usage/format.ts +278 -0
- package/extensions/hud/telemetry/codex-usage/index.ts +462 -0
- package/extensions/hud/telemetry/codex-usage/normalize.ts +264 -0
- package/extensions/hud/telemetry/codex-usage/query.ts +207 -0
- package/extensions/hud/telemetry/codex-usage/safety.ts +27 -0
- package/extensions/hud/telemetry/codex-usage/types.ts +137 -0
- package/extensions/hud/telemetry/format.ts +434 -0
- package/extensions/hud/telemetry/token-switch.ts +114 -0
- package/extensions/hud/ui/icons.ts +239 -0
- package/extensions/hud/ui/style.ts +276 -0
- package/package.json +56 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export type CategorizedFooterRows = {
|
|
4
|
+
project: string[];
|
|
5
|
+
session: string[];
|
|
6
|
+
usage: string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function joinStatusTexts(statusTexts: string[], separator: string): string {
|
|
10
|
+
return statusTexts.filter(Boolean).join(separator);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function fitStatusTexts(statusTexts: string[], maxWidth: number, separator: string): string {
|
|
14
|
+
if (maxWidth <= 0) return "";
|
|
15
|
+
|
|
16
|
+
const fitted: string[] = [];
|
|
17
|
+
for (const text of statusTexts) {
|
|
18
|
+
const candidate = joinStatusTexts([...fitted, text], separator);
|
|
19
|
+
if (visibleWidth(candidate) <= maxWidth) {
|
|
20
|
+
fitted.push(text);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (fitted.length === 0) {
|
|
25
|
+
return maxWidth > 1 ? truncateToWidth(text, maxWidth, "…") : "";
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return joinStatusTexts(fitted, separator);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function appendStatusArea(base: string, statusText: string, separator: string): string {
|
|
34
|
+
if (!base) return statusText;
|
|
35
|
+
if (!statusText) return base;
|
|
36
|
+
return `${base}${separator}${statusText}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function prependStatusArea(base: string, statusText: string, separator: string): string {
|
|
40
|
+
if (!base) return statusText;
|
|
41
|
+
if (!statusText) return base;
|
|
42
|
+
return `${statusText}${separator}${base}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function composeBuiltInFooterContent(left: string, right: string, innerWidth: number): string {
|
|
46
|
+
const leftWidth = visibleWidth(left);
|
|
47
|
+
const rightWidth = visibleWidth(right);
|
|
48
|
+
if (!right) return truncateToWidth(left, innerWidth, "");
|
|
49
|
+
if (!left) {
|
|
50
|
+
const fittedRight = truncateToWidth(right, innerWidth, "");
|
|
51
|
+
return `${" ".repeat(Math.max(0, innerWidth - visibleWidth(fittedRight)))}${fittedRight}`;
|
|
52
|
+
}
|
|
53
|
+
if (leftWidth + 1 + rightWidth <= innerWidth) {
|
|
54
|
+
return `${left}${" ".repeat(innerWidth - leftWidth - rightWidth)}${right}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const gap = innerWidth > 1 ? 1 : 0;
|
|
58
|
+
const available = Math.max(0, innerWidth - gap);
|
|
59
|
+
let rightBudget = Math.min(rightWidth, Math.max(0, Math.floor(available * 0.48)));
|
|
60
|
+
let leftBudget = Math.min(leftWidth, Math.max(0, available - rightBudget));
|
|
61
|
+
let remaining = Math.max(0, available - leftBudget - rightBudget);
|
|
62
|
+
const leftExtra = Math.min(remaining, Math.max(0, leftWidth - leftBudget));
|
|
63
|
+
leftBudget += leftExtra;
|
|
64
|
+
remaining -= leftExtra;
|
|
65
|
+
rightBudget += Math.min(remaining, Math.max(0, rightWidth - rightBudget));
|
|
66
|
+
let fittedLeft = truncateToWidth(left, leftBudget, "");
|
|
67
|
+
let fittedRight = truncateToWidth(right, rightBudget, "");
|
|
68
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
69
|
+
let spare = Math.max(
|
|
70
|
+
0,
|
|
71
|
+
available - visibleWidth(fittedLeft) - visibleWidth(fittedRight),
|
|
72
|
+
);
|
|
73
|
+
if (spare === 0) break;
|
|
74
|
+
if (visibleWidth(fittedLeft) < leftWidth) {
|
|
75
|
+
fittedLeft = truncateToWidth(left, visibleWidth(fittedLeft) + spare, "");
|
|
76
|
+
spare = Math.max(
|
|
77
|
+
0,
|
|
78
|
+
available - visibleWidth(fittedLeft) - visibleWidth(fittedRight),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (spare > 0 && visibleWidth(fittedRight) < rightWidth) {
|
|
82
|
+
fittedRight = truncateToWidth(right, visibleWidth(fittedRight) + spare, "");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const padding = Math.max(0, innerWidth - visibleWidth(fittedLeft) - visibleWidth(fittedRight));
|
|
86
|
+
return `${fittedLeft}${" ".repeat(padding)}${fittedRight}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function composeFooterContent(
|
|
90
|
+
builtInLeft: string,
|
|
91
|
+
builtInRight: string,
|
|
92
|
+
extensionLeft: string[],
|
|
93
|
+
extensionMiddle: string[],
|
|
94
|
+
extensionRight: string[],
|
|
95
|
+
separator: string,
|
|
96
|
+
innerWidth: number,
|
|
97
|
+
): string {
|
|
98
|
+
const builtInLeftWidth = visibleWidth(builtInLeft);
|
|
99
|
+
const builtInRightWidth = visibleWidth(builtInRight);
|
|
100
|
+
const minimumGap = builtInLeft && builtInRight ? 1 : 0;
|
|
101
|
+
|
|
102
|
+
if (builtInLeftWidth + minimumGap + builtInRightWidth > innerWidth) {
|
|
103
|
+
return composeBuiltInFooterContent(builtInLeft, builtInRight, innerWidth);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const available = Math.max(0, innerWidth - builtInLeftWidth - builtInRightWidth - minimumGap);
|
|
107
|
+
const reservedMiddle = fitStatusTexts(
|
|
108
|
+
extensionMiddle,
|
|
109
|
+
Math.max(0, Math.floor(available * 0.4)),
|
|
110
|
+
separator,
|
|
111
|
+
);
|
|
112
|
+
const sideAvailable = Math.max(0, available - visibleWidth(reservedMiddle));
|
|
113
|
+
let remaining = sideAvailable;
|
|
114
|
+
const leftConnectorWidth = builtInLeft && extensionLeft.length > 0 ? visibleWidth(separator) : 0;
|
|
115
|
+
const rightConnectorWidth =
|
|
116
|
+
builtInRight && extensionRight.length > 0 ? visibleWidth(separator) : 0;
|
|
117
|
+
let leftStatus = "";
|
|
118
|
+
let rightStatus = "";
|
|
119
|
+
|
|
120
|
+
if (extensionLeft.length > 0 && extensionRight.length > 0) {
|
|
121
|
+
const leftBudget = Math.max(0, Math.floor(sideAvailable / 2) - leftConnectorWidth);
|
|
122
|
+
leftStatus = fitStatusTexts(extensionLeft, leftBudget, separator);
|
|
123
|
+
remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
|
|
124
|
+
|
|
125
|
+
const rightBudget = Math.max(0, remaining - rightConnectorWidth);
|
|
126
|
+
rightStatus = fitStatusTexts(extensionRight, rightBudget, separator);
|
|
127
|
+
remaining -= rightStatus ? rightConnectorWidth + visibleWidth(rightStatus) : 0;
|
|
128
|
+
|
|
129
|
+
const expandedLeftBudget = Math.max(0, remaining + visibleWidth(leftStatus));
|
|
130
|
+
const expandedLeftStatus = fitStatusTexts(extensionLeft, expandedLeftBudget, separator);
|
|
131
|
+
if (visibleWidth(expandedLeftStatus) > visibleWidth(leftStatus)) {
|
|
132
|
+
remaining += leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
|
|
133
|
+
leftStatus = expandedLeftStatus;
|
|
134
|
+
remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
|
|
135
|
+
}
|
|
136
|
+
} else if (extensionLeft.length > 0) {
|
|
137
|
+
leftStatus = fitStatusTexts(
|
|
138
|
+
extensionLeft,
|
|
139
|
+
Math.max(0, sideAvailable - leftConnectorWidth),
|
|
140
|
+
separator,
|
|
141
|
+
);
|
|
142
|
+
remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
|
|
143
|
+
} else if (extensionRight.length > 0) {
|
|
144
|
+
rightStatus = fitStatusTexts(
|
|
145
|
+
extensionRight,
|
|
146
|
+
Math.max(0, sideAvailable - rightConnectorWidth),
|
|
147
|
+
separator,
|
|
148
|
+
);
|
|
149
|
+
remaining -= rightStatus ? rightConnectorWidth + visibleWidth(rightStatus) : 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const left = appendStatusArea(builtInLeft, leftStatus, separator);
|
|
153
|
+
const right = prependStatusArea(builtInRight, rightStatus, separator);
|
|
154
|
+
const gapWidth = Math.max(0, innerWidth - visibleWidth(left) - visibleWidth(right));
|
|
155
|
+
const middle = fitStatusTexts(extensionMiddle, gapWidth, separator);
|
|
156
|
+
const middleWidth = visibleWidth(middle);
|
|
157
|
+
|
|
158
|
+
if (!middle || middleWidth <= 0) {
|
|
159
|
+
return `${left}${" ".repeat(gapWidth)}${right}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const leftPadding = Math.floor((gapWidth - middleWidth) / 2);
|
|
163
|
+
const rightPadding = gapWidth - middleWidth - leftPadding;
|
|
164
|
+
return `${left}${" ".repeat(leftPadding)}${middle}${" ".repeat(rightPadding)}${right}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function composeLeftAlignedRow(parts: string[], separator: string, innerWidth: number): string {
|
|
168
|
+
const content = joinStatusTexts(parts, separator);
|
|
169
|
+
if (innerWidth <= 1) return truncateToWidth(content, Math.max(0, innerWidth), "");
|
|
170
|
+
return truncateToWidth(content, innerWidth, "…");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function composeCategorizedFooterRows(
|
|
174
|
+
rows: CategorizedFooterRows,
|
|
175
|
+
separator: string,
|
|
176
|
+
innerWidth: number,
|
|
177
|
+
): [string, string, string] {
|
|
178
|
+
return [
|
|
179
|
+
composeLeftAlignedRow(rows.project, separator, innerWidth),
|
|
180
|
+
composeLeftAlignedRow(rows.session, separator, innerWidth),
|
|
181
|
+
composeLeftAlignedRow(rows.usage, separator, innerWidth),
|
|
182
|
+
];
|
|
183
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footer render-time label builders.
|
|
3
|
+
*
|
|
4
|
+
* `installFooter`'s render callback composes dozens of small styled label
|
|
5
|
+
* strings (git status, telemetry counters, sanitized state fields) before
|
|
6
|
+
* dispatching to either the custom `footerFormat` template or the default
|
|
7
|
+
* categorized rows. Those label computations are pulled out here as pure
|
|
8
|
+
* functions of `(state, config, theme, colorSource, ...)` so the render
|
|
9
|
+
* callback itself stays focused on composition/layout.
|
|
10
|
+
*/
|
|
11
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
13
|
+
import type { ColorSource, PolishedTuiConfig } from "../config/config.js";
|
|
14
|
+
import type { McpStatus } from "../segments/mcp-status.js";
|
|
15
|
+
import { formatSkillCounts } from "../segments/skill-activity.js";
|
|
16
|
+
import type { FooterState } from "../state/index.js";
|
|
17
|
+
import { getTelemetryStats } from "../state/telemetry.js";
|
|
18
|
+
import { buildSessionDurationLabel, formatCount } from "../telemetry/format.js";
|
|
19
|
+
import { renderStyleForSource, type ThemeLike } from "../ui/style.js";
|
|
20
|
+
import { sanitizeExtensionStatusText } from "../segments/extension-status.js";
|
|
21
|
+
|
|
22
|
+
export function safeStatusText(value: string | null | undefined, maxLength = 256): string {
|
|
23
|
+
if (!value) return "";
|
|
24
|
+
return sanitizeExtensionStatusText(value.slice(0, maxLength * 4)).slice(0, maxLength);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SanitizedFooterInputs = {
|
|
28
|
+
safeCwd: string;
|
|
29
|
+
safeRuntime:
|
|
30
|
+
| { name: string; symbol: string; style: string; version: string | undefined }
|
|
31
|
+
| undefined;
|
|
32
|
+
safePackageVersion: { ecosystem: string; version: string } | undefined;
|
|
33
|
+
safeCommit:
|
|
34
|
+
| { oid: string | null; detached: boolean; tag: string | null }
|
|
35
|
+
| undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Sanitize the state/ctx fields that flow into the terminal so a malicious
|
|
39
|
+
* runtime name, git branch, etc. cannot inject ANSI/control sequences. */
|
|
40
|
+
export function sanitizeFooterInputs(
|
|
41
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
42
|
+
state: FooterState,
|
|
43
|
+
): SanitizedFooterInputs {
|
|
44
|
+
const safeCwd = safeStatusText(ctx.cwd, 1024);
|
|
45
|
+
const safeRuntime = state.runtime
|
|
46
|
+
? {
|
|
47
|
+
...state.runtime,
|
|
48
|
+
name: safeStatusText(state.runtime.name, 64),
|
|
49
|
+
symbol: safeStatusText(state.runtime.symbol, 32),
|
|
50
|
+
version: safeStatusText(state.runtime.version, 128) || undefined,
|
|
51
|
+
}
|
|
52
|
+
: undefined;
|
|
53
|
+
const safePackageVersion = state.packageVersion
|
|
54
|
+
? {
|
|
55
|
+
ecosystem: safeStatusText(state.packageVersion.ecosystem, 64),
|
|
56
|
+
version: safeStatusText(state.packageVersion.version, 160),
|
|
57
|
+
}
|
|
58
|
+
: undefined;
|
|
59
|
+
const safeCommit = state.commit
|
|
60
|
+
? {
|
|
61
|
+
...state.commit,
|
|
62
|
+
oid: safeStatusText(state.commit.oid, 64) || null,
|
|
63
|
+
tag: safeStatusText(state.commit.tag, 160) || null,
|
|
64
|
+
}
|
|
65
|
+
: undefined;
|
|
66
|
+
return { safeCwd, safeRuntime, safePackageVersion, safeCommit };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type GitStatusLabels = {
|
|
70
|
+
gitColor: (text: string) => string;
|
|
71
|
+
gitStatusColor: (text: string) => string;
|
|
72
|
+
gitIcon: string;
|
|
73
|
+
statusBlock: string;
|
|
74
|
+
gitStateLabel: string;
|
|
75
|
+
gitStateBlock: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Build the ` [icons ahead/behind]` git-status block and the git operation
|
|
79
|
+
* state (REBASING/MERGING/…) block, both pre-styled for the active theme. */
|
|
80
|
+
export function buildGitStatusLabels(
|
|
81
|
+
state: FooterState,
|
|
82
|
+
config: PolishedTuiConfig,
|
|
83
|
+
theme: ThemeLike,
|
|
84
|
+
colorSource: ColorSource,
|
|
85
|
+
): GitStatusLabels {
|
|
86
|
+
const gitColor = (text: string) =>
|
|
87
|
+
renderStyleForSource(theme, colorSource, config.colors.gitBranch, text);
|
|
88
|
+
const gitStatusColor = (text: string) =>
|
|
89
|
+
renderStyleForSource(theme, colorSource, config.colors.gitStatus, text);
|
|
90
|
+
const gitIcon = config.icons.git ? gitColor(config.icons.git) : "";
|
|
91
|
+
const gitCounts = config.footerSegments.gitCounts;
|
|
92
|
+
const stashLabel =
|
|
93
|
+
state.stashed > 0
|
|
94
|
+
? gitCounts
|
|
95
|
+
? `${config.icons.stashed} ${state.stashed}`
|
|
96
|
+
: config.icons.stashed
|
|
97
|
+
: "";
|
|
98
|
+
const allStatus = [
|
|
99
|
+
state.conflicted > 0 ? config.icons.conflicted : "",
|
|
100
|
+
stashLabel,
|
|
101
|
+
state.deleted > 0 ? config.icons.deleted : "",
|
|
102
|
+
state.renamed > 0 ? config.icons.renamed : "",
|
|
103
|
+
state.modified > 0 ? config.icons.modified : "",
|
|
104
|
+
state.typechanged > 0 ? config.icons.typechanged : "",
|
|
105
|
+
state.staged > 0 ? config.icons.staged : "",
|
|
106
|
+
state.untracked > 0 ? config.icons.untracked : "",
|
|
107
|
+
]
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(" ");
|
|
110
|
+
const aheadBehind = (() => {
|
|
111
|
+
if (state.ahead > 0 && state.behind > 0) {
|
|
112
|
+
return gitCounts
|
|
113
|
+
? `${config.icons.ahead} ${state.ahead} ${config.icons.behind} ${state.behind}`
|
|
114
|
+
: config.icons.diverged;
|
|
115
|
+
}
|
|
116
|
+
if (state.ahead > 0)
|
|
117
|
+
return gitCounts ? `${config.icons.ahead} ${state.ahead}` : config.icons.ahead;
|
|
118
|
+
if (state.behind > 0)
|
|
119
|
+
return gitCounts ? `${config.icons.behind} ${state.behind}` : config.icons.behind;
|
|
120
|
+
return "";
|
|
121
|
+
})();
|
|
122
|
+
const gitStatusText = [allStatus, aheadBehind].filter(Boolean).join(" ");
|
|
123
|
+
const statusBlock = gitStatusText ? gitStatusColor(`[${gitStatusText}]`) : "";
|
|
124
|
+
const gitStateLabel = safeStatusText(state.gitStateLabel, 64);
|
|
125
|
+
const gitStateBlock = gitStateLabel ? gitStatusColor(gitStateLabel) : "";
|
|
126
|
+
return { gitColor, gitStatusColor, gitIcon, statusBlock, gitStateLabel, gitStateBlock };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type TelemetryLabels = {
|
|
130
|
+
projectCategoryLabel: string;
|
|
131
|
+
sessionCategoryLabel: string;
|
|
132
|
+
usageCategoryLabel: string;
|
|
133
|
+
sessionNameLabel: string;
|
|
134
|
+
modelValueLabel: string;
|
|
135
|
+
modelStatusLabel: string;
|
|
136
|
+
providerId: string;
|
|
137
|
+
modelId: string;
|
|
138
|
+
thinkingLabel: string;
|
|
139
|
+
turnLabel: string;
|
|
140
|
+
cacheReadLabel: string;
|
|
141
|
+
cacheWriteLabel: string;
|
|
142
|
+
cacheHitLabel: string;
|
|
143
|
+
cacheDetailsLabel: string;
|
|
144
|
+
codexUsageLabel: string;
|
|
145
|
+
instructionFilesLabel: string;
|
|
146
|
+
agentsFilesLabel: string;
|
|
147
|
+
claudeFilesLabel: string;
|
|
148
|
+
skillsLabel: string;
|
|
149
|
+
activeSkillsLabel: string;
|
|
150
|
+
extensionsLabel: string;
|
|
151
|
+
configCountsLabel: string;
|
|
152
|
+
mcpLabel: string;
|
|
153
|
+
toolCountsLabel: string;
|
|
154
|
+
runningToolsLabel: string;
|
|
155
|
+
activeAgentsLabel: string;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** Build every session/usage-row telemetry label (model, thinking level, turn
|
|
159
|
+
* count, cache/tool/agent counters, skills, MCP, Codex usage, …). */
|
|
160
|
+
export function buildTelemetryLabels(
|
|
161
|
+
ctx: Pick<ExtensionContext, "model">,
|
|
162
|
+
state: FooterState,
|
|
163
|
+
config: PolishedTuiConfig,
|
|
164
|
+
theme: ThemeLike,
|
|
165
|
+
colorSource: ColorSource,
|
|
166
|
+
mcpStatus: McpStatus | undefined,
|
|
167
|
+
): TelemetryLabels {
|
|
168
|
+
const statusStyle = (style: string, text: string) =>
|
|
169
|
+
text ? renderStyleForSource(theme, colorSource, style, text) : "";
|
|
170
|
+
|
|
171
|
+
const projectCategoryLabel = statusStyle(config.colors.cwd, "⌂ project");
|
|
172
|
+
const sessionCategoryLabel = statusStyle(config.colors.runtimePrefix, "λ session");
|
|
173
|
+
const usageCategoryLabel = statusStyle(config.colors.contextNormal, "◉ usage");
|
|
174
|
+
|
|
175
|
+
const sessionName = safeStatusText(state.telemetry.sessionName, 256);
|
|
176
|
+
const sessionNameLabel = statusStyle(config.colors.gitBranch, sessionName ? `◈ ${sessionName}` : "");
|
|
177
|
+
|
|
178
|
+
const providerId = safeStatusText(ctx.model?.provider, 128);
|
|
179
|
+
const modelId = safeStatusText(ctx.model?.id, 256);
|
|
180
|
+
const modelText = providerId && modelId ? `${providerId}/${modelId}` : modelId || providerId;
|
|
181
|
+
const modelValueLabel = statusStyle(config.colors.runtimePrefix, modelText);
|
|
182
|
+
const modelStatusLabel = statusStyle(config.colors.runtimePrefix, modelText ? `λ ${modelText}` : "");
|
|
183
|
+
|
|
184
|
+
const thinkingLabel = state.telemetry.modelSupportsReasoning
|
|
185
|
+
? statusStyle(
|
|
186
|
+
config.colors.extensionStatus,
|
|
187
|
+
`◈ ${state.telemetry.thinkingLevel === "off" ? "thinking off" : state.telemetry.thinkingLevel}`,
|
|
188
|
+
)
|
|
189
|
+
: "";
|
|
190
|
+
const turnLabel =
|
|
191
|
+
state.telemetry.turnIndex > 0
|
|
192
|
+
? statusStyle(config.colors.extensionStatus, `↺ ${state.telemetry.turnIndex}`)
|
|
193
|
+
: "";
|
|
194
|
+
|
|
195
|
+
const cacheReadLabel =
|
|
196
|
+
state.usageTotals.cacheRead > 0
|
|
197
|
+
? statusStyle(config.colors.tokens, `R ${formatCount(state.usageTotals.cacheRead)}`)
|
|
198
|
+
: "";
|
|
199
|
+
const cacheWriteLabel =
|
|
200
|
+
state.usageTotals.cacheWrite > 0
|
|
201
|
+
? statusStyle(config.colors.tokens, `W ${formatCount(state.usageTotals.cacheWrite)}`)
|
|
202
|
+
: "";
|
|
203
|
+
const cacheHitLabel =
|
|
204
|
+
state.usageTotals.latestCacheHitRate !== undefined
|
|
205
|
+
? statusStyle(
|
|
206
|
+
config.colors.contextNormal,
|
|
207
|
+
`CH ${state.usageTotals.latestCacheHitRate.toFixed(1)}%`,
|
|
208
|
+
)
|
|
209
|
+
: "";
|
|
210
|
+
const cacheDetailsLabel = [cacheReadLabel, cacheWriteLabel].filter(Boolean).join(" ");
|
|
211
|
+
|
|
212
|
+
const codexStyle =
|
|
213
|
+
state.codexUsageStatus === "checking"
|
|
214
|
+
? config.colors.contextWarning
|
|
215
|
+
: state.codexUsageStatus === "usage error"
|
|
216
|
+
? config.colors.contextError
|
|
217
|
+
: config.colors.contextNormal;
|
|
218
|
+
const codexUsageLabel = statusStyle(
|
|
219
|
+
codexStyle,
|
|
220
|
+
state.codexUsageStatus ? `◉ ${safeStatusText(state.codexUsageStatus, 256)}` : "",
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const instructionFilesLabel =
|
|
224
|
+
state.configCounts.instructionFiles.total > 0
|
|
225
|
+
? statusStyle(config.colors.runtimePrefix, `※ ${state.configCounts.instructionFiles.total}`)
|
|
226
|
+
: "";
|
|
227
|
+
const agentsFilesLabel =
|
|
228
|
+
state.configCounts.instructionFiles.agentsMd > 0
|
|
229
|
+
? statusStyle(
|
|
230
|
+
config.colors.runtimePrefix,
|
|
231
|
+
String(state.configCounts.instructionFiles.agentsMd),
|
|
232
|
+
)
|
|
233
|
+
: "";
|
|
234
|
+
const claudeFilesLabel =
|
|
235
|
+
state.configCounts.instructionFiles.claudeMd > 0
|
|
236
|
+
? statusStyle(
|
|
237
|
+
config.colors.runtimePrefix,
|
|
238
|
+
String(state.configCounts.instructionFiles.claudeMd),
|
|
239
|
+
)
|
|
240
|
+
: "";
|
|
241
|
+
|
|
242
|
+
const skillCounts = state.skillCounts ?? { total: 0, active: 0 };
|
|
243
|
+
const skillsLabel = statusStyle(config.colors.extensionStatus, formatSkillCounts(skillCounts));
|
|
244
|
+
const activeSkillsLabel =
|
|
245
|
+
skillCounts.total > 0
|
|
246
|
+
? statusStyle(config.colors.extensionStatus, String(skillCounts.active))
|
|
247
|
+
: "";
|
|
248
|
+
|
|
249
|
+
const extensionsLabel =
|
|
250
|
+
state.configCounts.packages > 0
|
|
251
|
+
? statusStyle(config.colors.sessionDuration, `◈ ${state.configCounts.packages}`)
|
|
252
|
+
: "";
|
|
253
|
+
const configCountsLabel = [instructionFilesLabel, extensionsLabel].filter(Boolean).join(" ");
|
|
254
|
+
|
|
255
|
+
const mcpLabel = mcpStatus
|
|
256
|
+
? statusStyle(config.colors.gitStatus, `⊕ ${mcpStatus.connected}/${mcpStatus.total}`)
|
|
257
|
+
: "";
|
|
258
|
+
|
|
259
|
+
const telemetryStats = getTelemetryStats(state.telemetry);
|
|
260
|
+
const toolCountsText = Object.entries(telemetryStats.completedToolCounts)
|
|
261
|
+
.filter(([, count]) => count > 0)
|
|
262
|
+
.map(([tool, count]) => `${tool}${count > 1 ? ` × ${count}` : ""}`)
|
|
263
|
+
.join(" ");
|
|
264
|
+
const toolCountsLabel = statusStyle(config.colors.contextNormal, toolCountsText);
|
|
265
|
+
const runningToolsText = telemetryStats.recentRunningTools
|
|
266
|
+
.map((tool) => {
|
|
267
|
+
const target = tool.target ? `:${truncateToWidth(tool.target, 18, "…")}` : "";
|
|
268
|
+
return `↻ ${tool.name}${target} (${buildSessionDurationLabel(tool.startTime)})`;
|
|
269
|
+
})
|
|
270
|
+
.join(" ");
|
|
271
|
+
const runningToolsLabel = statusStyle(config.colors.contextWarning, runningToolsText);
|
|
272
|
+
const activeAgentsLabel =
|
|
273
|
+
telemetryStats.activeAgentRuns > 0
|
|
274
|
+
? statusStyle(config.colors.extensionStatus, `↻ agent × ${telemetryStats.activeAgentRuns}`)
|
|
275
|
+
: statusStyle(config.colors.muted, "agent idle");
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
projectCategoryLabel,
|
|
279
|
+
sessionCategoryLabel,
|
|
280
|
+
usageCategoryLabel,
|
|
281
|
+
sessionNameLabel,
|
|
282
|
+
modelValueLabel,
|
|
283
|
+
modelStatusLabel,
|
|
284
|
+
providerId,
|
|
285
|
+
modelId,
|
|
286
|
+
thinkingLabel,
|
|
287
|
+
turnLabel,
|
|
288
|
+
cacheReadLabel,
|
|
289
|
+
cacheWriteLabel,
|
|
290
|
+
cacheHitLabel,
|
|
291
|
+
cacheDetailsLabel,
|
|
292
|
+
codexUsageLabel,
|
|
293
|
+
instructionFilesLabel,
|
|
294
|
+
agentsFilesLabel,
|
|
295
|
+
claudeFilesLabel,
|
|
296
|
+
skillsLabel,
|
|
297
|
+
activeSkillsLabel,
|
|
298
|
+
extensionsLabel,
|
|
299
|
+
configCountsLabel,
|
|
300
|
+
mcpLabel,
|
|
301
|
+
toolCountsLabel,
|
|
302
|
+
runningToolsLabel,
|
|
303
|
+
activeAgentsLabel,
|
|
304
|
+
};
|
|
305
|
+
}
|