pi-mono-all 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/CHANGELOG.md +13 -0
- package/LICENCE.md +7 -0
- package/node_modules/pi-common/package.json +22 -0
- package/node_modules/pi-common/src/auth-config.ts +290 -0
- package/node_modules/pi-common/src/auth.ts +63 -0
- package/node_modules/pi-common/src/cache.ts +60 -0
- package/node_modules/pi-common/src/errors.ts +47 -0
- package/node_modules/pi-common/src/http-client.ts +118 -0
- package/node_modules/pi-common/src/index.ts +7 -0
- package/node_modules/pi-common/src/rate-limiter.ts +32 -0
- package/node_modules/pi-common/src/tool-result.ts +27 -0
- package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
- package/node_modules/pi-mono-ask-user-question/README.md +226 -0
- package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
- package/node_modules/pi-mono-ask-user-question/package.json +29 -0
- package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
- package/node_modules/pi-mono-auto-fix/README.md +77 -0
- package/node_modules/pi-mono-auto-fix/index.ts +488 -0
- package/node_modules/pi-mono-auto-fix/package.json +23 -0
- package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-btw/README.md +24 -0
- package/node_modules/pi-mono-btw/index.ts +499 -0
- package/node_modules/pi-mono-btw/package.json +29 -0
- package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-clear/README.md +40 -0
- package/node_modules/pi-mono-clear/index.ts +45 -0
- package/node_modules/pi-mono-clear/package.json +29 -0
- package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
- package/node_modules/pi-mono-context/README.md +74 -0
- package/node_modules/pi-mono-context/index.ts +641 -0
- package/node_modules/pi-mono-context/package.json +29 -0
- package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
- package/node_modules/pi-mono-context-guard/README.md +81 -0
- package/node_modules/pi-mono-context-guard/index.ts +212 -0
- package/node_modules/pi-mono-context-guard/package.json +23 -0
- package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
- package/node_modules/pi-mono-figma/README.md +236 -0
- package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
- package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
- package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
- package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
- package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
- package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
- package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
- package/node_modules/pi-mono-figma/index.ts +6 -0
- package/node_modules/pi-mono-figma/package.json +33 -0
- package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
- package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
- package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
- package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
- package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
- package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
- package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
- package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
- package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
- package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
- package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
- package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
- package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
- package/node_modules/pi-mono-linear/README.md +159 -0
- package/node_modules/pi-mono-linear/index.ts +6 -0
- package/node_modules/pi-mono-linear/package.json +30 -0
- package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
- package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
- package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
- package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
- package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
- package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
- package/node_modules/pi-mono-loop/README.md +54 -0
- package/node_modules/pi-mono-loop/index.ts +291 -0
- package/node_modules/pi-mono-loop/package.json +26 -0
- package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
- package/node_modules/pi-mono-multi-edit/README.md +244 -0
- package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
- package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
- package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
- package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
- package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
- package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
- package/node_modules/pi-mono-multi-edit/index.ts +266 -0
- package/node_modules/pi-mono-multi-edit/package.json +37 -0
- package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
- package/node_modules/pi-mono-multi-edit/types.ts +53 -0
- package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
- package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
- package/node_modules/pi-mono-review/README.md +30 -0
- package/node_modules/pi-mono-review/common.ts +930 -0
- package/node_modules/pi-mono-review/index.ts +8 -0
- package/node_modules/pi-mono-review/package.json +29 -0
- package/node_modules/pi-mono-review/review-tui.ts +194 -0
- package/node_modules/pi-mono-review/review.ts +119 -0
- package/node_modules/pi-mono-review/reviewer.ts +339 -0
- package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
- package/node_modules/pi-mono-sentinel/README.md +87 -0
- package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
- package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
- package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
- package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
- package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
- package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
- package/node_modules/pi-mono-sentinel/index.ts +43 -0
- package/node_modules/pi-mono-sentinel/package.json +26 -0
- package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
- package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
- package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
- package/node_modules/pi-mono-sentinel/session.ts +95 -0
- package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
- package/node_modules/pi-mono-sentinel/types.ts +39 -0
- package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
- package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
- package/node_modules/pi-mono-simplify/README.md +56 -0
- package/node_modules/pi-mono-simplify/index.ts +78 -0
- package/node_modules/pi-mono-simplify/package.json +29 -0
- package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-status-line/README.md +96 -0
- package/node_modules/pi-mono-status-line/basic.ts +89 -0
- package/node_modules/pi-mono-status-line/expert.ts +689 -0
- package/node_modules/pi-mono-status-line/index.ts +54 -0
- package/node_modules/pi-mono-status-line/package.json +29 -0
- package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
- package/node_modules/pi-mono-team-mode/README.md +246 -0
- package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
- package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
- package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
- package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
- package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
- package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
- package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
- package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
- package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
- package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
- package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
- package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
- package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
- package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
- package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
- package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
- package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
- package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
- package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
- package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
- package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
- package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
- package/node_modules/pi-mono-team-mode/index.ts +825 -0
- package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
- package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
- package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
- package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
- package/node_modules/pi-mono-team-mode/package.json +33 -0
- package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
- package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
- package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
- package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
- package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
- package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
- package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
- package/package.json +76 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expert Status Line
|
|
3
|
+
*
|
|
4
|
+
* Rich footer with visual context gauge, enhanced git status,
|
|
5
|
+
* and subscription usage indicators.
|
|
6
|
+
*
|
|
7
|
+
* Layout:
|
|
8
|
+
* gpt-5.4 (high) - ◑ 14% (38k/272k $0.33)
|
|
9
|
+
* 🗀 ~/project ⎇ main * ↑2
|
|
10
|
+
* Codex > 5h ◔ 34% 50m > Week ○ 5% 4d18h
|
|
11
|
+
*
|
|
12
|
+
* Inspired by ogulcancelik/pi-extensions pi-minimal-footer.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
16
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
17
|
+
import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
|
|
18
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
20
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
|
|
24
|
+
// ─── Types ───────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
interface RateWindow {
|
|
27
|
+
label: string;
|
|
28
|
+
usedPercent: number;
|
|
29
|
+
resetsIn?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface UsageSnapshot {
|
|
33
|
+
provider: string;
|
|
34
|
+
windows: RateWindow[];
|
|
35
|
+
error?: string;
|
|
36
|
+
fetchedAt: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface GitCache {
|
|
40
|
+
branch: string | null;
|
|
41
|
+
dirty: boolean;
|
|
42
|
+
ahead: number;
|
|
43
|
+
behind: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Constants ───────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const CTX_PIES = ["○", "◔", "◑", "◕", "●"] as const;
|
|
49
|
+
const USAGE_REFRESH_MS = 5 * 60_000;
|
|
50
|
+
|
|
51
|
+
// ─── Formatting ──────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function fmtTokens(n: number): string {
|
|
54
|
+
if (n >= 1_000_000) {
|
|
55
|
+
const m = n / 1_000_000;
|
|
56
|
+
return m % 1 === 0 ? `${m}M` : `${m.toFixed(1).replace(/\.0$/, "")}M`;
|
|
57
|
+
}
|
|
58
|
+
if (n >= 1_000) return `${Math.round(n / 1_000)}k`;
|
|
59
|
+
return `${n}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fmtResetTime(date: Date): string {
|
|
63
|
+
const diffMs = date.getTime() - Date.now();
|
|
64
|
+
if (diffMs < 0) return "now";
|
|
65
|
+
|
|
66
|
+
const mins = Math.floor(diffMs / 60_000);
|
|
67
|
+
if (mins < 60) return `${mins}m`;
|
|
68
|
+
|
|
69
|
+
const hours = Math.floor(mins / 60);
|
|
70
|
+
const remMins = mins % 60;
|
|
71
|
+
if (hours < 24) return remMins > 0 ? `${hours}h${remMins}m` : `${hours}h`;
|
|
72
|
+
|
|
73
|
+
const days = Math.floor(hours / 24);
|
|
74
|
+
const remHours = hours % 24;
|
|
75
|
+
return remHours > 0 ? `${days}d${remHours}h` : `${days}d`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function wrapSegments(segments: string[], width: number, sep: string): string[] {
|
|
79
|
+
const normalized = segments.filter((segment) => visibleWidth(segment) > 0);
|
|
80
|
+
if (!normalized.length) return [];
|
|
81
|
+
|
|
82
|
+
const lines: string[] = [];
|
|
83
|
+
let current = normalized[0];
|
|
84
|
+
|
|
85
|
+
for (const segment of normalized.slice(1)) {
|
|
86
|
+
const candidate = `${current}${sep}${segment}`;
|
|
87
|
+
if (visibleWidth(candidate) <= width) {
|
|
88
|
+
current = candidate;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
lines.push(truncateToWidth(current, width));
|
|
93
|
+
current = segment;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
lines.push(truncateToWidth(current, width));
|
|
97
|
+
return lines;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function clampPct(v: number): number {
|
|
101
|
+
if (!Number.isFinite(v)) return 0;
|
|
102
|
+
return Math.max(0, Math.min(100, v));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Normalize 0-1 fraction or 0-100 percentage, then clamp. */
|
|
106
|
+
function normalizePct(v: number): number {
|
|
107
|
+
if (!Number.isFinite(v)) return 0;
|
|
108
|
+
const n = v <= 1 && v >= 0 ? v * 100 : v;
|
|
109
|
+
return Math.max(0, Math.min(100, n));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function windowLabel(durationMs: number | undefined, fallback: string): string {
|
|
113
|
+
if (!durationMs || !Number.isFinite(durationMs) || durationMs <= 0) return fallback;
|
|
114
|
+
|
|
115
|
+
const hourMs = 3_600_000;
|
|
116
|
+
const dayMs = 86_400_000;
|
|
117
|
+
const weekMs = 7 * dayMs;
|
|
118
|
+
|
|
119
|
+
if (Math.abs(durationMs - weekMs) <= hourMs * 2 || fallback === "Week") return "Week";
|
|
120
|
+
if (Math.abs(durationMs - dayMs) <= hourMs * 2 || fallback === "Day") return "Day";
|
|
121
|
+
if (Math.abs(durationMs - 5 * hourMs) <= hourMs * 2) return fallback;
|
|
122
|
+
|
|
123
|
+
const hours = Math.round(durationMs / hourMs);
|
|
124
|
+
if (hours >= 1 && hours < 48) return `${hours}h`;
|
|
125
|
+
|
|
126
|
+
const days = Math.round(durationMs / dayMs);
|
|
127
|
+
if (days >= 1) return `${days}d`;
|
|
128
|
+
|
|
129
|
+
return `${Math.max(1, Math.round(durationMs / 60_000))}m`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Git Cache ───────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
let gitCache: GitCache | null = null;
|
|
135
|
+
|
|
136
|
+
function refreshGitCache(): void {
|
|
137
|
+
gitCache = null;
|
|
138
|
+
try {
|
|
139
|
+
const gitRoot = execSync("git rev-parse --show-toplevel 2>/dev/null", {
|
|
140
|
+
encoding: "utf8",
|
|
141
|
+
timeout: 500,
|
|
142
|
+
}).trim();
|
|
143
|
+
if (!gitRoot) return;
|
|
144
|
+
|
|
145
|
+
let branch: string | null = null;
|
|
146
|
+
try {
|
|
147
|
+
const b = execSync("git rev-parse --abbrev-ref HEAD 2>/dev/null", {
|
|
148
|
+
encoding: "utf8",
|
|
149
|
+
timeout: 500,
|
|
150
|
+
}).trim();
|
|
151
|
+
branch = b && b !== "HEAD" ? b : null;
|
|
152
|
+
} catch {}
|
|
153
|
+
|
|
154
|
+
let dirty = false;
|
|
155
|
+
try {
|
|
156
|
+
const st = execSync("git status --porcelain 2>/dev/null", {
|
|
157
|
+
encoding: "utf8",
|
|
158
|
+
timeout: 500,
|
|
159
|
+
});
|
|
160
|
+
dirty = st.trim().length > 0;
|
|
161
|
+
} catch {}
|
|
162
|
+
|
|
163
|
+
let ahead = 0;
|
|
164
|
+
let behind = 0;
|
|
165
|
+
try {
|
|
166
|
+
const counts = execSync("git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null", {
|
|
167
|
+
encoding: "utf8",
|
|
168
|
+
timeout: 500,
|
|
169
|
+
}).trim();
|
|
170
|
+
const [a, b] = counts.split(/\s+/);
|
|
171
|
+
ahead = parseInt(a, 10) || 0;
|
|
172
|
+
behind = parseInt(b, 10) || 0;
|
|
173
|
+
} catch {}
|
|
174
|
+
|
|
175
|
+
gitCache = { branch, dirty, ahead, behind };
|
|
176
|
+
} catch {
|
|
177
|
+
gitCache = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── Auth Helpers ────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function loadAuthJson(): Record<string, unknown> {
|
|
184
|
+
const authPath = join(homedir(), ".pi", "agent", "auth.json");
|
|
185
|
+
try {
|
|
186
|
+
if (existsSync(authPath)) return JSON.parse(readFileSync(authPath, "utf-8")) as Record<string, unknown>;
|
|
187
|
+
} catch {}
|
|
188
|
+
return {};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
// ─── Token Getters ────────��────────────────────────────────────��─────
|
|
193
|
+
|
|
194
|
+
function getClaudeToken(): string | undefined {
|
|
195
|
+
const auth = loadAuthJson();
|
|
196
|
+
const anthropic = auth.anthropic as Record<string, unknown> | undefined;
|
|
197
|
+
if (anthropic?.access) return anthropic.access as string;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const keychainData = execSync(
|
|
201
|
+
'security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null',
|
|
202
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
|
|
203
|
+
).trim();
|
|
204
|
+
if (keychainData) {
|
|
205
|
+
const parsed = JSON.parse(keychainData) as Record<string, Record<string, string>>;
|
|
206
|
+
if (parsed.claudeAiOauth?.accessToken) return parsed.claudeAiOauth.accessToken;
|
|
207
|
+
}
|
|
208
|
+
} catch {}
|
|
209
|
+
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getCopilotToken(): string | undefined {
|
|
214
|
+
const auth = loadAuthJson();
|
|
215
|
+
const entry = auth["github-copilot"] as Record<string, unknown> | undefined;
|
|
216
|
+
return entry?.refresh as string | undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getCodexToken(): { token: string; accountId?: string } | undefined {
|
|
220
|
+
const auth = loadAuthJson();
|
|
221
|
+
const entry = auth["openai-codex"] as Record<string, unknown> | undefined;
|
|
222
|
+
if (entry?.access) return { token: entry.access as string, accountId: entry.accountId as string | undefined };
|
|
223
|
+
|
|
224
|
+
const codexPath = join(process.env.CODEX_HOME || join(homedir(), ".codex"), "auth.json");
|
|
225
|
+
try {
|
|
226
|
+
if (existsSync(codexPath)) {
|
|
227
|
+
const data = JSON.parse(readFileSync(codexPath, "utf-8")) as Record<string, unknown>;
|
|
228
|
+
if (data.OPENAI_API_KEY) return { token: data.OPENAI_API_KEY as string };
|
|
229
|
+
const tokens = data.tokens as Record<string, string> | undefined;
|
|
230
|
+
if (tokens?.access_token) return { token: tokens.access_token, accountId: tokens.account_id };
|
|
231
|
+
}
|
|
232
|
+
} catch {}
|
|
233
|
+
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function getGeminiToken(): string | undefined {
|
|
238
|
+
const auth = loadAuthJson();
|
|
239
|
+
const entry = auth["google-gemini-cli"] as Record<string, unknown> | undefined;
|
|
240
|
+
if (entry?.access) return entry.access as string;
|
|
241
|
+
|
|
242
|
+
const geminiPath = join(homedir(), ".gemini", "oauth_creds.json");
|
|
243
|
+
try {
|
|
244
|
+
if (existsSync(geminiPath)) {
|
|
245
|
+
const data = JSON.parse(readFileSync(geminiPath, "utf-8")) as Record<string, string>;
|
|
246
|
+
return data.access_token;
|
|
247
|
+
}
|
|
248
|
+
} catch {}
|
|
249
|
+
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Fetch Helpers ───────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
async function fetchWithTimeout(url: string, init: RequestInit, timeoutMs = 5000): Promise<Response> {
|
|
256
|
+
const controller = new AbortController();
|
|
257
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
258
|
+
try {
|
|
259
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
260
|
+
} finally {
|
|
261
|
+
clearTimeout(timeout);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── Usage Fetchers ──────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
async function fetchClaudeUsage(): Promise<UsageSnapshot> {
|
|
268
|
+
const token = getClaudeToken();
|
|
269
|
+
if (!token) return { provider: "Claude", windows: [], error: "no-auth", fetchedAt: Date.now() };
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const res = await fetchWithTimeout("https://api.anthropic.com/api/oauth/usage", {
|
|
273
|
+
headers: { Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20" },
|
|
274
|
+
});
|
|
275
|
+
if (!res.ok) return { provider: "Claude", windows: [], error: `HTTP ${res.status}`, fetchedAt: Date.now() };
|
|
276
|
+
|
|
277
|
+
const data = (await res.json()) as Record<string, Record<string, unknown>>;
|
|
278
|
+
const windows: RateWindow[] = [];
|
|
279
|
+
|
|
280
|
+
if (data.five_hour?.utilization !== undefined) {
|
|
281
|
+
windows.push({
|
|
282
|
+
label: "5h",
|
|
283
|
+
usedPercent: normalizePct(data.five_hour.utilization as number),
|
|
284
|
+
resetsIn: data.five_hour.resets_at ? fmtResetTime(new Date(data.five_hour.resets_at as string)) : undefined,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (data.seven_day?.utilization !== undefined) {
|
|
288
|
+
windows.push({
|
|
289
|
+
label: "Week",
|
|
290
|
+
usedPercent: normalizePct(data.seven_day.utilization as number),
|
|
291
|
+
resetsIn: data.seven_day.resets_at ? fmtResetTime(new Date(data.seven_day.resets_at as string)) : undefined,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { provider: "Claude", windows, fetchedAt: Date.now() };
|
|
296
|
+
} catch (e) {
|
|
297
|
+
return { provider: "Claude", windows: [], error: String(e), fetchedAt: Date.now() };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function fetchCopilotUsage(): Promise<UsageSnapshot> {
|
|
302
|
+
const token = getCopilotToken();
|
|
303
|
+
if (!token) return { provider: "Copilot", windows: [], error: "no-auth", fetchedAt: Date.now() };
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const res = await fetchWithTimeout("https://api.github.com/copilot_internal/user", {
|
|
307
|
+
headers: {
|
|
308
|
+
"Editor-Version": "vscode/1.96.2",
|
|
309
|
+
"User-Agent": "GitHubCopilotChat/0.26.7",
|
|
310
|
+
"X-Github-Api-Version": "2025-04-01",
|
|
311
|
+
Accept: "application/json",
|
|
312
|
+
Authorization: `token ${token}`,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
if (!res.ok) return { provider: "Copilot", windows: [], error: `HTTP ${res.status}`, fetchedAt: Date.now() };
|
|
316
|
+
|
|
317
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
318
|
+
const windows: RateWindow[] = [];
|
|
319
|
+
const resetDate = (data as Record<string, string>).quota_reset_date_utc
|
|
320
|
+
? new Date((data as Record<string, string>).quota_reset_date_utc)
|
|
321
|
+
: undefined;
|
|
322
|
+
const resetsIn = resetDate ? fmtResetTime(resetDate) : undefined;
|
|
323
|
+
|
|
324
|
+
const snapshots = data.quota_snapshots as Record<string, Record<string, unknown>> | undefined;
|
|
325
|
+
if (snapshots?.premium_interactions) {
|
|
326
|
+
const pi = snapshots.premium_interactions;
|
|
327
|
+
windows.push({ label: "Premium", usedPercent: clampPct(100 - ((pi.percent_remaining as number) || 0)), resetsIn });
|
|
328
|
+
}
|
|
329
|
+
if (snapshots?.chat && !(snapshots.chat as Record<string, unknown>).unlimited) {
|
|
330
|
+
const chat = snapshots.chat;
|
|
331
|
+
windows.push({
|
|
332
|
+
label: "Chat",
|
|
333
|
+
usedPercent: clampPct(100 - ((chat.percent_remaining as number) || 0)),
|
|
334
|
+
resetsIn,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { provider: "Copilot", windows, fetchedAt: Date.now() };
|
|
339
|
+
} catch (e) {
|
|
340
|
+
return { provider: "Copilot", windows: [], error: String(e), fetchedAt: Date.now() };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function fetchCodexUsage(): Promise<UsageSnapshot> {
|
|
345
|
+
const creds = getCodexToken();
|
|
346
|
+
if (!creds) return { provider: "Codex", windows: [], error: "no-auth", fetchedAt: Date.now() };
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const headers: Record<string, string> = {
|
|
350
|
+
Authorization: `Bearer ${creds.token}`,
|
|
351
|
+
"User-Agent": "pi-agent",
|
|
352
|
+
Accept: "application/json",
|
|
353
|
+
};
|
|
354
|
+
if (creds.accountId) headers["ChatGPT-Account-Id"] = creds.accountId;
|
|
355
|
+
|
|
356
|
+
const res = await fetchWithTimeout("https://chatgpt.com/backend-api/wham/usage", { method: "GET", headers });
|
|
357
|
+
if (!res.ok) return { provider: "Codex", windows: [], error: `HTTP ${res.status}`, fetchedAt: Date.now() };
|
|
358
|
+
|
|
359
|
+
const data = (await res.json()) as Record<string, Record<string, Record<string, unknown>>>;
|
|
360
|
+
const windows: RateWindow[] = [];
|
|
361
|
+
|
|
362
|
+
const primary = data.rate_limit?.primary_window;
|
|
363
|
+
if (primary) {
|
|
364
|
+
const resetDate = primary.reset_at ? new Date((primary.reset_at as number) * 1000) : undefined;
|
|
365
|
+
const durationMs =
|
|
366
|
+
typeof primary.limit_window_seconds === "number" ? (primary.limit_window_seconds as number) * 1000 : undefined;
|
|
367
|
+
windows.push({
|
|
368
|
+
label: windowLabel(durationMs, "5h"),
|
|
369
|
+
usedPercent: clampPct((primary.used_percent as number) || 0),
|
|
370
|
+
resetsIn: resetDate ? fmtResetTime(resetDate) : undefined,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const secondary = data.rate_limit?.secondary_window;
|
|
375
|
+
if (secondary) {
|
|
376
|
+
const resetDate = secondary.reset_at ? new Date((secondary.reset_at as number) * 1000) : undefined;
|
|
377
|
+
const durationMs =
|
|
378
|
+
typeof secondary.limit_window_seconds === "number"
|
|
379
|
+
? (secondary.limit_window_seconds as number) * 1000
|
|
380
|
+
: undefined;
|
|
381
|
+
windows.push({
|
|
382
|
+
label: windowLabel(durationMs, "Week"),
|
|
383
|
+
usedPercent: clampPct((secondary.used_percent as number) || 0),
|
|
384
|
+
resetsIn: resetDate ? fmtResetTime(resetDate) : undefined,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { provider: "Codex", windows, fetchedAt: Date.now() };
|
|
389
|
+
} catch (e) {
|
|
390
|
+
return { provider: "Codex", windows: [], error: String(e), fetchedAt: Date.now() };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function fetchGeminiUsage(): Promise<UsageSnapshot> {
|
|
395
|
+
const token = getGeminiToken();
|
|
396
|
+
if (!token) return { provider: "Gemini", windows: [], error: "no-auth", fetchedAt: Date.now() };
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const res = await fetchWithTimeout("https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota", {
|
|
400
|
+
method: "POST",
|
|
401
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
402
|
+
body: "{}",
|
|
403
|
+
});
|
|
404
|
+
if (!res.ok) return { provider: "Gemini", windows: [], error: `HTTP ${res.status}`, fetchedAt: Date.now() };
|
|
405
|
+
|
|
406
|
+
const data = (await res.json()) as Record<string, Array<Record<string, unknown>>>;
|
|
407
|
+
const quotas: Record<string, number> = {};
|
|
408
|
+
|
|
409
|
+
for (const bucket of data.buckets || []) {
|
|
410
|
+
const model = (bucket.modelId as string) || "unknown";
|
|
411
|
+
const frac = (bucket.remainingFraction as number) ?? 1;
|
|
412
|
+
if (!quotas[model] || frac < quotas[model]) quotas[model] = frac;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const windows: RateWindow[] = [];
|
|
416
|
+
let proMin = 1;
|
|
417
|
+
let flashMin = 1;
|
|
418
|
+
let hasPro = false;
|
|
419
|
+
let hasFlash = false;
|
|
420
|
+
|
|
421
|
+
for (const [model, frac] of Object.entries(quotas)) {
|
|
422
|
+
const lower = model.toLowerCase();
|
|
423
|
+
if (lower.includes("pro")) {
|
|
424
|
+
hasPro = true;
|
|
425
|
+
if (frac < proMin) proMin = frac;
|
|
426
|
+
}
|
|
427
|
+
if (lower.includes("flash")) {
|
|
428
|
+
hasFlash = true;
|
|
429
|
+
if (frac < flashMin) flashMin = frac;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (hasPro) windows.push({ label: "Pro", usedPercent: clampPct((1 - proMin) * 100) });
|
|
434
|
+
if (hasFlash) windows.push({ label: "Flash", usedPercent: clampPct((1 - flashMin) * 100) });
|
|
435
|
+
|
|
436
|
+
return { provider: "Gemini", windows, fetchedAt: Date.now() };
|
|
437
|
+
} catch (e) {
|
|
438
|
+
return { provider: "Gemini", windows: [], error: String(e), fetchedAt: Date.now() };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ─── Provider Detection ──────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
const PROVIDER_MAP: Record<string, string> = {
|
|
445
|
+
anthropic: "claude",
|
|
446
|
+
"openai-codex": "codex",
|
|
447
|
+
"github-copilot": "copilot",
|
|
448
|
+
"google-gemini-cli": "gemini",
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const USAGE_FETCHERS: Record<string, () => Promise<UsageSnapshot>> = {
|
|
452
|
+
claude: fetchClaudeUsage,
|
|
453
|
+
codex: fetchCodexUsage,
|
|
454
|
+
copilot: fetchCopilotUsage,
|
|
455
|
+
gemini: fetchGeminiUsage,
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// ─── Rendering ───────────────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
function gaugeColor(pct: number): ThemeColor {
|
|
461
|
+
if (pct >= 50) return "error";
|
|
462
|
+
if (pct >= 35) return "warning";
|
|
463
|
+
return "success";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function ctxPie(pct: number): string {
|
|
467
|
+
if (pct >= 80) return CTX_PIES[4];
|
|
468
|
+
if (pct >= 75) return CTX_PIES[3];
|
|
469
|
+
if (pct >= 50) return CTX_PIES[2];
|
|
470
|
+
if (pct >= 25) return CTX_PIES[1];
|
|
471
|
+
return CTX_PIES[0];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function renderContextGauge(
|
|
475
|
+
pct: number,
|
|
476
|
+
theme: Theme,
|
|
477
|
+
used?: number,
|
|
478
|
+
total?: number,
|
|
479
|
+
cost?: number,
|
|
480
|
+
): string {
|
|
481
|
+
const clamped = clampPct(pct);
|
|
482
|
+
const pie = theme.fg(gaugeColor(clamped), ctxPie(clamped));
|
|
483
|
+
const details: string[] = [];
|
|
484
|
+
if (used !== undefined && total) details.push(`${fmtTokens(used)}/${fmtTokens(total)}`);
|
|
485
|
+
if (cost !== undefined) details.push(`$${cost.toFixed(2)}`);
|
|
486
|
+
const detailsStr = details.length ? " " + theme.fg("dim", `(${details.join(" ")})`) : "";
|
|
487
|
+
|
|
488
|
+
return `${pie} ${theme.fg("dim", `${Math.round(clamped)}%`)}${detailsStr}`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function renderUsageProgress(pct: number, theme: Theme): string {
|
|
492
|
+
const clamped = clampPct(pct);
|
|
493
|
+
return theme.fg(gaugeColor(clamped), ctxPie(clamped));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function renderUsageLine(usage: UsageSnapshot, width: number, theme: Theme): string[] {
|
|
497
|
+
if (!usage.windows.length) return [];
|
|
498
|
+
|
|
499
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
500
|
+
const sep = " " + dim(">") + " ";
|
|
501
|
+
|
|
502
|
+
const parts: string[] = [theme.fg("accent", usage.provider)];
|
|
503
|
+
for (const w of usage.windows) {
|
|
504
|
+
const progress = renderUsageProgress(w.usedPercent, theme);
|
|
505
|
+
const pctStr = dim(`${Math.round(w.usedPercent)}%`);
|
|
506
|
+
const time = w.resetsIn ? " " + dim(w.resetsIn) : "";
|
|
507
|
+
parts.push(`${dim(w.label)} ${progress} ${pctStr}${time}`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return wrapSegments(parts, width, sep);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ─── Extension ───────────────────────────────────────────────────────
|
|
514
|
+
|
|
515
|
+
export default function expertStatusLine(pi: ExtensionAPI): void {
|
|
516
|
+
const usageCache = new Map<string, UsageSnapshot>();
|
|
517
|
+
let latestUsage: UsageSnapshot | null = null;
|
|
518
|
+
let activeProvider: string | null = null;
|
|
519
|
+
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
520
|
+
let tuiRef: { requestRender: () => void } | null = null;
|
|
521
|
+
|
|
522
|
+
function fetchUsage(modelProvider: string): void {
|
|
523
|
+
const provider = PROVIDER_MAP[modelProvider];
|
|
524
|
+
if (!provider) {
|
|
525
|
+
activeProvider = null;
|
|
526
|
+
latestUsage = null;
|
|
527
|
+
stopRefreshTimer();
|
|
528
|
+
tuiRef?.requestRender();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
activeProvider = provider;
|
|
533
|
+
|
|
534
|
+
const cached = usageCache.get(provider);
|
|
535
|
+
if (cached?.windows.length) {
|
|
536
|
+
latestUsage = cached;
|
|
537
|
+
tuiRef?.requestRender();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const fetcher = USAGE_FETCHERS[provider];
|
|
541
|
+
if (!fetcher) return;
|
|
542
|
+
|
|
543
|
+
fetcher()
|
|
544
|
+
.then((u) => {
|
|
545
|
+
if (!u || activeProvider !== provider) return;
|
|
546
|
+
if (!u.windows.length && u.error && cached?.windows.length) return;
|
|
547
|
+
usageCache.set(provider, u);
|
|
548
|
+
latestUsage = u;
|
|
549
|
+
tuiRef?.requestRender();
|
|
550
|
+
})
|
|
551
|
+
.catch(() => {});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function startRefreshTimer(): void {
|
|
555
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
556
|
+
refreshTimer = setInterval(() => {
|
|
557
|
+
if (!activeProvider) return;
|
|
558
|
+
const provider = activeProvider;
|
|
559
|
+
const cached = usageCache.get(provider);
|
|
560
|
+
const fetcher = USAGE_FETCHERS[provider];
|
|
561
|
+
if (!fetcher) return;
|
|
562
|
+
|
|
563
|
+
fetcher()
|
|
564
|
+
.then((u) => {
|
|
565
|
+
if (!u || activeProvider !== provider) return;
|
|
566
|
+
if (!u.windows.length && u.error && cached?.windows.length) return;
|
|
567
|
+
usageCache.set(provider, u);
|
|
568
|
+
latestUsage = u;
|
|
569
|
+
tuiRef?.requestRender();
|
|
570
|
+
})
|
|
571
|
+
.catch(() => {});
|
|
572
|
+
}, USAGE_REFRESH_MS);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function stopRefreshTimer(): void {
|
|
576
|
+
if (refreshTimer) {
|
|
577
|
+
clearInterval(refreshTimer);
|
|
578
|
+
refreshTimer = null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
pi.on("session_start", (_event, ctx) => {
|
|
583
|
+
refreshGitCache();
|
|
584
|
+
if (!ctx.hasUI) return;
|
|
585
|
+
|
|
586
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
587
|
+
tuiRef = tui;
|
|
588
|
+
const unsub = footerData.onBranchChange(() => {
|
|
589
|
+
refreshGitCache();
|
|
590
|
+
tui.requestRender();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
if (ctx.model?.provider) {
|
|
594
|
+
fetchUsage(ctx.model.provider);
|
|
595
|
+
startRefreshTimer();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
dispose: () => {
|
|
600
|
+
unsub();
|
|
601
|
+
tuiRef = null;
|
|
602
|
+
stopRefreshTimer();
|
|
603
|
+
},
|
|
604
|
+
invalidate() {},
|
|
605
|
+
render(width: number): string[] {
|
|
606
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
607
|
+
const sep = " " + dim(">") + " ";
|
|
608
|
+
const dashSep = " " + dim("-") + " ";
|
|
609
|
+
|
|
610
|
+
// ── CWD ──
|
|
611
|
+
let pwd = process.cwd();
|
|
612
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
613
|
+
if (home && pwd.startsWith(home)) pwd = `~${pwd.slice(home.length)}`;
|
|
614
|
+
|
|
615
|
+
// ── Git branch + status ──
|
|
616
|
+
let branchStr = "";
|
|
617
|
+
if (gitCache?.branch) {
|
|
618
|
+
const branchColor = gitCache.dirty ? "warning" : "success";
|
|
619
|
+
branchStr = dim("⎇") + " " + theme.fg(branchColor, gitCache.branch);
|
|
620
|
+
if (gitCache.dirty) branchStr += theme.fg("warning", " *");
|
|
621
|
+
if (gitCache.ahead) branchStr += theme.fg("success", ` ↑${gitCache.ahead}`);
|
|
622
|
+
if (gitCache.behind) branchStr += theme.fg("error", ` ↓${gitCache.behind}`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ── Model + thinking ──
|
|
626
|
+
const modelName = ctx.model?.id?.split("/").pop() || "no-model";
|
|
627
|
+
let modelStr = theme.fg("muted", modelName);
|
|
628
|
+
const thinkingLevel = pi.getThinkingLevel();
|
|
629
|
+
if (thinkingLevel !== "off") {
|
|
630
|
+
modelStr += " " + theme.fg("dim", `(${thinkingLevel})`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ── Extension statuses ──
|
|
634
|
+
const statuses = footerData.getExtensionStatuses();
|
|
635
|
+
const statusParts: string[] = [];
|
|
636
|
+
for (const [, text] of statuses) {
|
|
637
|
+
if (text) statusParts.push(text);
|
|
638
|
+
}
|
|
639
|
+
const statusStr = statusParts.join(" ");
|
|
640
|
+
|
|
641
|
+
// ── Context gauge ──
|
|
642
|
+
const usage = ctx.getContextUsage();
|
|
643
|
+
let ctxPct = 0;
|
|
644
|
+
let ctxUsed: number | undefined;
|
|
645
|
+
let ctxTotal: number | undefined;
|
|
646
|
+
if (usage && usage.contextWindow > 0) {
|
|
647
|
+
ctxPct = usage.percent ?? 0;
|
|
648
|
+
ctxUsed = usage.tokens ?? undefined;
|
|
649
|
+
ctxTotal = usage.contextWindow;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ── Session cost ──
|
|
653
|
+
let cost = 0;
|
|
654
|
+
for (const e of ctx.sessionManager.getBranch()) {
|
|
655
|
+
if (e.type === "message" && e.message.role === "assistant") {
|
|
656
|
+
cost += (e.message as AssistantMessage).usage.cost.total;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const gauge = renderContextGauge(ctxPct, theme, ctxUsed, ctxTotal, cost);
|
|
661
|
+
|
|
662
|
+
// ── Layout ──
|
|
663
|
+
const lines: string[] = [];
|
|
664
|
+
|
|
665
|
+
const headerSegments = [modelStr, statusStr, gauge];
|
|
666
|
+
lines.push(...wrapSegments(headerSegments, width, dashSep));
|
|
667
|
+
|
|
668
|
+
const pwdColored = dim("🗀") + " " + theme.fg("accent", pwd);
|
|
669
|
+
const locationSegments = [pwdColored, branchStr];
|
|
670
|
+
lines.push(...wrapSegments(locationSegments, width, " "));
|
|
671
|
+
|
|
672
|
+
if (latestUsage?.windows.length) {
|
|
673
|
+
for (const line of renderUsageLine(latestUsage, width, theme)) {
|
|
674
|
+
lines.push(truncateToWidth(line, width));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return lines;
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
pi.on("model_select", (event) => {
|
|
685
|
+
if (!event.model?.provider) return;
|
|
686
|
+
fetchUsage(event.model.provider);
|
|
687
|
+
startRefreshTimer();
|
|
688
|
+
});
|
|
689
|
+
}
|