@diegopetrucci/pi-extensions 0.1.74 → 0.1.78
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 +2 -0
- package/README.md +17 -12
- package/extensions/agent-workflow-audit/README.md +1 -1
- package/extensions/agent-workflow-audit/index.ts +1631 -1009
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/README.md +3 -1
- package/extensions/annotate-git-diff/clipboard.ts +109 -100
- package/extensions/annotate-git-diff/git.ts +846 -768
- package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
- package/extensions/annotate-git-diff/index.ts +712 -671
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-git-diff/prompt.ts +45 -45
- package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
- package/extensions/annotate-git-diff/review-server.ts +337 -311
- package/extensions/annotate-git-diff/types.ts +132 -132
- package/extensions/annotate-git-diff/ui.ts +27 -27
- package/extensions/annotate-git-diff/watch.ts +74 -74
- package/extensions/annotate-git-diff/web/app.js +1947 -1884
- package/extensions/annotate-git-diff/web/index.html +5 -6
- package/extensions/annotate-last-message/README.md +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
- package/extensions/annotate-last-message/index.ts +210 -206
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/prompt.ts +87 -66
- package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
- package/extensions/annotate-last-message/session.ts +94 -89
- package/extensions/annotate-last-message/theme.ts +199 -193
- package/extensions/annotate-last-message/types.ts +23 -23
- package/extensions/annotate-last-message/ui.ts +27 -25
- package/extensions/annotate-last-message/web/app.js +4 -7
- package/extensions/annotate-last-message/web/index.html +3 -3
- package/extensions/annotate-last-message/web/md-renderer.js +1 -1
- package/extensions/brrr/README.md +1 -1
- package/extensions/brrr/index.ts +213 -191
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/README.md +5 -79
- package/extensions/claude-fast/index.ts +31 -289
- package/extensions/claude-fast/package.json +2 -3
- package/extensions/code-reviewer/README.md +6 -6
- package/extensions/code-reviewer/index.ts +1825 -1373
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +50 -52
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/README.md +9 -9
- package/extensions/context-cap/index.ts +313 -305
- package/extensions/context-cap/package.json +2 -2
- package/extensions/context-inspector/index.ts +1211 -1064
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/README.md +4 -3
- package/extensions/contrarian/index.ts +1863 -1737
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +37 -41
- package/extensions/dirty-repo-guard/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +11 -43
- package/extensions/dynamic-context-pruning/index.ts +2131 -2105
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +52 -14
- package/extensions/fast/index.ts +513 -317
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/README.md +1 -1
- package/extensions/git-footer/index.ts +458 -411
- package/extensions/git-footer/package.json +2 -3
- package/extensions/gnosis/index.ts +180 -130
- package/extensions/gnosis/package.json +1 -1
- package/extensions/inline-bash/index.ts +87 -103
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/README.md +1 -1
- package/extensions/librarian/index.ts +1877 -1419
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/README.md +5 -5
- package/extensions/minimal-footer/index.ts +987 -1012
- package/extensions/minimal-footer/minimal-footer.example.json +1 -1
- package/extensions/minimal-footer/openai-usage.ts +195 -190
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/notify/index.ts +222 -230
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/README.md +5 -73
- package/extensions/openai-fast/index.ts +32 -272
- package/extensions/openai-fast/package.json +2 -3
- package/extensions/oracle/README.md +4 -3
- package/extensions/oracle/index.ts +1740 -1627
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/README.md +3 -0
- package/extensions/permission-gate/index.ts +785 -612
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/permission-gate/powershell-safety.ts +840 -793
- package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
- package/extensions/project-mcp-json/README.md +199 -0
- package/extensions/project-mcp-json/config.ts +564 -0
- package/extensions/project-mcp-json/index.ts +119 -0
- package/extensions/project-mcp-json/package.json +38 -0
- package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
- package/extensions/quiet-tools/README.md +30 -19
- package/extensions/quiet-tools/index.ts +376 -291
- package/extensions/quiet-tools/package.json +2 -2
- package/extensions/review/README.md +2 -2
- package/extensions/review/index.ts +1745 -1872
- package/extensions/review/package.json +1 -1
- package/extensions/todo/index.ts +280 -280
- package/extensions/todo/package.json +1 -1
- package/extensions/triage-comments/README.md +6 -4
- package/extensions/triage-comments/index.ts +2620 -1888
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +9 -6
- package/assets/social-preview.png +0 -0
- package/extensions/claude-fast/claude-fast.example.json +0 -4
- package/extensions/openai-fast/openai-fast.example.json +0 -4
|
@@ -2,69 +2,62 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
CONFIG_DIR_NAME,
|
|
6
|
+
getAgentDir,
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ExtensionContext,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
fetchOpenAICodexUsage,
|
|
13
|
+
formatUsageStatus,
|
|
14
|
+
formatUsageSummary,
|
|
15
|
+
getOpenAICodexAccountId,
|
|
16
|
+
isOpenAICodexProvider,
|
|
17
|
+
isOpenAIProvider,
|
|
18
|
+
type UsageAvailability,
|
|
19
|
+
type UsageSnapshot,
|
|
20
20
|
} from "./openai-usage.ts";
|
|
21
21
|
|
|
22
|
-
|
|
23
22
|
const DEFAULT_CONFIG: MinimalFooterConfig = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
23
|
+
context: {
|
|
24
|
+
showPercent: true,
|
|
25
|
+
dumbZone: {
|
|
26
|
+
enabled: true,
|
|
27
|
+
thresholdTokens: 300_000,
|
|
28
|
+
label: "DUMB ZONE",
|
|
29
|
+
color: "error",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
codexUsage: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
cacheTtlMs: 5 * 60 * 1000,
|
|
35
|
+
requestTimeoutMs: 10 * 1000,
|
|
36
|
+
windows: {
|
|
37
|
+
primary: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
label: "auto",
|
|
40
|
+
},
|
|
41
|
+
secondary: {
|
|
42
|
+
enabled: true,
|
|
43
|
+
label: "auto",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
experimentalMarker: {
|
|
48
|
+
enabled: true,
|
|
49
|
+
label: "xp",
|
|
50
|
+
color: "warning",
|
|
51
|
+
},
|
|
52
|
+
gitStatus: {
|
|
53
|
+
enabled: true,
|
|
54
|
+
refreshIntervalMs: 8_000,
|
|
55
|
+
gitTimeoutMs: 1_500,
|
|
56
|
+
ghTimeoutMs: 3_000,
|
|
57
|
+
},
|
|
59
58
|
};
|
|
60
59
|
|
|
61
|
-
const DUMB_ZONE_COLORS = new Set<DumbZoneColor>([
|
|
62
|
-
"error",
|
|
63
|
-
"warning",
|
|
64
|
-
"accent",
|
|
65
|
-
"text",
|
|
66
|
-
"dim",
|
|
67
|
-
]);
|
|
60
|
+
const DUMB_ZONE_COLORS = new Set<DumbZoneColor>(["error", "warning", "accent", "text", "dim"]);
|
|
68
61
|
|
|
69
62
|
// Node treats delays above this value as 1ms. Clamp only the identity-check
|
|
70
63
|
// schedule; the configured cache TTL remains the source of truth for lifecycle
|
|
@@ -73,808 +66,796 @@ const MAX_NODE_TIMER_DELAY_MS = 2_147_483_647;
|
|
|
73
66
|
const ZERO_CACHE_IDENTITY_CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
|
74
67
|
|
|
75
68
|
type RecursivePartial<T> = {
|
|
76
|
-
|
|
69
|
+
[P in keyof T]?: T[P] extends object ? RecursivePartial<T[P]> : T[P];
|
|
77
70
|
};
|
|
78
71
|
|
|
79
72
|
type DumbZoneColor = "error" | "warning" | "accent" | "text" | "dim";
|
|
80
73
|
|
|
81
74
|
interface MinimalFooterConfig {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
75
|
+
context: {
|
|
76
|
+
showPercent: boolean;
|
|
77
|
+
dumbZone: {
|
|
78
|
+
enabled: boolean;
|
|
79
|
+
thresholdTokens: number;
|
|
80
|
+
label: string;
|
|
81
|
+
color: DumbZoneColor;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
codexUsage: {
|
|
85
|
+
enabled: boolean;
|
|
86
|
+
cacheTtlMs: number;
|
|
87
|
+
requestTimeoutMs: number;
|
|
88
|
+
windows: {
|
|
89
|
+
primary: {
|
|
90
|
+
enabled: boolean;
|
|
91
|
+
label: string;
|
|
92
|
+
};
|
|
93
|
+
secondary: {
|
|
94
|
+
enabled: boolean;
|
|
95
|
+
label: string;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
experimentalMarker: {
|
|
100
|
+
enabled: boolean;
|
|
101
|
+
label: string;
|
|
102
|
+
color: DumbZoneColor;
|
|
103
|
+
};
|
|
104
|
+
gitStatus: {
|
|
105
|
+
enabled: boolean;
|
|
106
|
+
refreshIntervalMs: number;
|
|
107
|
+
gitTimeoutMs: number;
|
|
108
|
+
ghTimeoutMs: number;
|
|
109
|
+
};
|
|
117
110
|
}
|
|
118
111
|
|
|
119
112
|
type UsageIdentity = {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
renderKey: string;
|
|
114
|
+
usageKey: string;
|
|
115
|
+
oauth: boolean;
|
|
116
|
+
accountId?: string;
|
|
124
117
|
};
|
|
125
118
|
|
|
126
119
|
type UsageSessionState = {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
gitCache?: GitFooterCache;
|
|
120
|
+
config: MinimalFooterConfig;
|
|
121
|
+
snapshot?: UsageSnapshot;
|
|
122
|
+
lastFetchedAt?: number;
|
|
123
|
+
loading: boolean;
|
|
124
|
+
availability?: UsageAvailability;
|
|
125
|
+
usageIdentity?: UsageIdentity;
|
|
126
|
+
requestVersion: number;
|
|
127
|
+
disposed: boolean;
|
|
128
|
+
inflight?: Promise<void>;
|
|
129
|
+
requestRender?: () => void;
|
|
130
|
+
usageRefreshTimer?: TimerHandle;
|
|
131
|
+
gitCache?: GitFooterCache;
|
|
140
132
|
};
|
|
141
133
|
|
|
142
134
|
type ProjectConfigContext = {
|
|
143
|
-
|
|
144
|
-
|
|
135
|
+
cwd: string;
|
|
136
|
+
isProjectTrusted?: () => boolean;
|
|
145
137
|
};
|
|
146
138
|
|
|
147
139
|
type GitStatusSnapshot = {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
140
|
+
branch?: string;
|
|
141
|
+
staged: number;
|
|
142
|
+
unstaged: number;
|
|
143
|
+
untracked: number;
|
|
144
|
+
conflict: number;
|
|
145
|
+
ahead: number;
|
|
146
|
+
behind: number;
|
|
155
147
|
};
|
|
156
148
|
|
|
157
149
|
type PullRequestSnapshot = {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
150
|
+
number?: number | string;
|
|
151
|
+
state?: string;
|
|
152
|
+
isDraft?: boolean;
|
|
153
|
+
url?: string;
|
|
154
|
+
title?: string;
|
|
163
155
|
};
|
|
164
156
|
|
|
165
157
|
type CommandResult = {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
158
|
+
stdout: string;
|
|
159
|
+
stderr: string;
|
|
160
|
+
exitCode: number | null;
|
|
169
161
|
};
|
|
170
162
|
|
|
171
163
|
type CommandRunner = (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
164
|
+
command: string,
|
|
165
|
+
args: readonly string[],
|
|
166
|
+
options: { cwd: string; signal: AbortSignal },
|
|
175
167
|
) => Promise<CommandResult>;
|
|
176
168
|
|
|
177
169
|
type RunCommandSafelyResult =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
170
|
+
| { kind: "ok"; result: CommandResult }
|
|
171
|
+
| { kind: "transient" }
|
|
172
|
+
| { kind: "unavailable" };
|
|
181
173
|
|
|
182
174
|
type TimerHandle = unknown;
|
|
183
175
|
|
|
184
176
|
type Clock = {
|
|
185
|
-
|
|
186
|
-
|
|
177
|
+
setInterval(callback: () => void, ms: number): TimerHandle;
|
|
178
|
+
clearInterval(handle: TimerHandle): void;
|
|
187
179
|
};
|
|
188
180
|
|
|
189
181
|
type GitFooterCacheOptions = {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
182
|
+
cwd: () => string;
|
|
183
|
+
canRun?: () => boolean;
|
|
184
|
+
runner?: CommandRunner;
|
|
185
|
+
clock?: Clock;
|
|
186
|
+
refreshIntervalMs?: number;
|
|
187
|
+
gitTimeoutMs?: number;
|
|
188
|
+
ghTimeoutMs?: number;
|
|
189
|
+
onChange?: () => void;
|
|
198
190
|
};
|
|
199
191
|
|
|
200
192
|
const BRANCH_HEAD_PREFIX = "# branch.head ";
|
|
201
193
|
const BRANCH_AB_PREFIX = "# branch.ab ";
|
|
202
194
|
const STATUS_SEPARATOR = " • ";
|
|
203
195
|
const GIT_STATUS_ARGS = [
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
196
|
+
"--no-optional-locks",
|
|
197
|
+
"-c",
|
|
198
|
+
"core.fsmonitor=false",
|
|
199
|
+
"status",
|
|
200
|
+
"--porcelain=v2",
|
|
201
|
+
"--branch",
|
|
210
202
|
] as const;
|
|
211
203
|
const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number,state,isDraft,url,title"] as const;
|
|
212
204
|
|
|
213
205
|
function readConfigFile(path: string): RecursivePartial<MinimalFooterConfig> {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
206
|
+
if (!existsSync(path)) return {};
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(readFileSync(path, "utf-8")) as RecursivePartial<MinimalFooterConfig>;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error(`Warning: Could not parse ${path}: ${error}`);
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
222
214
|
}
|
|
223
215
|
|
|
224
216
|
function mergeConfig(
|
|
225
|
-
|
|
226
|
-
|
|
217
|
+
base: MinimalFooterConfig,
|
|
218
|
+
overrides: RecursivePartial<MinimalFooterConfig>,
|
|
227
219
|
): MinimalFooterConfig {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
enabled: normalizeBoolean(gitStatus?.enabled, base.gitStatus.enabled),
|
|
289
|
-
refreshIntervalMs: normalizePositiveNumber(
|
|
290
|
-
gitStatus?.refreshIntervalMs,
|
|
291
|
-
base.gitStatus.refreshIntervalMs,
|
|
292
|
-
),
|
|
293
|
-
gitTimeoutMs: normalizePositiveNumber(
|
|
294
|
-
gitStatus?.gitTimeoutMs,
|
|
295
|
-
base.gitStatus.gitTimeoutMs,
|
|
296
|
-
),
|
|
297
|
-
ghTimeoutMs: normalizePositiveNumber(
|
|
298
|
-
gitStatus?.ghTimeoutMs,
|
|
299
|
-
base.gitStatus.ghTimeoutMs,
|
|
300
|
-
),
|
|
301
|
-
},
|
|
302
|
-
};
|
|
220
|
+
const context = overrides.context;
|
|
221
|
+
const dumbZone = context?.dumbZone;
|
|
222
|
+
const codexUsage = overrides.codexUsage;
|
|
223
|
+
const primaryWindow = codexUsage?.windows?.primary;
|
|
224
|
+
const secondaryWindow = codexUsage?.windows?.secondary;
|
|
225
|
+
const experimentalMarker = overrides.experimentalMarker;
|
|
226
|
+
const gitStatus = overrides.gitStatus;
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
context: {
|
|
230
|
+
showPercent: normalizeBoolean(context?.showPercent, base.context.showPercent),
|
|
231
|
+
dumbZone: {
|
|
232
|
+
enabled: normalizeBoolean(dumbZone?.enabled, base.context.dumbZone.enabled),
|
|
233
|
+
thresholdTokens: normalizeNonNegativeNumber(
|
|
234
|
+
dumbZone?.thresholdTokens,
|
|
235
|
+
base.context.dumbZone.thresholdTokens,
|
|
236
|
+
),
|
|
237
|
+
label: normalizeLabel(dumbZone?.label, base.context.dumbZone.label),
|
|
238
|
+
color: normalizeDumbZoneColor(dumbZone?.color, base.context.dumbZone.color),
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
codexUsage: {
|
|
242
|
+
enabled: normalizeBoolean(codexUsage?.enabled, base.codexUsage.enabled),
|
|
243
|
+
cacheTtlMs: normalizeNonNegativeNumber(codexUsage?.cacheTtlMs, base.codexUsage.cacheTtlMs),
|
|
244
|
+
requestTimeoutMs: normalizePositiveNumber(
|
|
245
|
+
codexUsage?.requestTimeoutMs,
|
|
246
|
+
base.codexUsage.requestTimeoutMs,
|
|
247
|
+
),
|
|
248
|
+
windows: {
|
|
249
|
+
primary: {
|
|
250
|
+
enabled: normalizeBoolean(
|
|
251
|
+
primaryWindow?.enabled,
|
|
252
|
+
base.codexUsage.windows.primary.enabled,
|
|
253
|
+
),
|
|
254
|
+
label: normalizeLabel(primaryWindow?.label, base.codexUsage.windows.primary.label),
|
|
255
|
+
},
|
|
256
|
+
secondary: {
|
|
257
|
+
enabled: normalizeBoolean(
|
|
258
|
+
secondaryWindow?.enabled,
|
|
259
|
+
base.codexUsage.windows.secondary.enabled,
|
|
260
|
+
),
|
|
261
|
+
label: normalizeLabel(secondaryWindow?.label, base.codexUsage.windows.secondary.label),
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
experimentalMarker: {
|
|
266
|
+
enabled: normalizeBoolean(experimentalMarker?.enabled, base.experimentalMarker.enabled),
|
|
267
|
+
label: normalizeLabel(experimentalMarker?.label, base.experimentalMarker.label),
|
|
268
|
+
color: normalizeDumbZoneColor(experimentalMarker?.color, base.experimentalMarker.color),
|
|
269
|
+
},
|
|
270
|
+
gitStatus: {
|
|
271
|
+
enabled: normalizeBoolean(gitStatus?.enabled, base.gitStatus.enabled),
|
|
272
|
+
refreshIntervalMs: normalizePositiveNumber(
|
|
273
|
+
gitStatus?.refreshIntervalMs,
|
|
274
|
+
base.gitStatus.refreshIntervalMs,
|
|
275
|
+
),
|
|
276
|
+
gitTimeoutMs: normalizePositiveNumber(gitStatus?.gitTimeoutMs, base.gitStatus.gitTimeoutMs),
|
|
277
|
+
ghTimeoutMs: normalizePositiveNumber(gitStatus?.ghTimeoutMs, base.gitStatus.ghTimeoutMs),
|
|
278
|
+
},
|
|
279
|
+
};
|
|
303
280
|
}
|
|
304
281
|
|
|
305
282
|
function normalizeBoolean(value: unknown, fallback: boolean): boolean {
|
|
306
|
-
|
|
283
|
+
return typeof value === "boolean" ? value : fallback;
|
|
307
284
|
}
|
|
308
285
|
|
|
309
286
|
function normalizeNonNegativeNumber(value: unknown, fallback: number): number {
|
|
310
|
-
|
|
287
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
311
288
|
}
|
|
312
289
|
|
|
313
290
|
function normalizePositiveNumber(value: unknown, fallback: number): number {
|
|
314
|
-
|
|
291
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
315
292
|
}
|
|
316
293
|
|
|
317
294
|
function normalizeLabel(value: unknown, fallback: string): string {
|
|
318
|
-
|
|
295
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
319
296
|
}
|
|
320
297
|
|
|
321
298
|
function normalizeDumbZoneColor(value: unknown, fallback: DumbZoneColor): DumbZoneColor {
|
|
322
|
-
|
|
299
|
+
return DUMB_ZONE_COLORS.has(value as DumbZoneColor) ? (value as DumbZoneColor) : fallback;
|
|
323
300
|
}
|
|
324
301
|
|
|
325
302
|
function canReadProjectConfig(ctx: ProjectConfigContext): boolean {
|
|
326
|
-
|
|
303
|
+
return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted();
|
|
327
304
|
}
|
|
328
305
|
|
|
329
306
|
function findProjectConfigPath(cwd: string): string {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
307
|
+
let current = cwd;
|
|
308
|
+
while (true) {
|
|
309
|
+
const candidate = join(current, CONFIG_DIR_NAME, "minimal-footer.json");
|
|
310
|
+
if (existsSync(candidate)) return candidate;
|
|
311
|
+
|
|
312
|
+
const parent = dirname(current);
|
|
313
|
+
if (parent === current) return join(cwd, CONFIG_DIR_NAME, "minimal-footer.json");
|
|
314
|
+
current = parent;
|
|
315
|
+
}
|
|
339
316
|
}
|
|
340
317
|
|
|
341
318
|
function loadConfig(ctx: ProjectConfigContext): MinimalFooterConfig {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
319
|
+
const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "minimal-footer.json"));
|
|
320
|
+
const projectConfig = canReadProjectConfig(ctx)
|
|
321
|
+
? readConfigFile(findProjectConfigPath(ctx.cwd))
|
|
322
|
+
: {};
|
|
323
|
+
return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
|
|
345
324
|
}
|
|
346
325
|
|
|
347
326
|
function shouldShowCodexUsage(config: MinimalFooterConfig): boolean {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
327
|
+
return (
|
|
328
|
+
config.codexUsage.enabled &&
|
|
329
|
+
(config.codexUsage.windows.primary.enabled || config.codexUsage.windows.secondary.enabled)
|
|
330
|
+
);
|
|
352
331
|
}
|
|
353
332
|
|
|
354
333
|
function shouldShowExperimentalMarker(config: MinimalFooterConfig): boolean {
|
|
355
|
-
|
|
334
|
+
return config.experimentalMarker.enabled && process.env.PI_EXPERIMENTAL === "1";
|
|
356
335
|
}
|
|
357
336
|
|
|
358
337
|
function shouldShowGitStatus(config: MinimalFooterConfig): boolean {
|
|
359
|
-
|
|
338
|
+
return config.gitStatus.enabled;
|
|
360
339
|
}
|
|
361
340
|
|
|
362
341
|
function canRunProjectGit(ctx: ProjectConfigContext): boolean {
|
|
363
|
-
|
|
342
|
+
return canReadProjectConfig(ctx);
|
|
364
343
|
}
|
|
365
344
|
|
|
366
345
|
function createEmptyGitStatus(): GitStatusSnapshot {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
346
|
+
return {
|
|
347
|
+
branch: undefined,
|
|
348
|
+
staged: 0,
|
|
349
|
+
unstaged: 0,
|
|
350
|
+
untracked: 0,
|
|
351
|
+
conflict: 0,
|
|
352
|
+
ahead: 0,
|
|
353
|
+
behind: 0,
|
|
354
|
+
};
|
|
376
355
|
}
|
|
377
356
|
|
|
378
357
|
function positiveCount(value: number): number {
|
|
379
|
-
|
|
358
|
+
return Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0;
|
|
380
359
|
}
|
|
381
360
|
|
|
382
361
|
function addTrackedStatusCounts(status: GitStatusSnapshot, xy: string): void {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
362
|
+
if (xy.length !== 2) return;
|
|
363
|
+
if (xy[0] !== ".") status.staged += 1;
|
|
364
|
+
if (xy[1] !== ".") status.unstaged += 1;
|
|
386
365
|
}
|
|
387
366
|
|
|
388
367
|
function parseBranchAheadBehind(line: string, status: GitStatusSnapshot): void {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
368
|
+
const match = /^# branch\.ab \+(\d+) -(\d+)$/.exec(line);
|
|
369
|
+
if (!match) return;
|
|
370
|
+
status.ahead = Number.parseInt(match[1]!, 10);
|
|
371
|
+
status.behind = Number.parseInt(match[2]!, 10);
|
|
393
372
|
}
|
|
394
373
|
|
|
395
374
|
function normalizeBranchHead(value: string): string {
|
|
396
|
-
|
|
397
|
-
|
|
375
|
+
const branch = value.trim();
|
|
376
|
+
return branch === "(detached)" ? "detached" : branch;
|
|
398
377
|
}
|
|
399
378
|
|
|
400
379
|
function parseGitStatusPorcelainV2(output: string): GitStatusSnapshot {
|
|
401
|
-
|
|
380
|
+
const status = createEmptyGitStatus();
|
|
402
381
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
382
|
+
for (const rawLine of output.split("\n")) {
|
|
383
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
384
|
+
if (!line) continue;
|
|
406
385
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
386
|
+
if (line.startsWith(BRANCH_HEAD_PREFIX)) {
|
|
387
|
+
status.branch = normalizeBranchHead(line.slice(BRANCH_HEAD_PREFIX.length)) || undefined;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
411
390
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
391
|
+
if (line.startsWith(BRANCH_AB_PREFIX)) {
|
|
392
|
+
parseBranchAheadBehind(line, status);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
416
395
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
396
|
+
if (line.startsWith("1 ") || line.startsWith("2 ")) {
|
|
397
|
+
addTrackedStatusCounts(status, line.slice(2, 4));
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
421
400
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
401
|
+
if (line.startsWith("u ")) {
|
|
402
|
+
status.conflict += 1;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
426
405
|
|
|
427
|
-
|
|
428
|
-
|
|
406
|
+
if (line.startsWith("? ")) status.untracked += 1;
|
|
407
|
+
}
|
|
429
408
|
|
|
430
|
-
|
|
409
|
+
return status;
|
|
431
410
|
}
|
|
432
411
|
|
|
433
412
|
function formatGitStatusFooterSegment(status: GitStatusSnapshot | undefined): string | undefined {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
413
|
+
if (!status) return undefined;
|
|
414
|
+
|
|
415
|
+
const parts: string[] = [];
|
|
416
|
+
const indicators: Array<[string, number]> = [
|
|
417
|
+
["!", positiveCount(status.conflict)],
|
|
418
|
+
["+", positiveCount(status.staged)],
|
|
419
|
+
["~", positiveCount(status.unstaged)],
|
|
420
|
+
["?", positiveCount(status.untracked)],
|
|
421
|
+
["↑", positiveCount(status.ahead)],
|
|
422
|
+
["↓", positiveCount(status.behind)],
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
for (const [prefix, count] of indicators) {
|
|
426
|
+
if (count > 0) parts.push(`${prefix}${count}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
451
430
|
}
|
|
452
431
|
|
|
453
|
-
function formatPullRequestFooterSegment(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
432
|
+
function formatPullRequestFooterSegment(
|
|
433
|
+
pullRequest: PullRequestSnapshot | undefined,
|
|
434
|
+
): string | undefined {
|
|
435
|
+
const value = pullRequest?.number;
|
|
436
|
+
if (Number.isSafeInteger(value) && Number(value) > 0) return `PR #${value}`;
|
|
437
|
+
if (typeof value === "string") {
|
|
438
|
+
const trimmed = value.trim();
|
|
439
|
+
if (/^[1-9]\d*$/.test(trimmed)) return `PR #${trimmed}`;
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
461
442
|
}
|
|
462
443
|
|
|
463
444
|
function formatGitFooterStatus(
|
|
464
|
-
|
|
465
|
-
|
|
445
|
+
status: GitStatusSnapshot | undefined,
|
|
446
|
+
pullRequest: PullRequestSnapshot | undefined,
|
|
466
447
|
): string | undefined {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
448
|
+
const parts = [
|
|
449
|
+
formatGitStatusFooterSegment(status),
|
|
450
|
+
formatPullRequestFooterSegment(pullRequest),
|
|
451
|
+
].filter((part): part is string => !!part);
|
|
452
|
+
return parts.length > 0 ? parts.join(STATUS_SEPARATOR) : undefined;
|
|
472
453
|
}
|
|
473
454
|
|
|
474
455
|
function parsePullRequestJson(stdout: string): PullRequestSnapshot | undefined {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
456
|
+
const trimmed = stdout.trim();
|
|
457
|
+
if (!trimmed) return undefined;
|
|
458
|
+
let parsed: unknown;
|
|
459
|
+
try {
|
|
460
|
+
parsed = JSON.parse(trimmed);
|
|
461
|
+
} catch {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
465
|
+
|
|
466
|
+
const record = parsed as Record<string, unknown>;
|
|
467
|
+
const snapshot: PullRequestSnapshot = {};
|
|
468
|
+
if (typeof record.number === "number" || typeof record.number === "string") {
|
|
469
|
+
snapshot.number = record.number;
|
|
470
|
+
}
|
|
471
|
+
if (typeof record.state === "string") snapshot.state = record.state;
|
|
472
|
+
if (typeof record.isDraft === "boolean") snapshot.isDraft = record.isDraft;
|
|
473
|
+
if (typeof record.url === "string") snapshot.url = record.url;
|
|
474
|
+
if (typeof record.title === "string") snapshot.title = record.title;
|
|
475
|
+
return snapshot;
|
|
495
476
|
}
|
|
496
477
|
|
|
497
478
|
function gitStatusSnapshotsEqual(
|
|
498
|
-
|
|
499
|
-
|
|
479
|
+
left: GitStatusSnapshot | undefined,
|
|
480
|
+
right: GitStatusSnapshot | undefined,
|
|
500
481
|
): boolean {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
482
|
+
return (
|
|
483
|
+
left?.branch === right?.branch &&
|
|
484
|
+
left?.staged === right?.staged &&
|
|
485
|
+
left?.unstaged === right?.unstaged &&
|
|
486
|
+
left?.untracked === right?.untracked &&
|
|
487
|
+
left?.conflict === right?.conflict &&
|
|
488
|
+
left?.ahead === right?.ahead &&
|
|
489
|
+
left?.behind === right?.behind
|
|
490
|
+
);
|
|
510
491
|
}
|
|
511
492
|
|
|
512
493
|
function pullRequestSnapshotsEqual(
|
|
513
|
-
|
|
514
|
-
|
|
494
|
+
left: PullRequestSnapshot | undefined,
|
|
495
|
+
right: PullRequestSnapshot | undefined,
|
|
515
496
|
): boolean {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
497
|
+
return (
|
|
498
|
+
left?.number === right?.number &&
|
|
499
|
+
left?.state === right?.state &&
|
|
500
|
+
left?.isDraft === right?.isDraft &&
|
|
501
|
+
left?.url === right?.url &&
|
|
502
|
+
left?.title === right?.title
|
|
503
|
+
);
|
|
523
504
|
}
|
|
524
505
|
|
|
525
506
|
function defaultRunner(
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
507
|
+
command: string,
|
|
508
|
+
args: readonly string[],
|
|
509
|
+
options: { cwd: string; signal: AbortSignal },
|
|
529
510
|
): Promise<CommandResult> {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
511
|
+
return new Promise((resolve, reject) => {
|
|
512
|
+
let child;
|
|
513
|
+
try {
|
|
514
|
+
child = spawn(command, [...args], {
|
|
515
|
+
cwd: options.cwd,
|
|
516
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
517
|
+
windowsHide: true,
|
|
518
|
+
});
|
|
519
|
+
} catch (error) {
|
|
520
|
+
reject(error);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let stdout = "";
|
|
525
|
+
let stderr = "";
|
|
526
|
+
let settled = false;
|
|
527
|
+
|
|
528
|
+
const finish = (result: CommandResult | Error) => {
|
|
529
|
+
if (settled) return;
|
|
530
|
+
settled = true;
|
|
531
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
532
|
+
if (result instanceof Error) reject(result);
|
|
533
|
+
else resolve(result);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const onAbort = () => {
|
|
537
|
+
try {
|
|
538
|
+
child.kill("SIGTERM");
|
|
539
|
+
} catch {
|
|
540
|
+
// Ignore: process may already be gone.
|
|
541
|
+
}
|
|
542
|
+
finish(new Error("aborted"));
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
child.stdout?.on("data", (chunk: Buffer | string) => {
|
|
546
|
+
stdout += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
547
|
+
});
|
|
548
|
+
child.stderr?.on("data", (chunk: Buffer | string) => {
|
|
549
|
+
stderr += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
550
|
+
});
|
|
551
|
+
child.on("error", finish);
|
|
552
|
+
child.on("close", (code) => finish({ stdout, stderr, exitCode: code }));
|
|
553
|
+
|
|
554
|
+
if (options.signal.aborted) {
|
|
555
|
+
onAbort();
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
559
|
+
});
|
|
579
560
|
}
|
|
580
561
|
|
|
581
562
|
function defaultClock(): Clock {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
563
|
+
return {
|
|
564
|
+
setInterval(callback, ms) {
|
|
565
|
+
const handle = setInterval(callback, ms);
|
|
566
|
+
(handle as { unref?: () => void }).unref?.();
|
|
567
|
+
return handle;
|
|
568
|
+
},
|
|
569
|
+
clearInterval(handle) {
|
|
570
|
+
clearInterval(handle as ReturnType<typeof setInterval>);
|
|
571
|
+
},
|
|
572
|
+
};
|
|
592
573
|
}
|
|
593
574
|
|
|
594
575
|
function isCommandUnavailableError(error: unknown): boolean {
|
|
595
|
-
|
|
576
|
+
return !!error && typeof error === "object" && (error as { code?: unknown }).code === "ENOENT";
|
|
596
577
|
}
|
|
597
578
|
|
|
598
579
|
class GitFooterCache {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
580
|
+
private readonly cwd: () => string;
|
|
581
|
+
private readonly canRun: () => boolean;
|
|
582
|
+
private readonly runner: CommandRunner;
|
|
583
|
+
private readonly clock: Clock;
|
|
584
|
+
private readonly refreshIntervalMs: number;
|
|
585
|
+
private readonly gitTimeoutMs: number;
|
|
586
|
+
private readonly ghTimeoutMs: number;
|
|
587
|
+
private readonly onChange: (() => void) | undefined;
|
|
588
|
+
|
|
589
|
+
private intervalHandle: TimerHandle | undefined;
|
|
590
|
+
private readonly inflightControllers = new Set<AbortController>();
|
|
591
|
+
private disposed = false;
|
|
592
|
+
private refreshInFlight: Promise<void> | undefined;
|
|
593
|
+
private statusSnapshot: GitStatusSnapshot | undefined;
|
|
594
|
+
private pullRequestSnapshot: PullRequestSnapshot | undefined;
|
|
595
|
+
private lastSeenBranch: string | undefined;
|
|
596
|
+
|
|
597
|
+
constructor(options: GitFooterCacheOptions) {
|
|
598
|
+
this.cwd = options.cwd;
|
|
599
|
+
this.canRun = options.canRun ?? (() => true);
|
|
600
|
+
this.runner = options.runner ?? defaultRunner;
|
|
601
|
+
this.clock = options.clock ?? defaultClock();
|
|
602
|
+
this.refreshIntervalMs =
|
|
603
|
+
options.refreshIntervalMs ?? DEFAULT_CONFIG.gitStatus.refreshIntervalMs;
|
|
604
|
+
this.gitTimeoutMs = options.gitTimeoutMs ?? DEFAULT_CONFIG.gitStatus.gitTimeoutMs;
|
|
605
|
+
this.ghTimeoutMs = options.ghTimeoutMs ?? DEFAULT_CONFIG.gitStatus.ghTimeoutMs;
|
|
606
|
+
this.onChange = options.onChange;
|
|
607
|
+
|
|
608
|
+
this.intervalHandle = this.clock.setInterval(() => {
|
|
609
|
+
void this.refresh();
|
|
610
|
+
}, this.refreshIntervalMs);
|
|
611
|
+
void this.refresh();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
getStatusSnapshot(): GitStatusSnapshot | undefined {
|
|
615
|
+
return this.statusSnapshot;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
getPullRequestSnapshot(): PullRequestSnapshot | undefined {
|
|
619
|
+
return this.pullRequestSnapshot;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
refresh(): Promise<void> {
|
|
623
|
+
if (this.disposed) return Promise.resolve();
|
|
624
|
+
if (!this.canRun()) {
|
|
625
|
+
const previousStatusSnapshot = this.statusSnapshot;
|
|
626
|
+
const previousPullRequestSnapshot = this.pullRequestSnapshot;
|
|
627
|
+
this.statusSnapshot = undefined;
|
|
628
|
+
this.pullRequestSnapshot = undefined;
|
|
629
|
+
this.lastSeenBranch = undefined;
|
|
630
|
+
this.emitChangeIfSnapshotsChanged(previousStatusSnapshot, previousPullRequestSnapshot);
|
|
631
|
+
return Promise.resolve();
|
|
632
|
+
}
|
|
633
|
+
if (this.refreshInFlight) return this.refreshInFlight;
|
|
634
|
+
const run = this.runRefresh()
|
|
635
|
+
.finally(() => {
|
|
636
|
+
this.refreshInFlight = undefined;
|
|
637
|
+
})
|
|
638
|
+
.catch(() => undefined);
|
|
639
|
+
this.refreshInFlight = run;
|
|
640
|
+
return run;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private async runRefresh(): Promise<void> {
|
|
644
|
+
const previousStatusSnapshot = this.statusSnapshot;
|
|
645
|
+
const previousPullRequestSnapshot = this.pullRequestSnapshot;
|
|
646
|
+
|
|
647
|
+
const result = await this.fetchGitStatus();
|
|
648
|
+
if (this.disposed) return;
|
|
649
|
+
if (result.kind === "transient") return;
|
|
650
|
+
if (result.kind === "not-a-repo" || result.kind === "unavailable") {
|
|
651
|
+
this.statusSnapshot = undefined;
|
|
652
|
+
this.pullRequestSnapshot = undefined;
|
|
653
|
+
this.lastSeenBranch = undefined;
|
|
654
|
+
this.emitChangeIfSnapshotsChanged(previousStatusSnapshot, previousPullRequestSnapshot);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const status = result.status;
|
|
659
|
+
this.statusSnapshot = status;
|
|
660
|
+
|
|
661
|
+
const branch = typeof status.branch === "string" ? status.branch : undefined;
|
|
662
|
+
const isValidBranch = !!branch && branch !== "detached";
|
|
663
|
+
const branchChanged = branch !== this.lastSeenBranch;
|
|
664
|
+
|
|
665
|
+
if (branchChanged) this.pullRequestSnapshot = undefined;
|
|
666
|
+
this.lastSeenBranch = branch;
|
|
667
|
+
|
|
668
|
+
if (!isValidBranch) {
|
|
669
|
+
this.pullRequestSnapshot = undefined;
|
|
670
|
+
this.emitChangeIfSnapshotsChanged(previousStatusSnapshot, previousPullRequestSnapshot);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const pr = await this.fetchPullRequest();
|
|
675
|
+
if (this.disposed) return;
|
|
676
|
+
if (pr.kind === "ok") this.pullRequestSnapshot = pr.pullRequest;
|
|
677
|
+
else if (pr.kind === "not-found" || pr.kind === "unavailable")
|
|
678
|
+
this.pullRequestSnapshot = undefined;
|
|
679
|
+
|
|
680
|
+
this.emitChangeIfSnapshotsChanged(previousStatusSnapshot, previousPullRequestSnapshot);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private emitChangeIfSnapshotsChanged(
|
|
684
|
+
previousStatusSnapshot: GitStatusSnapshot | undefined,
|
|
685
|
+
previousPullRequestSnapshot: PullRequestSnapshot | undefined,
|
|
686
|
+
): void {
|
|
687
|
+
if (this.disposed) return;
|
|
688
|
+
if (
|
|
689
|
+
gitStatusSnapshotsEqual(previousStatusSnapshot, this.statusSnapshot) &&
|
|
690
|
+
pullRequestSnapshotsEqual(previousPullRequestSnapshot, this.pullRequestSnapshot)
|
|
691
|
+
) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
this.onChange?.();
|
|
696
|
+
} catch {
|
|
697
|
+
// Rendering hooks should not break refreshes.
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private async fetchGitStatus(): Promise<
|
|
702
|
+
| { kind: "ok"; status: GitStatusSnapshot }
|
|
703
|
+
| { kind: "not-a-repo" }
|
|
704
|
+
| { kind: "transient" }
|
|
705
|
+
| { kind: "unavailable" }
|
|
706
|
+
> {
|
|
707
|
+
const result = await this.runCommandSafely("git", GIT_STATUS_ARGS, this.gitTimeoutMs);
|
|
708
|
+
if (result.kind !== "ok") return result;
|
|
709
|
+
if (result.result.exitCode !== 0) return { kind: "not-a-repo" };
|
|
710
|
+
return { kind: "ok", status: parseGitStatusPorcelainV2(result.result.stdout) };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
private async fetchPullRequest(): Promise<
|
|
714
|
+
| { kind: "ok"; pullRequest: PullRequestSnapshot | undefined }
|
|
715
|
+
| { kind: "not-found" }
|
|
716
|
+
| { kind: "transient" }
|
|
717
|
+
| { kind: "unavailable" }
|
|
718
|
+
> {
|
|
719
|
+
const result = await this.runCommandSafely("gh", GH_PR_VIEW_ARGS, this.ghTimeoutMs);
|
|
720
|
+
if (result.kind !== "ok") return result;
|
|
721
|
+
if (result.result.exitCode !== 0) return { kind: "not-found" };
|
|
722
|
+
return { kind: "ok", pullRequest: parsePullRequestJson(result.result.stdout) };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
private async runCommandSafely(
|
|
726
|
+
command: string,
|
|
727
|
+
args: readonly string[],
|
|
728
|
+
timeoutMs: number,
|
|
729
|
+
): Promise<RunCommandSafelyResult> {
|
|
730
|
+
if (this.disposed) return { kind: "transient" };
|
|
731
|
+
const controller = new AbortController();
|
|
732
|
+
this.inflightControllers.add(controller);
|
|
733
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
734
|
+
try {
|
|
735
|
+
const result = await this.runner(command, args, {
|
|
736
|
+
cwd: this.cwd(),
|
|
737
|
+
signal: controller.signal,
|
|
738
|
+
});
|
|
739
|
+
return { kind: "ok", result };
|
|
740
|
+
} catch (error) {
|
|
741
|
+
return isCommandUnavailableError(error) ? { kind: "unavailable" } : { kind: "transient" };
|
|
742
|
+
} finally {
|
|
743
|
+
clearTimeout(timeoutId);
|
|
744
|
+
this.inflightControllers.delete(controller);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
dispose(): void {
|
|
749
|
+
if (this.disposed) return;
|
|
750
|
+
this.disposed = true;
|
|
751
|
+
if (this.intervalHandle !== undefined) {
|
|
752
|
+
this.clock.clearInterval(this.intervalHandle);
|
|
753
|
+
this.intervalHandle = undefined;
|
|
754
|
+
}
|
|
755
|
+
for (const controller of this.inflightControllers) controller.abort();
|
|
756
|
+
this.inflightControllers.clear();
|
|
757
|
+
}
|
|
772
758
|
}
|
|
773
759
|
|
|
774
760
|
function renderFooterLines(options: {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
761
|
+
width: number;
|
|
762
|
+
cwd: string;
|
|
763
|
+
config: MinimalFooterConfig;
|
|
764
|
+
branch: string;
|
|
765
|
+
gitStatus?: string;
|
|
766
|
+
contextUsage?: { percent?: number | null; tokens?: number | null };
|
|
767
|
+
modelId?: string;
|
|
768
|
+
modelProvider?: string;
|
|
769
|
+
thinkingLevel: string;
|
|
770
|
+
theme: { fg(color: DumbZoneColor | "dim", text: string): string };
|
|
771
|
+
usageSnapshot?: UsageSnapshot;
|
|
772
|
+
usageStatus?: UsageAvailability;
|
|
787
773
|
}): string[] {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
truncateToWidth(repoStyled, options.width),
|
|
845
|
-
truncateToWidth(contextStyled, options.width),
|
|
846
|
-
truncateToWidth(modelStyled, options.width),
|
|
847
|
-
];
|
|
774
|
+
const repo = basename(options.cwd);
|
|
775
|
+
const branchText = options.gitStatus
|
|
776
|
+
? [options.branch, options.gitStatus].filter(Boolean).join(" · ")
|
|
777
|
+
: options.branch;
|
|
778
|
+
|
|
779
|
+
const context =
|
|
780
|
+
options.contextUsage?.percent == null ? "?" : `${options.contextUsage.percent.toFixed(1)}%`;
|
|
781
|
+
const dumbZone = options.config.context.dumbZone;
|
|
782
|
+
const inDumbZone =
|
|
783
|
+
dumbZone.enabled && (options.contextUsage?.tokens ?? 0) > dumbZone.thresholdTokens;
|
|
784
|
+
const usageEnabled = shouldShowCodexUsage(options.config);
|
|
785
|
+
const usageSummary =
|
|
786
|
+
usageEnabled && isOpenAICodexProvider(options.modelProvider)
|
|
787
|
+
? formatUsageSummary(options.usageSnapshot, options.config.codexUsage.windows)
|
|
788
|
+
: undefined;
|
|
789
|
+
const usageStatus =
|
|
790
|
+
usageEnabled &&
|
|
791
|
+
(isOpenAICodexProvider(options.modelProvider) || isOpenAIProvider(options.modelProvider))
|
|
792
|
+
? formatUsageStatus(options.usageStatus)
|
|
793
|
+
: undefined;
|
|
794
|
+
|
|
795
|
+
const model = options.modelId ?? "no-model";
|
|
796
|
+
const modelText = options.thinkingLevel === "off" ? model : `${model} ${options.thinkingLevel}`;
|
|
797
|
+
|
|
798
|
+
const branchStyled = options.theme.fg("dim", branchText);
|
|
799
|
+
const repoStyled = options.theme.fg("dim", repo);
|
|
800
|
+
const contextParts: string[] = [];
|
|
801
|
+
if (options.config.context.showPercent) contextParts.push(options.theme.fg("dim", context));
|
|
802
|
+
if (inDumbZone) contextParts.push(options.theme.fg(dumbZone.color, dumbZone.label));
|
|
803
|
+
if (usageSummary) contextParts.push(options.theme.fg("dim", usageSummary));
|
|
804
|
+
else if (usageStatus) contextParts.push(options.theme.fg("dim", usageStatus));
|
|
805
|
+
if (shouldShowExperimentalMarker(options.config)) {
|
|
806
|
+
const marker = options.config.experimentalMarker;
|
|
807
|
+
contextParts.push(options.theme.fg(marker.color, marker.label));
|
|
808
|
+
}
|
|
809
|
+
const contextStyled = contextParts.join(options.theme.fg("dim", " · "));
|
|
810
|
+
const modelStyled = options.theme.fg("dim", modelText);
|
|
811
|
+
|
|
812
|
+
const renderSplitLine = (left: string, right: string): string => {
|
|
813
|
+
const gap = " ".repeat(Math.max(2, options.width - visibleWidth(left) - visibleWidth(right)));
|
|
814
|
+
return truncateToWidth(left + gap + right, options.width);
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const line1Fits = visibleWidth(branchStyled) + visibleWidth(repoStyled) + 2 <= options.width;
|
|
818
|
+
const line2Fits = visibleWidth(contextStyled) + visibleWidth(modelStyled) + 2 <= options.width;
|
|
819
|
+
|
|
820
|
+
if (line1Fits && line2Fits) {
|
|
821
|
+
return [renderSplitLine(branchStyled, repoStyled), renderSplitLine(contextStyled, modelStyled)];
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return [
|
|
825
|
+
truncateToWidth(branchStyled, options.width),
|
|
826
|
+
truncateToWidth(repoStyled, options.width),
|
|
827
|
+
truncateToWidth(contextStyled, options.width),
|
|
828
|
+
truncateToWidth(modelStyled, options.width),
|
|
829
|
+
];
|
|
848
830
|
}
|
|
849
831
|
|
|
850
832
|
function clearUsageState(state: UsageSessionState): void {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
state.availability = undefined;
|
|
833
|
+
state.snapshot = undefined;
|
|
834
|
+
state.lastFetchedAt = undefined;
|
|
835
|
+
state.loading = false;
|
|
836
|
+
state.availability = undefined;
|
|
856
837
|
}
|
|
857
838
|
|
|
858
839
|
function isModelUsingOAuth(ctx: ExtensionContext): boolean {
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
840
|
+
if (!ctx.model) return false;
|
|
841
|
+
try {
|
|
842
|
+
return ctx.modelRegistry.isUsingOAuth(ctx.model);
|
|
843
|
+
} catch {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
865
846
|
}
|
|
866
847
|
|
|
867
848
|
function buildUsageRenderKey(
|
|
868
|
-
|
|
869
|
-
|
|
849
|
+
model: NonNullable<ExtensionContext["model"]>,
|
|
850
|
+
oauth: boolean,
|
|
870
851
|
): string {
|
|
871
|
-
|
|
852
|
+
return [model.provider, model.api, model.id, oauth ? "oauth" : "api-key"].join("/");
|
|
872
853
|
}
|
|
873
854
|
|
|
874
855
|
function getUsageRenderKey(ctx: ExtensionContext): string | undefined {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
856
|
+
const model = ctx.model;
|
|
857
|
+
if (!model) return undefined;
|
|
858
|
+
return buildUsageRenderKey(model, isModelUsingOAuth(ctx));
|
|
878
859
|
}
|
|
879
860
|
|
|
880
861
|
/**
|
|
@@ -883,351 +864,345 @@ function getUsageRenderKey(ctx: ExtensionContext): string | undefined {
|
|
|
883
864
|
* auth.json or otherwise reloads account metadata.
|
|
884
865
|
*/
|
|
885
866
|
function getUsageIdentity(ctx: ExtensionContext): UsageIdentity | undefined {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
867
|
+
const model = ctx.model;
|
|
868
|
+
if (!model) return undefined;
|
|
869
|
+
|
|
870
|
+
const oauth = isModelUsingOAuth(ctx);
|
|
871
|
+
const renderKey = buildUsageRenderKey(model, oauth);
|
|
872
|
+
const accountId =
|
|
873
|
+
isOpenAICodexProvider(model.provider) && oauth
|
|
874
|
+
? getOpenAICodexAccountId(ctx.modelRegistry)
|
|
875
|
+
: undefined;
|
|
876
|
+
return {
|
|
877
|
+
renderKey,
|
|
878
|
+
usageKey: [renderKey, accountId ?? "account-unknown"].join("/"),
|
|
879
|
+
oauth,
|
|
880
|
+
accountId,
|
|
881
|
+
};
|
|
900
882
|
}
|
|
901
883
|
|
|
902
884
|
type UsageMode = "codex" | UsageAvailability;
|
|
903
885
|
|
|
904
886
|
function getUsageMode(
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
887
|
+
ctx: ExtensionContext,
|
|
888
|
+
config: MinimalFooterConfig,
|
|
889
|
+
identity?: UsageIdentity,
|
|
908
890
|
): UsageMode | undefined {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
891
|
+
if (!shouldShowCodexUsage(config)) return undefined;
|
|
892
|
+
const oauth = identity?.oauth ?? isModelUsingOAuth(ctx);
|
|
893
|
+
if (isOpenAICodexProvider(ctx.model?.provider)) {
|
|
894
|
+
return oauth ? "codex" : "unavailable";
|
|
895
|
+
}
|
|
896
|
+
if (isOpenAIProvider(ctx.model?.provider) && oauth) {
|
|
897
|
+
// Pi's new OpenAI ChatGPT OAuth flow targets api.openai.com. There is
|
|
898
|
+
// no documented account-usage contract for that token, so never reuse
|
|
899
|
+
// the legacy openai-codex WHAM request here.
|
|
900
|
+
return "unsupported";
|
|
901
|
+
}
|
|
902
|
+
return undefined;
|
|
921
903
|
}
|
|
922
904
|
|
|
923
|
-
function invalidateUsageState(
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
// The previous promise may still be resolving. Clearing the reference lets
|
|
935
|
-
// a newly selected provider/account start its own request; requestVersion
|
|
936
|
-
// prevents the old promise from repainting this state when it settles.
|
|
937
|
-
state.inflight = undefined;
|
|
905
|
+
function invalidateUsageState(state: UsageSessionState, usageIdentity?: UsageIdentity): void {
|
|
906
|
+
state.requestVersion += 1;
|
|
907
|
+
state.snapshot = undefined;
|
|
908
|
+
state.lastFetchedAt = undefined;
|
|
909
|
+
state.loading = false;
|
|
910
|
+
state.availability = undefined;
|
|
911
|
+
state.usageIdentity = usageIdentity;
|
|
912
|
+
// The previous promise may still be resolving. Clearing the reference lets
|
|
913
|
+
// a newly selected provider/account start its own request; requestVersion
|
|
914
|
+
// prevents the old promise from repainting this state when it settles.
|
|
915
|
+
state.inflight = undefined;
|
|
938
916
|
}
|
|
939
917
|
|
|
940
918
|
function usageRequestStillCurrent(
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
919
|
+
ctx: ExtensionContext,
|
|
920
|
+
state: UsageSessionState,
|
|
921
|
+
requestVersion: number,
|
|
922
|
+
requestUsageKey: string | undefined,
|
|
945
923
|
): boolean {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
924
|
+
if (
|
|
925
|
+
state.disposed ||
|
|
926
|
+
state.requestVersion !== requestVersion ||
|
|
927
|
+
state.usageIdentity?.usageKey !== requestUsageKey
|
|
928
|
+
) {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Account-only login changes do not emit an extension event in Pi 0.99.
|
|
933
|
+
// Re-read the lifecycle identity before accepting a response, but never
|
|
934
|
+
// perform this check from render().
|
|
935
|
+
const currentIdentity = getUsageIdentity(ctx);
|
|
936
|
+
if (currentIdentity?.usageKey === requestUsageKey) return true;
|
|
937
|
+
|
|
938
|
+
invalidateUsageState(state, currentIdentity);
|
|
939
|
+
state.requestRender?.();
|
|
940
|
+
void refreshUsageIfNeeded(ctx, state, true, currentIdentity);
|
|
941
|
+
return false;
|
|
964
942
|
}
|
|
965
943
|
|
|
966
944
|
async function refreshUsageIfNeeded(
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
945
|
+
ctx: ExtensionContext,
|
|
946
|
+
state: UsageSessionState,
|
|
947
|
+
force = false,
|
|
948
|
+
usageIdentity?: UsageIdentity,
|
|
971
949
|
): Promise<void> {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
950
|
+
if (state.disposed) return;
|
|
951
|
+
|
|
952
|
+
const resolvedUsageIdentity = usageIdentity ?? getUsageIdentity(ctx);
|
|
953
|
+
const config = state.config;
|
|
954
|
+
if (state.usageIdentity?.usageKey !== resolvedUsageIdentity?.usageKey) {
|
|
955
|
+
invalidateUsageState(state, resolvedUsageIdentity);
|
|
956
|
+
state.requestRender?.();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const mode = getUsageMode(ctx, config, resolvedUsageIdentity);
|
|
960
|
+
if (!mode) {
|
|
961
|
+
if (state.snapshot || state.availability || state.loading) {
|
|
962
|
+
clearUsageState(state);
|
|
963
|
+
state.requestRender?.();
|
|
964
|
+
}
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (mode !== "codex") {
|
|
969
|
+
const status: UsageAvailability = mode;
|
|
970
|
+
if (!force && state.availability === status && state.lastFetchedAt !== undefined) return;
|
|
971
|
+
clearUsageState(state);
|
|
972
|
+
state.availability = status;
|
|
973
|
+
state.lastFetchedAt = Date.now();
|
|
974
|
+
state.requestRender?.();
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const now = Date.now();
|
|
979
|
+
if (
|
|
980
|
+
!force &&
|
|
981
|
+
state.lastFetchedAt !== undefined &&
|
|
982
|
+
now - state.lastFetchedAt < config.codexUsage.cacheTtlMs
|
|
983
|
+
) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (state.inflight) return state.inflight;
|
|
988
|
+
|
|
989
|
+
const requestVersion = state.requestVersion;
|
|
990
|
+
const requestUsageKey = resolvedUsageIdentity?.usageKey;
|
|
991
|
+
state.loading = true;
|
|
992
|
+
state.availability = undefined;
|
|
993
|
+
state.requestRender?.();
|
|
994
|
+
const request = (async () => {
|
|
995
|
+
try {
|
|
996
|
+
const snapshot = await fetchOpenAICodexUsage(ctx.modelRegistry, {
|
|
997
|
+
timeoutMs: config.codexUsage.requestTimeoutMs,
|
|
998
|
+
// Passing the lifecycle-resolved identity avoids a second account
|
|
999
|
+
// lookup and binds the response to the account that was requested.
|
|
1000
|
+
accountId: resolvedUsageIdentity?.accountId,
|
|
1001
|
+
});
|
|
1002
|
+
if (!usageRequestStillCurrent(ctx, state, requestVersion, requestUsageKey)) return;
|
|
1003
|
+
if (snapshot) {
|
|
1004
|
+
state.snapshot = snapshot;
|
|
1005
|
+
state.lastFetchedAt = snapshot.fetchedAt;
|
|
1006
|
+
} else {
|
|
1007
|
+
state.snapshot = undefined;
|
|
1008
|
+
state.lastFetchedAt = Date.now();
|
|
1009
|
+
state.availability = "unavailable";
|
|
1010
|
+
}
|
|
1011
|
+
} catch {
|
|
1012
|
+
if (!usageRequestStillCurrent(ctx, state, requestVersion, requestUsageKey)) return;
|
|
1013
|
+
state.snapshot = undefined;
|
|
1014
|
+
state.lastFetchedAt = Date.now();
|
|
1015
|
+
state.availability = "unavailable";
|
|
1016
|
+
} finally {
|
|
1017
|
+
if (
|
|
1018
|
+
state.disposed ||
|
|
1019
|
+
state.requestVersion !== requestVersion ||
|
|
1020
|
+
state.usageIdentity?.usageKey !== requestUsageKey
|
|
1021
|
+
)
|
|
1022
|
+
return;
|
|
1023
|
+
state.loading = false;
|
|
1024
|
+
state.inflight = undefined;
|
|
1025
|
+
state.requestRender?.();
|
|
1026
|
+
}
|
|
1027
|
+
})();
|
|
1028
|
+
|
|
1029
|
+
state.inflight = request;
|
|
1030
|
+
return request;
|
|
1051
1031
|
}
|
|
1052
1032
|
|
|
1053
1033
|
function checkUsageIdentity(ctx: ExtensionContext, state: UsageSessionState): void {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1034
|
+
if (state.disposed) return;
|
|
1035
|
+
|
|
1036
|
+
const currentIdentity = getUsageIdentity(ctx);
|
|
1037
|
+
if (state.usageIdentity?.usageKey === currentIdentity?.usageKey) return;
|
|
1038
|
+
|
|
1039
|
+
// The timer is deliberately identity-only. It performs no WHAM request for
|
|
1040
|
+
// an unchanged account; an identity change uses the existing account-bound
|
|
1041
|
+
// refresh path so token/header validation and late-result isolation remain in
|
|
1042
|
+
// one place.
|
|
1043
|
+
invalidateUsageState(state, currentIdentity);
|
|
1044
|
+
state.requestRender?.();
|
|
1045
|
+
void refreshUsageIfNeeded(ctx, state, true, currentIdentity);
|
|
1066
1046
|
}
|
|
1067
1047
|
|
|
1068
1048
|
function startUsageRefreshTimer(ctx: ExtensionContext, state: UsageSessionState): void {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const intervalMs = Math.min(requestedIntervalMs, MAX_NODE_TIMER_DELAY_MS);
|
|
1081
|
-
const handle = setInterval(() => checkUsageIdentity(ctx, state), intervalMs);
|
|
1082
|
-
(handle as { unref?: () => void }).unref?.();
|
|
1083
|
-
state.usageRefreshTimer = handle;
|
|
1049
|
+
const configuredIntervalMs = state.config.codexUsage.cacheTtlMs;
|
|
1050
|
+
if (state.usageRefreshTimer !== undefined || !shouldShowCodexUsage(state.config)) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const requestedIntervalMs =
|
|
1055
|
+
configuredIntervalMs === 0 ? ZERO_CACHE_IDENTITY_CHECK_INTERVAL_MS : configuredIntervalMs;
|
|
1056
|
+
const intervalMs = Math.min(requestedIntervalMs, MAX_NODE_TIMER_DELAY_MS);
|
|
1057
|
+
const handle = setInterval(() => checkUsageIdentity(ctx, state), intervalMs);
|
|
1058
|
+
(handle as { unref?: () => void }).unref?.();
|
|
1059
|
+
state.usageRefreshTimer = handle;
|
|
1084
1060
|
}
|
|
1085
1061
|
|
|
1086
1062
|
function disposeUsageRefreshTimer(state: UsageSessionState): void {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1063
|
+
if (state.usageRefreshTimer === undefined) return;
|
|
1064
|
+
clearInterval(state.usageRefreshTimer as ReturnType<typeof setInterval>);
|
|
1065
|
+
state.usageRefreshTimer = undefined;
|
|
1090
1066
|
}
|
|
1091
1067
|
|
|
1092
1068
|
export default function (pi: ExtensionAPI) {
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
});
|
|
1069
|
+
const states = new WeakMap<object, UsageSessionState>();
|
|
1070
|
+
|
|
1071
|
+
function disposeGitCache(state: UsageSessionState): void {
|
|
1072
|
+
state.gitCache?.dispose();
|
|
1073
|
+
state.gitCache = undefined;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function disposeState(state: UsageSessionState): void {
|
|
1077
|
+
state.disposed = true;
|
|
1078
|
+
state.requestVersion += 1;
|
|
1079
|
+
state.snapshot = undefined;
|
|
1080
|
+
state.lastFetchedAt = undefined;
|
|
1081
|
+
state.loading = false;
|
|
1082
|
+
state.availability = undefined;
|
|
1083
|
+
state.usageIdentity = undefined;
|
|
1084
|
+
state.inflight = undefined;
|
|
1085
|
+
state.requestRender = undefined;
|
|
1086
|
+
disposeUsageRefreshTimer(state);
|
|
1087
|
+
disposeGitCache(state);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function ensureGitCache(ctx: ExtensionContext, state: UsageSessionState): void {
|
|
1091
|
+
if (!shouldShowGitStatus(state.config) || !canRunProjectGit(ctx)) {
|
|
1092
|
+
disposeGitCache(state);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (state.gitCache) return;
|
|
1097
|
+
state.gitCache = new GitFooterCache({
|
|
1098
|
+
cwd: () => ctx.cwd,
|
|
1099
|
+
canRun: () => canRunProjectGit(ctx),
|
|
1100
|
+
refreshIntervalMs: state.config.gitStatus.refreshIntervalMs,
|
|
1101
|
+
gitTimeoutMs: state.config.gitStatus.gitTimeoutMs,
|
|
1102
|
+
ghTimeoutMs: state.config.gitStatus.ghTimeoutMs,
|
|
1103
|
+
onChange: () => state.requestRender?.(),
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1108
|
+
const previous = states.get(ctx.sessionManager);
|
|
1109
|
+
if (previous) disposeState(previous);
|
|
1110
|
+
|
|
1111
|
+
const state: UsageSessionState = {
|
|
1112
|
+
config: loadConfig(ctx),
|
|
1113
|
+
loading: false,
|
|
1114
|
+
requestVersion: 0,
|
|
1115
|
+
disposed: false,
|
|
1116
|
+
};
|
|
1117
|
+
states.set(ctx.sessionManager, state);
|
|
1118
|
+
|
|
1119
|
+
ensureGitCache(ctx, state);
|
|
1120
|
+
|
|
1121
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
1122
|
+
const unsub = footerData.onBranchChange(() => tui.requestRender());
|
|
1123
|
+
state.requestRender = () => tui.requestRender();
|
|
1124
|
+
void refreshUsageIfNeeded(ctx, state);
|
|
1125
|
+
|
|
1126
|
+
return {
|
|
1127
|
+
dispose() {
|
|
1128
|
+
disposeState(state);
|
|
1129
|
+
unsub();
|
|
1130
|
+
},
|
|
1131
|
+
invalidate() {},
|
|
1132
|
+
render(width: number): string[] {
|
|
1133
|
+
// Keep repainting cheap: account-bound identity is resolved only
|
|
1134
|
+
// by lifecycle refreshes, never while streaming output renders.
|
|
1135
|
+
const usageRenderKey = getUsageRenderKey(ctx);
|
|
1136
|
+
const usageStateMatchesModel = state.usageIdentity?.renderKey === usageRenderKey;
|
|
1137
|
+
return renderFooterLines({
|
|
1138
|
+
width,
|
|
1139
|
+
cwd: ctx.cwd,
|
|
1140
|
+
config: state.config,
|
|
1141
|
+
branch: footerData.getGitBranch() ?? state.gitCache?.getStatusSnapshot()?.branch ?? "",
|
|
1142
|
+
gitStatus: shouldShowGitStatus(state.config)
|
|
1143
|
+
? formatGitFooterStatus(
|
|
1144
|
+
state.gitCache?.getStatusSnapshot(),
|
|
1145
|
+
state.gitCache?.getPullRequestSnapshot(),
|
|
1146
|
+
)
|
|
1147
|
+
: undefined,
|
|
1148
|
+
contextUsage: ctx.getContextUsage(),
|
|
1149
|
+
modelId: ctx.model?.id,
|
|
1150
|
+
modelProvider: ctx.model?.provider,
|
|
1151
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
1152
|
+
theme,
|
|
1153
|
+
usageSnapshot: usageStateMatchesModel ? state.snapshot : undefined,
|
|
1154
|
+
usageStatus: usageStateMatchesModel ? state.availability : undefined,
|
|
1155
|
+
});
|
|
1156
|
+
},
|
|
1157
|
+
};
|
|
1158
|
+
});
|
|
1159
|
+
startUsageRefreshTimer(ctx, state);
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
pi.on("model_select", (_event, ctx) => {
|
|
1163
|
+
const state = states.get(ctx.sessionManager);
|
|
1164
|
+
if (!state) return;
|
|
1165
|
+
// A model-select event is also the boundary for an account reload. Do
|
|
1166
|
+
// not let a prior provider/account's snapshot or promise survive it.
|
|
1167
|
+
const usageIdentity = getUsageIdentity(ctx);
|
|
1168
|
+
invalidateUsageState(state, usageIdentity);
|
|
1169
|
+
state.requestRender?.();
|
|
1170
|
+
void refreshUsageIfNeeded(ctx, state, true, usageIdentity);
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
1174
|
+
const state = states.get(ctx.sessionManager);
|
|
1175
|
+
if (!state) return;
|
|
1176
|
+
// Prompt boundaries provide an additional account-change check without
|
|
1177
|
+
// forcing a usage request when the current cache is still fresh.
|
|
1178
|
+
void refreshUsageIfNeeded(ctx, state);
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
1182
|
+
const state = states.get(ctx.sessionManager);
|
|
1183
|
+
if (!state) return;
|
|
1184
|
+
void refreshUsageIfNeeded(ctx, state);
|
|
1185
|
+
ensureGitCache(ctx, state);
|
|
1186
|
+
void state.gitCache?.refresh();
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
1190
|
+
const state = states.get(ctx.sessionManager);
|
|
1191
|
+
if (!state) return;
|
|
1192
|
+
disposeState(state);
|
|
1193
|
+
states.delete(ctx.sessionManager);
|
|
1194
|
+
});
|
|
1220
1195
|
}
|
|
1221
1196
|
|
|
1222
1197
|
export const __testing = {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1198
|
+
GitFooterCache,
|
|
1199
|
+
GIT_STATUS_ARGS,
|
|
1200
|
+
GH_PR_VIEW_ARGS,
|
|
1201
|
+
loadConfig,
|
|
1202
|
+
renderFooterLines,
|
|
1203
|
+
parseGitStatusPorcelainV2,
|
|
1204
|
+
parsePullRequestJson,
|
|
1205
|
+
formatGitStatusFooterSegment,
|
|
1206
|
+
formatPullRequestFooterSegment,
|
|
1207
|
+
formatGitFooterStatus,
|
|
1233
1208
|
};
|