@hheei/pi-status 0.1.2 → 0.2.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 +32 -1
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +8 -1
- package/dist/footer.d.ts +63 -0
- package/dist/footer.js +185 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
1
|
# @hheei/pi-status
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
High-density response telemetry and compact two-line footer extension for Pi. Compatible with Pi `>=0.99.0` and `1.0.0`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pi install npm:@hheei/pi-status
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The package mounts an ext-core response status tracker and replaces Pi's default TUI footer with an informative, compact two-line layout.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Compact Two-Line Footer**: Maximizes vertical editor space while presenting critical session context at a glance.
|
|
14
|
+
- **Top Row (Session & Worktree)**:
|
|
15
|
+
- Current working directory with `~` home abbreviation and path fitting.
|
|
16
|
+
- Active Git branch name and dirty status indicators (`*`).
|
|
17
|
+
- Active session stats and timing.
|
|
18
|
+
- **Bottom Row (Model & Telemetry)**:
|
|
19
|
+
- Full model identifier with provider prefix (e.g. `anthropic/claude-3.5-sonnet`, `cx/gpt-6.1-sol`).
|
|
20
|
+
- Thinking level indicator when active (e.g. `(high)`, `(low)`).
|
|
21
|
+
- Routing indicator when a virtual model resolves to a distinct physical model (`virtual -> physical`).
|
|
22
|
+
- Context window visualization gauge with percentage and token counts (e.g. `[████░░░░░░] 42% (84k/200k)`), colored dynamically as limits approach.
|
|
23
|
+
- Session token usage and cumulative cost tracking.
|
|
24
|
+
- **Lifecycle Cleanliness**: Gracefully unmounts and restores original UI footers on session disposal or reload.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Add to your Pi configuration or install directly via npm:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pi install npm:@hheei/pi-status
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The extension entrypoint is declared under `dist/extension.js` and automatically activates in TUI mode.
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
/** Mounts ext-core response telemetry
|
|
2
|
+
/** Mounts ext-core response telemetry and compact two-line footer. */
|
|
3
3
|
export default function piStatusExtension(pi: ExtensionAPI): void;
|
package/dist/extension.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createResponseStatusFeature, registerExtensionLifecycle } from "@hheei/pi-ext-core";
|
|
2
|
-
|
|
2
|
+
import { CompactFooterComponent } from "./footer.js";
|
|
3
|
+
/** Mounts ext-core response telemetry and compact two-line footer. */
|
|
3
4
|
export default function piStatusExtension(pi) {
|
|
4
5
|
const status = createResponseStatusFeature(pi);
|
|
5
6
|
registerExtensionLifecycle(pi, {
|
|
@@ -8,6 +9,12 @@ export default function piStatusExtension(pi) {
|
|
|
8
9
|
status.start(extension);
|
|
9
10
|
const sessionId = extension.sessionManager.getSessionId();
|
|
10
11
|
resources.add("response-status", () => status.dispose(sessionId));
|
|
12
|
+
if (extension.mode === "tui") {
|
|
13
|
+
extension.ui.setFooter((tui, theme, footerData) => new CompactFooterComponent(extension, tui, theme, footerData));
|
|
14
|
+
resources.add("footer", () => {
|
|
15
|
+
extension.ui.setFooter(undefined);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
11
18
|
},
|
|
12
19
|
});
|
|
13
20
|
}
|
package/dist/footer.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import { type ThemeLike } from "@hheei/pi-ext-core";
|
|
4
|
+
type FooterFactory = NonNullable<Parameters<ExtensionContext["ui"]["setFooter"]>[0]>;
|
|
5
|
+
export type ReadonlyFooterDataProvider = Parameters<FooterFactory>[2];
|
|
6
|
+
export type FooterTheme = Parameters<FooterFactory>[1];
|
|
7
|
+
/** Formats cwd replacing HOME with ~ */
|
|
8
|
+
export declare function formatCwdForFooter(cwd: string, home: string | undefined): string;
|
|
9
|
+
export interface RoutedModelInfo {
|
|
10
|
+
readonly model: {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly provider?: string;
|
|
13
|
+
};
|
|
14
|
+
readonly thinkingLevel?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Formats model identifier with provider prefix and thinking level:
|
|
18
|
+
* - When theme is provided:
|
|
19
|
+
* [provider/ (dim)][modelId (text)][(thinking) ((dim) + (accent) + (dim))]
|
|
20
|
+
* - When theme is omitted:
|
|
21
|
+
* gm/gemini-3.8-flash(high) or gm/gemini-3.8-flash
|
|
22
|
+
* - When routedModel is provided and resolves to a distinct model:
|
|
23
|
+
* [virtual-model] -> [physical-model]
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatFooterModel(model: {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly provider?: string;
|
|
28
|
+
} | undefined, thinkingLevel: string | undefined, theme?: ThemeLike, routedModel?: RoutedModelInfo | undefined): string;
|
|
29
|
+
/**
|
|
30
|
+
* Formats context usage ratio: N%/100K with warning/error colors:
|
|
31
|
+
* - <= 70%: [N% (muted)][/100K (dim)]
|
|
32
|
+
* - 70% ~ 90%: warning
|
|
33
|
+
* - > 90%: error
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatFooterContext(contextUsage: {
|
|
36
|
+
readonly percent?: number | null;
|
|
37
|
+
readonly contextWindow?: number;
|
|
38
|
+
} | undefined, defaultWindow: number | undefined, theme: ThemeLike): string;
|
|
39
|
+
/**
|
|
40
|
+
* Formats cwd and optional git branch for footer display:
|
|
41
|
+
* [path (muted)] [( (dim)][branch (accent)][) (dim)]
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatFooterPath(cwd: string, branch: string | null | undefined, theme: ThemeLike, home?: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Arranges a two-column row with left content aligned left and right content aligned right.
|
|
46
|
+
* When constrained, shrinks/truncates or drops the right column to prioritize the left column.
|
|
47
|
+
*/
|
|
48
|
+
export declare function layoutTwoColumnRow(left: string, right: string | undefined, width: number, minGap?: number): string;
|
|
49
|
+
/**
|
|
50
|
+
* Custom 2-line compact footer:
|
|
51
|
+
* Line 1: path (git) title
|
|
52
|
+
* Line 2: provider/model(level) · N%/100K todo status
|
|
53
|
+
*/
|
|
54
|
+
export declare class CompactFooterComponent implements Component {
|
|
55
|
+
#private;
|
|
56
|
+
constructor(extension: ExtensionContext, tui: {
|
|
57
|
+
requestRender(): void;
|
|
58
|
+
}, theme: FooterTheme, footerData: ReadonlyFooterDataProvider);
|
|
59
|
+
invalidate(): void;
|
|
60
|
+
dispose(): void;
|
|
61
|
+
render(width: number): string[];
|
|
62
|
+
}
|
|
63
|
+
export {};
|
package/dist/footer.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import { fitRow, formatCompactNumber } from "@hheei/pi-ext-core";
|
|
5
|
+
/** Formats cwd replacing HOME with ~ */
|
|
6
|
+
export function formatCwdForFooter(cwd, home) {
|
|
7
|
+
if (!home)
|
|
8
|
+
return cwd;
|
|
9
|
+
const resolvedCwd = resolve(cwd);
|
|
10
|
+
const resolvedHome = resolve(home);
|
|
11
|
+
const relativeToHome = relative(resolvedHome, resolvedCwd);
|
|
12
|
+
const isInsideHome = relativeToHome === "" ||
|
|
13
|
+
(relativeToHome !== ".." &&
|
|
14
|
+
!relativeToHome.startsWith(`..${sep}`) &&
|
|
15
|
+
!isAbsolute(relativeToHome));
|
|
16
|
+
if (!isInsideHome)
|
|
17
|
+
return cwd;
|
|
18
|
+
return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Formats model identifier with provider prefix and thinking level:
|
|
22
|
+
* - When theme is provided:
|
|
23
|
+
* [provider/ (dim)][modelId (text)][(thinking) ((dim) + (accent) + (dim))]
|
|
24
|
+
* - When theme is omitted:
|
|
25
|
+
* gm/gemini-3.8-flash(high) or gm/gemini-3.8-flash
|
|
26
|
+
* - When routedModel is provided and resolves to a distinct model:
|
|
27
|
+
* [virtual-model] -> [physical-model]
|
|
28
|
+
*/
|
|
29
|
+
export function formatFooterModel(model, thinkingLevel, theme, routedModel) {
|
|
30
|
+
if (model === undefined)
|
|
31
|
+
return theme !== undefined ? theme.fg("dim", "no-model") : "no-model";
|
|
32
|
+
const hasProvider = model.provider !== undefined && model.provider.trim() !== "";
|
|
33
|
+
const providerText = hasProvider ? `${model.provider}/` : "";
|
|
34
|
+
const hasThinking = thinkingLevel !== undefined && thinkingLevel !== "off" && thinkingLevel.trim() !== "";
|
|
35
|
+
const baseText = (() => {
|
|
36
|
+
if (theme === undefined) {
|
|
37
|
+
const levelSuffix = hasThinking ? `(${thinkingLevel})` : "";
|
|
38
|
+
return `${providerText}${model.id}${levelSuffix}`;
|
|
39
|
+
}
|
|
40
|
+
const providerPart = hasProvider ? theme.fg("dim", providerText) : "";
|
|
41
|
+
const modelPart = theme.fg("text", model.id);
|
|
42
|
+
const thinkingPart = hasThinking
|
|
43
|
+
? `${theme.fg("dim", "(")}${theme.fg("accent", thinkingLevel)}${theme.fg("dim", ")")}`
|
|
44
|
+
: "";
|
|
45
|
+
return `${providerPart}${modelPart}${thinkingPart}`;
|
|
46
|
+
})();
|
|
47
|
+
if (!routedModel ||
|
|
48
|
+
(routedModel.model.id === model.id && routedModel.model.provider === model.provider)) {
|
|
49
|
+
return baseText;
|
|
50
|
+
}
|
|
51
|
+
const routedHasProvider = routedModel.model.provider !== undefined &&
|
|
52
|
+
routedModel.model.provider.trim() !== "" &&
|
|
53
|
+
routedModel.model.provider !== model.provider;
|
|
54
|
+
const routedProviderText = routedHasProvider ? `${routedModel.model.provider}/` : "";
|
|
55
|
+
const routedThinkingLevel = routedModel.thinkingLevel;
|
|
56
|
+
const routedHasThinking = routedThinkingLevel !== undefined &&
|
|
57
|
+
routedThinkingLevel !== "off" &&
|
|
58
|
+
routedThinkingLevel.trim() !== "";
|
|
59
|
+
if (theme === undefined) {
|
|
60
|
+
const routedLevelSuffix = routedHasThinking ? `(${routedThinkingLevel})` : "";
|
|
61
|
+
return `${baseText} → ${routedProviderText}${routedModel.model.id}${routedLevelSuffix}`;
|
|
62
|
+
}
|
|
63
|
+
const arrowPart = theme.fg("dim", "→");
|
|
64
|
+
const routedProviderPart = routedHasProvider ? theme.fg("dim", routedProviderText) : "";
|
|
65
|
+
const routedModelPart = theme.fg("text", routedModel.model.id);
|
|
66
|
+
const routedThinkingPart = routedHasThinking
|
|
67
|
+
? `${theme.fg("dim", "(")}${theme.fg("accent", routedThinkingLevel)}${theme.fg("dim", ")")}`
|
|
68
|
+
: "";
|
|
69
|
+
return `${baseText} ${arrowPart} ${routedProviderPart}${routedModelPart}${routedThinkingPart}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Formats context usage ratio: N%/100K with warning/error colors:
|
|
73
|
+
* - <= 70%: [N% (muted)][/100K (dim)]
|
|
74
|
+
* - 70% ~ 90%: warning
|
|
75
|
+
* - > 90%: error
|
|
76
|
+
*/
|
|
77
|
+
export function formatFooterContext(contextUsage, defaultWindow, theme) {
|
|
78
|
+
const window = contextUsage?.contextWindow ?? defaultWindow ?? 0;
|
|
79
|
+
const windowStr = window > 0 ? formatCompactNumber(window) : "0";
|
|
80
|
+
const percentValue = contextUsage?.percent;
|
|
81
|
+
const percentStr = percentValue !== null && percentValue !== undefined ? `${Math.round(percentValue)}%` : "?%";
|
|
82
|
+
if (percentValue !== null && percentValue !== undefined) {
|
|
83
|
+
if (percentValue > 90)
|
|
84
|
+
return theme.fg("error", `${percentStr}/${windowStr}`);
|
|
85
|
+
if (percentValue > 70)
|
|
86
|
+
return theme.fg("warning", `${percentStr}/${windowStr}`);
|
|
87
|
+
}
|
|
88
|
+
return `${theme.fg("muted", percentStr)}${theme.fg("dim", `/${windowStr}`)}`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Formats cwd and optional git branch for footer display:
|
|
92
|
+
* [path (muted)] [( (dim)][branch (accent)][) (dim)]
|
|
93
|
+
*/
|
|
94
|
+
export function formatFooterPath(cwd, branch, theme, home) {
|
|
95
|
+
const formattedCwd = formatCwdForFooter(cwd, home);
|
|
96
|
+
const cwdPart = theme.fg("muted", formattedCwd);
|
|
97
|
+
if (branch === null || branch === undefined || branch.trim() === "") {
|
|
98
|
+
return cwdPart;
|
|
99
|
+
}
|
|
100
|
+
const branchPart = `${theme.fg("dim", "(")}${theme.fg("accent", branch)}${theme.fg("dim", ")")}`;
|
|
101
|
+
return `${cwdPart} ${branchPart}`;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Arranges a two-column row with left content aligned left and right content aligned right.
|
|
105
|
+
* When constrained, shrinks/truncates or drops the right column to prioritize the left column.
|
|
106
|
+
*/
|
|
107
|
+
export function layoutTwoColumnRow(left, right, width, minGap = 2) {
|
|
108
|
+
const leftWidth = visibleWidth(left);
|
|
109
|
+
if (leftWidth >= width) {
|
|
110
|
+
return fitRow(left, width, "");
|
|
111
|
+
}
|
|
112
|
+
if (right === undefined || right.trim() === "") {
|
|
113
|
+
return left;
|
|
114
|
+
}
|
|
115
|
+
const rightWidth = visibleWidth(right);
|
|
116
|
+
const totalNeeded = leftWidth + minGap + rightWidth;
|
|
117
|
+
if (totalNeeded <= width) {
|
|
118
|
+
const spaces = " ".repeat(width - leftWidth - rightWidth);
|
|
119
|
+
return `${left}${spaces}${right}`;
|
|
120
|
+
}
|
|
121
|
+
// Insufficient space: truncate right side with ellipsis if at least 4 cols available
|
|
122
|
+
const availableForRight = width - leftWidth - minGap;
|
|
123
|
+
if (availableForRight >= 4) {
|
|
124
|
+
const truncatedRight = fitRow(right, availableForRight, "…");
|
|
125
|
+
const truncatedWidth = visibleWidth(truncatedRight);
|
|
126
|
+
const spaces = " ".repeat(Math.max(minGap, width - leftWidth - truncatedWidth));
|
|
127
|
+
return `${left}${spaces}${truncatedRight}`;
|
|
128
|
+
}
|
|
129
|
+
// Extremely narrow: drop right side entirely to preserve full left side
|
|
130
|
+
return left;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Custom 2-line compact footer:
|
|
134
|
+
* Line 1: path (git) title
|
|
135
|
+
* Line 2: provider/model(level) · N%/100K todo status
|
|
136
|
+
*/
|
|
137
|
+
export class CompactFooterComponent {
|
|
138
|
+
#extension;
|
|
139
|
+
#footerData;
|
|
140
|
+
#theme;
|
|
141
|
+
#unsubscribeBranch;
|
|
142
|
+
constructor(extension, tui, theme, footerData) {
|
|
143
|
+
this.#extension = extension;
|
|
144
|
+
this.#footerData = footerData;
|
|
145
|
+
this.#theme = theme;
|
|
146
|
+
this.#unsubscribeBranch = footerData.onBranchChange(() => {
|
|
147
|
+
tui.requestRender();
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
invalidate() { }
|
|
151
|
+
dispose() {
|
|
152
|
+
this.#unsubscribeBranch?.();
|
|
153
|
+
}
|
|
154
|
+
render(width) {
|
|
155
|
+
const cwd = this.#extension.sessionManager.getCwd();
|
|
156
|
+
const branch = this.#footerData.getGitBranch();
|
|
157
|
+
const line1Left = formatFooterPath(cwd, branch, this.#theme, homedir());
|
|
158
|
+
const extensionStatuses = this.#footerData.getExtensionStatuses();
|
|
159
|
+
const autoTitleStatus = extensionStatuses.get("auto-title");
|
|
160
|
+
const sessionName = this.#extension.sessionManager.getSessionName();
|
|
161
|
+
const line1Right = autoTitleStatus !== undefined && autoTitleStatus.trim() !== ""
|
|
162
|
+
? autoTitleStatus
|
|
163
|
+
: sessionName !== undefined && sessionName.trim() !== ""
|
|
164
|
+
? this.#theme.fg("muted", sessionName)
|
|
165
|
+
: undefined;
|
|
166
|
+
const line1 = layoutTwoColumnRow(line1Left, line1Right, width);
|
|
167
|
+
// Line 2 left: model + thinking level · context%
|
|
168
|
+
const routed = this.#extension.routedModel;
|
|
169
|
+
const modelPart = formatFooterModel(this.#extension.model, this.#extension.thinkingLevel, this.#theme, routed);
|
|
170
|
+
const contextPart = formatFooterContext(this.#extension.getContextUsage(), this.#extension.model?.contextWindow, this.#theme);
|
|
171
|
+
const line2Left = `${modelPart} ${this.#theme.fg("dim", "·")} ${contextPart}`;
|
|
172
|
+
// Line 2 right: todo status or extension statuses (excluding auto-title which belongs to line 1)
|
|
173
|
+
let todoStatus = extensionStatuses.get("pi-ext-tools:todo");
|
|
174
|
+
if (todoStatus === undefined && extensionStatuses.size > 0) {
|
|
175
|
+
for (const [key, value] of extensionStatuses) {
|
|
176
|
+
if (key !== "auto-title" && value.trim() !== "") {
|
|
177
|
+
todoStatus = value;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const line2 = layoutTwoColumnRow(line2Left, todoStatus, width);
|
|
183
|
+
return [line1, line2];
|
|
184
|
+
}
|
|
185
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hheei/pi-status",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Response telemetry for Pi sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,13 +35,15 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@hheei/pi-ext-core": "^0.1
|
|
38
|
+
"@hheei/pi-ext-core": "^0.2.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
42
|
+
"@earendil-works/pi-tui": ">=1.0.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
45
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
46
|
+
"@earendil-works/pi-tui": "1.0.0"
|
|
45
47
|
},
|
|
46
48
|
"scripts": {
|
|
47
49
|
"build": "node -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json"
|