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