@dreb/coding-agent 2.68.0 → 2.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/core/daily-cost-tracker.d.ts +42 -7
- package/dist/core/daily-cost-tracker.d.ts.map +1 -1
- package/dist/core/daily-cost-tracker.js +105 -117
- package/dist/core/daily-cost-tracker.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +2 -2
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +2 -2
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +2 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +2 -2
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/development.md +8 -3
- package/docs/providers.md +2 -1
- package/docs/rpc.md +2 -0
- package/docs/settings.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ Then just talk to dreb. All 13 standard built-in tools are enabled by default (u
|
|
|
97
97
|
|
|
98
98
|
For each built-in provider, dreb maintains a list of tool-capable models, updated with every release. Authenticate via subscription (`/login`) or API key, then select any model from that provider via `/model`.
|
|
99
99
|
|
|
100
|
-
GPT-6 Astra
|
|
100
|
+
GPT-6 models (Astra, Sol, Luna, 6.1 Sol) are available on `openai-codex` and `github-copilot`, with `xhigh` and native `max` thinking support. The `openai-codex` catalog is derived automatically from the OpenAI catalog, so new frontier models appear there without manual updates (whether they work depends on your ChatGPT plan/workspace). Codex defaults to `gpt-5.6-luna`. Missing saved defaults now warn and prefer the same provider's available default; explicit removed model IDs are not automatically migrated. See [model selection and removed models](docs/providers.md#model-selection-and-removed-models).
|
|
101
101
|
|
|
102
102
|
**Subscriptions:**
|
|
103
103
|
- OpenAI ChatGPT Plus/Pro (Codex)
|
|
@@ -19,9 +19,23 @@ export interface DailyCostBreakdown {
|
|
|
19
19
|
main: number;
|
|
20
20
|
subagent: number;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Session storage roots the daily tracker scans. Every root that may hold
|
|
24
|
+
* today's transcripts must be listed: the built-in nested main-session tree,
|
|
25
|
+
* any custom flat `sessionDir`, and every subagent root (configured + legacy).
|
|
26
|
+
* Files reachable from more than one root are counted once.
|
|
27
|
+
*/
|
|
28
|
+
export interface DailyCostSources {
|
|
29
|
+
/** Nested main-session trees: root / projectDir / *.jsonl. Default: built-in sessions dir. */
|
|
30
|
+
nestedMainRoots?: readonly string[];
|
|
31
|
+
/** Flat main-session directories: root / *.jsonl (custom `sessionDir`). */
|
|
32
|
+
flatMainRoots?: readonly string[];
|
|
33
|
+
/** Subagent roots: root / sessionId / *.jsonl and root / sessionId / step-N / *.jsonl. Default: legacy dir. */
|
|
34
|
+
subagentRoots?: readonly string[];
|
|
35
|
+
}
|
|
22
36
|
/**
|
|
23
37
|
* Tracks aggregate cost across all sessions for the current calendar day.
|
|
24
|
-
* Scans
|
|
38
|
+
* Scans main session roots and subagent session roots, caches result for O(1) footer access.
|
|
25
39
|
* Refreshes periodically (60s) and on-demand via refresh().
|
|
26
40
|
*/
|
|
27
41
|
export declare class DailyCostTracker {
|
|
@@ -30,9 +44,10 @@ export declare class DailyCostTracker {
|
|
|
30
44
|
private cachedSubagent;
|
|
31
45
|
private refreshTimer;
|
|
32
46
|
private disposed;
|
|
33
|
-
private
|
|
34
|
-
private
|
|
35
|
-
|
|
47
|
+
private readonly nestedMainRoots;
|
|
48
|
+
private readonly flatMainRoots;
|
|
49
|
+
private readonly subagentRoots;
|
|
50
|
+
constructor(sources?: DailyCostSources);
|
|
36
51
|
/** Get cached daily cost total (main + subagent). O(1). */
|
|
37
52
|
getDailyCost(): number;
|
|
38
53
|
/** Get cached daily cost breakdown: total, main, and subagent. O(1). */
|
|
@@ -43,8 +58,28 @@ export declare class DailyCostTracker {
|
|
|
43
58
|
dispose(): void;
|
|
44
59
|
private initialScan;
|
|
45
60
|
private scheduleNextRefresh;
|
|
46
|
-
private
|
|
47
|
-
private
|
|
48
|
-
private
|
|
61
|
+
private scanCosts;
|
|
62
|
+
private collectMainFiles;
|
|
63
|
+
private collectSubagentFiles;
|
|
49
64
|
}
|
|
65
|
+
/** Inputs needed to derive every storage root a session's process may write today's transcripts to. */
|
|
66
|
+
export interface DailyCostSessionContext {
|
|
67
|
+
cwd: string;
|
|
68
|
+
sessionManager: {
|
|
69
|
+
getCustomSessionInventoryRoot(): string | undefined;
|
|
70
|
+
};
|
|
71
|
+
settingsManager: {
|
|
72
|
+
getGlobalSettings(): {
|
|
73
|
+
sessionDir?: string;
|
|
74
|
+
};
|
|
75
|
+
getSessionDir(): string | undefined;
|
|
76
|
+
};
|
|
77
|
+
subagentSessionDiscoveryRoots: readonly string[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build daily-cost sources that honour custom session storage: the built-in nested
|
|
81
|
+
* tree, the session's explicit flat root (CLI/extension/settings), the global and
|
|
82
|
+
* effective `sessionDir` settings, and all subagent discovery roots (configured + legacy).
|
|
83
|
+
*/
|
|
84
|
+
export declare function dailyCostSourcesForSession(context: DailyCostSessionContext): DailyCostSources;
|
|
50
85
|
//# sourceMappingURL=daily-cost-tracker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daily-cost-tracker.d.ts","sourceRoot":"","sources":["../../src/core/daily-cost-tracker.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAQ1E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,GAAG,OAAO,CAM/D;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsBvE;AAED,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,qBAAa,gBAAgB;IAC5B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAU;IAErD,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,mBAAmB,CAAS;IAEpC,YAAY,WAAW,CAAC,EAAE,MAAM,EAAE,mBAAmB,CAAC,EAAE,MAAM,EAK7D;IAED,2DAA2D;IAC3D,YAAY,IAAI,MAAM,CAErB;IAED,wEAAwE;IACxE,qBAAqB,IAAI,kBAAkB,CAM1C;IAED,gDAAgD;IAC1C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ7B;IAED,sEAAsE;IACtE,OAAO,IAAI,IAAI,CAMd;YAEa,WAAW;IASzB,OAAO,CAAC,mBAAmB;YAsBb,gBAAgB;YA2BhB,oBAAoB;YAwCpB,mBAAmB;CA6BjC","sourcesContent":["import { type Dirent, existsSync } from \"fs\";\nimport { readdir, readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { getSessionsDir, getSubagentSessionsDir } from \"../config.js\";\n\n/**\n * Parse a session filename timestamp back to a Date.\n * Filename timestamps look like \"2026-04-09T18-49-11-406Z\" (colons and dots replaced with hyphens).\n * Returns null if the timestamp doesn't match the expected format.\n */\nexport function filenameTimestampToDate(fileTimestamp: string): Date | null {\n\t// fileTimestamp like \"2026-04-09T18-49-11-406Z\"\n\t// Reconstruct: YYYY-MM-DDThh:mm:ss.mmmZ\n\tconst match = fileTimestamp.match(/^(\\d{4}-\\d{2}-\\d{2})T(\\d{2})-(\\d{2})-(\\d{2})-(\\d{3})Z$/);\n\tif (!match) return null;\n\tconst iso = `${match[1]}T${match[2]}:${match[3]}:${match[4]}.${match[5]}Z`;\n\tconst date = new Date(iso);\n\treturn Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Check if two dates fall on the same local calendar day.\n */\nexport function isSameLocalDay(date: Date, today: Date): boolean {\n\treturn (\n\t\tdate.getFullYear() === today.getFullYear() &&\n\t\tdate.getMonth() === today.getMonth() &&\n\t\tdate.getDate() === today.getDate()\n\t);\n}\n\n/**\n * Sum `usage.cost.total` from all assistant messages in a JSONL session file.\n * Works identically for both main sessions and subagent sessions.\n */\nexport async function sumCostFromFile(filePath: string): Promise<number> {\n\ttry {\n\t\tconst content = await readFile(filePath, \"utf8\");\n\t\tlet total = 0;\n\n\t\tfor (const line of content.split(\"\\n\")) {\n\t\t\tif (!line.trim()) continue;\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"message\" && entry.message?.role === \"assistant\") {\n\t\t\t\t\ttotal += entry.message.usage?.cost?.total ?? 0;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\treturn total;\n\t} catch {\n\t\t/* File unreadable — treat as zero cost */\n\t\treturn 0;\n\t}\n}\n\n/** Breakdown of daily costs between main sessions and subagent sessions. */\nexport interface DailyCostBreakdown {\n\ttotal: number;\n\tmain: number;\n\tsubagent: number;\n}\n\n/**\n * Tracks aggregate cost across all sessions for the current calendar day.\n * Scans both main session files and subagent session files, caches result for O(1) footer access.\n * Refreshes periodically (60s) and on-demand via refresh().\n */\nexport class DailyCostTracker {\n\tprivate static readonly REFRESH_INTERVAL_MS = 60_000;\n\n\tprivate cachedMain = 0;\n\tprivate cachedSubagent = 0;\n\tprivate refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate disposed = false;\n\tprivate sessionsDir: string;\n\tprivate subagentSessionsDir: string;\n\n\tconstructor(sessionsDir?: string, subagentSessionsDir?: string) {\n\t\tthis.sessionsDir = sessionsDir ?? getSessionsDir();\n\t\tthis.subagentSessionsDir = subagentSessionsDir ?? getSubagentSessionsDir();\n\t\t// Kick off initial async scan — getDailyCost() returns 0 until it completes\n\t\tvoid this.initialScan();\n\t}\n\n\t/** Get cached daily cost total (main + subagent). O(1). */\n\tgetDailyCost(): number {\n\t\treturn this.cachedMain + this.cachedSubagent;\n\t}\n\n\t/** Get cached daily cost breakdown: total, main, and subagent. O(1). */\n\tgetDailyCostBreakdown(): DailyCostBreakdown {\n\t\treturn {\n\t\t\ttotal: this.cachedMain + this.cachedSubagent,\n\t\t\tmain: this.cachedMain,\n\t\t\tsubagent: this.cachedSubagent,\n\t\t};\n\t}\n\n\t/** Force an async refresh of the daily cost. */\n\tasync refresh(): Promise<void> {\n\t\tif (!this.disposed) {\n\t\t\tconst [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);\n\t\t\tif (!this.disposed) {\n\t\t\t\tthis.cachedMain = main;\n\t\t\t\tthis.cachedSubagent = subagent;\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Clean up timer and prevent in-flight scans from updating cache. */\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t\tthis.refreshTimer = null;\n\t\t}\n\t}\n\n\tprivate async initialScan(): Promise<void> {\n\t\tconst [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);\n\t\tif (!this.disposed) {\n\t\t\tthis.cachedMain = main;\n\t\t\tthis.cachedSubagent = subagent;\n\t\t\tthis.scheduleNextRefresh();\n\t\t}\n\t}\n\n\tprivate scheduleNextRefresh(): void {\n\t\tif (this.disposed) return;\n\t\tthis.refreshTimer = setTimeout(async () => {\n\t\t\tthis.refreshTimer = null;\n\t\t\tif (this.disposed) return;\n\t\t\tconst [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);\n\t\t\tif (!this.disposed) {\n\t\t\t\tthis.cachedMain = main;\n\t\t\t\tthis.cachedSubagent = subagent;\n\t\t\t\tthis.scheduleNextRefresh();\n\t\t\t}\n\t\t}, DailyCostTracker.REFRESH_INTERVAL_MS);\n\t\t// Allow the timer to not keep the process alive\n\t\tif (this.refreshTimer && typeof this.refreshTimer === \"object\" && \"unref\" in this.refreshTimer) {\n\t\t\tthis.refreshTimer.unref();\n\t\t}\n\t}\n\n\t/**\n\t * Scan main sessions directory.\n\t * Structure: sessionsDir / projectDir / TIMESTAMP_UUID.jsonl\n\t */\n\tprivate async scanMainSessions(): Promise<number> {\n\t\ttry {\n\t\t\tif (!existsSync(this.sessionsDir)) return 0;\n\n\t\t\tconst now = new Date();\n\t\t\tlet total = 0;\n\t\t\tconst projectDirs = await readdir(this.sessionsDir, { withFileTypes: true });\n\n\t\t\tfor (const dirEntry of projectDirs) {\n\t\t\t\tif (!dirEntry.isDirectory()) continue;\n\n\t\t\t\tconst projectDir = join(this.sessionsDir, dirEntry.name);\n\t\t\t\ttotal += await this.scanJsonlFilesInDir(projectDir, now);\n\t\t\t}\n\n\t\t\treturn total;\n\t\t} catch {\n\t\t\t// Never crash the app\n\t\t\treturn 0;\n\t\t}\n\t}\n\n\t/**\n\t * Scan subagent sessions directory.\n\t * Structure: subagentSessionsDir / sessionId / TIMESTAMP_UUID.jsonl\n\t * subagentSessionsDir / sessionId / step-N / TIMESTAMP_UUID.jsonl\n\t */\n\tprivate async scanSubagentSessions(): Promise<number> {\n\t\ttry {\n\t\t\tif (!existsSync(this.subagentSessionsDir)) return 0;\n\n\t\t\tconst now = new Date();\n\t\t\tlet total = 0;\n\t\t\tconst sessionIdDirs = await readdir(this.subagentSessionsDir, { withFileTypes: true });\n\n\t\t\tfor (const dirEntry of sessionIdDirs) {\n\t\t\t\tif (!dirEntry.isDirectory()) continue;\n\n\t\t\t\tconst sessionIdDir = join(this.subagentSessionsDir, dirEntry.name);\n\n\t\t\t\t// Scan direct JSONL files in the session-id directory\n\t\t\t\ttotal += await this.scanJsonlFilesInDir(sessionIdDir, now);\n\n\t\t\t\t// Scan step-N subdirectories for chain agents\n\t\t\t\tlet subEntries: Dirent[];\n\t\t\t\ttry {\n\t\t\t\t\tsubEntries = await readdir(sessionIdDir, { withFileTypes: true });\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tfor (const subEntry of subEntries) {\n\t\t\t\t\tif (!subEntry.isDirectory()) continue;\n\t\t\t\t\tconst stepDir = join(sessionIdDir, subEntry.name);\n\t\t\t\t\ttotal += await this.scanJsonlFilesInDir(stepDir, now);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn total;\n\t\t} catch {\n\t\t\t// Never crash the app\n\t\t\treturn 0;\n\t\t}\n\t}\n\n\t/**\n\t * Scan a directory for today's JSONL session files and sum their costs.\n\t */\n\tprivate async scanJsonlFilesInDir(dirPath: string, now: Date): Promise<number> {\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = (await readdir(dirPath)).filter((f) => f.endsWith(\".jsonl\"));\n\t\t} catch {\n\t\t\t/* Directory unreadable — skip */\n\t\t\treturn 0;\n\t\t}\n\n\t\tlet total = 0;\n\t\tfor (const filename of files) {\n\t\t\t// Extract timestamp part: everything before the UUID\n\t\t\t// Filename: 2026-04-09T18-49-11-406Z_33137d5d-d1e4-4a0e-baca-ebd08ab0e2e0.jsonl\n\t\t\tconst underscoreIdx = filename.indexOf(\"_\", 20);\n\t\t\tif (underscoreIdx === -1) continue;\n\n\t\t\tconst timestampPart = filename.slice(0, underscoreIdx);\n\t\t\tconst fileDate = filenameTimestampToDate(timestampPart);\n\t\t\tif (!fileDate) continue;\n\n\t\t\t// Skip sessions not from today (compares local calendar day)\n\t\t\tif (!isSameLocalDay(fileDate, now)) continue;\n\n\t\t\t// Read and parse the JSONL file\n\t\t\ttotal += await sumCostFromFile(join(dirPath, filename));\n\t\t}\n\n\t\treturn total;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"daily-cost-tracker.d.ts","sourceRoot":"","sources":["../../src/core/daily-cost-tracker.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAQ1E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,GAAG,OAAO,CAM/D;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsBvE;AAED,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAChC,8FAA8F;IAC9F,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,2EAA2E;IAC3E,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,+GAA+G;IAC/G,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAMD;;;;GAIG;AACH,qBAAa,gBAAgB;IAC5B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAU;IAErD,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAW;IAC3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAW;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAW;IAEzC,YAAY,OAAO,GAAE,gBAAqB,EAMzC;IAED,2DAA2D;IAC3D,YAAY,IAAI,MAAM,CAErB;IAED,wEAAwE;IACxE,qBAAqB,IAAI,kBAAkB,CAM1C;IAED,gDAAgD;IAC1C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAO7B;IAED,sEAAsE;IACtE,OAAO,IAAI,IAAI,CAMd;YAEa,WAAW;IAKzB,OAAO,CAAC,mBAAmB;YAab,SAAS;YAaT,gBAAgB;YAUhB,oBAAoB;CAUlC;AAqCD,uGAAuG;AACvG,MAAM,WAAW,uBAAuB;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE;QAAE,6BAA6B,IAAI,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IACxE,eAAe,EAAE;QAAE,iBAAiB,IAAI;YAAE,UAAU,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,aAAa,IAAI,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IACvG,6BAA6B,EAAE,SAAS,MAAM,EAAE,CAAC;CACjD;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CAW7F","sourcesContent":["import { readdir, readFile } from \"fs/promises\";\nimport { join, resolve } from \"path\";\nimport { getSessionsDir, getSubagentSessionsDir, resolveConfiguredDirectory } from \"../config.js\";\n\n/**\n * Parse a session filename timestamp back to a Date.\n * Filename timestamps look like \"2026-04-09T18-49-11-406Z\" (colons and dots replaced with hyphens).\n * Returns null if the timestamp doesn't match the expected format.\n */\nexport function filenameTimestampToDate(fileTimestamp: string): Date | null {\n\t// fileTimestamp like \"2026-04-09T18-49-11-406Z\"\n\t// Reconstruct: YYYY-MM-DDThh:mm:ss.mmmZ\n\tconst match = fileTimestamp.match(/^(\\d{4}-\\d{2}-\\d{2})T(\\d{2})-(\\d{2})-(\\d{2})-(\\d{3})Z$/);\n\tif (!match) return null;\n\tconst iso = `${match[1]}T${match[2]}:${match[3]}:${match[4]}.${match[5]}Z`;\n\tconst date = new Date(iso);\n\treturn Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Check if two dates fall on the same local calendar day.\n */\nexport function isSameLocalDay(date: Date, today: Date): boolean {\n\treturn (\n\t\tdate.getFullYear() === today.getFullYear() &&\n\t\tdate.getMonth() === today.getMonth() &&\n\t\tdate.getDate() === today.getDate()\n\t);\n}\n\n/**\n * Sum `usage.cost.total` from all assistant messages in a JSONL session file.\n * Works identically for both main sessions and subagent sessions.\n */\nexport async function sumCostFromFile(filePath: string): Promise<number> {\n\ttry {\n\t\tconst content = await readFile(filePath, \"utf8\");\n\t\tlet total = 0;\n\n\t\tfor (const line of content.split(\"\\n\")) {\n\t\t\tif (!line.trim()) continue;\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"message\" && entry.message?.role === \"assistant\") {\n\t\t\t\t\ttotal += entry.message.usage?.cost?.total ?? 0;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\treturn total;\n\t} catch {\n\t\t/* File unreadable — treat as zero cost */\n\t\treturn 0;\n\t}\n}\n\n/** Breakdown of daily costs between main sessions and subagent sessions. */\nexport interface DailyCostBreakdown {\n\ttotal: number;\n\tmain: number;\n\tsubagent: number;\n}\n\n/**\n * Session storage roots the daily tracker scans. Every root that may hold\n * today's transcripts must be listed: the built-in nested main-session tree,\n * any custom flat `sessionDir`, and every subagent root (configured + legacy).\n * Files reachable from more than one root are counted once.\n */\nexport interface DailyCostSources {\n\t/** Nested main-session trees: root / projectDir / *.jsonl. Default: built-in sessions dir. */\n\tnestedMainRoots?: readonly string[];\n\t/** Flat main-session directories: root / *.jsonl (custom `sessionDir`). */\n\tflatMainRoots?: readonly string[];\n\t/** Subagent roots: root / sessionId / *.jsonl and root / sessionId / step-N / *.jsonl. Default: legacy dir. */\n\tsubagentRoots?: readonly string[];\n}\n\nfunction uniqueRoots(roots: readonly (string | undefined)[]): string[] {\n\treturn [...new Set(roots.filter((root): root is string => !!root).map((root) => resolve(root)))];\n}\n\n/**\n * Tracks aggregate cost across all sessions for the current calendar day.\n * Scans main session roots and subagent session roots, caches result for O(1) footer access.\n * Refreshes periodically (60s) and on-demand via refresh().\n */\nexport class DailyCostTracker {\n\tprivate static readonly REFRESH_INTERVAL_MS = 60_000;\n\n\tprivate cachedMain = 0;\n\tprivate cachedSubagent = 0;\n\tprivate refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate disposed = false;\n\tprivate readonly nestedMainRoots: string[];\n\tprivate readonly flatMainRoots: string[];\n\tprivate readonly subagentRoots: string[];\n\n\tconstructor(sources: DailyCostSources = {}) {\n\t\tthis.nestedMainRoots = uniqueRoots(sources.nestedMainRoots ?? [getSessionsDir()]);\n\t\tthis.flatMainRoots = uniqueRoots(sources.flatMainRoots ?? []);\n\t\tthis.subagentRoots = uniqueRoots(sources.subagentRoots ?? [getSubagentSessionsDir()]);\n\t\t// Kick off initial async scan — getDailyCost() returns 0 until it completes\n\t\tvoid this.initialScan();\n\t}\n\n\t/** Get cached daily cost total (main + subagent). O(1). */\n\tgetDailyCost(): number {\n\t\treturn this.cachedMain + this.cachedSubagent;\n\t}\n\n\t/** Get cached daily cost breakdown: total, main, and subagent. O(1). */\n\tgetDailyCostBreakdown(): DailyCostBreakdown {\n\t\treturn {\n\t\t\ttotal: this.cachedMain + this.cachedSubagent,\n\t\t\tmain: this.cachedMain,\n\t\t\tsubagent: this.cachedSubagent,\n\t\t};\n\t}\n\n\t/** Force an async refresh of the daily cost. */\n\tasync refresh(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tconst [main, subagent] = await this.scanCosts();\n\t\tif (!this.disposed) {\n\t\t\tthis.cachedMain = main;\n\t\t\tthis.cachedSubagent = subagent;\n\t\t}\n\t}\n\n\t/** Clean up timer and prevent in-flight scans from updating cache. */\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t\tthis.refreshTimer = null;\n\t\t}\n\t}\n\n\tprivate async initialScan(): Promise<void> {\n\t\tawait this.refresh();\n\t\tthis.scheduleNextRefresh();\n\t}\n\n\tprivate scheduleNextRefresh(): void {\n\t\tif (this.disposed) return;\n\t\tthis.refreshTimer = setTimeout(async () => {\n\t\t\tthis.refreshTimer = null;\n\t\t\tawait this.refresh();\n\t\t\tthis.scheduleNextRefresh();\n\t\t}, DailyCostTracker.REFRESH_INTERVAL_MS);\n\t\t// Allow the timer to not keep the process alive\n\t\tif (this.refreshTimer && typeof this.refreshTimer === \"object\" && \"unref\" in this.refreshTimer) {\n\t\t\tthis.refreshTimer.unref();\n\t\t}\n\t}\n\n\tprivate async scanCosts(): Promise<[number, number]> {\n\t\tconst now = new Date();\n\t\tconst [mainFiles, subagentFiles] = await Promise.all([\n\t\t\tthis.collectMainFiles(now),\n\t\t\tthis.collectSubagentFiles(now),\n\t\t]);\n\t\t// A file reachable from both a main and a subagent root is counted as main only.\n\t\tfor (const file of mainFiles) subagentFiles.delete(file);\n\t\tconst [main, subagent] = await Promise.all([sumFiles(mainFiles), sumFiles(subagentFiles)]);\n\t\treturn [main, subagent];\n\t}\n\n\t/** Main sessions: nested root / projectDir / *.jsonl, and flat root / *.jsonl. */\n\tprivate async collectMainFiles(now: Date): Promise<Set<string>> {\n\t\tconst files = new Set<string>();\n\t\tfor (const root of this.nestedMainRoots) {\n\t\t\tfor (const projectDir of await listSubdirs(root)) await addTodaysJsonlFiles(files, projectDir, now);\n\t\t}\n\t\tfor (const root of this.flatMainRoots) await addTodaysJsonlFiles(files, root, now);\n\t\treturn files;\n\t}\n\n\t/** Subagent sessions: root / sessionId / *.jsonl and root / sessionId / step-N / *.jsonl. */\n\tprivate async collectSubagentFiles(now: Date): Promise<Set<string>> {\n\t\tconst files = new Set<string>();\n\t\tfor (const root of this.subagentRoots) {\n\t\t\tfor (const sessionIdDir of await listSubdirs(root)) {\n\t\t\t\tawait addTodaysJsonlFiles(files, sessionIdDir, now);\n\t\t\t\tfor (const stepDir of await listSubdirs(sessionIdDir)) await addTodaysJsonlFiles(files, stepDir, now);\n\t\t\t}\n\t\t}\n\t\treturn files;\n\t}\n}\n\n/** List child directories; a missing or unreadable directory yields none (never crash the footer). */\nasync function listSubdirs(dir: string): Promise<string[]> {\n\ttry {\n\t\tconst entries = await readdir(dir, { withFileTypes: true });\n\t\treturn entries.filter((entry) => entry.isDirectory()).map((entry) => join(dir, entry.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/** Add today's JSONL session files in dirPath (by filename timestamp, local calendar day). */\nasync function addTodaysJsonlFiles(files: Set<string>, dirPath: string, now: Date): Promise<void> {\n\tlet names: string[];\n\ttry {\n\t\tnames = (await readdir(dirPath)).filter((f) => f.endsWith(\".jsonl\"));\n\t} catch {\n\t\t/* Directory missing or unreadable — skip */\n\t\treturn;\n\t}\n\tfor (const filename of names) {\n\t\t// Filename: 2026-04-09T18-49-11-406Z_33137d5d-d1e4-4a0e-baca-ebd08ab0e2e0.jsonl\n\t\tconst underscoreIdx = filename.indexOf(\"_\", 20);\n\t\tif (underscoreIdx === -1) continue;\n\t\tconst fileDate = filenameTimestampToDate(filename.slice(0, underscoreIdx));\n\t\tif (!fileDate || !isSameLocalDay(fileDate, now)) continue;\n\t\tfiles.add(join(dirPath, filename));\n\t}\n}\n\nasync function sumFiles(files: Iterable<string>): Promise<number> {\n\tlet total = 0;\n\tfor (const file of files) total += await sumCostFromFile(file);\n\treturn total;\n}\n\n/** Inputs needed to derive every storage root a session's process may write today's transcripts to. */\nexport interface DailyCostSessionContext {\n\tcwd: string;\n\tsessionManager: { getCustomSessionInventoryRoot(): string | undefined };\n\tsettingsManager: { getGlobalSettings(): { sessionDir?: string }; getSessionDir(): string | undefined };\n\tsubagentSessionDiscoveryRoots: readonly string[];\n}\n\n/**\n * Build daily-cost sources that honour custom session storage: the built-in nested\n * tree, the session's explicit flat root (CLI/extension/settings), the global and\n * effective `sessionDir` settings, and all subagent discovery roots (configured + legacy).\n */\nexport function dailyCostSourcesForSession(context: DailyCostSessionContext): DailyCostSources {\n\tconst { cwd, sessionManager, settingsManager } = context;\n\treturn {\n\t\tnestedMainRoots: [getSessionsDir()],\n\t\tflatMainRoots: uniqueRoots([\n\t\t\tsessionManager.getCustomSessionInventoryRoot(),\n\t\t\tresolveConfiguredDirectory(settingsManager.getGlobalSettings().sessionDir, cwd),\n\t\t\tresolveConfiguredDirectory(settingsManager.getSessionDir(), cwd),\n\t\t]),\n\t\tsubagentRoots: [...context.subagentSessionDiscoveryRoots],\n\t};\n}\n"]}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { existsSync } from "fs";
|
|
2
1
|
import { readdir, readFile } from "fs/promises";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { getSessionsDir, getSubagentSessionsDir } from "../config.js";
|
|
2
|
+
import { join, resolve } from "path";
|
|
3
|
+
import { getSessionsDir, getSubagentSessionsDir, resolveConfiguredDirectory } from "../config.js";
|
|
5
4
|
/**
|
|
6
5
|
* Parse a session filename timestamp back to a Date.
|
|
7
6
|
* Filename timestamps look like "2026-04-09T18-49-11-406Z" (colons and dots replaced with hyphens).
|
|
@@ -53,9 +52,12 @@ export async function sumCostFromFile(filePath) {
|
|
|
53
52
|
return 0;
|
|
54
53
|
}
|
|
55
54
|
}
|
|
55
|
+
function uniqueRoots(roots) {
|
|
56
|
+
return [...new Set(roots.filter((root) => !!root).map((root) => resolve(root)))];
|
|
57
|
+
}
|
|
56
58
|
/**
|
|
57
59
|
* Tracks aggregate cost across all sessions for the current calendar day.
|
|
58
|
-
* Scans
|
|
60
|
+
* Scans main session roots and subagent session roots, caches result for O(1) footer access.
|
|
59
61
|
* Refreshes periodically (60s) and on-demand via refresh().
|
|
60
62
|
*/
|
|
61
63
|
export class DailyCostTracker {
|
|
@@ -64,11 +66,13 @@ export class DailyCostTracker {
|
|
|
64
66
|
cachedSubagent = 0;
|
|
65
67
|
refreshTimer = null;
|
|
66
68
|
disposed = false;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.
|
|
69
|
+
nestedMainRoots;
|
|
70
|
+
flatMainRoots;
|
|
71
|
+
subagentRoots;
|
|
72
|
+
constructor(sources = {}) {
|
|
73
|
+
this.nestedMainRoots = uniqueRoots(sources.nestedMainRoots ?? [getSessionsDir()]);
|
|
74
|
+
this.flatMainRoots = uniqueRoots(sources.flatMainRoots ?? []);
|
|
75
|
+
this.subagentRoots = uniqueRoots(sources.subagentRoots ?? [getSubagentSessionsDir()]);
|
|
72
76
|
// Kick off initial async scan — getDailyCost() returns 0 until it completes
|
|
73
77
|
void this.initialScan();
|
|
74
78
|
}
|
|
@@ -86,12 +90,12 @@ export class DailyCostTracker {
|
|
|
86
90
|
}
|
|
87
91
|
/** Force an async refresh of the daily cost. */
|
|
88
92
|
async refresh() {
|
|
93
|
+
if (this.disposed)
|
|
94
|
+
return;
|
|
95
|
+
const [main, subagent] = await this.scanCosts();
|
|
89
96
|
if (!this.disposed) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.cachedMain = main;
|
|
93
|
-
this.cachedSubagent = subagent;
|
|
94
|
-
}
|
|
97
|
+
this.cachedMain = main;
|
|
98
|
+
this.cachedSubagent = subagent;
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
/** Clean up timer and prevent in-flight scans from updating cache. */
|
|
@@ -103,126 +107,110 @@ export class DailyCostTracker {
|
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
async initialScan() {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.cachedMain = main;
|
|
109
|
-
this.cachedSubagent = subagent;
|
|
110
|
-
this.scheduleNextRefresh();
|
|
111
|
-
}
|
|
110
|
+
await this.refresh();
|
|
111
|
+
this.scheduleNextRefresh();
|
|
112
112
|
}
|
|
113
113
|
scheduleNextRefresh() {
|
|
114
114
|
if (this.disposed)
|
|
115
115
|
return;
|
|
116
116
|
this.refreshTimer = setTimeout(async () => {
|
|
117
117
|
this.refreshTimer = null;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);
|
|
121
|
-
if (!this.disposed) {
|
|
122
|
-
this.cachedMain = main;
|
|
123
|
-
this.cachedSubagent = subagent;
|
|
124
|
-
this.scheduleNextRefresh();
|
|
125
|
-
}
|
|
118
|
+
await this.refresh();
|
|
119
|
+
this.scheduleNextRefresh();
|
|
126
120
|
}, DailyCostTracker.REFRESH_INTERVAL_MS);
|
|
127
121
|
// Allow the timer to not keep the process alive
|
|
128
122
|
if (this.refreshTimer && typeof this.refreshTimer === "object" && "unref" in this.refreshTimer) {
|
|
129
123
|
this.refreshTimer.unref();
|
|
130
124
|
}
|
|
131
125
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
// Never crash the app
|
|
153
|
-
return 0;
|
|
126
|
+
async scanCosts() {
|
|
127
|
+
const now = new Date();
|
|
128
|
+
const [mainFiles, subagentFiles] = await Promise.all([
|
|
129
|
+
this.collectMainFiles(now),
|
|
130
|
+
this.collectSubagentFiles(now),
|
|
131
|
+
]);
|
|
132
|
+
// A file reachable from both a main and a subagent root is counted as main only.
|
|
133
|
+
for (const file of mainFiles)
|
|
134
|
+
subagentFiles.delete(file);
|
|
135
|
+
const [main, subagent] = await Promise.all([sumFiles(mainFiles), sumFiles(subagentFiles)]);
|
|
136
|
+
return [main, subagent];
|
|
137
|
+
}
|
|
138
|
+
/** Main sessions: nested root / projectDir / *.jsonl, and flat root / *.jsonl. */
|
|
139
|
+
async collectMainFiles(now) {
|
|
140
|
+
const files = new Set();
|
|
141
|
+
for (const root of this.nestedMainRoots) {
|
|
142
|
+
for (const projectDir of await listSubdirs(root))
|
|
143
|
+
await addTodaysJsonlFiles(files, projectDir, now);
|
|
154
144
|
}
|
|
145
|
+
for (const root of this.flatMainRoots)
|
|
146
|
+
await addTodaysJsonlFiles(files, root, now);
|
|
147
|
+
return files;
|
|
155
148
|
}
|
|
156
|
-
/**
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return 0;
|
|
165
|
-
const now = new Date();
|
|
166
|
-
let total = 0;
|
|
167
|
-
const sessionIdDirs = await readdir(this.subagentSessionsDir, { withFileTypes: true });
|
|
168
|
-
for (const dirEntry of sessionIdDirs) {
|
|
169
|
-
if (!dirEntry.isDirectory())
|
|
170
|
-
continue;
|
|
171
|
-
const sessionIdDir = join(this.subagentSessionsDir, dirEntry.name);
|
|
172
|
-
// Scan direct JSONL files in the session-id directory
|
|
173
|
-
total += await this.scanJsonlFilesInDir(sessionIdDir, now);
|
|
174
|
-
// Scan step-N subdirectories for chain agents
|
|
175
|
-
let subEntries;
|
|
176
|
-
try {
|
|
177
|
-
subEntries = await readdir(sessionIdDir, { withFileTypes: true });
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
for (const subEntry of subEntries) {
|
|
183
|
-
if (!subEntry.isDirectory())
|
|
184
|
-
continue;
|
|
185
|
-
const stepDir = join(sessionIdDir, subEntry.name);
|
|
186
|
-
total += await this.scanJsonlFilesInDir(stepDir, now);
|
|
187
|
-
}
|
|
149
|
+
/** Subagent sessions: root / sessionId / *.jsonl and root / sessionId / step-N / *.jsonl. */
|
|
150
|
+
async collectSubagentFiles(now) {
|
|
151
|
+
const files = new Set();
|
|
152
|
+
for (const root of this.subagentRoots) {
|
|
153
|
+
for (const sessionIdDir of await listSubdirs(root)) {
|
|
154
|
+
await addTodaysJsonlFiles(files, sessionIdDir, now);
|
|
155
|
+
for (const stepDir of await listSubdirs(sessionIdDir))
|
|
156
|
+
await addTodaysJsonlFiles(files, stepDir, now);
|
|
188
157
|
}
|
|
189
|
-
return total;
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
// Never crash the app
|
|
193
|
-
return 0;
|
|
194
158
|
}
|
|
159
|
+
return files;
|
|
195
160
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
/* Directory unreadable — skip */
|
|
206
|
-
return 0;
|
|
207
|
-
}
|
|
208
|
-
let total = 0;
|
|
209
|
-
for (const filename of files) {
|
|
210
|
-
// Extract timestamp part: everything before the UUID
|
|
211
|
-
// Filename: 2026-04-09T18-49-11-406Z_33137d5d-d1e4-4a0e-baca-ebd08ab0e2e0.jsonl
|
|
212
|
-
const underscoreIdx = filename.indexOf("_", 20);
|
|
213
|
-
if (underscoreIdx === -1)
|
|
214
|
-
continue;
|
|
215
|
-
const timestampPart = filename.slice(0, underscoreIdx);
|
|
216
|
-
const fileDate = filenameTimestampToDate(timestampPart);
|
|
217
|
-
if (!fileDate)
|
|
218
|
-
continue;
|
|
219
|
-
// Skip sessions not from today (compares local calendar day)
|
|
220
|
-
if (!isSameLocalDay(fileDate, now))
|
|
221
|
-
continue;
|
|
222
|
-
// Read and parse the JSONL file
|
|
223
|
-
total += await sumCostFromFile(join(dirPath, filename));
|
|
224
|
-
}
|
|
225
|
-
return total;
|
|
161
|
+
}
|
|
162
|
+
/** List child directories; a missing or unreadable directory yields none (never crash the footer). */
|
|
163
|
+
async function listSubdirs(dir) {
|
|
164
|
+
try {
|
|
165
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
166
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => join(dir, entry.name));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return [];
|
|
226
170
|
}
|
|
227
171
|
}
|
|
172
|
+
/** Add today's JSONL session files in dirPath (by filename timestamp, local calendar day). */
|
|
173
|
+
async function addTodaysJsonlFiles(files, dirPath, now) {
|
|
174
|
+
let names;
|
|
175
|
+
try {
|
|
176
|
+
names = (await readdir(dirPath)).filter((f) => f.endsWith(".jsonl"));
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
/* Directory missing or unreadable — skip */
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
for (const filename of names) {
|
|
183
|
+
// Filename: 2026-04-09T18-49-11-406Z_33137d5d-d1e4-4a0e-baca-ebd08ab0e2e0.jsonl
|
|
184
|
+
const underscoreIdx = filename.indexOf("_", 20);
|
|
185
|
+
if (underscoreIdx === -1)
|
|
186
|
+
continue;
|
|
187
|
+
const fileDate = filenameTimestampToDate(filename.slice(0, underscoreIdx));
|
|
188
|
+
if (!fileDate || !isSameLocalDay(fileDate, now))
|
|
189
|
+
continue;
|
|
190
|
+
files.add(join(dirPath, filename));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function sumFiles(files) {
|
|
194
|
+
let total = 0;
|
|
195
|
+
for (const file of files)
|
|
196
|
+
total += await sumCostFromFile(file);
|
|
197
|
+
return total;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Build daily-cost sources that honour custom session storage: the built-in nested
|
|
201
|
+
* tree, the session's explicit flat root (CLI/extension/settings), the global and
|
|
202
|
+
* effective `sessionDir` settings, and all subagent discovery roots (configured + legacy).
|
|
203
|
+
*/
|
|
204
|
+
export function dailyCostSourcesForSession(context) {
|
|
205
|
+
const { cwd, sessionManager, settingsManager } = context;
|
|
206
|
+
return {
|
|
207
|
+
nestedMainRoots: [getSessionsDir()],
|
|
208
|
+
flatMainRoots: uniqueRoots([
|
|
209
|
+
sessionManager.getCustomSessionInventoryRoot(),
|
|
210
|
+
resolveConfiguredDirectory(settingsManager.getGlobalSettings().sessionDir, cwd),
|
|
211
|
+
resolveConfiguredDirectory(settingsManager.getSessionDir(), cwd),
|
|
212
|
+
]),
|
|
213
|
+
subagentRoots: [...context.subagentSessionDiscoveryRoots],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
228
216
|
//# sourceMappingURL=daily-cost-tracker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daily-cost-tracker.js","sourceRoot":"","sources":["../../src/core/daily-cost-tracker.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Dirent, existsSync } from \"fs\";\nimport { readdir, readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { getSessionsDir, getSubagentSessionsDir } from \"../config.js\";\n\n/**\n * Parse a session filename timestamp back to a Date.\n * Filename timestamps look like \"2026-04-09T18-49-11-406Z\" (colons and dots replaced with hyphens).\n * Returns null if the timestamp doesn't match the expected format.\n */\nexport function filenameTimestampToDate(fileTimestamp: string): Date | null {\n\t// fileTimestamp like \"2026-04-09T18-49-11-406Z\"\n\t// Reconstruct: YYYY-MM-DDThh:mm:ss.mmmZ\n\tconst match = fileTimestamp.match(/^(\\d{4}-\\d{2}-\\d{2})T(\\d{2})-(\\d{2})-(\\d{2})-(\\d{3})Z$/);\n\tif (!match) return null;\n\tconst iso = `${match[1]}T${match[2]}:${match[3]}:${match[4]}.${match[5]}Z`;\n\tconst date = new Date(iso);\n\treturn Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Check if two dates fall on the same local calendar day.\n */\nexport function isSameLocalDay(date: Date, today: Date): boolean {\n\treturn (\n\t\tdate.getFullYear() === today.getFullYear() &&\n\t\tdate.getMonth() === today.getMonth() &&\n\t\tdate.getDate() === today.getDate()\n\t);\n}\n\n/**\n * Sum `usage.cost.total` from all assistant messages in a JSONL session file.\n * Works identically for both main sessions and subagent sessions.\n */\nexport async function sumCostFromFile(filePath: string): Promise<number> {\n\ttry {\n\t\tconst content = await readFile(filePath, \"utf8\");\n\t\tlet total = 0;\n\n\t\tfor (const line of content.split(\"\\n\")) {\n\t\t\tif (!line.trim()) continue;\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"message\" && entry.message?.role === \"assistant\") {\n\t\t\t\t\ttotal += entry.message.usage?.cost?.total ?? 0;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\treturn total;\n\t} catch {\n\t\t/* File unreadable — treat as zero cost */\n\t\treturn 0;\n\t}\n}\n\n/** Breakdown of daily costs between main sessions and subagent sessions. */\nexport interface DailyCostBreakdown {\n\ttotal: number;\n\tmain: number;\n\tsubagent: number;\n}\n\n/**\n * Tracks aggregate cost across all sessions for the current calendar day.\n * Scans both main session files and subagent session files, caches result for O(1) footer access.\n * Refreshes periodically (60s) and on-demand via refresh().\n */\nexport class DailyCostTracker {\n\tprivate static readonly REFRESH_INTERVAL_MS = 60_000;\n\n\tprivate cachedMain = 0;\n\tprivate cachedSubagent = 0;\n\tprivate refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate disposed = false;\n\tprivate sessionsDir: string;\n\tprivate subagentSessionsDir: string;\n\n\tconstructor(sessionsDir?: string, subagentSessionsDir?: string) {\n\t\tthis.sessionsDir = sessionsDir ?? getSessionsDir();\n\t\tthis.subagentSessionsDir = subagentSessionsDir ?? getSubagentSessionsDir();\n\t\t// Kick off initial async scan — getDailyCost() returns 0 until it completes\n\t\tvoid this.initialScan();\n\t}\n\n\t/** Get cached daily cost total (main + subagent). O(1). */\n\tgetDailyCost(): number {\n\t\treturn this.cachedMain + this.cachedSubagent;\n\t}\n\n\t/** Get cached daily cost breakdown: total, main, and subagent. O(1). */\n\tgetDailyCostBreakdown(): DailyCostBreakdown {\n\t\treturn {\n\t\t\ttotal: this.cachedMain + this.cachedSubagent,\n\t\t\tmain: this.cachedMain,\n\t\t\tsubagent: this.cachedSubagent,\n\t\t};\n\t}\n\n\t/** Force an async refresh of the daily cost. */\n\tasync refresh(): Promise<void> {\n\t\tif (!this.disposed) {\n\t\t\tconst [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);\n\t\t\tif (!this.disposed) {\n\t\t\t\tthis.cachedMain = main;\n\t\t\t\tthis.cachedSubagent = subagent;\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Clean up timer and prevent in-flight scans from updating cache. */\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t\tthis.refreshTimer = null;\n\t\t}\n\t}\n\n\tprivate async initialScan(): Promise<void> {\n\t\tconst [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);\n\t\tif (!this.disposed) {\n\t\t\tthis.cachedMain = main;\n\t\t\tthis.cachedSubagent = subagent;\n\t\t\tthis.scheduleNextRefresh();\n\t\t}\n\t}\n\n\tprivate scheduleNextRefresh(): void {\n\t\tif (this.disposed) return;\n\t\tthis.refreshTimer = setTimeout(async () => {\n\t\t\tthis.refreshTimer = null;\n\t\t\tif (this.disposed) return;\n\t\t\tconst [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);\n\t\t\tif (!this.disposed) {\n\t\t\t\tthis.cachedMain = main;\n\t\t\t\tthis.cachedSubagent = subagent;\n\t\t\t\tthis.scheduleNextRefresh();\n\t\t\t}\n\t\t}, DailyCostTracker.REFRESH_INTERVAL_MS);\n\t\t// Allow the timer to not keep the process alive\n\t\tif (this.refreshTimer && typeof this.refreshTimer === \"object\" && \"unref\" in this.refreshTimer) {\n\t\t\tthis.refreshTimer.unref();\n\t\t}\n\t}\n\n\t/**\n\t * Scan main sessions directory.\n\t * Structure: sessionsDir / projectDir / TIMESTAMP_UUID.jsonl\n\t */\n\tprivate async scanMainSessions(): Promise<number> {\n\t\ttry {\n\t\t\tif (!existsSync(this.sessionsDir)) return 0;\n\n\t\t\tconst now = new Date();\n\t\t\tlet total = 0;\n\t\t\tconst projectDirs = await readdir(this.sessionsDir, { withFileTypes: true });\n\n\t\t\tfor (const dirEntry of projectDirs) {\n\t\t\t\tif (!dirEntry.isDirectory()) continue;\n\n\t\t\t\tconst projectDir = join(this.sessionsDir, dirEntry.name);\n\t\t\t\ttotal += await this.scanJsonlFilesInDir(projectDir, now);\n\t\t\t}\n\n\t\t\treturn total;\n\t\t} catch {\n\t\t\t// Never crash the app\n\t\t\treturn 0;\n\t\t}\n\t}\n\n\t/**\n\t * Scan subagent sessions directory.\n\t * Structure: subagentSessionsDir / sessionId / TIMESTAMP_UUID.jsonl\n\t * subagentSessionsDir / sessionId / step-N / TIMESTAMP_UUID.jsonl\n\t */\n\tprivate async scanSubagentSessions(): Promise<number> {\n\t\ttry {\n\t\t\tif (!existsSync(this.subagentSessionsDir)) return 0;\n\n\t\t\tconst now = new Date();\n\t\t\tlet total = 0;\n\t\t\tconst sessionIdDirs = await readdir(this.subagentSessionsDir, { withFileTypes: true });\n\n\t\t\tfor (const dirEntry of sessionIdDirs) {\n\t\t\t\tif (!dirEntry.isDirectory()) continue;\n\n\t\t\t\tconst sessionIdDir = join(this.subagentSessionsDir, dirEntry.name);\n\n\t\t\t\t// Scan direct JSONL files in the session-id directory\n\t\t\t\ttotal += await this.scanJsonlFilesInDir(sessionIdDir, now);\n\n\t\t\t\t// Scan step-N subdirectories for chain agents\n\t\t\t\tlet subEntries: Dirent[];\n\t\t\t\ttry {\n\t\t\t\t\tsubEntries = await readdir(sessionIdDir, { withFileTypes: true });\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tfor (const subEntry of subEntries) {\n\t\t\t\t\tif (!subEntry.isDirectory()) continue;\n\t\t\t\t\tconst stepDir = join(sessionIdDir, subEntry.name);\n\t\t\t\t\ttotal += await this.scanJsonlFilesInDir(stepDir, now);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn total;\n\t\t} catch {\n\t\t\t// Never crash the app\n\t\t\treturn 0;\n\t\t}\n\t}\n\n\t/**\n\t * Scan a directory for today's JSONL session files and sum their costs.\n\t */\n\tprivate async scanJsonlFilesInDir(dirPath: string, now: Date): Promise<number> {\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = (await readdir(dirPath)).filter((f) => f.endsWith(\".jsonl\"));\n\t\t} catch {\n\t\t\t/* Directory unreadable — skip */\n\t\t\treturn 0;\n\t\t}\n\n\t\tlet total = 0;\n\t\tfor (const filename of files) {\n\t\t\t// Extract timestamp part: everything before the UUID\n\t\t\t// Filename: 2026-04-09T18-49-11-406Z_33137d5d-d1e4-4a0e-baca-ebd08ab0e2e0.jsonl\n\t\t\tconst underscoreIdx = filename.indexOf(\"_\", 20);\n\t\t\tif (underscoreIdx === -1) continue;\n\n\t\t\tconst timestampPart = filename.slice(0, underscoreIdx);\n\t\t\tconst fileDate = filenameTimestampToDate(timestampPart);\n\t\t\tif (!fileDate) continue;\n\n\t\t\t// Skip sessions not from today (compares local calendar day)\n\t\t\tif (!isSameLocalDay(fileDate, now)) continue;\n\n\t\t\t// Read and parse the JSONL file\n\t\t\ttotal += await sumCostFromFile(join(dirPath, filename));\n\t\t}\n\n\t\treturn total;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"daily-cost-tracker.js","sourceRoot":"","sources":["../../src/core/daily-cost-tracker.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdir, readFile } from \"fs/promises\";\nimport { join, resolve } from \"path\";\nimport { getSessionsDir, getSubagentSessionsDir, resolveConfiguredDirectory } from \"../config.js\";\n\n/**\n * Parse a session filename timestamp back to a Date.\n * Filename timestamps look like \"2026-04-09T18-49-11-406Z\" (colons and dots replaced with hyphens).\n * Returns null if the timestamp doesn't match the expected format.\n */\nexport function filenameTimestampToDate(fileTimestamp: string): Date | null {\n\t// fileTimestamp like \"2026-04-09T18-49-11-406Z\"\n\t// Reconstruct: YYYY-MM-DDThh:mm:ss.mmmZ\n\tconst match = fileTimestamp.match(/^(\\d{4}-\\d{2}-\\d{2})T(\\d{2})-(\\d{2})-(\\d{2})-(\\d{3})Z$/);\n\tif (!match) return null;\n\tconst iso = `${match[1]}T${match[2]}:${match[3]}:${match[4]}.${match[5]}Z`;\n\tconst date = new Date(iso);\n\treturn Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Check if two dates fall on the same local calendar day.\n */\nexport function isSameLocalDay(date: Date, today: Date): boolean {\n\treturn (\n\t\tdate.getFullYear() === today.getFullYear() &&\n\t\tdate.getMonth() === today.getMonth() &&\n\t\tdate.getDate() === today.getDate()\n\t);\n}\n\n/**\n * Sum `usage.cost.total` from all assistant messages in a JSONL session file.\n * Works identically for both main sessions and subagent sessions.\n */\nexport async function sumCostFromFile(filePath: string): Promise<number> {\n\ttry {\n\t\tconst content = await readFile(filePath, \"utf8\");\n\t\tlet total = 0;\n\n\t\tfor (const line of content.split(\"\\n\")) {\n\t\t\tif (!line.trim()) continue;\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"message\" && entry.message?.role === \"assistant\") {\n\t\t\t\t\ttotal += entry.message.usage?.cost?.total ?? 0;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\treturn total;\n\t} catch {\n\t\t/* File unreadable — treat as zero cost */\n\t\treturn 0;\n\t}\n}\n\n/** Breakdown of daily costs between main sessions and subagent sessions. */\nexport interface DailyCostBreakdown {\n\ttotal: number;\n\tmain: number;\n\tsubagent: number;\n}\n\n/**\n * Session storage roots the daily tracker scans. Every root that may hold\n * today's transcripts must be listed: the built-in nested main-session tree,\n * any custom flat `sessionDir`, and every subagent root (configured + legacy).\n * Files reachable from more than one root are counted once.\n */\nexport interface DailyCostSources {\n\t/** Nested main-session trees: root / projectDir / *.jsonl. Default: built-in sessions dir. */\n\tnestedMainRoots?: readonly string[];\n\t/** Flat main-session directories: root / *.jsonl (custom `sessionDir`). */\n\tflatMainRoots?: readonly string[];\n\t/** Subagent roots: root / sessionId / *.jsonl and root / sessionId / step-N / *.jsonl. Default: legacy dir. */\n\tsubagentRoots?: readonly string[];\n}\n\nfunction uniqueRoots(roots: readonly (string | undefined)[]): string[] {\n\treturn [...new Set(roots.filter((root): root is string => !!root).map((root) => resolve(root)))];\n}\n\n/**\n * Tracks aggregate cost across all sessions for the current calendar day.\n * Scans main session roots and subagent session roots, caches result for O(1) footer access.\n * Refreshes periodically (60s) and on-demand via refresh().\n */\nexport class DailyCostTracker {\n\tprivate static readonly REFRESH_INTERVAL_MS = 60_000;\n\n\tprivate cachedMain = 0;\n\tprivate cachedSubagent = 0;\n\tprivate refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate disposed = false;\n\tprivate readonly nestedMainRoots: string[];\n\tprivate readonly flatMainRoots: string[];\n\tprivate readonly subagentRoots: string[];\n\n\tconstructor(sources: DailyCostSources = {}) {\n\t\tthis.nestedMainRoots = uniqueRoots(sources.nestedMainRoots ?? [getSessionsDir()]);\n\t\tthis.flatMainRoots = uniqueRoots(sources.flatMainRoots ?? []);\n\t\tthis.subagentRoots = uniqueRoots(sources.subagentRoots ?? [getSubagentSessionsDir()]);\n\t\t// Kick off initial async scan — getDailyCost() returns 0 until it completes\n\t\tvoid this.initialScan();\n\t}\n\n\t/** Get cached daily cost total (main + subagent). O(1). */\n\tgetDailyCost(): number {\n\t\treturn this.cachedMain + this.cachedSubagent;\n\t}\n\n\t/** Get cached daily cost breakdown: total, main, and subagent. O(1). */\n\tgetDailyCostBreakdown(): DailyCostBreakdown {\n\t\treturn {\n\t\t\ttotal: this.cachedMain + this.cachedSubagent,\n\t\t\tmain: this.cachedMain,\n\t\t\tsubagent: this.cachedSubagent,\n\t\t};\n\t}\n\n\t/** Force an async refresh of the daily cost. */\n\tasync refresh(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tconst [main, subagent] = await this.scanCosts();\n\t\tif (!this.disposed) {\n\t\t\tthis.cachedMain = main;\n\t\t\tthis.cachedSubagent = subagent;\n\t\t}\n\t}\n\n\t/** Clean up timer and prevent in-flight scans from updating cache. */\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t\tthis.refreshTimer = null;\n\t\t}\n\t}\n\n\tprivate async initialScan(): Promise<void> {\n\t\tawait this.refresh();\n\t\tthis.scheduleNextRefresh();\n\t}\n\n\tprivate scheduleNextRefresh(): void {\n\t\tif (this.disposed) return;\n\t\tthis.refreshTimer = setTimeout(async () => {\n\t\t\tthis.refreshTimer = null;\n\t\t\tawait this.refresh();\n\t\t\tthis.scheduleNextRefresh();\n\t\t}, DailyCostTracker.REFRESH_INTERVAL_MS);\n\t\t// Allow the timer to not keep the process alive\n\t\tif (this.refreshTimer && typeof this.refreshTimer === \"object\" && \"unref\" in this.refreshTimer) {\n\t\t\tthis.refreshTimer.unref();\n\t\t}\n\t}\n\n\tprivate async scanCosts(): Promise<[number, number]> {\n\t\tconst now = new Date();\n\t\tconst [mainFiles, subagentFiles] = await Promise.all([\n\t\t\tthis.collectMainFiles(now),\n\t\t\tthis.collectSubagentFiles(now),\n\t\t]);\n\t\t// A file reachable from both a main and a subagent root is counted as main only.\n\t\tfor (const file of mainFiles) subagentFiles.delete(file);\n\t\tconst [main, subagent] = await Promise.all([sumFiles(mainFiles), sumFiles(subagentFiles)]);\n\t\treturn [main, subagent];\n\t}\n\n\t/** Main sessions: nested root / projectDir / *.jsonl, and flat root / *.jsonl. */\n\tprivate async collectMainFiles(now: Date): Promise<Set<string>> {\n\t\tconst files = new Set<string>();\n\t\tfor (const root of this.nestedMainRoots) {\n\t\t\tfor (const projectDir of await listSubdirs(root)) await addTodaysJsonlFiles(files, projectDir, now);\n\t\t}\n\t\tfor (const root of this.flatMainRoots) await addTodaysJsonlFiles(files, root, now);\n\t\treturn files;\n\t}\n\n\t/** Subagent sessions: root / sessionId / *.jsonl and root / sessionId / step-N / *.jsonl. */\n\tprivate async collectSubagentFiles(now: Date): Promise<Set<string>> {\n\t\tconst files = new Set<string>();\n\t\tfor (const root of this.subagentRoots) {\n\t\t\tfor (const sessionIdDir of await listSubdirs(root)) {\n\t\t\t\tawait addTodaysJsonlFiles(files, sessionIdDir, now);\n\t\t\t\tfor (const stepDir of await listSubdirs(sessionIdDir)) await addTodaysJsonlFiles(files, stepDir, now);\n\t\t\t}\n\t\t}\n\t\treturn files;\n\t}\n}\n\n/** List child directories; a missing or unreadable directory yields none (never crash the footer). */\nasync function listSubdirs(dir: string): Promise<string[]> {\n\ttry {\n\t\tconst entries = await readdir(dir, { withFileTypes: true });\n\t\treturn entries.filter((entry) => entry.isDirectory()).map((entry) => join(dir, entry.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/** Add today's JSONL session files in dirPath (by filename timestamp, local calendar day). */\nasync function addTodaysJsonlFiles(files: Set<string>, dirPath: string, now: Date): Promise<void> {\n\tlet names: string[];\n\ttry {\n\t\tnames = (await readdir(dirPath)).filter((f) => f.endsWith(\".jsonl\"));\n\t} catch {\n\t\t/* Directory missing or unreadable — skip */\n\t\treturn;\n\t}\n\tfor (const filename of names) {\n\t\t// Filename: 2026-04-09T18-49-11-406Z_33137d5d-d1e4-4a0e-baca-ebd08ab0e2e0.jsonl\n\t\tconst underscoreIdx = filename.indexOf(\"_\", 20);\n\t\tif (underscoreIdx === -1) continue;\n\t\tconst fileDate = filenameTimestampToDate(filename.slice(0, underscoreIdx));\n\t\tif (!fileDate || !isSameLocalDay(fileDate, now)) continue;\n\t\tfiles.add(join(dirPath, filename));\n\t}\n}\n\nasync function sumFiles(files: Iterable<string>): Promise<number> {\n\tlet total = 0;\n\tfor (const file of files) total += await sumCostFromFile(file);\n\treturn total;\n}\n\n/** Inputs needed to derive every storage root a session's process may write today's transcripts to. */\nexport interface DailyCostSessionContext {\n\tcwd: string;\n\tsessionManager: { getCustomSessionInventoryRoot(): string | undefined };\n\tsettingsManager: { getGlobalSettings(): { sessionDir?: string }; getSessionDir(): string | undefined };\n\tsubagentSessionDiscoveryRoots: readonly string[];\n}\n\n/**\n * Build daily-cost sources that honour custom session storage: the built-in nested\n * tree, the session's explicit flat root (CLI/extension/settings), the global and\n * effective `sessionDir` settings, and all subagent discovery roots (configured + legacy).\n */\nexport function dailyCostSourcesForSession(context: DailyCostSessionContext): DailyCostSources {\n\tconst { cwd, sessionManager, settingsManager } = context;\n\treturn {\n\t\tnestedMainRoots: [getSessionsDir()],\n\t\tflatMainRoots: uniqueRoots([\n\t\t\tsessionManager.getCustomSessionInventoryRoot(),\n\t\t\tresolveConfiguredDirectory(settingsManager.getGlobalSettings().sessionDir, cwd),\n\t\t\tresolveConfiguredDirectory(settingsManager.getSessionDir(), cwd),\n\t\t]),\n\t\tsubagentRoots: [...context.subagentSessionDiscoveryRoots],\n\t};\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DailyCostBreakdown } from "./daily-cost-tracker.js";
|
|
1
|
+
import { type DailyCostBreakdown, type DailyCostSources } from "./daily-cost-tracker.js";
|
|
2
2
|
/**
|
|
3
3
|
* Provides git branch and extension statuses - data not otherwise accessible to extensions.
|
|
4
4
|
* Token stats, model info available via ctx.sessionManager and ctx.model.
|
|
@@ -18,7 +18,7 @@ export declare class FooterDataProvider {
|
|
|
18
18
|
private refreshInFlight;
|
|
19
19
|
private refreshPending;
|
|
20
20
|
private disposed;
|
|
21
|
-
constructor();
|
|
21
|
+
constructor(dailyCostSources?: DailyCostSources);
|
|
22
22
|
/** Current git branch, null if not in repo, "detached" if detached HEAD */
|
|
23
23
|
getGitBranch(): string | null;
|
|
24
24
|
/** Extension status texts set via ctx.ui.setStatus() */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer-data-provider.d.ts","sourceRoot":"","sources":["../../src/core/footer-data-provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,kBAAkB,EAAoB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"footer-data-provider.d.ts","sourceRoot":"","sources":["../../src/core/footer-data-provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,gBAAgB,EAAoB,MAAM,yBAAyB,CAAC;AAI3G;;;GAGG;AACH,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IAEhD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,QAAQ,CAA0C;IAC1D,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,eAAe,CAA0B;IACjD,OAAO,CAAC,qBAAqB,CAAyB;IACtD,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,0BAA0B,CAA6B;IAC/D,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,gBAAgB,CAAC,EAAE,gBAAgB,EAK9C;IAED,2EAA2E;IAC3E,YAAY,IAAI,MAAM,GAAG,IAAI,CAK5B;IAED,wDAAwD;IACxD,oBAAoB,IAAI,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAElD;IAED,qEAAqE;IACrE,cAAc,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAG/C;IAED,qCAAqC;IACrC,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAM9D;IAED,yCAAyC;IACzC,sBAAsB,IAAI,IAAI,CAE7B;IAED,2EAA2E;IAC3E,YAAY,IAAI,MAAM,CAErB;IAED,oEAAoE;IACpE,qBAAqB,IAAI,kBAAkB,CAE1C;IAED,6CAA6C;IACvC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAEtC;IAED,0EAA0E;IAC1E,sBAAsB,IAAI,MAAM,CAE/B;IAED,yDAAyD;IACnD,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC,CAEhD;IAED,4EAA4E;IAC5E,yBAAyB,IAAI,MAAM,CAElC;IAED,gDAAgD;IAChD,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE7C;IAED,wBAAwB;IACxB,OAAO,IAAI,IAAI,CAiBd;IAED,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,eAAe;YAWT,qBAAqB;IA0BnC,OAAO,CAAC,oBAAoB;YAId,qBAAqB;IAInC,OAAO,CAAC,eAAe;CA6BvB;AAED,yGAAyG;AACzG,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAC5C,kBAAkB,EAChB,cAAc,GACd,sBAAsB,GACtB,2BAA2B,GAC3B,gBAAgB,GAChB,cAAc,GACd,uBAAuB,GACvB,wBAAwB,CAC1B,CAAC","sourcesContent":["import { existsSync, type FSWatcher, watch } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { type DailyCostBreakdown, type DailyCostSources, DailyCostTracker } from \"./daily-cost-tracker.js\";\nimport { findGitPaths, type GitPaths, getGitBranch, getGitBranchAsync } from \"./git-branch.js\";\nimport { SubagentSessionCostTracker } from \"./subagent-session-cost-tracker.js\";\n\n/**\n * Provides git branch and extension statuses - data not otherwise accessible to extensions.\n * Token stats, model info available via ctx.sessionManager and ctx.model.\n */\nexport class FooterDataProvider {\n\tprivate static readonly WATCH_DEBOUNCE_MS = 500;\n\n\tprivate extensionStatuses = new Map<string, string>();\n\tprivate cachedBranch: string | null | undefined = undefined;\n\tprivate gitPaths: GitPaths | null | undefined = undefined;\n\tprivate headWatcher: FSWatcher | null = null;\n\tprivate reftableWatcher: FSWatcher | null = null;\n\tprivate branchChangeCallbacks = new Set<() => void>();\n\tprivate dailyCostTracker: DailyCostTracker;\n\tprivate subagentSessionCostTracker: SubagentSessionCostTracker;\n\tprivate availableProviderCount = 0;\n\tprivate refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate refreshInFlight = false;\n\tprivate refreshPending = false;\n\tprivate disposed = false;\n\n\tconstructor(dailyCostSources?: DailyCostSources) {\n\t\tthis.gitPaths = findGitPaths();\n\t\tthis.setupGitWatcher();\n\t\tthis.dailyCostTracker = new DailyCostTracker(dailyCostSources);\n\t\tthis.subagentSessionCostTracker = new SubagentSessionCostTracker();\n\t}\n\n\t/** Current git branch, null if not in repo, \"detached\" if detached HEAD */\n\tgetGitBranch(): string | null {\n\t\tif (this.cachedBranch === undefined) {\n\t\t\tthis.cachedBranch = this.resolveGitBranchSync();\n\t\t}\n\t\treturn this.cachedBranch;\n\t}\n\n\t/** Extension status texts set via ctx.ui.setStatus() */\n\tgetExtensionStatuses(): ReadonlyMap<string, string> {\n\t\treturn this.extensionStatuses;\n\t}\n\n\t/** Subscribe to git branch changes. Returns unsubscribe function. */\n\tonBranchChange(callback: () => void): () => void {\n\t\tthis.branchChangeCallbacks.add(callback);\n\t\treturn () => this.branchChangeCallbacks.delete(callback);\n\t}\n\n\t/** Internal: set extension status */\n\tsetExtensionStatus(key: string, text: string | undefined): void {\n\t\tif (text === undefined) {\n\t\t\tthis.extensionStatuses.delete(key);\n\t\t} else {\n\t\t\tthis.extensionStatuses.set(key, text);\n\t\t}\n\t}\n\n\t/** Internal: clear extension statuses */\n\tclearExtensionStatuses(): void {\n\t\tthis.extensionStatuses.clear();\n\t}\n\n\t/** Cached daily cost total across all sessions (main + subagent). O(1). */\n\tgetDailyCost(): number {\n\t\treturn this.dailyCostTracker.getDailyCost();\n\t}\n\n\t/** Cached daily cost breakdown: total, main, and subagent. O(1). */\n\tgetDailyCostBreakdown(): DailyCostBreakdown {\n\t\treturn this.dailyCostTracker.getDailyCostBreakdown();\n\t}\n\n\t/** Force refresh of the daily cost cache. */\n\tasync refreshDailyCost(): Promise<void> {\n\t\tawait this.dailyCostTracker.refresh();\n\t}\n\n\t/** Cached sub-agent session cost for the current parent session. O(1). */\n\tgetSubagentSessionCost(): number {\n\t\treturn this.subagentSessionCostTracker.getCost();\n\t}\n\n\t/** Force refresh of the sub-agent session cost cache. */\n\tasync refreshSubagentSessionCost(): Promise<void> {\n\t\tawait this.subagentSessionCostTracker.refresh();\n\t}\n\n\t/** Number of unique providers with available models (for footer display) */\n\tgetAvailableProviderCount(): number {\n\t\treturn this.availableProviderCount;\n\t}\n\n\t/** Internal: update available provider count */\n\tsetAvailableProviderCount(count: number): void {\n\t\tthis.availableProviderCount = count;\n\t}\n\n\t/** Internal: cleanup */\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t\tthis.refreshTimer = null;\n\t\t}\n\t\tthis.dailyCostTracker.dispose();\n\t\tthis.subagentSessionCostTracker.dispose();\n\t\tif (this.headWatcher) {\n\t\t\tthis.headWatcher.close();\n\t\t\tthis.headWatcher = null;\n\t\t}\n\t\tif (this.reftableWatcher) {\n\t\t\tthis.reftableWatcher.close();\n\t\t\tthis.reftableWatcher = null;\n\t\t}\n\t\tthis.branchChangeCallbacks.clear();\n\t}\n\n\tprivate notifyBranchChange(): void {\n\t\tfor (const cb of this.branchChangeCallbacks) cb();\n\t}\n\n\tprivate scheduleRefresh(): void {\n\t\tif (this.disposed) return;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t}\n\t\tthis.refreshTimer = setTimeout(() => {\n\t\t\tthis.refreshTimer = null;\n\t\t\tvoid this.refreshGitBranchAsync();\n\t\t}, FooterDataProvider.WATCH_DEBOUNCE_MS);\n\t}\n\n\tprivate async refreshGitBranchAsync(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tif (this.refreshInFlight) {\n\t\t\tthis.refreshPending = true;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.refreshInFlight = true;\n\t\ttry {\n\t\t\tconst nextBranch = await this.resolveGitBranchAsync();\n\t\t\tif (this.disposed) return;\n\t\t\tif (this.cachedBranch !== undefined && this.cachedBranch !== nextBranch) {\n\t\t\t\tthis.cachedBranch = nextBranch;\n\t\t\t\tthis.notifyBranchChange();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.cachedBranch = nextBranch;\n\t\t} finally {\n\t\t\tthis.refreshInFlight = false;\n\t\t\tif (this.refreshPending && !this.disposed) {\n\t\t\t\tthis.refreshPending = false;\n\t\t\t\tthis.scheduleRefresh();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate resolveGitBranchSync(): string | null {\n\t\treturn getGitBranch();\n\t}\n\n\tprivate async resolveGitBranchAsync(): Promise<string | null> {\n\t\treturn getGitBranchAsync();\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\tif (!this.gitPaths) return;\n\n\t\t// Watch the directory containing HEAD, not HEAD itself.\n\t\t// Git uses atomic writes (write temp, rename over HEAD), which changes the inode.\n\t\t// fs.watch on a file stops working after the inode changes.\n\t\ttry {\n\t\t\tthis.headWatcher = watch(dirname(this.gitPaths.headPath), (_eventType, filename) => {\n\t\t\t\tif (!filename || filename.toString() === \"HEAD\") {\n\t\t\t\t\tthis.scheduleRefresh();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\n\t\t// In reftable repos, branch switches update files in the reftable directory\n\t\t// instead of HEAD. Watch it separately so the footer picks up those changes.\n\t\tconst reftableDir = join(this.gitPaths.commonGitDir, \"reftable\");\n\t\tif (existsSync(reftableDir)) {\n\t\t\ttry {\n\t\t\t\tthis.reftableWatcher = watch(reftableDir, () => {\n\t\t\t\t\tthis.scheduleRefresh();\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// Silently fail if we can't watch\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Read-only view for extensions - excludes setExtensionStatus, setAvailableProviderCount and dispose */\nexport type ReadonlyFooterDataProvider = Pick<\n\tFooterDataProvider,\n\t| \"getGitBranch\"\n\t| \"getExtensionStatuses\"\n\t| \"getAvailableProviderCount\"\n\t| \"onBranchChange\"\n\t| \"getDailyCost\"\n\t| \"getDailyCostBreakdown\"\n\t| \"getSubagentSessionCost\"\n>;\n"]}
|
|
@@ -22,10 +22,10 @@ export class FooterDataProvider {
|
|
|
22
22
|
refreshInFlight = false;
|
|
23
23
|
refreshPending = false;
|
|
24
24
|
disposed = false;
|
|
25
|
-
constructor() {
|
|
25
|
+
constructor(dailyCostSources) {
|
|
26
26
|
this.gitPaths = findGitPaths();
|
|
27
27
|
this.setupGitWatcher();
|
|
28
|
-
this.dailyCostTracker = new DailyCostTracker();
|
|
28
|
+
this.dailyCostTracker = new DailyCostTracker(dailyCostSources);
|
|
29
29
|
this.subagentSessionCostTracker = new SubagentSessionCostTracker();
|
|
30
30
|
}
|
|
31
31
|
/** Current git branch, null if not in repo, "detached" if detached HEAD */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer-data-provider.js","sourceRoot":"","sources":["../../src/core/footer-data-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, type FSWatcher, watch } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { type DailyCostBreakdown, DailyCostTracker } from \"./daily-cost-tracker.js\";\nimport { findGitPaths, type GitPaths, getGitBranch, getGitBranchAsync } from \"./git-branch.js\";\nimport { SubagentSessionCostTracker } from \"./subagent-session-cost-tracker.js\";\n\n/**\n * Provides git branch and extension statuses - data not otherwise accessible to extensions.\n * Token stats, model info available via ctx.sessionManager and ctx.model.\n */\nexport class FooterDataProvider {\n\tprivate static readonly WATCH_DEBOUNCE_MS = 500;\n\n\tprivate extensionStatuses = new Map<string, string>();\n\tprivate cachedBranch: string | null | undefined = undefined;\n\tprivate gitPaths: GitPaths | null | undefined = undefined;\n\tprivate headWatcher: FSWatcher | null = null;\n\tprivate reftableWatcher: FSWatcher | null = null;\n\tprivate branchChangeCallbacks = new Set<() => void>();\n\tprivate dailyCostTracker: DailyCostTracker;\n\tprivate subagentSessionCostTracker: SubagentSessionCostTracker;\n\tprivate availableProviderCount = 0;\n\tprivate refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate refreshInFlight = false;\n\tprivate refreshPending = false;\n\tprivate disposed = false;\n\n\tconstructor() {\n\t\tthis.gitPaths = findGitPaths();\n\t\tthis.setupGitWatcher();\n\t\tthis.dailyCostTracker = new DailyCostTracker();\n\t\tthis.subagentSessionCostTracker = new SubagentSessionCostTracker();\n\t}\n\n\t/** Current git branch, null if not in repo, \"detached\" if detached HEAD */\n\tgetGitBranch(): string | null {\n\t\tif (this.cachedBranch === undefined) {\n\t\t\tthis.cachedBranch = this.resolveGitBranchSync();\n\t\t}\n\t\treturn this.cachedBranch;\n\t}\n\n\t/** Extension status texts set via ctx.ui.setStatus() */\n\tgetExtensionStatuses(): ReadonlyMap<string, string> {\n\t\treturn this.extensionStatuses;\n\t}\n\n\t/** Subscribe to git branch changes. Returns unsubscribe function. */\n\tonBranchChange(callback: () => void): () => void {\n\t\tthis.branchChangeCallbacks.add(callback);\n\t\treturn () => this.branchChangeCallbacks.delete(callback);\n\t}\n\n\t/** Internal: set extension status */\n\tsetExtensionStatus(key: string, text: string | undefined): void {\n\t\tif (text === undefined) {\n\t\t\tthis.extensionStatuses.delete(key);\n\t\t} else {\n\t\t\tthis.extensionStatuses.set(key, text);\n\t\t}\n\t}\n\n\t/** Internal: clear extension statuses */\n\tclearExtensionStatuses(): void {\n\t\tthis.extensionStatuses.clear();\n\t}\n\n\t/** Cached daily cost total across all sessions (main + subagent). O(1). */\n\tgetDailyCost(): number {\n\t\treturn this.dailyCostTracker.getDailyCost();\n\t}\n\n\t/** Cached daily cost breakdown: total, main, and subagent. O(1). */\n\tgetDailyCostBreakdown(): DailyCostBreakdown {\n\t\treturn this.dailyCostTracker.getDailyCostBreakdown();\n\t}\n\n\t/** Force refresh of the daily cost cache. */\n\tasync refreshDailyCost(): Promise<void> {\n\t\tawait this.dailyCostTracker.refresh();\n\t}\n\n\t/** Cached sub-agent session cost for the current parent session. O(1). */\n\tgetSubagentSessionCost(): number {\n\t\treturn this.subagentSessionCostTracker.getCost();\n\t}\n\n\t/** Force refresh of the sub-agent session cost cache. */\n\tasync refreshSubagentSessionCost(): Promise<void> {\n\t\tawait this.subagentSessionCostTracker.refresh();\n\t}\n\n\t/** Number of unique providers with available models (for footer display) */\n\tgetAvailableProviderCount(): number {\n\t\treturn this.availableProviderCount;\n\t}\n\n\t/** Internal: update available provider count */\n\tsetAvailableProviderCount(count: number): void {\n\t\tthis.availableProviderCount = count;\n\t}\n\n\t/** Internal: cleanup */\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t\tthis.refreshTimer = null;\n\t\t}\n\t\tthis.dailyCostTracker.dispose();\n\t\tthis.subagentSessionCostTracker.dispose();\n\t\tif (this.headWatcher) {\n\t\t\tthis.headWatcher.close();\n\t\t\tthis.headWatcher = null;\n\t\t}\n\t\tif (this.reftableWatcher) {\n\t\t\tthis.reftableWatcher.close();\n\t\t\tthis.reftableWatcher = null;\n\t\t}\n\t\tthis.branchChangeCallbacks.clear();\n\t}\n\n\tprivate notifyBranchChange(): void {\n\t\tfor (const cb of this.branchChangeCallbacks) cb();\n\t}\n\n\tprivate scheduleRefresh(): void {\n\t\tif (this.disposed) return;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t}\n\t\tthis.refreshTimer = setTimeout(() => {\n\t\t\tthis.refreshTimer = null;\n\t\t\tvoid this.refreshGitBranchAsync();\n\t\t}, FooterDataProvider.WATCH_DEBOUNCE_MS);\n\t}\n\n\tprivate async refreshGitBranchAsync(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tif (this.refreshInFlight) {\n\t\t\tthis.refreshPending = true;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.refreshInFlight = true;\n\t\ttry {\n\t\t\tconst nextBranch = await this.resolveGitBranchAsync();\n\t\t\tif (this.disposed) return;\n\t\t\tif (this.cachedBranch !== undefined && this.cachedBranch !== nextBranch) {\n\t\t\t\tthis.cachedBranch = nextBranch;\n\t\t\t\tthis.notifyBranchChange();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.cachedBranch = nextBranch;\n\t\t} finally {\n\t\t\tthis.refreshInFlight = false;\n\t\t\tif (this.refreshPending && !this.disposed) {\n\t\t\t\tthis.refreshPending = false;\n\t\t\t\tthis.scheduleRefresh();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate resolveGitBranchSync(): string | null {\n\t\treturn getGitBranch();\n\t}\n\n\tprivate async resolveGitBranchAsync(): Promise<string | null> {\n\t\treturn getGitBranchAsync();\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\tif (!this.gitPaths) return;\n\n\t\t// Watch the directory containing HEAD, not HEAD itself.\n\t\t// Git uses atomic writes (write temp, rename over HEAD), which changes the inode.\n\t\t// fs.watch on a file stops working after the inode changes.\n\t\ttry {\n\t\t\tthis.headWatcher = watch(dirname(this.gitPaths.headPath), (_eventType, filename) => {\n\t\t\t\tif (!filename || filename.toString() === \"HEAD\") {\n\t\t\t\t\tthis.scheduleRefresh();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\n\t\t// In reftable repos, branch switches update files in the reftable directory\n\t\t// instead of HEAD. Watch it separately so the footer picks up those changes.\n\t\tconst reftableDir = join(this.gitPaths.commonGitDir, \"reftable\");\n\t\tif (existsSync(reftableDir)) {\n\t\t\ttry {\n\t\t\t\tthis.reftableWatcher = watch(reftableDir, () => {\n\t\t\t\t\tthis.scheduleRefresh();\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// Silently fail if we can't watch\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Read-only view for extensions - excludes setExtensionStatus, setAvailableProviderCount and dispose */\nexport type ReadonlyFooterDataProvider = Pick<\n\tFooterDataProvider,\n\t| \"getGitBranch\"\n\t| \"getExtensionStatuses\"\n\t| \"getAvailableProviderCount\"\n\t| \"onBranchChange\"\n\t| \"getDailyCost\"\n\t| \"getDailyCostBreakdown\"\n\t| \"getSubagentSessionCost\"\n>;\n"]}
|
|
1
|
+
{"version":3,"file":"footer-data-provider.js","sourceRoot":"","sources":["../../src/core/footer-data-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, type FSWatcher, watch } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { type DailyCostBreakdown, type DailyCostSources, DailyCostTracker } from \"./daily-cost-tracker.js\";\nimport { findGitPaths, type GitPaths, getGitBranch, getGitBranchAsync } from \"./git-branch.js\";\nimport { SubagentSessionCostTracker } from \"./subagent-session-cost-tracker.js\";\n\n/**\n * Provides git branch and extension statuses - data not otherwise accessible to extensions.\n * Token stats, model info available via ctx.sessionManager and ctx.model.\n */\nexport class FooterDataProvider {\n\tprivate static readonly WATCH_DEBOUNCE_MS = 500;\n\n\tprivate extensionStatuses = new Map<string, string>();\n\tprivate cachedBranch: string | null | undefined = undefined;\n\tprivate gitPaths: GitPaths | null | undefined = undefined;\n\tprivate headWatcher: FSWatcher | null = null;\n\tprivate reftableWatcher: FSWatcher | null = null;\n\tprivate branchChangeCallbacks = new Set<() => void>();\n\tprivate dailyCostTracker: DailyCostTracker;\n\tprivate subagentSessionCostTracker: SubagentSessionCostTracker;\n\tprivate availableProviderCount = 0;\n\tprivate refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate refreshInFlight = false;\n\tprivate refreshPending = false;\n\tprivate disposed = false;\n\n\tconstructor(dailyCostSources?: DailyCostSources) {\n\t\tthis.gitPaths = findGitPaths();\n\t\tthis.setupGitWatcher();\n\t\tthis.dailyCostTracker = new DailyCostTracker(dailyCostSources);\n\t\tthis.subagentSessionCostTracker = new SubagentSessionCostTracker();\n\t}\n\n\t/** Current git branch, null if not in repo, \"detached\" if detached HEAD */\n\tgetGitBranch(): string | null {\n\t\tif (this.cachedBranch === undefined) {\n\t\t\tthis.cachedBranch = this.resolveGitBranchSync();\n\t\t}\n\t\treturn this.cachedBranch;\n\t}\n\n\t/** Extension status texts set via ctx.ui.setStatus() */\n\tgetExtensionStatuses(): ReadonlyMap<string, string> {\n\t\treturn this.extensionStatuses;\n\t}\n\n\t/** Subscribe to git branch changes. Returns unsubscribe function. */\n\tonBranchChange(callback: () => void): () => void {\n\t\tthis.branchChangeCallbacks.add(callback);\n\t\treturn () => this.branchChangeCallbacks.delete(callback);\n\t}\n\n\t/** Internal: set extension status */\n\tsetExtensionStatus(key: string, text: string | undefined): void {\n\t\tif (text === undefined) {\n\t\t\tthis.extensionStatuses.delete(key);\n\t\t} else {\n\t\t\tthis.extensionStatuses.set(key, text);\n\t\t}\n\t}\n\n\t/** Internal: clear extension statuses */\n\tclearExtensionStatuses(): void {\n\t\tthis.extensionStatuses.clear();\n\t}\n\n\t/** Cached daily cost total across all sessions (main + subagent). O(1). */\n\tgetDailyCost(): number {\n\t\treturn this.dailyCostTracker.getDailyCost();\n\t}\n\n\t/** Cached daily cost breakdown: total, main, and subagent. O(1). */\n\tgetDailyCostBreakdown(): DailyCostBreakdown {\n\t\treturn this.dailyCostTracker.getDailyCostBreakdown();\n\t}\n\n\t/** Force refresh of the daily cost cache. */\n\tasync refreshDailyCost(): Promise<void> {\n\t\tawait this.dailyCostTracker.refresh();\n\t}\n\n\t/** Cached sub-agent session cost for the current parent session. O(1). */\n\tgetSubagentSessionCost(): number {\n\t\treturn this.subagentSessionCostTracker.getCost();\n\t}\n\n\t/** Force refresh of the sub-agent session cost cache. */\n\tasync refreshSubagentSessionCost(): Promise<void> {\n\t\tawait this.subagentSessionCostTracker.refresh();\n\t}\n\n\t/** Number of unique providers with available models (for footer display) */\n\tgetAvailableProviderCount(): number {\n\t\treturn this.availableProviderCount;\n\t}\n\n\t/** Internal: update available provider count */\n\tsetAvailableProviderCount(count: number): void {\n\t\tthis.availableProviderCount = count;\n\t}\n\n\t/** Internal: cleanup */\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t\tthis.refreshTimer = null;\n\t\t}\n\t\tthis.dailyCostTracker.dispose();\n\t\tthis.subagentSessionCostTracker.dispose();\n\t\tif (this.headWatcher) {\n\t\t\tthis.headWatcher.close();\n\t\t\tthis.headWatcher = null;\n\t\t}\n\t\tif (this.reftableWatcher) {\n\t\t\tthis.reftableWatcher.close();\n\t\t\tthis.reftableWatcher = null;\n\t\t}\n\t\tthis.branchChangeCallbacks.clear();\n\t}\n\n\tprivate notifyBranchChange(): void {\n\t\tfor (const cb of this.branchChangeCallbacks) cb();\n\t}\n\n\tprivate scheduleRefresh(): void {\n\t\tif (this.disposed) return;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t}\n\t\tthis.refreshTimer = setTimeout(() => {\n\t\t\tthis.refreshTimer = null;\n\t\t\tvoid this.refreshGitBranchAsync();\n\t\t}, FooterDataProvider.WATCH_DEBOUNCE_MS);\n\t}\n\n\tprivate async refreshGitBranchAsync(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tif (this.refreshInFlight) {\n\t\t\tthis.refreshPending = true;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.refreshInFlight = true;\n\t\ttry {\n\t\t\tconst nextBranch = await this.resolveGitBranchAsync();\n\t\t\tif (this.disposed) return;\n\t\t\tif (this.cachedBranch !== undefined && this.cachedBranch !== nextBranch) {\n\t\t\t\tthis.cachedBranch = nextBranch;\n\t\t\t\tthis.notifyBranchChange();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.cachedBranch = nextBranch;\n\t\t} finally {\n\t\t\tthis.refreshInFlight = false;\n\t\t\tif (this.refreshPending && !this.disposed) {\n\t\t\t\tthis.refreshPending = false;\n\t\t\t\tthis.scheduleRefresh();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate resolveGitBranchSync(): string | null {\n\t\treturn getGitBranch();\n\t}\n\n\tprivate async resolveGitBranchAsync(): Promise<string | null> {\n\t\treturn getGitBranchAsync();\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\tif (!this.gitPaths) return;\n\n\t\t// Watch the directory containing HEAD, not HEAD itself.\n\t\t// Git uses atomic writes (write temp, rename over HEAD), which changes the inode.\n\t\t// fs.watch on a file stops working after the inode changes.\n\t\ttry {\n\t\t\tthis.headWatcher = watch(dirname(this.gitPaths.headPath), (_eventType, filename) => {\n\t\t\t\tif (!filename || filename.toString() === \"HEAD\") {\n\t\t\t\t\tthis.scheduleRefresh();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\n\t\t// In reftable repos, branch switches update files in the reftable directory\n\t\t// instead of HEAD. Watch it separately so the footer picks up those changes.\n\t\tconst reftableDir = join(this.gitPaths.commonGitDir, \"reftable\");\n\t\tif (existsSync(reftableDir)) {\n\t\t\ttry {\n\t\t\t\tthis.reftableWatcher = watch(reftableDir, () => {\n\t\t\t\t\tthis.scheduleRefresh();\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// Silently fail if we can't watch\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Read-only view for extensions - excludes setExtensionStatus, setAvailableProviderCount and dispose */\nexport type ReadonlyFooterDataProvider = Pick<\n\tFooterDataProvider,\n\t| \"getGitBranch\"\n\t| \"getExtensionStatuses\"\n\t| \"getAvailableProviderCount\"\n\t| \"onBranchChange\"\n\t| \"getDailyCost\"\n\t| \"getDailyCostBreakdown\"\n\t| \"getSubagentSessionCost\"\n>;\n"]}
|