@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
- package/dist/cli.js +10009 -9654
- package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
- package/dist/types/advisor/runtime.d.ts +8 -2
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/model-discovery.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +73 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/internal-urls/security-protocol.d.ts +15 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +18 -0
- package/dist/types/security/auth.d.ts +30 -0
- package/dist/types/security/cloud.d.ts +79 -0
- package/dist/types/security/comparison.d.ts +49 -0
- package/dist/types/security/contracts/ids.d.ts +15 -0
- package/dist/types/security/contracts/index.d.ts +4 -0
- package/dist/types/security/contracts/schemas.d.ts +660 -0
- package/dist/types/security/contracts/types.d.ts +234 -0
- package/dist/types/security/contracts/validation.d.ts +5 -0
- package/dist/types/security/coordinator.d.ts +100 -0
- package/dist/types/security/importers/codex-security.d.ts +7 -0
- package/dist/types/security/importers/index.d.ts +2 -0
- package/dist/types/security/importers/sarif.d.ts +9 -0
- package/dist/types/security/index.d.ts +13 -0
- package/dist/types/security/preflight.d.ts +61 -0
- package/dist/types/security/provenance.d.ts +24 -0
- package/dist/types/security/publication.d.ts +78 -0
- package/dist/types/security/remediation.d.ts +27 -0
- package/dist/types/security/resource-output.d.ts +8 -0
- package/dist/types/security/sarif.d.ts +2 -0
- package/dist/types/security/store.d.ts +40 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/slash-commands/helpers/security.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +4 -1
- package/dist/types/task/executor.d.ts +14 -2
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +10 -2
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/security-scan.d.ts +96 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/scripts/security-compare.ts +40 -0
- package/src/advisor/runtime.ts +138 -12
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli.ts +9 -0
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-discovery.ts +32 -5
- package/src/config/model-registry.ts +107 -25
- package/src/config/settings-schema.ts +77 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/eval/py/prelude.py +7 -3
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/internal-urls/router.ts +4 -1
- package/src/internal-urls/security-protocol.ts +261 -0
- package/src/internal-urls/types.ts +1 -1
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +7 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +15 -9
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +31 -17
- package/src/modes/rpc/host-uris.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/agents/security-reviewer.md +75 -0
- package/src/prompts/security/scan-coordinator.md +7 -0
- package/src/prompts/security/scan-request.md +21 -0
- package/src/prompts/security/validate-request.md +8 -0
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/grep.md +7 -6
- package/src/prompts/tools/security-publish.md +1 -0
- package/src/prompts/tools/security-scan.md +1 -0
- package/src/sdk.ts +212 -21
- package/src/security/auth.ts +98 -0
- package/src/security/cloud.ts +686 -0
- package/src/security/comparison.ts +255 -0
- package/src/security/contracts/ids.ts +111 -0
- package/src/security/contracts/index.ts +4 -0
- package/src/security/contracts/schemas.ts +201 -0
- package/src/security/contracts/types.ts +254 -0
- package/src/security/contracts/validation.ts +65 -0
- package/src/security/coordinator.ts +708 -0
- package/src/security/importers/codex-security.ts +387 -0
- package/src/security/importers/index.ts +2 -0
- package/src/security/importers/sarif.ts +357 -0
- package/src/security/index.ts +13 -0
- package/src/security/preflight.ts +405 -0
- package/src/security/provenance.ts +106 -0
- package/src/security/publication.ts +326 -0
- package/src/security/remediation.ts +93 -0
- package/src/security/resource-output.ts +50 -0
- package/src/security/sarif.ts +78 -0
- package/src/security/store.ts +430 -0
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +302 -175
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +11 -3
- package/src/session/session-advisors.ts +140 -16
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +152 -3
- package/src/slash-commands/helpers/security.ts +451 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +20 -4
- package/src/task/render.ts +20 -4
- package/src/task/structured-subagent.ts +6 -4
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/browser.ts +5 -0
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +37 -5
- package/src/tools/path-utils.ts +91 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/security-scan.ts +287 -0
- package/src/tools/write.ts +9 -3
- package/src/tools/xdev.ts +39 -14
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/types.ts +1 -1
|
@@ -15,6 +15,11 @@ import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/sessio
|
|
|
15
15
|
import { calculateTokensPerSecond } from "../../../utils/token-rate";
|
|
16
16
|
import { sanitizeStatusText } from "../../shared";
|
|
17
17
|
import { theme } from "../../theme/theme";
|
|
18
|
+
import {
|
|
19
|
+
type CodexResetFireworksEvent,
|
|
20
|
+
type CodexResetUsageSnapshot,
|
|
21
|
+
detectCodexResetFireworks,
|
|
22
|
+
} from "../codex-reset-fireworks";
|
|
18
23
|
import { canReuseCachedPr, createPrCacheContext, isSamePrCacheContext, type PrCacheContext } from "./git-utils";
|
|
19
24
|
import { getPreset } from "./presets";
|
|
20
25
|
import { renderSegment, type SegmentContext } from "./segments";
|
|
@@ -28,6 +33,37 @@ import type {
|
|
|
28
33
|
} from "./types";
|
|
29
34
|
|
|
30
35
|
const JJ_REFRESH_TTL_MS = 5000;
|
|
36
|
+
const WATCHER_FAILURE_POLL_TTL_MS = 5000;
|
|
37
|
+
|
|
38
|
+
function normalizeCodexIdentityValue(value: unknown): string | undefined {
|
|
39
|
+
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fireworks are stateful, so their report match must be stricter than the
|
|
44
|
+
* status display's fallback matching: every known credential identifier must
|
|
45
|
+
* be present and equal or a workspace sibling can mutate this account's
|
|
46
|
+
* baseline.
|
|
47
|
+
*/
|
|
48
|
+
function codexReportMatchesExactIdentity(report: UsageReport, identity: OAuthAccountIdentity | undefined): boolean {
|
|
49
|
+
if (!identity) return false;
|
|
50
|
+
const accountId = normalizeCodexIdentityValue(identity.accountId);
|
|
51
|
+
const email = normalizeCodexIdentityValue(identity.email);
|
|
52
|
+
const projectId = normalizeCodexIdentityValue(identity.projectId);
|
|
53
|
+
const orgId = normalizeCodexIdentityValue(identity.orgId);
|
|
54
|
+
if (!accountId && !email && !projectId && !orgId) return false;
|
|
55
|
+
|
|
56
|
+
const metadata = report.metadata ?? {};
|
|
57
|
+
const reportAccountId =
|
|
58
|
+
normalizeCodexIdentityValue(metadata.accountId) ?? normalizeCodexIdentityValue(metadata.account_id);
|
|
59
|
+
const reportProjectId =
|
|
60
|
+
normalizeCodexIdentityValue(metadata.projectId) ?? normalizeCodexIdentityValue(metadata.project_id);
|
|
61
|
+
if (accountId && reportAccountId !== accountId) return false;
|
|
62
|
+
if (email && normalizeCodexIdentityValue(metadata.email) !== email) return false;
|
|
63
|
+
if (projectId && reportProjectId !== projectId) return false;
|
|
64
|
+
if (orgId && normalizeCodexIdentityValue(metadata.orgId) !== orgId) return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
31
67
|
|
|
32
68
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
33
69
|
// Context-usage memo
|
|
@@ -174,6 +210,17 @@ interface ActiveRepoCache {
|
|
|
174
210
|
worktree: WorktreeContext | null;
|
|
175
211
|
}
|
|
176
212
|
|
|
213
|
+
interface BranchResolveRequest {
|
|
214
|
+
id: number;
|
|
215
|
+
cwd: string;
|
|
216
|
+
controller: AbortController;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
interface JjResolveRequest {
|
|
220
|
+
id: number;
|
|
221
|
+
controller: AbortController;
|
|
222
|
+
}
|
|
223
|
+
|
|
177
224
|
interface WorktreeContext {
|
|
178
225
|
/** Primary-checkout (project) name shown by the path segment. */
|
|
179
226
|
projectName: string;
|
|
@@ -250,7 +297,31 @@ export class StatusLineComponent implements Component {
|
|
|
250
297
|
#cachedBranch: string | null | undefined = undefined;
|
|
251
298
|
#cachedBranchRepoId: string | null | undefined = undefined;
|
|
252
299
|
#cachedBranchCwd: string | undefined = undefined;
|
|
300
|
+
#cachedBranchHasGitRepository = false;
|
|
301
|
+
// In-flight reftable resolve slot. Ownership is the launch id, not the cwd:
|
|
302
|
+
// two live resolves can share a cwd string across an invalidation, and a
|
|
303
|
+
// stale one must never free (or poison) a slot it no longer owns.
|
|
304
|
+
#branchResolveSeq = 0;
|
|
305
|
+
#branchResolveActive: BranchResolveRequest | undefined = undefined;
|
|
306
|
+
// Bumped on every branch-cache reset (#invalidateGitCaches — a HEAD move or
|
|
307
|
+
// repo-context change). An in-flight reftable resolve captures this at
|
|
308
|
+
// launch; a mismatch on resolve means the cache was invalidated underneath
|
|
309
|
+
// it (a newer resolve superseded it), so its result is stale and must be
|
|
310
|
+
// dropped rather than overwrite the value the newer resolve committed.
|
|
311
|
+
// Mirrors #jjCacheGeneration / #getJjBranch in this file.
|
|
312
|
+
// Timestamp of the latest branch read; only bounds cache freshness when the
|
|
313
|
+
// HEAD watcher could not be installed.
|
|
314
|
+
#branchLastFetch: number | undefined = undefined;
|
|
315
|
+
// Bumped on every branch-cache reset (invalidateGitCaches — a HEAD move or
|
|
316
|
+
// repo-context change). An in-flight reftable resolve captures this at
|
|
317
|
+
// launch; a mismatch on resolve means the cache was invalidated underneath
|
|
318
|
+
// it (a newer resolve superseded it), so its result is stale and must be
|
|
319
|
+
// dropped rather than overwrite the value the newer resolve committed.
|
|
320
|
+
// Mirrors #jjCacheGeneration / #getJjBranch in this file.
|
|
321
|
+
#branchCacheGeneration = 0;
|
|
253
322
|
#gitWatcher: fs.FSWatcher | null = null;
|
|
323
|
+
#gitWatcherErrorListener: (() => void) | undefined = undefined;
|
|
324
|
+
#gitWatcherUnavailable = false;
|
|
254
325
|
#onBranchChange: (() => void) | null = null;
|
|
255
326
|
#disposed = false;
|
|
256
327
|
#autoCompactEnabled: boolean = true;
|
|
@@ -299,10 +370,11 @@ export class StatusLineComponent implements Component {
|
|
|
299
370
|
#jjRootCwd: string | undefined = undefined;
|
|
300
371
|
#cachedJjBranch: string | null = null;
|
|
301
372
|
#jjBranchLastFetch = 0;
|
|
302
|
-
#
|
|
373
|
+
#jjResolveSeq = 0;
|
|
374
|
+
#jjBranchActive: JjResolveRequest | undefined = undefined;
|
|
303
375
|
#cachedJjStatus: { staged: number; unstaged: number; untracked: number } | null = null;
|
|
304
376
|
#jjStatusLastFetch = 0;
|
|
305
|
-
#
|
|
377
|
+
#jjStatusActive: JjResolveRequest | undefined = undefined;
|
|
306
378
|
// Bumped on every jj-cache reset — a cwd switch (#jjRootFor) or a HEAD /
|
|
307
379
|
// bookmark move (#invalidateGitCaches). An in-flight jj query captures this
|
|
308
380
|
// at launch; a mismatch on resolve means the caches were reset underneath it
|
|
@@ -330,6 +402,12 @@ export class StatusLineComponent implements Component {
|
|
|
330
402
|
#usageFetchedAt = 0;
|
|
331
403
|
#usageInFlight = false;
|
|
332
404
|
#usageStartTimer: Timer | null = null;
|
|
405
|
+
// A timed-out request may still resolve. Its result remains eligible only
|
|
406
|
+
// until a newer request has applied.
|
|
407
|
+
#usageRefreshSequence = 0;
|
|
408
|
+
#latestAppliedUsageRefreshSequence = 0;
|
|
409
|
+
#codexResetSnapshots = new Map<string, CodexResetUsageSnapshot>();
|
|
410
|
+
#onCodexResetFireworks: ((event: CodexResetFireworksEvent) => void) | undefined;
|
|
333
411
|
// Context-usage memo. The status line redraws on every agent event, so the
|
|
334
412
|
// hot path must not recompute context tokens unless an input changed.
|
|
335
413
|
// `getContextUsage()` anchors on the last assistant's real prompt-token
|
|
@@ -548,6 +626,11 @@ export class StatusLineComponent implements Component {
|
|
|
548
626
|
this.#collabStatus = status;
|
|
549
627
|
}
|
|
550
628
|
|
|
629
|
+
/** Set the callback that presents detected Codex reset celebrations, or clear it with `undefined`. */
|
|
630
|
+
setCodexResetFireworksHandler(handler: ((event: CodexResetFireworksEvent) => void) | undefined): void {
|
|
631
|
+
this.#onCodexResetFireworks = handler;
|
|
632
|
+
}
|
|
633
|
+
|
|
551
634
|
setHookStatus(key: string, text: string | undefined): void {
|
|
552
635
|
if (text === undefined) {
|
|
553
636
|
this.#hookStatuses.delete(key);
|
|
@@ -562,45 +645,70 @@ export class StatusLineComponent implements Component {
|
|
|
562
645
|
}
|
|
563
646
|
|
|
564
647
|
#setupGitWatcher(): void {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
this.#gitWatcher = null;
|
|
568
|
-
}
|
|
648
|
+
this.#retireGitWatcher();
|
|
649
|
+
this.#gitWatcherUnavailable = false;
|
|
569
650
|
|
|
570
651
|
if (!this.#gitEnabled() || !this.#hasGitBackedSegment()) {
|
|
571
|
-
this
|
|
652
|
+
this.invalidateGitCaches();
|
|
572
653
|
return;
|
|
573
654
|
}
|
|
574
655
|
|
|
575
656
|
const { effectiveGitCwd } = this.#resolveActiveRepoCache();
|
|
576
657
|
const repository = git.repo.resolveSync(effectiveGitCwd);
|
|
577
|
-
if (!repository)
|
|
658
|
+
if (!repository) {
|
|
659
|
+
// There is no path to watch yet. Cache the negative result only for the
|
|
660
|
+
// fallback poll interval so a later `git init` becomes visible without
|
|
661
|
+
// generic invalidations or a render-path probe on every paint.
|
|
662
|
+
this.#gitWatcherUnavailable = true;
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
578
665
|
|
|
579
666
|
const watchPath = git.repo.isReftableSync(repository)
|
|
580
667
|
? path.join(repository.gitDir, "reftable")
|
|
581
668
|
: repository.headPath;
|
|
582
669
|
|
|
583
670
|
try {
|
|
584
|
-
|
|
585
|
-
if (this.#disposed) return;
|
|
586
|
-
this
|
|
587
|
-
|
|
588
|
-
this.#onBranchChange();
|
|
589
|
-
}
|
|
671
|
+
const watcher = fs.watch(watchPath, () => {
|
|
672
|
+
if (this.#disposed || this.#gitWatcher !== watcher) return;
|
|
673
|
+
this.invalidateGitCaches();
|
|
674
|
+
this.#onBranchChange?.();
|
|
590
675
|
});
|
|
676
|
+
const onError = () => {
|
|
677
|
+
if (this.#gitWatcher !== watcher) return;
|
|
678
|
+
this.#retireGitWatcher();
|
|
679
|
+
this.#gitWatcherUnavailable = true;
|
|
680
|
+
if (this.#disposed) return;
|
|
681
|
+
this.invalidateGitCaches();
|
|
682
|
+
this.#onBranchChange?.();
|
|
683
|
+
};
|
|
684
|
+
this.#gitWatcher = watcher;
|
|
685
|
+
this.#gitWatcherErrorListener = onError;
|
|
686
|
+
watcher.on("error", onError);
|
|
591
687
|
} catch {
|
|
592
|
-
this.#
|
|
688
|
+
this.#gitWatcherUnavailable = true;
|
|
593
689
|
}
|
|
594
690
|
}
|
|
595
691
|
|
|
692
|
+
#retireGitWatcher(): void {
|
|
693
|
+
const watcher = this.#gitWatcher;
|
|
694
|
+
const onError = this.#gitWatcherErrorListener;
|
|
695
|
+
this.#gitWatcher = null;
|
|
696
|
+
this.#gitWatcherErrorListener = undefined;
|
|
697
|
+
if (!watcher) return;
|
|
698
|
+
if (onError) watcher.off("error", onError);
|
|
699
|
+
watcher.close();
|
|
700
|
+
}
|
|
701
|
+
|
|
596
702
|
dispose(): void {
|
|
597
703
|
this.#disposed = true;
|
|
704
|
+
this.#branchResolveActive?.controller.abort();
|
|
705
|
+
this.#branchResolveActive = undefined;
|
|
706
|
+
this.#resetJjRequests();
|
|
598
707
|
this.#onBranchChange = null;
|
|
599
708
|
this.#clearUsageStartTimer();
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
709
|
+
this.#onCodexResetFireworks = undefined;
|
|
710
|
+
this.#codexResetSnapshots.clear();
|
|
711
|
+
this.#retireGitWatcher();
|
|
604
712
|
}
|
|
605
713
|
|
|
606
714
|
#clearUsageStartTimer(): void {
|
|
@@ -610,7 +718,17 @@ export class StatusLineComponent implements Component {
|
|
|
610
718
|
}
|
|
611
719
|
|
|
612
720
|
invalidate(): void {
|
|
613
|
-
|
|
721
|
+
// Generic repaint invalidation (theme change, message event, model
|
|
722
|
+
// switch, …). Must NOT abort or restart a live reftable HEAD/PR resolve:
|
|
723
|
+
// the render path self-invalidates via cwd/context cache-miss checks, so
|
|
724
|
+
// a generic paint only needs to re-render — not tear down in-flight VCS
|
|
725
|
+
// work. Aborting here would fan out a new git subprocess on every agent
|
|
726
|
+
// event, re-introducing the render-path spawn churn the async resolve
|
|
727
|
+
// was designed to avoid. Explicit Git/repository invalidation (watcher
|
|
728
|
+
// HEAD-move, cwd/repo switch) goes through {@link invalidateGitCaches}.
|
|
729
|
+
// A tool may open, close, or merge a PR without moving HEAD. Expire the
|
|
730
|
+
// settled PR context on ordinary activity while leaving HEAD work intact.
|
|
731
|
+
this.#cachedPrContext = undefined;
|
|
614
732
|
}
|
|
615
733
|
#invalidateSessionCaches(): void {
|
|
616
734
|
this.#clearUsageStartTimer();
|
|
@@ -622,14 +740,30 @@ export class StatusLineComponent implements Component {
|
|
|
622
740
|
this.#lastTokensPerSecondTimestamp = null;
|
|
623
741
|
}
|
|
624
742
|
|
|
625
|
-
|
|
743
|
+
/**
|
|
744
|
+
* Explicit Git/repository cache invalidation. Aborts any in-flight
|
|
745
|
+
* reftable HEAD/PR resolve, bumps the stale-result generation, and drops
|
|
746
|
+
* the branch/PR/jj caches so the next render refetches from disk. Called
|
|
747
|
+
* by the git watcher on a HEAD move and by {@link applyCwdChange} on a
|
|
748
|
+
* repo/cwd switch. Generic repaints use {@link invalidate} instead and
|
|
749
|
+
* must never reach this path.
|
|
750
|
+
*/
|
|
751
|
+
invalidateGitCaches(): void {
|
|
626
752
|
this.#cachedBranch = undefined;
|
|
627
753
|
this.#cachedBranchRepoId = undefined;
|
|
628
754
|
this.#cachedBranchCwd = undefined;
|
|
755
|
+
this.#cachedBranchHasGitRepository = false;
|
|
756
|
+
// Abort before releasing the in-flight slot. Releasing alone would allow
|
|
757
|
+
// repeated invalidations to fan out still-running git subprocesses.
|
|
758
|
+
this.#branchResolveActive?.controller.abort();
|
|
759
|
+
this.#branchResolveActive = undefined;
|
|
760
|
+
this.#branchLastFetch = undefined;
|
|
761
|
+
this.#branchCacheGeneration++;
|
|
629
762
|
this.#cachedPrContext = undefined;
|
|
630
763
|
// jj label/status share the git segment's lifecycle: a HEAD move (e.g. a
|
|
631
764
|
// colocated `jj new`/bookmark move) must drop the throttled jj caches too,
|
|
632
765
|
// mirroring #jjRootFor's per-cwd reset so the next render refetches.
|
|
766
|
+
this.#resetJjRequests();
|
|
633
767
|
this.#jjRoot = undefined;
|
|
634
768
|
this.#jjRootCwd = undefined;
|
|
635
769
|
this.#cachedJjBranch = null;
|
|
@@ -638,25 +772,111 @@ export class StatusLineComponent implements Component {
|
|
|
638
772
|
this.#jjStatusLastFetch = 0;
|
|
639
773
|
this.#jjCacheGeneration++;
|
|
640
774
|
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Re-point the status line's VCS watcher and caches at a new cwd/repository.
|
|
778
|
+
* Atomically retires the old watcher/listeners, invalidates VCS caches and
|
|
779
|
+
* in-flight controllers, then runs watcher setup for the new cwd and requests
|
|
780
|
+
* a repaint. Called by {@link InteractiveMode.applyCwdChange} after the
|
|
781
|
+
* SessionManager's cwd has moved — the watcher ownership always follows the
|
|
782
|
+
* effective cwd/repo, so a stale watcher for the previous repo can never
|
|
783
|
+
* invalidate the new one. Generic repaints use {@link invalidate} and must
|
|
784
|
+
* never retire the watcher or abort a live resolve.
|
|
785
|
+
*/
|
|
786
|
+
applyCwdChange(): void {
|
|
787
|
+
this.#retireGitWatcher();
|
|
788
|
+
this.invalidateGitCaches();
|
|
789
|
+
this.#setupGitWatcher();
|
|
790
|
+
this.#onBranchChange?.();
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
#resetJjRequests(): void {
|
|
794
|
+
this.#jjBranchActive?.controller.abort();
|
|
795
|
+
this.#jjBranchActive = undefined;
|
|
796
|
+
this.#jjStatusActive?.controller.abort();
|
|
797
|
+
this.#jjStatusActive = undefined;
|
|
798
|
+
}
|
|
641
799
|
#getCurrentBranch(effectiveGitCwd?: string): string | null {
|
|
642
800
|
if (!this.#gitEnabled()) return null;
|
|
643
801
|
|
|
644
802
|
const gitCwd = effectiveGitCwd ?? this.#resolveActiveRepoCache().effectiveGitCwd;
|
|
645
|
-
|
|
803
|
+
const fallbackCacheExpired =
|
|
804
|
+
this.#gitWatcherUnavailable &&
|
|
805
|
+
(this.#branchLastFetch === undefined || Date.now() - this.#branchLastFetch >= WATCHER_FAILURE_POLL_TTL_MS);
|
|
806
|
+
if (this.#cachedBranch !== undefined && this.#cachedBranchCwd === gitCwd && !fallbackCacheExpired) {
|
|
646
807
|
return this.#cachedBranch;
|
|
647
808
|
}
|
|
648
809
|
|
|
810
|
+
// A reftable repo resolves HEAD by spawning `git symbolic-ref` +
|
|
811
|
+
// `git rev-parse` — the unbounded spawn that froze the render path (F7).
|
|
812
|
+
// A non-reftable repo resolves HEAD with cheap sync filesystem reads, so
|
|
813
|
+
// only the reftable branch moves off the render path, mirroring
|
|
814
|
+
// #getGitStatus and #getJjBranch in this file.
|
|
815
|
+
const repository = git.repo.resolveSync(gitCwd);
|
|
816
|
+
if (repository && git.repo.isReftableSync(repository)) {
|
|
817
|
+
if (this.#branchResolveActive !== undefined) {
|
|
818
|
+
return this.#branchResolveActive.cwd === gitCwd && this.#cachedBranchCwd === gitCwd
|
|
819
|
+
? (this.#cachedBranch ?? null)
|
|
820
|
+
: null;
|
|
821
|
+
}
|
|
822
|
+
const request: BranchResolveRequest = {
|
|
823
|
+
id: ++this.#branchResolveSeq,
|
|
824
|
+
cwd: gitCwd,
|
|
825
|
+
controller: new AbortController(),
|
|
826
|
+
};
|
|
827
|
+
this.#branchResolveActive = request;
|
|
828
|
+
// Capture the cache generation at launch. invalidateGitCaches bumps it
|
|
829
|
+
// on a HEAD move and clears the in-flight slot, so a fresher resolve can
|
|
830
|
+
// start while this one is still pending. Without a generation check the
|
|
831
|
+
// older resolve would finish later, install its stale HEAD, and clear the
|
|
832
|
+
// slot — dropping the fresh result and freezing the status line on the
|
|
833
|
+
// pre-change branch. Mirrors #jjCacheGeneration / #getJjBranch.
|
|
834
|
+
const generation = this.#branchCacheGeneration;
|
|
835
|
+
(async () => {
|
|
836
|
+
let next: string | null = null;
|
|
837
|
+
let repoId: string | null = null;
|
|
838
|
+
try {
|
|
839
|
+
const headState = await git.head.resolve(gitCwd, request.controller.signal);
|
|
840
|
+
repoId = headState?.headPath ?? null;
|
|
841
|
+
next = !headState
|
|
842
|
+
? null
|
|
843
|
+
: headState.kind === "ref"
|
|
844
|
+
? (headState.branchName ?? headState.ref)
|
|
845
|
+
: "detached";
|
|
846
|
+
} catch {
|
|
847
|
+
next = null;
|
|
848
|
+
} finally {
|
|
849
|
+
// Release the slot only if this resolve still owns it: after an
|
|
850
|
+
// invalidation a fresher resolve may hold it, and freeing that
|
|
851
|
+
// slot here would let a third same-generation resolve launch and
|
|
852
|
+
// race the fresh one to the cache commit.
|
|
853
|
+
if (this.#branchResolveActive?.id === request.id) this.#branchResolveActive = undefined;
|
|
854
|
+
}
|
|
855
|
+
// Only the latest generation may update the cache; a mismatch means a
|
|
856
|
+
// newer resolve superseded this one (or the component disposed).
|
|
857
|
+
if (this.#branchCacheGeneration !== generation || this.#disposed) return;
|
|
858
|
+
const prev = this.#cachedBranchCwd === gitCwd ? this.#cachedBranch : undefined;
|
|
859
|
+
this.#cachedBranchCwd = gitCwd;
|
|
860
|
+
this.#cachedBranchRepoId = repoId;
|
|
861
|
+
this.#cachedBranchHasGitRepository = next === null;
|
|
862
|
+
this.#cachedBranch = next;
|
|
863
|
+
this.#branchLastFetch = Date.now();
|
|
864
|
+
if (prev !== next && this.#onBranchChange) this.#onBranchChange();
|
|
865
|
+
})();
|
|
866
|
+
return this.#cachedBranchCwd === gitCwd ? (this.#cachedBranch ?? null) : null;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Non-reftable: cheap sync filesystem read, safe on the render path.
|
|
649
870
|
const head = git.head.resolveSync(gitCwd);
|
|
650
871
|
const gitHeadPath = head?.headPath ?? null;
|
|
651
872
|
this.#cachedBranchCwd = gitCwd;
|
|
652
873
|
this.#cachedBranchRepoId = gitHeadPath;
|
|
874
|
+
this.#branchLastFetch = Date.now();
|
|
653
875
|
if (!head) {
|
|
654
876
|
this.#cachedBranch = null;
|
|
655
877
|
return null;
|
|
656
878
|
}
|
|
657
|
-
|
|
658
879
|
this.#cachedBranch = head.kind === "ref" ? (head.branchName ?? head.ref) : "detached";
|
|
659
|
-
|
|
660
880
|
return this.#cachedBranch ?? null;
|
|
661
881
|
}
|
|
662
882
|
|
|
@@ -741,17 +961,24 @@ export class StatusLineComponent implements Component {
|
|
|
741
961
|
const cwd = effectiveGitCwd ?? this.#resolveActiveRepoCache().effectiveGitCwd;
|
|
742
962
|
const root = this.#jjRootFor(cwd);
|
|
743
963
|
if (!root) return null;
|
|
744
|
-
if (this.#
|
|
964
|
+
if (this.#jjBranchActive || Date.now() - this.#jjBranchLastFetch < JJ_REFRESH_TTL_MS) {
|
|
745
965
|
return this.#cachedJjBranch;
|
|
746
966
|
}
|
|
747
|
-
|
|
967
|
+
const request: JjResolveRequest = {
|
|
968
|
+
id: ++this.#jjResolveSeq,
|
|
969
|
+
controller: new AbortController(),
|
|
970
|
+
};
|
|
971
|
+
this.#jjBranchActive = request;
|
|
748
972
|
const generation = this.#jjCacheGeneration;
|
|
749
973
|
(async () => {
|
|
750
974
|
let next: string | null = null;
|
|
751
975
|
try {
|
|
752
|
-
next = await jj.workingCopy.label(root
|
|
976
|
+
next = await jj.workingCopy.label(root, {
|
|
977
|
+
signal: request.controller.signal,
|
|
978
|
+
timeoutMs: jj.JJ_COMMAND_TIMEOUT_MS,
|
|
979
|
+
});
|
|
753
980
|
} finally {
|
|
754
|
-
this.#
|
|
981
|
+
if (this.#jjBranchActive?.id === request.id) this.#jjBranchActive = undefined;
|
|
755
982
|
// Advance the throttle only if no reset raced this query; a reset
|
|
756
983
|
// leaves LastFetch at 0 so the current root refetches instead of
|
|
757
984
|
// being throttled on a superseded result.
|
|
@@ -763,7 +990,7 @@ export class StatusLineComponent implements Component {
|
|
|
763
990
|
if (this.#jjCacheGeneration !== generation || this.#disposed) return;
|
|
764
991
|
const changed = next !== this.#cachedJjBranch;
|
|
765
992
|
this.#cachedJjBranch = next;
|
|
766
|
-
if (changed
|
|
993
|
+
if (changed) this.#onBranchChange?.();
|
|
767
994
|
})();
|
|
768
995
|
return this.#cachedJjBranch;
|
|
769
996
|
}
|
|
@@ -775,23 +1002,30 @@ export class StatusLineComponent implements Component {
|
|
|
775
1002
|
const cwd = effectiveGitCwd ?? this.#resolveActiveRepoCache().effectiveGitCwd;
|
|
776
1003
|
const root = this.#jjRootFor(cwd);
|
|
777
1004
|
if (!root) return null;
|
|
778
|
-
if (this.#
|
|
1005
|
+
if (this.#jjStatusActive || Date.now() - this.#jjStatusLastFetch < JJ_REFRESH_TTL_MS) {
|
|
779
1006
|
return this.#cachedJjStatus;
|
|
780
1007
|
}
|
|
781
|
-
|
|
1008
|
+
const request: JjResolveRequest = {
|
|
1009
|
+
id: ++this.#jjResolveSeq,
|
|
1010
|
+
controller: new AbortController(),
|
|
1011
|
+
};
|
|
1012
|
+
this.#jjStatusActive = request;
|
|
782
1013
|
const generation = this.#jjCacheGeneration;
|
|
783
1014
|
(async () => {
|
|
784
1015
|
let next: { staged: number; unstaged: number; untracked: number } | null = null;
|
|
785
1016
|
try {
|
|
786
|
-
next = await jj.status.summary(root
|
|
1017
|
+
next = await jj.status.summary(root, {
|
|
1018
|
+
signal: request.controller.signal,
|
|
1019
|
+
timeoutMs: jj.JJ_COMMAND_TIMEOUT_MS,
|
|
1020
|
+
});
|
|
787
1021
|
} finally {
|
|
788
|
-
this.#
|
|
1022
|
+
if (this.#jjStatusActive?.id === request.id) this.#jjStatusActive = undefined;
|
|
789
1023
|
if (this.#jjCacheGeneration === generation) this.#jjStatusLastFetch = Date.now();
|
|
790
1024
|
}
|
|
791
1025
|
if (this.#jjCacheGeneration !== generation || this.#disposed) return;
|
|
792
1026
|
const prev = this.#cachedJjStatus;
|
|
793
1027
|
this.#cachedJjStatus = next;
|
|
794
|
-
if (
|
|
1028
|
+
if (JSON.stringify(prev) !== JSON.stringify(next)) this.#onBranchChange?.();
|
|
795
1029
|
})();
|
|
796
1030
|
return this.#cachedJjStatus;
|
|
797
1031
|
}
|
|
@@ -939,10 +1173,8 @@ export class StatusLineComponent implements Component {
|
|
|
939
1173
|
return this.#vibeWorkerTokenRate?.() ?? null;
|
|
940
1174
|
}
|
|
941
1175
|
|
|
942
|
-
#
|
|
943
|
-
const activeProvider = session.state.model?.provider ?? session.model?.provider ?? "";
|
|
1176
|
+
#formatUsageContextKey(activeProvider: string | undefined, identity: OAuthAccountIdentity | undefined): string {
|
|
944
1177
|
if (!activeProvider) return "";
|
|
945
|
-
const identity = session.modelRegistry?.authStorage?.getOAuthAccountIdentity(activeProvider, session.sessionId);
|
|
946
1178
|
// orgId is part of the key: rotating between two same-email Anthropic
|
|
947
1179
|
// subscriptions must invalidate the cached usage immediately instead of
|
|
948
1180
|
// showing the previous org's quota for the rest of the cache TTL.
|
|
@@ -955,6 +1187,14 @@ export class StatusLineComponent implements Component {
|
|
|
955
1187
|
].join("\0");
|
|
956
1188
|
}
|
|
957
1189
|
|
|
1190
|
+
#getUsageContextKey(session: AgentSession): string {
|
|
1191
|
+
const activeProvider = session.state.model?.provider ?? session.model?.provider;
|
|
1192
|
+
const identity = activeProvider
|
|
1193
|
+
? session.modelRegistry?.authStorage?.getOAuthAccountIdentity(activeProvider, session.sessionId)
|
|
1194
|
+
: undefined;
|
|
1195
|
+
return this.#formatUsageContextKey(activeProvider, identity);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
958
1198
|
/**
|
|
959
1199
|
* Startup redraws only arm a short-delayed task; timeout releases the render
|
|
960
1200
|
* cadence while a late successful fetch can still refresh the cached segment.
|
|
@@ -984,40 +1224,60 @@ export class StatusLineComponent implements Component {
|
|
|
984
1224
|
this.#usageInFlight = false;
|
|
985
1225
|
return;
|
|
986
1226
|
}
|
|
1227
|
+
const sequence = ++this.#usageRefreshSequence;
|
|
987
1228
|
const signal = AbortSignal.timeout(STATUS_USAGE_REFRESH_TIMEOUT_MS);
|
|
988
1229
|
let reportsPromise: Promise<unknown> | undefined;
|
|
989
1230
|
try {
|
|
990
1231
|
reportsPromise = fetcher.call(session, signal);
|
|
991
|
-
this.#applyUsageRefreshReports(
|
|
1232
|
+
this.#applyUsageRefreshReports(
|
|
1233
|
+
session,
|
|
1234
|
+
await this.#raceUsageRefreshWithSignal(reportsPromise, signal),
|
|
1235
|
+
sequence,
|
|
1236
|
+
);
|
|
992
1237
|
} catch {
|
|
993
1238
|
if (this.session !== session) return;
|
|
994
1239
|
this.#usageFetchedAt = Date.now();
|
|
995
1240
|
if (signal.aborted && reportsPromise) {
|
|
996
|
-
this.#observeLateUsageRefresh(session, reportsPromise);
|
|
1241
|
+
this.#observeLateUsageRefresh(session, reportsPromise, sequence);
|
|
997
1242
|
}
|
|
998
1243
|
} finally {
|
|
999
1244
|
if (this.session === session) this.#usageInFlight = false;
|
|
1000
1245
|
}
|
|
1001
1246
|
}
|
|
1002
1247
|
|
|
1003
|
-
#applyUsageRefreshReports(session: AgentSession, reports: unknown): void {
|
|
1004
|
-
if (this.#disposed || this.session !== session)
|
|
1248
|
+
#applyUsageRefreshReports(session: AgentSession, reports: unknown, sequence: number): void {
|
|
1249
|
+
if (this.#disposed || this.session !== session || sequence < this.#latestAppliedUsageRefreshSequence) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
this.#latestAppliedUsageRefreshSequence = sequence;
|
|
1005
1253
|
const activeProvider = session.state.model?.provider ?? session.model?.provider;
|
|
1006
1254
|
const activeIdentity =
|
|
1007
1255
|
activeProvider && session.modelRegistry?.authStorage
|
|
1008
1256
|
? session.modelRegistry.authStorage.getOAuthAccountIdentity(activeProvider, session.sessionId)
|
|
1009
1257
|
: undefined;
|
|
1010
|
-
|
|
1258
|
+
const normalized = this.#normalizeUsageReports(reports, activeProvider, activeIdentity);
|
|
1259
|
+
const resetSnapshot =
|
|
1260
|
+
activeProvider === "openai-codex" ? this.#normalizeCodexResetSnapshot(reports, activeIdentity) : null;
|
|
1261
|
+
this.#cachedUsage = normalized;
|
|
1011
1262
|
this.#usageFetchedAt = Date.now();
|
|
1263
|
+
if (!resetSnapshot) return;
|
|
1264
|
+
const contextKey = this.#formatUsageContextKey(activeProvider, activeIdentity);
|
|
1265
|
+
const previous = this.#codexResetSnapshots.get(contextKey);
|
|
1266
|
+
this.#codexResetSnapshots.set(contextKey, resetSnapshot);
|
|
1267
|
+
if (!previous || !settings.get("tui.codexResetFireworks")) return;
|
|
1268
|
+
const event = detectCodexResetFireworks(previous, resetSnapshot);
|
|
1269
|
+
if (event) this.#onCodexResetFireworks?.(event);
|
|
1012
1270
|
}
|
|
1013
1271
|
|
|
1014
|
-
#observeLateUsageRefresh(session: AgentSession, reportsPromise: Promise<unknown
|
|
1272
|
+
#observeLateUsageRefresh(session: AgentSession, reportsPromise: Promise<unknown>, sequence: number): void {
|
|
1015
1273
|
void reportsPromise
|
|
1016
1274
|
.then(reports => {
|
|
1017
|
-
this.#applyUsageRefreshReports(session, reports);
|
|
1275
|
+
this.#applyUsageRefreshReports(session, reports, sequence);
|
|
1018
1276
|
})
|
|
1019
1277
|
.catch(() => {
|
|
1020
|
-
if (this.#disposed || this.session !== session)
|
|
1278
|
+
if (this.#disposed || this.session !== session || sequence < this.#latestAppliedUsageRefreshSequence) {
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1021
1281
|
this.#usageFetchedAt = Date.now();
|
|
1022
1282
|
});
|
|
1023
1283
|
}
|
|
@@ -1034,6 +1294,72 @@ export class StatusLineComponent implements Component {
|
|
|
1034
1294
|
}
|
|
1035
1295
|
}
|
|
1036
1296
|
|
|
1297
|
+
#normalizeCodexResetSnapshot(
|
|
1298
|
+
reports: unknown,
|
|
1299
|
+
activeIdentity: OAuthAccountIdentity | undefined,
|
|
1300
|
+
): CodexResetUsageSnapshot | null {
|
|
1301
|
+
if (!Array.isArray(reports)) return null;
|
|
1302
|
+
let matchingReport: UsageReport | undefined;
|
|
1303
|
+
for (const report of reports) {
|
|
1304
|
+
if (!report || typeof report !== "object") continue;
|
|
1305
|
+
if (
|
|
1306
|
+
!("provider" in report) ||
|
|
1307
|
+
report.provider !== "openai-codex" ||
|
|
1308
|
+
!("limits" in report) ||
|
|
1309
|
+
!Array.isArray(report.limits)
|
|
1310
|
+
) {
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
// The report boundary above validates the fields this extractor iterates;
|
|
1314
|
+
// optional metadata and credit fields are narrowed again before use.
|
|
1315
|
+
const usageReport = report as UsageReport;
|
|
1316
|
+
if (!codexReportMatchesExactIdentity(usageReport, activeIdentity)) continue;
|
|
1317
|
+
matchingReport = usageReport;
|
|
1318
|
+
break;
|
|
1319
|
+
}
|
|
1320
|
+
if (!matchingReport) return null;
|
|
1321
|
+
|
|
1322
|
+
const plan =
|
|
1323
|
+
typeof matchingReport.metadata?.planType === "string" && matchingReport.metadata.planType
|
|
1324
|
+
? matchingReport.metadata.planType
|
|
1325
|
+
: undefined;
|
|
1326
|
+
let sevenDay: CodexResetUsageSnapshot["sevenDay"];
|
|
1327
|
+
let sevenDayTier: string | undefined;
|
|
1328
|
+
for (const limit of matchingReport.limits) {
|
|
1329
|
+
if (!limit || typeof limit !== "object") continue;
|
|
1330
|
+
const candidate = limit as {
|
|
1331
|
+
scope?: { windowId?: string; tier?: string };
|
|
1332
|
+
window?: { resetsAt?: number };
|
|
1333
|
+
amount?: { usedFraction?: number };
|
|
1334
|
+
};
|
|
1335
|
+
const fraction = candidate.amount?.usedFraction;
|
|
1336
|
+
if (candidate.scope?.windowId !== "7d" || typeof fraction !== "number" || !Number.isFinite(fraction)) {
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const tier =
|
|
1340
|
+
typeof candidate.scope?.tier === "string" && candidate.scope.tier ? candidate.scope.tier : undefined;
|
|
1341
|
+
if (sevenDay && (sevenDayTier === undefined || tier)) continue;
|
|
1342
|
+
const resetsAt = candidate.window?.resetsAt;
|
|
1343
|
+
sevenDay = {
|
|
1344
|
+
percent: fraction * 100,
|
|
1345
|
+
resetsAt: typeof resetsAt === "number" && Number.isFinite(resetsAt) ? resetsAt : undefined,
|
|
1346
|
+
tier,
|
|
1347
|
+
plan,
|
|
1348
|
+
};
|
|
1349
|
+
sevenDayTier = tier;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const fetchedAt = matchingReport.fetchedAt;
|
|
1353
|
+
const availableCount = matchingReport.resetCredits?.availableCount;
|
|
1354
|
+
const observedAt = typeof fetchedAt === "number" && Number.isFinite(fetchedAt) ? fetchedAt : undefined;
|
|
1355
|
+
const savedResets =
|
|
1356
|
+
typeof availableCount === "number" && Number.isFinite(availableCount)
|
|
1357
|
+
? Math.max(0, Math.trunc(availableCount))
|
|
1358
|
+
: undefined;
|
|
1359
|
+
if (!sevenDay && savedResets === undefined) return null;
|
|
1360
|
+
return { observedAt, sevenDay, savedResets };
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1037
1363
|
#normalizeUsageReports(
|
|
1038
1364
|
reports: unknown,
|
|
1039
1365
|
activeProvider?: string,
|
|
@@ -1055,12 +1381,10 @@ export class StatusLineComponent implements Component {
|
|
|
1055
1381
|
if (activeProvider && provider !== activeProvider) continue;
|
|
1056
1382
|
const limits = (report as { limits?: unknown }).limits;
|
|
1057
1383
|
if (!Array.isArray(limits)) continue;
|
|
1384
|
+
const usageReport = report as UsageReport;
|
|
1058
1385
|
for (const limit of limits) {
|
|
1059
1386
|
if (!limit || typeof limit !== "object") continue;
|
|
1060
|
-
if (
|
|
1061
|
-
activeIdentity &&
|
|
1062
|
-
!limitMatchesActiveAccount(report as UsageReport, limit as UsageLimit, activeIdentity)
|
|
1063
|
-
) {
|
|
1387
|
+
if (activeIdentity && !limitMatchesActiveAccount(usageReport, limit as UsageLimit, activeIdentity)) {
|
|
1064
1388
|
continue;
|
|
1065
1389
|
}
|
|
1066
1390
|
const l = limit as {
|
|
@@ -1213,11 +1537,14 @@ export class StatusLineComponent implements Component {
|
|
|
1213
1537
|
: { projectDir, activeRepo: null, effectiveGitCwd: projectDir, worktree: null };
|
|
1214
1538
|
let gitBranch = includeGit || includePr ? this.#getCurrentBranch(activeRepoCache.effectiveGitCwd) : null;
|
|
1215
1539
|
// A jj repo has no git branch to read: git HEAD is detached (colocated) or
|
|
1216
|
-
// absent.
|
|
1217
|
-
//
|
|
1218
|
-
//
|
|
1219
|
-
|
|
1220
|
-
const gitHeadIsJjLike =
|
|
1540
|
+
// absent. A pending reftable resolve owns this cwd as an explicit Git repo,
|
|
1541
|
+
// so it must not be mistaken for an absent Git checkout and fall through to
|
|
1542
|
+
// an ancestor jj workspace.
|
|
1543
|
+
const gitHeadResolvePending = this.#branchResolveActive?.cwd === activeRepoCache.effectiveGitCwd;
|
|
1544
|
+
const gitHeadIsJjLike =
|
|
1545
|
+
!this.#cachedBranchHasGitRepository &&
|
|
1546
|
+
!gitHeadResolvePending &&
|
|
1547
|
+
(gitBranch === "detached" || gitBranch === null);
|
|
1221
1548
|
if (includeGit && gitHeadIsJjLike) {
|
|
1222
1549
|
gitBranch = this.#getJjBranch(activeRepoCache.effectiveGitCwd) ?? gitBranch;
|
|
1223
1550
|
}
|