@zigai/pi-footer 0.1.6 → 0.1.8
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 +9 -2
- package/package.json +2 -2
- package/src/constants.ts +0 -2
- package/src/footer-component.ts +41 -0
- package/src/footer-rendering.ts +5 -5
- package/src/footer-transition.ts +189 -0
- package/src/index.ts +11 -89
- package/src/types.ts +11 -0
- package/src/worked-for-widget.ts +0 -43
package/README.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# Pi Footer
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@zigai/pi-footer)
|
|
4
|
+
[](https://www.npmjs.com/package/@zigai/pi-footer)
|
|
5
|
+
[](../../LICENSE)
|
|
6
|
+
|
|
3
7
|
This Pi extension replaces Pi's footer with a single compact plain-text status line.
|
|
4
8
|
|
|
5
9
|

|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
Footer contents:
|
|
8
12
|
|
|
9
13
|
- current working directory
|
|
10
14
|
- git branch
|
|
@@ -12,10 +16,13 @@ The footer keeps key session information visible without taking much space:
|
|
|
12
16
|
- thinking level
|
|
13
17
|
- MCP status
|
|
14
18
|
- context usage
|
|
15
|
-
- a short post-run summary with total agent time and output token speed
|
|
16
19
|
|
|
17
20
|
## Install
|
|
18
21
|
|
|
19
22
|
```sh
|
|
20
23
|
pi install npm:@zigai/pi-footer
|
|
21
24
|
```
|
|
25
|
+
|
|
26
|
+
## License
|
|
27
|
+
|
|
28
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zigai/pi-footer",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Pi package
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "Pi package that replaces Pi's footer with a compact status line.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"footer",
|
|
7
7
|
"pi",
|
package/src/constants.ts
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const FOOTER_COMPONENT_MARKER = Symbol.for("zigai.pi-footer.component");
|
|
2
|
+
export const FOOTER_COMPONENT_KIND = Symbol.for("zigai.pi-footer.component-kind");
|
|
3
|
+
|
|
4
|
+
export type FooterComponentKind = "live" | "bridge";
|
|
5
|
+
|
|
6
|
+
export type MarkedFooterComponent = {
|
|
7
|
+
[FOOTER_COMPONENT_MARKER]: true;
|
|
8
|
+
[FOOTER_COMPONENT_KIND]: FooterComponentKind;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function isObject(value: unknown): value is object {
|
|
12
|
+
return typeof value === "object" && value !== null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function markFooterComponent<T extends object>(
|
|
16
|
+
component: T,
|
|
17
|
+
kind: FooterComponentKind,
|
|
18
|
+
): T & MarkedFooterComponent {
|
|
19
|
+
Object.defineProperty(component, FOOTER_COMPONENT_MARKER, {
|
|
20
|
+
configurable: false,
|
|
21
|
+
enumerable: false,
|
|
22
|
+
value: true,
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(component, FOOTER_COMPONENT_KIND, {
|
|
25
|
+
configurable: true,
|
|
26
|
+
enumerable: false,
|
|
27
|
+
value: kind,
|
|
28
|
+
});
|
|
29
|
+
return component as T & MarkedFooterComponent;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getFooterComponentKind(value: unknown): FooterComponentKind | undefined {
|
|
33
|
+
if (!isObject(value)) return undefined;
|
|
34
|
+
|
|
35
|
+
const candidate = value as Partial<MarkedFooterComponent>;
|
|
36
|
+
if (candidate[FOOTER_COMPONENT_MARKER] !== true) return undefined;
|
|
37
|
+
|
|
38
|
+
const kind = candidate[FOOTER_COMPONENT_KIND];
|
|
39
|
+
if (kind === "live" || kind === "bridge") return kind;
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
package/src/footer-rendering.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
2
|
|
|
4
3
|
import {
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
} from "./constants.ts";
|
|
12
11
|
import type {
|
|
13
12
|
ContextUsage,
|
|
13
|
+
FooterContext,
|
|
14
14
|
FooterData,
|
|
15
15
|
FooterItem,
|
|
16
16
|
FooterKey,
|
|
@@ -128,7 +128,7 @@ function getContextText(usage: ContextUsage, fallbackWindow?: number): string {
|
|
|
128
128
|
return `${contextPercent.toFixed(1)}%/${formatTokens(contextWindow)}`;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
function getMcpText(ctx:
|
|
131
|
+
function getMcpText(ctx: FooterContext, footerData: FooterData): string | null {
|
|
132
132
|
const statuses = Array.from(footerData.getExtensionStatuses().values())
|
|
133
133
|
.map(sanitizeStatusText)
|
|
134
134
|
.filter((status) => status.length > 0);
|
|
@@ -136,7 +136,7 @@ function getMcpText(ctx: ExtensionContext, footerData: FooterData): string | nul
|
|
|
136
136
|
const mcpStatus = statuses.find((status) => /^MCP:/i.test(status));
|
|
137
137
|
if (mcpStatus !== undefined && mcpStatus.length > 0) return mcpStatus;
|
|
138
138
|
|
|
139
|
-
const serverCount =
|
|
139
|
+
const serverCount = ctx.mcpServers?.length;
|
|
140
140
|
if (typeof serverCount === "number") {
|
|
141
141
|
return `MCP: ${serverCount} servers`;
|
|
142
142
|
}
|
|
@@ -217,7 +217,7 @@ function renderPadding(width: number, variant: FooterVariant): string {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
function buildFooterItems(
|
|
220
|
-
ctx:
|
|
220
|
+
ctx: FooterContext,
|
|
221
221
|
footerData: FooterData,
|
|
222
222
|
thinkingLevel: string,
|
|
223
223
|
): Partial<Record<FooterKey, FooterItem>> {
|
|
@@ -278,7 +278,7 @@ function buildFooterItems(
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
export function createFooterComponent(
|
|
281
|
-
ctx:
|
|
281
|
+
ctx: FooterContext,
|
|
282
282
|
footerData: FooterData,
|
|
283
283
|
getThinkingLevel: () => string,
|
|
284
284
|
requestRender: () => void,
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InteractiveMode,
|
|
3
|
+
type ExtensionContext,
|
|
4
|
+
type SessionShutdownEvent,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { markFooterComponent, getFooterComponentKind } from "./footer-component.ts";
|
|
8
|
+
import { createFooterComponent } from "./footer-rendering.ts";
|
|
9
|
+
import type { ContextUsage, FooterContext, FooterData, FooterModel } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
const RESET_PATCH_MARKER = Symbol.for("zigai.pi-footer.reset-extension-ui-patch");
|
|
12
|
+
const TRANSITION_STATE_KEY = "__zigaiPiFooterTransitionState__";
|
|
13
|
+
const BRIDGE_FOOTER_TTL_MS = 15_000;
|
|
14
|
+
|
|
15
|
+
type FooterComponent = ReturnType<typeof createFooterComponent>;
|
|
16
|
+
type FooterFactory = (
|
|
17
|
+
tui: { requestRender(): void },
|
|
18
|
+
theme: unknown,
|
|
19
|
+
footerData: FooterData,
|
|
20
|
+
) => FooterComponent;
|
|
21
|
+
|
|
22
|
+
type FooterResetHost = {
|
|
23
|
+
customFooter?: unknown;
|
|
24
|
+
resetExtensionUI(): void;
|
|
25
|
+
setExtensionFooter(factory: FooterFactory | undefined): void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type PatchableInteractiveModePrototype = {
|
|
29
|
+
customFooter?: unknown;
|
|
30
|
+
resetExtensionUI?: (this: FooterResetHost) => void;
|
|
31
|
+
setExtensionFooter?: (this: FooterResetHost, factory: FooterFactory | undefined) => void;
|
|
32
|
+
[RESET_PATCH_MARKER]?: true;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type FooterSnapshot = {
|
|
36
|
+
context: FooterContext;
|
|
37
|
+
thinkingLevel: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type MaybeMcpContext = ExtensionContext & {
|
|
41
|
+
mcpServers?: unknown[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type FooterTransitionState = {
|
|
45
|
+
latestSnapshot?: FooterSnapshot;
|
|
46
|
+
pendingShutdownReason?: SessionShutdownEvent["reason"];
|
|
47
|
+
liveInstallGeneration: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type FooterTransitionGlobal = typeof globalThis & {
|
|
51
|
+
[TRANSITION_STATE_KEY]?: FooterTransitionState;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function getTransitionState(): FooterTransitionState {
|
|
55
|
+
const globalState = globalThis as FooterTransitionGlobal;
|
|
56
|
+
let state = globalState[TRANSITION_STATE_KEY];
|
|
57
|
+
if (state === undefined) {
|
|
58
|
+
state = { liveInstallGeneration: 0 };
|
|
59
|
+
globalState[TRANSITION_STATE_KEY] = state;
|
|
60
|
+
}
|
|
61
|
+
return state;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cloneContextUsage(usage: ContextUsage): ContextUsage {
|
|
65
|
+
if (usage === undefined) return undefined;
|
|
66
|
+
return { ...usage };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cloneModel(model: ExtensionContext["model"]): FooterModel | undefined {
|
|
70
|
+
if (model === undefined) return undefined;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
provider: model.provider,
|
|
74
|
+
id: model.id,
|
|
75
|
+
contextWindow: model.contextWindow,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function cloneMcpServers(ctx: ExtensionContext): unknown[] | undefined {
|
|
80
|
+
const servers = (ctx as MaybeMcpContext).mcpServers;
|
|
81
|
+
if (!Array.isArray(servers)) return undefined;
|
|
82
|
+
|
|
83
|
+
return Array.from<unknown>({ length: servers.length });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createFooterSnapshot(ctx: ExtensionContext, thinkingLevel: string): FooterSnapshot {
|
|
87
|
+
const usage = cloneContextUsage(ctx.getContextUsage());
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
context: {
|
|
91
|
+
cwd: ctx.cwd,
|
|
92
|
+
model: cloneModel(ctx.model),
|
|
93
|
+
mcpServers: cloneMcpServers(ctx),
|
|
94
|
+
getContextUsage() {
|
|
95
|
+
return usage;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
thinkingLevel,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function shouldBridgeFooter(kind: string | undefined, state: FooterTransitionState): boolean {
|
|
103
|
+
if (kind !== "live" && kind !== "bridge") return false;
|
|
104
|
+
if (state.latestSnapshot === undefined) return false;
|
|
105
|
+
if (state.pendingShutdownReason === undefined) return false;
|
|
106
|
+
if (state.pendingShutdownReason === "quit") return false;
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function installBridgeFooter(host: FooterResetHost, snapshot: FooterSnapshot): void {
|
|
111
|
+
const generationAtInstall = getTransitionState().liveInstallGeneration;
|
|
112
|
+
|
|
113
|
+
host.setExtensionFooter((tui, _theme, footerData) => {
|
|
114
|
+
const component = createFooterComponent(
|
|
115
|
+
snapshot.context,
|
|
116
|
+
footerData,
|
|
117
|
+
() => snapshot.thinkingLevel,
|
|
118
|
+
() => tui.requestRender(),
|
|
119
|
+
);
|
|
120
|
+
return markFooterComponent(component, "bridge");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const timeout = setTimeout(() => {
|
|
124
|
+
const state = getTransitionState();
|
|
125
|
+
if (state.liveInstallGeneration !== generationAtInstall) return;
|
|
126
|
+
if (getFooterComponentKind(host.customFooter) !== "bridge") return;
|
|
127
|
+
host.setExtensionFooter(undefined);
|
|
128
|
+
}, BRIDGE_FOOTER_TTL_MS);
|
|
129
|
+
timeout.unref?.();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function patchFooterReset(): void {
|
|
133
|
+
// Pi resets extension-owned UI between session replacements before the
|
|
134
|
+
// replacement session has emitted session_start. Reinstall a snapshot-backed
|
|
135
|
+
// bridge footer in that same reset call so the built-in footer never paints
|
|
136
|
+
// during the handoff.
|
|
137
|
+
const prototype = InteractiveMode.prototype as unknown as PatchableInteractiveModePrototype;
|
|
138
|
+
if (prototype[RESET_PATCH_MARKER] === true) return;
|
|
139
|
+
|
|
140
|
+
const originalReset = prototype.resetExtensionUI;
|
|
141
|
+
const setExtensionFooter = prototype.setExtensionFooter;
|
|
142
|
+
if (typeof originalReset !== "function") return;
|
|
143
|
+
if (typeof setExtensionFooter !== "function") return;
|
|
144
|
+
|
|
145
|
+
prototype.resetExtensionUI = function patchedFooterReset(this: FooterResetHost): void {
|
|
146
|
+
const footerKind = getFooterComponentKind(this.customFooter);
|
|
147
|
+
const state = getTransitionState();
|
|
148
|
+
const snapshot = state.latestSnapshot;
|
|
149
|
+
const bridgeFooter = shouldBridgeFooter(footerKind, state);
|
|
150
|
+
|
|
151
|
+
originalReset.call(this);
|
|
152
|
+
state.pendingShutdownReason = undefined;
|
|
153
|
+
|
|
154
|
+
if (!bridgeFooter || snapshot === undefined) return;
|
|
155
|
+
installBridgeFooter(this, snapshot);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
prototype[RESET_PATCH_MARKER] = true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function installLiveFooter(ctx: ExtensionContext, getThinkingLevel: () => string): void {
|
|
162
|
+
ctx.ui.setFooter((tui, _theme, footerData) => {
|
|
163
|
+
const component = createFooterComponent(ctx, footerData, getThinkingLevel, () =>
|
|
164
|
+
tui.requestRender(),
|
|
165
|
+
);
|
|
166
|
+
return markFooterComponent(component, "live");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const state = getTransitionState();
|
|
170
|
+
state.latestSnapshot = createFooterSnapshot(ctx, getThinkingLevel());
|
|
171
|
+
state.pendingShutdownReason = undefined;
|
|
172
|
+
state.liveInstallGeneration += 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function rememberFooterForTransition(
|
|
176
|
+
ctx: ExtensionContext,
|
|
177
|
+
reason: SessionShutdownEvent["reason"],
|
|
178
|
+
thinkingLevel: string,
|
|
179
|
+
): void {
|
|
180
|
+
const state = getTransitionState();
|
|
181
|
+
state.pendingShutdownReason = reason;
|
|
182
|
+
|
|
183
|
+
if (reason === "quit") {
|
|
184
|
+
state.latestSnapshot = undefined;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
state.latestSnapshot = createFooterSnapshot(ctx, thinkingLevel);
|
|
189
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,103 +1,25 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
installLiveFooter,
|
|
5
|
+
patchFooterReset,
|
|
6
|
+
rememberFooterForTransition,
|
|
7
|
+
} from "./footer-transition.ts";
|
|
5
8
|
|
|
6
9
|
export default function uiEnhancements(pi: ExtensionAPI) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
let estimatedStreamedTokens = 0;
|
|
11
|
-
let totalOutputTokens = 0;
|
|
12
|
-
let totalStreamMs = 0;
|
|
10
|
+
patchFooterReset();
|
|
11
|
+
|
|
12
|
+
const getThinkingLevel = () => pi.getThinkingLevel();
|
|
13
13
|
|
|
14
14
|
const installFooter = (ctx: ExtensionContext) => {
|
|
15
|
-
ctx
|
|
16
|
-
createFooterComponent(
|
|
17
|
-
ctx,
|
|
18
|
-
footerData,
|
|
19
|
-
() => pi.getThinkingLevel(),
|
|
20
|
-
() => tui.requestRender(),
|
|
21
|
-
),
|
|
22
|
-
);
|
|
15
|
+
installLiveFooter(ctx, getThinkingLevel);
|
|
23
16
|
};
|
|
24
17
|
|
|
25
18
|
pi.on("session_start", async (_event, ctx) => {
|
|
26
19
|
installFooter(ctx);
|
|
27
|
-
setWorkedForWidget(ctx, undefined);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
pi.on("agent_start", async (_event, ctx) => {
|
|
31
|
-
agentStartedAt = Date.now();
|
|
32
|
-
messageStart = undefined;
|
|
33
|
-
streamStart = undefined;
|
|
34
|
-
estimatedStreamedTokens = 0;
|
|
35
|
-
totalOutputTokens = 0;
|
|
36
|
-
totalStreamMs = 0;
|
|
37
|
-
setWorkedForWidget(ctx, undefined);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
pi.on("message_start", async (event) => {
|
|
41
|
-
if (event.message.role !== "assistant") return;
|
|
42
|
-
messageStart = Date.now();
|
|
43
|
-
streamStart = undefined;
|
|
44
|
-
estimatedStreamedTokens = 0;
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
pi.on("message_update", async (event) => {
|
|
48
|
-
if (event.message.role !== "assistant") return;
|
|
49
|
-
|
|
50
|
-
const streamEvent = event.assistantMessageEvent;
|
|
51
|
-
const isOutputDelta =
|
|
52
|
-
streamEvent.type === "text_delta" ||
|
|
53
|
-
streamEvent.type === "thinking_delta" ||
|
|
54
|
-
streamEvent.type === "toolcall_delta";
|
|
55
|
-
if (!isOutputDelta) return;
|
|
56
|
-
|
|
57
|
-
streamStart ??= Date.now();
|
|
58
|
-
estimatedStreamedTokens += Math.max(0, streamEvent.delta.length / 4);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
pi.on("message_end", async (event) => {
|
|
62
|
-
if (event.message.role !== "assistant") return;
|
|
63
|
-
|
|
64
|
-
const outputTokens = event.message.usage.output;
|
|
65
|
-
const timingStart = streamStart ?? messageStart;
|
|
66
|
-
if (timingStart === undefined || outputTokens <= 0) {
|
|
67
|
-
messageStart = undefined;
|
|
68
|
-
streamStart = undefined;
|
|
69
|
-
estimatedStreamedTokens = 0;
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
totalOutputTokens += outputTokens;
|
|
74
|
-
totalStreamMs += Math.max(0, Date.now() - timingStart);
|
|
75
|
-
|
|
76
|
-
messageStart = undefined;
|
|
77
|
-
streamStart = undefined;
|
|
78
|
-
estimatedStreamedTokens = 0;
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
pi.on("agent_end", async (_event, ctx) => {
|
|
82
|
-
if (agentStartedAt === undefined) return;
|
|
83
|
-
const duration = Date.now() - agentStartedAt;
|
|
84
|
-
const elapsedSeconds = totalStreamMs / 1000;
|
|
85
|
-
let tokensPerSecond: number | undefined;
|
|
86
|
-
if (totalOutputTokens > 0 && elapsedSeconds > 0) {
|
|
87
|
-
tokensPerSecond = Math.round(totalOutputTokens / elapsedSeconds);
|
|
88
|
-
}
|
|
89
|
-
agentStartedAt = undefined;
|
|
90
|
-
setWorkedForWidget(ctx, formatDuration(duration), tokensPerSecond);
|
|
91
20
|
});
|
|
92
21
|
|
|
93
|
-
pi.on("session_shutdown", async (
|
|
94
|
-
|
|
95
|
-
messageStart = undefined;
|
|
96
|
-
streamStart = undefined;
|
|
97
|
-
estimatedStreamedTokens = 0;
|
|
98
|
-
totalOutputTokens = 0;
|
|
99
|
-
totalStreamMs = 0;
|
|
100
|
-
ctx.ui.setFooter(undefined);
|
|
101
|
-
setWorkedForWidget(ctx, undefined);
|
|
22
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
23
|
+
rememberFooterForTransition(ctx, event.reason, getThinkingLevel());
|
|
102
24
|
});
|
|
103
25
|
}
|
package/src/types.ts
CHANGED
|
@@ -32,6 +32,17 @@ export type FooterKey = "path" | "branch" | "provider" | "model" | "thinking" |
|
|
|
32
32
|
export type Rgb = [number, number, number];
|
|
33
33
|
export type SegmentColors = { bg: string; fg: string };
|
|
34
34
|
export type ContextUsage = ReturnType<ExtensionContext["getContextUsage"]>;
|
|
35
|
+
export type FooterModel = {
|
|
36
|
+
provider: string;
|
|
37
|
+
id: string;
|
|
38
|
+
contextWindow?: number;
|
|
39
|
+
};
|
|
40
|
+
export type FooterContext = {
|
|
41
|
+
cwd: string;
|
|
42
|
+
model?: FooterModel;
|
|
43
|
+
mcpServers?: unknown[];
|
|
44
|
+
getContextUsage(): ContextUsage;
|
|
45
|
+
};
|
|
35
46
|
|
|
36
47
|
export type FooterData = {
|
|
37
48
|
getGitBranch(): string | null;
|
package/src/worked-for-widget.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
|
|
4
|
-
import { WIDGET_KEY } from "./constants.ts";
|
|
5
|
-
|
|
6
|
-
export function formatDuration(ms: number): string {
|
|
7
|
-
const wholeSeconds = Math.max(0, Math.round(ms / 1000));
|
|
8
|
-
if (wholeSeconds < 60) return `${wholeSeconds}s`;
|
|
9
|
-
|
|
10
|
-
const minutes = Math.floor(wholeSeconds / 60);
|
|
11
|
-
const remainingSeconds = wholeSeconds % 60;
|
|
12
|
-
if (minutes < 60) return `${minutes}m ${remainingSeconds.toString().padStart(2, "0")}s`;
|
|
13
|
-
|
|
14
|
-
const hours = Math.floor(minutes / 60);
|
|
15
|
-
const remainingMinutes = minutes % 60;
|
|
16
|
-
return `${hours}h ${remainingMinutes.toString().padStart(2, "0")}m`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function setWorkedForWidget(
|
|
20
|
-
ctx: ExtensionContext,
|
|
21
|
-
workedForText?: string,
|
|
22
|
-
tokensPerSecond?: number,
|
|
23
|
-
): void {
|
|
24
|
-
if (ctx.hasUI !== true) return;
|
|
25
|
-
|
|
26
|
-
if (workedForText === undefined || workedForText.length === 0) {
|
|
27
|
-
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
ctx.ui.setWidget(WIDGET_KEY, (_tui, theme) => ({
|
|
32
|
-
render(width: number): string[] {
|
|
33
|
-
if (width <= 0) return [""];
|
|
34
|
-
let text = `Worked for ${workedForText}.`;
|
|
35
|
-
if (tokensPerSecond !== undefined && tokensPerSecond > 0) {
|
|
36
|
-
text = `${text} [${tokensPerSecond} tok/s]`;
|
|
37
|
-
}
|
|
38
|
-
const truncated = truncateToWidth(text, Math.max(0, width - 1), "");
|
|
39
|
-
return [theme.fg("dim", ` ${truncated}`)];
|
|
40
|
-
},
|
|
41
|
-
invalidate() {},
|
|
42
|
-
}));
|
|
43
|
-
}
|